Skip to content

Commit

Permalink
add user leaderboards for challenges
Browse files Browse the repository at this point in the history
  • Loading branch information
jschwarz2030 committed Oct 3, 2024
1 parent 8c589f4 commit 3550a91
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,38 @@ class LeaderboardController @Inject() (
}
}

/**
* Gets the leaderboard ranking for a user on a challenge, based on task completion, over
* the given number of months (or start and end dates). Included with the user is their top challenges
* (by amount of activity). Also a bracketing number of users above and below
* the user in the rankings.
*
* @param userId user Id for user
* @param bracket the number of users to return above and below the given user (0 returns just the user)
* @return User with score and ranking based on task completion activity
*/
def getChallengeLeaderboardForUser(
userId: Int,
challengeId: Int,
monthDuration: Int,
bracket: Int
): Action[AnyContent] = Action.async { implicit request =>
this.sessionManager.userAwareRequest { implicit user =>
SearchParameters.withSearch { implicit params =>
Ok(
Json.toJson(
this.service.getChallengeLeaderboardForUser(
userId,
challengeId,
monthDuration,
bracket
)
)
)
}
}
}

/**
* Gets the top scoring users for a specific project, based on task completion,
* over the given number of months. Included with each user is their score
Expand All @@ -96,6 +128,39 @@ class LeaderboardController @Inject() (
}
}

// TODO: make this work for projects
/**
* Gets the leaderboard ranking for a user on a project, based on task completion, over
* the given number of months (or start and end dates). Included with the user is their top challenges
* (by amount of activity). Also a bracketing number of users above and below
* the user in the rankings.
*
* @param userId user Id for user
* @param bracket the number of users to return above and below the given user (0 returns just the user)
* @return User with score and ranking based on task completion activity
*/
def getProjectLeaderboardForUser(
userId: Int,
projectId: Int,
monthDuration: Int,
bracket: Int
): Action[AnyContent] = Action.async { implicit request =>
this.sessionManager.userAwareRequest { implicit user =>
SearchParameters.withSearch { implicit params =>
Ok(
Json.toJson(
this.service.getChallengeLeaderboardForUser(
userId,
projectId,
monthDuration,
bracket
)
)
)
}
}
}

/**
* Gets the leaderboard ranking for a user, based on task completion, over
* the given number of months (or start and end dates). Included with the user is their top challenges
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,40 @@ class LeaderboardRepository @Inject() (override val db: Database) extends Reposi
}
}

def queryUserChallengeLeaderboardWithRank(
userId: Int,
query: Query,
rankQuery: Query
)(implicit c: Option[Connection] = None): List[LeaderboardUser] = {
withMRConnection { implicit c =>
query.build(s"""
WITH ranked AS (
SELECT
utc.user_id,
u.name AS user_name,
u.avatar_url AS user_avatar_url,
utc.activity AS user_score,
ROW_NUMBER() OVER (ORDER BY utc.activity DESC) AS user_ranking
FROM user_top_challenges utc
JOIN users u ON u.id = utc.user_id
${rankQuery.sql()}
),
user_rank AS (
SELECT user_ranking
FROM ranked
WHERE user_id = ${userId}
)
SELECT
r.user_id as user_id,
r.user_name AS user_name,
r.user_avatar_url AS user_avatar_url,
r.user_score AS user_score,
r.user_ranking AS user_ranking
FROM ranked r
""").as(this.userLeaderboardParser(fetchedUserId => List()).*)
}
}

/**
* Queries the user_top_challenges table to retrieve leaderboard data for a specific project
*
Expand Down
65 changes: 65 additions & 0 deletions app/org/maproulette/framework/service/LeaderboardService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,71 @@ class LeaderboardService @Inject() (
return result
}

/**
* Gets leaderboard rank for a user based on task completion activity
* over the given period in a challenge. Scoring for each completed task is based on status
* assigned to the task (status point values are configurable). Also included
* is the user's top challenges (by amount of activity).
*
* @param userId user id
* @param params SearchLeaderboardParameters
* @param onlyEnabled only enabled in user top challenges (doesn't affect scoring)
* @return Returns leaderboard for user with score
*/
def getChallengeLeaderboardForUser(
userId: Int,
challengeId: Int,
monthDuration: Int = 1,
bracket: Int = 0
): List[LeaderboardUser] = {
// The userId must exist and must not be a system user, otherwise return NotFound (http 404).
if (userId <= 0 || this.userService.retrieve(userId).isEmpty) {
throw new NotFoundException(s"No user found with id $userId")
}
val result = this.repository.queryUserChallengeLeaderboardWithRank(
userId,
Query.simple(
List(),
finalClause =
s"""JOIN user_rank ur ON r.user_ranking BETWEEN (ur.user_ranking - ${bracket}) AND (ur.user_ranking + ${bracket});"""
),
Query.simple(
List(
BaseParameter(
"user_id",
userId,
Operator.EQ,
useValueDirectly = true,
table = Some("utc")
),
BaseParameter(
"challenge_id",
challengeId,
Operator.EQ,
useValueDirectly = true,
table = Some("utc")
),
BaseParameter(
"month_duration",
monthDuration,
Operator.EQ,
useValueDirectly = true,
table = Some("utc")
),
BaseParameter(
"country_code",
None,
Operator.NULL,
useValueDirectly = true,
table = Some("utc")
)
)
)
)

result
}

