From 87cca3a7aaf819219c72598f71c0dc18a3d456b3 Mon Sep 17 00:00:00 2001 From: CollinBeczak Date: Fri, 26 Jul 2024 21:55:35 -0500 Subject: [PATCH 1/8] add `Retrieve Users Locked Tasks` endpoint --- .../framework/controller/UserController.scala | 10 ++++ .../framework/model/LockedTask.scala | 41 ++++++++++++++++ .../UserSavedObjectsRepository.scala | 48 +++++++++++++++---- .../framework/service/UserService.scala | 17 +++++++ conf/v2_route/user.api | 24 ++++++++++ 5 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 app/org/maproulette/framework/model/LockedTask.scala diff --git a/app/org/maproulette/framework/controller/UserController.scala b/app/org/maproulette/framework/controller/UserController.scala index bcba62dff..32a449422 100644 --- a/app/org/maproulette/framework/controller/UserController.scala +++ b/app/org/maproulette/framework/controller/UserController.scala @@ -251,6 +251,16 @@ class UserController @Inject() ( } } + def getLockedTasks( + userId: Long, + limit: Long + ): Action[AnyContent] = Action.async { implicit request => + this.sessionManager.authenticatedRequest { implicit user => + val tasks = this.serviceManager.user.getLockedTasks(userId, user, limit) + Ok(Json.toJson(tasks)) + } + } + def saveTask(userId: Long, taskId: Long): Action[AnyContent] = Action.async { implicit request => this.sessionManager.authenticatedRequest { implicit user => this.serviceManager.user.saveTask(userId, taskId, user) diff --git a/app/org/maproulette/framework/model/LockedTask.scala b/app/org/maproulette/framework/model/LockedTask.scala new file mode 100644 index 000000000..c44578303 --- /dev/null +++ b/app/org/maproulette/framework/model/LockedTask.scala @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 MapRoulette contributors (see CONTRIBUTORS.md). + * Licensed under the Apache License, Version 2.0 (see LICENSE). + */ +package org.maproulette.framework.model + +import org.joda.time.DateTime +import org.maproulette.cache.CacheObject +import org.maproulette.framework.psql.CommonField +import play.api.libs.json.{Json, Format} +import play.api.libs.json.JodaWrites._ +import play.api.libs.json.JodaReads._ + +// Define the LockedTask case class +case class LockedTask( + override val id: Long, + challengeName: Option[String], + startedAt: DateTime +) extends CacheObject[Long] { + // Implement the abstract member 'name' + override def name: String = "LockedTask" +} + +/** + * Mapping between Task and Challenge and Lock + */ +case class LockedTaskData( + id: Long, + challengeName: Option[String], + startedAt: DateTime +) + +object LockedTask extends CommonField { + // Use Json.format to automatically derive both Reads and Writes + implicit val lockedTaskFormat: Format[LockedTask] = Json.format[LockedTask] +} + +// Define implicit Formats for LockedTaskData +object LockedTaskData { + implicit val lockedTaskDataFormat: Format[LockedTaskData] = Json.format[LockedTaskData] +} diff --git a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala index bf6830780..b4eff764e 100644 --- a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala +++ b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala @@ -6,18 +6,14 @@ package org.maproulette.framework.repository import java.sql.Connection +import anorm.{SQL, SqlParser} +import anorm.SqlParser._ +import org.joda.time.DateTime -import anorm.SQL import javax.inject.{Inject, Singleton} -import org.maproulette.framework.model.{Challenge, SavedChallenge, SavedTasks} -import org.maproulette.framework.psql.filter.{ - BaseParameter, - FilterParameter, - Operator, - SubQueryFilter -} +import org.maproulette.framework.model.{Challenge, LockedTaskData, SavedChallenge, SavedTasks, Task} +import org.maproulette.framework.psql.filter.{BaseParameter, FilterParameter, Operator, SubQueryFilter} import org.maproulette.framework.psql._ -import org.maproulette.framework.model.Task import org.maproulette.models.dal.{ChallengeDAL, TaskDAL} import play.api.db.Database @@ -150,6 +146,40 @@ class UserSavedObjectsRepository @Inject() ( } } + /** + * Retrieves a list of locked tasks for a specific user. + * + * @param userId The ID of the user for whom you are requesting the saved challenges. + * @param limit The maximum number of tasks to return. + * @param c An optional existing connection. + * @return A list tasks the user has locked, each item containing the task ID, its locked time, and the challenge name. + */ + def getLockedTasks( + userId: Long, + limit: Long + )(implicit c: Option[Connection] = None): List[LockedTaskData] = { + this.withMRTransaction { implicit c => + val parser = for { + id <- get[Long]("id") + challengeName <- get[Option[String]]("challenges.challenge_name") + lockedTime <- get[DateTime]("locked.locked_time") + } yield (LockedTaskData(id, challengeName, lockedTime)) + + val query = """ + SELECT t.id, l.locked_time, c.name AS challenge_name + FROM tasks t + INNER JOIN locked l ON t.id = l.item_id + LEFT JOIN challenges c ON t.parent_id = c.id + WHERE l.user_id = {userId} + LIMIT {limit} + """ + + SQL(query) + .on("userId" -> userId, "limit" -> limit) + .as(parser.*) + } + } + /** * Saves the task for the user, will validate that the task actually exists first based on the * provided id diff --git a/app/org/maproulette/framework/service/UserService.scala b/app/org/maproulette/framework/service/UserService.scala index ba02db74b..1a16cdea5 100644 --- a/app/org/maproulette/framework/service/UserService.scala +++ b/app/org/maproulette/framework/service/UserService.scala @@ -841,6 +841,23 @@ class UserService @Inject() ( this.savedObjectsRepository.getSavedTasks(userId, challengeIds, paging) } + /** + * Retrieve all the tasks that have been locked by the provided user + * + * @param userId The id of the user + * @param user The user making the actual request + * @param limit + * @return A list of Tasks that have been locked by the user + */ + def getLockedTasks( + userId: Long, + user: User, + limit: Long + ): List[LockedTaskData] = { + this.permission.hasReadAccess(UserType(), user)(userId) + this.savedObjectsRepository.getLockedTasks(userId, limit) + } + /** * Saves the task for the user, will validate that the task actually exists first based on the * provided id diff --git a/conf/v2_route/user.api b/conf/v2_route/user.api index 6fbae0554..2eb0feaf3 100644 --- a/conf/v2_route/user.api +++ b/conf/v2_route/user.api @@ -321,6 +321,30 @@ DELETE /user/:userId/unsave/:challengeId @org.maproulette.framework.c GET /user/:userId/savedTasks @org.maproulette.framework.controller.UserController.getSavedTasks(userId:Long, challengeIds:String ?= "", limit:Int ?= 10, page:Int ?= 0) ### # tags: [ User ] +# summary: Retrieves Users Locked Tasks +# description: Retrieves a list of all the tasks the user with the matching id has locked +# responses: +# '200': +# description: The retrieved Tasks +# content: +# application/json: +# schema: +# type: array +# items: +# $ref: '#/components/schemas/org.maproulette.framework.model.Task' +# '401': +# description: The user is not authorized to make this request +# parameters: +# - name: userId +# in: path +# description: The id of the user to retrieve the locked tasks for +# - name: limit +# in: query +# description: Limit the number of results returned in the response. Default value is 50. +### +GET /user/:userId/lockedTasks @org.maproulette.framework.controller.UserController.getLockedTasks(userId:Long, limit:Int ?= 50) +### +# tags: [ User ] # summary: Saves a Task for a User # description: Saves a Task to a user account # responses: From c4f227ae2746f25860030b812295a7c2633191af Mon Sep 17 00:00:00 2001 From: CollinBeczak Date: Mon, 29 Jul 2024 15:03:44 -0500 Subject: [PATCH 2/8] format and remove unused code --- .../framework/model/LockedTask.scala | 19 +------------------ .../UserSavedObjectsRepository.scala | 7 ++++++- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/app/org/maproulette/framework/model/LockedTask.scala b/app/org/maproulette/framework/model/LockedTask.scala index c44578303..afe8b6a89 100644 --- a/app/org/maproulette/framework/model/LockedTask.scala +++ b/app/org/maproulette/framework/model/LockedTask.scala @@ -5,24 +5,12 @@ package org.maproulette.framework.model import org.joda.time.DateTime -import org.maproulette.cache.CacheObject -import org.maproulette.framework.psql.CommonField import play.api.libs.json.{Json, Format} import play.api.libs.json.JodaWrites._ import play.api.libs.json.JodaReads._ -// Define the LockedTask case class -case class LockedTask( - override val id: Long, - challengeName: Option[String], - startedAt: DateTime -) extends CacheObject[Long] { - // Implement the abstract member 'name' - override def name: String = "LockedTask" -} - /** - * Mapping between Task and Challenge and Lock + * Mapping of object structure for fetching task lock data */ case class LockedTaskData( id: Long, @@ -30,11 +18,6 @@ case class LockedTaskData( startedAt: DateTime ) -object LockedTask extends CommonField { - // Use Json.format to automatically derive both Reads and Writes - implicit val lockedTaskFormat: Format[LockedTask] = Json.format[LockedTask] -} - // Define implicit Formats for LockedTaskData object LockedTaskData { implicit val lockedTaskDataFormat: Format[LockedTaskData] = Json.format[LockedTaskData] diff --git a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala index b4eff764e..ce7987cf5 100644 --- a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala +++ b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala @@ -12,7 +12,12 @@ import org.joda.time.DateTime import javax.inject.{Inject, Singleton} import org.maproulette.framework.model.{Challenge, LockedTaskData, SavedChallenge, SavedTasks, Task} -import org.maproulette.framework.psql.filter.{BaseParameter, FilterParameter, Operator, SubQueryFilter} +import org.maproulette.framework.psql.filter.{ + BaseParameter, + FilterParameter, + Operator, + SubQueryFilter +} import org.maproulette.framework.psql._ import org.maproulette.models.dal.{ChallengeDAL, TaskDAL} import play.api.db.Database From d43dbcafcc25f5db5df3ef3a99bb1a4890b91fa5 Mon Sep 17 00:00:00 2001 From: CollinBeczak Date: Mon, 29 Jul 2024 15:24:21 -0500 Subject: [PATCH 3/8] fix imports --- .../framework/repository/UserSavedObjectsRepository.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala index ce7987cf5..ed89737e6 100644 --- a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala +++ b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala @@ -6,7 +6,7 @@ package org.maproulette.framework.repository import java.sql.Connection -import anorm.{SQL, SqlParser} +import anorm.SQL import anorm.SqlParser._ import org.joda.time.DateTime From ffda33e639061b95c9a74d8549446474e51a49bb Mon Sep 17 00:00:00 2001 From: CollinBeczak Date: Tue, 30 Jul 2024 18:12:29 -0500 Subject: [PATCH 4/8] include challenge id in object --- app/org/maproulette/framework/model/LockedTask.scala | 3 ++- .../repository/UserSavedObjectsRepository.scala | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/org/maproulette/framework/model/LockedTask.scala b/app/org/maproulette/framework/model/LockedTask.scala index afe8b6a89..d41accac7 100644 --- a/app/org/maproulette/framework/model/LockedTask.scala +++ b/app/org/maproulette/framework/model/LockedTask.scala @@ -14,7 +14,8 @@ import play.api.libs.json.JodaReads._ */ case class LockedTaskData( id: Long, - challengeName: Option[String], + parent: Option[Long], + parentName: Option[String], startedAt: DateTime ) diff --git a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala index ed89737e6..696a93dd3 100644 --- a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala +++ b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala @@ -165,13 +165,14 @@ class UserSavedObjectsRepository @Inject() ( )(implicit c: Option[Connection] = None): List[LockedTaskData] = { this.withMRTransaction { implicit c => val parser = for { - id <- get[Long]("id") - challengeName <- get[Option[String]]("challenges.challenge_name") - lockedTime <- get[DateTime]("locked.locked_time") - } yield (LockedTaskData(id, challengeName, lockedTime)) + id <- get[Long]("id") + parent <- get[Option[Long]]("tasks.parent_id") + parentName <- get[Option[String]]("challenges.challenge_name") + lockedTime <- get[DateTime]("locked.locked_time") + } yield (LockedTaskData(id, parent, parentName, lockedTime)) val query = """ - SELECT t.id, l.locked_time, c.name AS challenge_name + SELECT t.id, t.parent_id, l.locked_time, c.name AS challenge_name FROM tasks t INNER JOIN locked l ON t.id = l.item_id LEFT JOIN challenges c ON t.parent_id = c.id From 901800841b5e0236b259c9c66bc1c4de4e98bce3 Mon Sep 17 00:00:00 2001 From: CollinBeczak Date: Fri, 9 Aug 2024 09:34:55 -0500 Subject: [PATCH 5/8] simplify parser and LockedTaskData object --- app/org/maproulette/framework/model/LockedTask.scala | 2 +- .../framework/repository/UserSavedObjectsRepository.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/org/maproulette/framework/model/LockedTask.scala b/app/org/maproulette/framework/model/LockedTask.scala index d41accac7..b7b99a938 100644 --- a/app/org/maproulette/framework/model/LockedTask.scala +++ b/app/org/maproulette/framework/model/LockedTask.scala @@ -14,7 +14,7 @@ import play.api.libs.json.JodaReads._ */ case class LockedTaskData( id: Long, - parent: Option[Long], + parent: Long, parentName: Option[String], startedAt: DateTime ) diff --git a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala index 696a93dd3..5afcd163d 100644 --- a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala +++ b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala @@ -166,7 +166,7 @@ class UserSavedObjectsRepository @Inject() ( this.withMRTransaction { implicit c => val parser = for { id <- get[Long]("id") - parent <- get[Option[Long]]("tasks.parent_id") + parent <- get[Long]("tasks.parent_id") parentName <- get[Option[String]]("challenges.challenge_name") lockedTime <- get[DateTime]("locked.locked_time") } yield (LockedTaskData(id, parent, parentName, lockedTime)) From 29c118c3239bc1e21d8a864903bcd9234a0fb04f Mon Sep 17 00:00:00 2001 From: CollinBeczak Date: Fri, 9 Aug 2024 16:58:10 -0500 Subject: [PATCH 6/8] add more descriptions and simplify queries and parses --- app/org/maproulette/framework/model/LockedTask.scala | 7 ++++++- .../repository/UserSavedObjectsRepository.scala | 4 ++-- conf/v2_route/user.api | 10 ++++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/org/maproulette/framework/model/LockedTask.scala b/app/org/maproulette/framework/model/LockedTask.scala index b7b99a938..03d15e4e6 100644 --- a/app/org/maproulette/framework/model/LockedTask.scala +++ b/app/org/maproulette/framework/model/LockedTask.scala @@ -11,11 +11,16 @@ import play.api.libs.json.JodaReads._ /** * Mapping of object structure for fetching task lock data + * + * id - A database assigned id for the Task + * parent - The id of the challenge of the locked task + * parentName - The name of the challenge of the locked task + * startedAt - The time that the task was locked */ case class LockedTaskData( id: Long, parent: Long, - parentName: Option[String], + parentName: String, startedAt: DateTime ) diff --git a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala index 5afcd163d..3132baf1a 100644 --- a/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala +++ b/app/org/maproulette/framework/repository/UserSavedObjectsRepository.scala @@ -167,7 +167,7 @@ class UserSavedObjectsRepository @Inject() ( val parser = for { id <- get[Long]("id") parent <- get[Long]("tasks.parent_id") - parentName <- get[Option[String]]("challenges.challenge_name") + parentName <- get[String]("challenges.challenge_name") lockedTime <- get[DateTime]("locked.locked_time") } yield (LockedTaskData(id, parent, parentName, lockedTime)) @@ -175,7 +175,7 @@ class UserSavedObjectsRepository @Inject() ( SELECT t.id, t.parent_id, l.locked_time, c.name AS challenge_name FROM tasks t INNER JOIN locked l ON t.id = l.item_id - LEFT JOIN challenges c ON t.parent_id = c.id + INNER JOIN challenges c ON t.parent_id = c.id WHERE l.user_id = {userId} LIMIT {limit} """ diff --git a/conf/v2_route/user.api b/conf/v2_route/user.api index 2eb0feaf3..8a1230a56 100644 --- a/conf/v2_route/user.api +++ b/conf/v2_route/user.api @@ -331,9 +331,11 @@ GET /user/:userId/savedTasks @org.maproulette.framework.c # schema: # type: array # items: -# $ref: '#/components/schemas/org.maproulette.framework.model.Task' +# $ref: '#/components/schemas/org.maproulette.framework.model.LockedTask' # '401': -# description: The user is not authorized to make this request +# description: The user is not authorized to make this request. +# '404': +# description: If User or Task for provided ID's is not found. # parameters: # - name: userId # in: path @@ -341,6 +343,10 @@ GET /user/:userId/savedTasks @org.maproulette.framework.c # - name: limit # in: query # description: Limit the number of results returned in the response. Default value is 50. +# required: false +# schema: +# type: integer +# default: 50 ### GET /user/:userId/lockedTasks @org.maproulette.framework.controller.UserController.getLockedTasks(userId:Long, limit:Int ?= 50) ### From 3769a7541980a45cd83e42e242cb90178aa37aa1 Mon Sep 17 00:00:00 2001 From: CollinBeczak Date: Fri, 9 Aug 2024 17:35:48 -0500 Subject: [PATCH 7/8] fix descriptions --- app/org/maproulette/framework/model/LockedTask.scala | 8 +++----- conf/v2_route/user.api | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/org/maproulette/framework/model/LockedTask.scala b/app/org/maproulette/framework/model/LockedTask.scala index 03d15e4e6..2f279d55a 100644 --- a/app/org/maproulette/framework/model/LockedTask.scala +++ b/app/org/maproulette/framework/model/LockedTask.scala @@ -11,16 +11,14 @@ import play.api.libs.json.JodaReads._ /** * Mapping of object structure for fetching task lock data - * - * id - A database assigned id for the Task - * parent - The id of the challenge of the locked task - * parentName - The name of the challenge of the locked task - * startedAt - The time that the task was locked */ case class LockedTaskData( id: Long, parent: Long, parentName: String, + /** + * The time that the task was locked + */ startedAt: DateTime ) diff --git a/conf/v2_route/user.api b/conf/v2_route/user.api index 8a1230a56..5a6668204 100644 --- a/conf/v2_route/user.api +++ b/conf/v2_route/user.api @@ -335,7 +335,7 @@ GET /user/:userId/savedTasks @org.maproulette.framework.c # '401': # description: The user is not authorized to make this request. # '404': -# description: If User or Task for provided ID's is not found. +# description: userId is not known. # parameters: # - name: userId # in: path From 2ab10c2fba1fa43d983204c73997bdbebcc86002 Mon Sep 17 00:00:00 2001 From: CollinBeczak Date: Fri, 9 Aug 2024 19:09:09 -0500 Subject: [PATCH 8/8] fix file name --- .../framework/model/{LockedTask.scala => LockedTaskData.scala} | 2 +- conf/v2_route/user.api | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/org/maproulette/framework/model/{LockedTask.scala => LockedTaskData.scala} (94%) diff --git a/app/org/maproulette/framework/model/LockedTask.scala b/app/org/maproulette/framework/model/LockedTaskData.scala similarity index 94% rename from app/org/maproulette/framework/model/LockedTask.scala rename to app/org/maproulette/framework/model/LockedTaskData.scala index 2f279d55a..1d0b3a899 100644 --- a/app/org/maproulette/framework/model/LockedTask.scala +++ b/app/org/maproulette/framework/model/LockedTaskData.scala @@ -5,7 +5,7 @@ package org.maproulette.framework.model import org.joda.time.DateTime -import play.api.libs.json.{Json, Format} +import play.api.libs.json.{Format, Json} import play.api.libs.json.JodaWrites._ import play.api.libs.json.JodaReads._ diff --git a/conf/v2_route/user.api b/conf/v2_route/user.api index 5a6668204..71a31d4a6 100644 --- a/conf/v2_route/user.api +++ b/conf/v2_route/user.api @@ -331,7 +331,7 @@ GET /user/:userId/savedTasks @org.maproulette.framework.c # schema: # type: array # items: -# $ref: '#/components/schemas/org.maproulette.framework.model.LockedTask' +# $ref: '#/components/schemas/org.maproulette.framework.model.LockedTaskData' # '401': # description: The user is not authorized to make this request. # '404':