def getProjectLeaderboard(
projectId: Int,
monthDuration: Int = 1,
Expand Down
70 changes: 70 additions & 0 deletions conf/v2_route/leaderboard.api
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,41 @@ GET /data/user/leaderboard @org.maproulette.framework.c
###
GET /data/user/challengeLeaderboard @org.maproulette.framework.controller.LeaderboardController.getChallengeLeaderboard(challengeId:Int, monthDuration:Int ?= 1, limit:Int ?= 20, offset:Int ?= 0)
###
# summary: Fetches leaderboard stats with ranking for the user for a challenge
# description: Fetches user's current ranking and stats in the leaderboard along with a number of mappers above and below in the rankings.
# responses:
# '200':
# description: List of leaderboard stats
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/org.maproulette.framework.model.LeaderboardUser'
# '404':
# description: User not found
# parameters:
# - name: userId
# in: path
# description: User id to fetch ranking for.
# schema:
# type: integer
# - name: challengeId
# in: query
# description: The challenge id to search by
# schema:
# type: integer
# - name: monthDuration
# in: query
# description: The optional number of past months to search by (with 0 as current month and -1 as all time)
# schema:
# type: integer
# - name: bracket
# in: query
# description: How many results before and after the found user to return
# schema:
# type: integer
###
GET /data/user/:userId/challengeLeaderboard @org.maproulette.framework.controller.LeaderboardController.getChallengeLeaderboardForUser(userId:Int, challengeId:Int, monthDuration: Int ?= 1, bracket:Int ?= 0)
###
# tags: [ Leaderboard ]
# summary: Fetches leaderboard for a specific project
# description: Fetches the top mappers for a specific project within a time period
Expand Down Expand Up @@ -135,6 +170,41 @@ GET /data/user/challengeLeaderboard @org.maproulette.framewor
###
GET /data/user/projectLeaderboard @org.maproulette.framework.controller.LeaderboardController.getProjectLeaderboard(projectId:Int, monthDuration:Int ?= 1, limit:Int ?= 20, offset:Int ?= 0)
###
# summary: Fetches leaderboard stats with ranking for the user for a project
# description: Fetches user's current ranking and stats in the leaderboard along with a number of mappers above and below in the rankings.
# responses:
# '200':
# description: List of leaderboard stats
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/org.maproulette.framework.model.LeaderboardUser'
# '404':
# description: User not found
# parameters:
# - name: userId
# in: path
# description: User id to fetch ranking for.
# schema:
# type: integer
# - name: projectId
# in: query
# description: The project id to search by
# schema:
# type: integer
# - name: monthDuration
# in: query
# description: The optional number of past months to search by (with 0 as current month and -1 as all time)
# schema:
# type: integer
# - name: bracket
# in: query
# description: How many results before and after the found user to return
# schema:
# type: integer
###
GET /data/user/:userId/projectLeaderboard @org.maproulette.framework.controller.LeaderboardController.getProjectLeaderboardForUser(userId:Int, projectId:Int, monthDuration: Int ?= 1, bracket:Int ?= 0)
###
# tags: [ Leaderboard ]
# summary: Fetches leaderboard stats with ranking for the user
# description: Fetches user's current ranking and stats in the leaderboard along with a number of mappers above and below in the rankings.
Expand Down

0 comments on commit 3550a91

Please sign in to comment.