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

add challenge and project leaderboard endpoints #1152

Merged
merged 5 commits into from
Oct 14, 2024
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
109 changes: 109 additions & 0 deletions app/org/maproulette/framework/controller/LeaderboardController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,115 @@ class LeaderboardController @Inject() (
}
}

/**
* Gets the top scoring users, based on task completion, over the given
* number of months (or using start and end dates). Included with each user is their top challenges
* (by amount of activity).
*
* @param id the ID of the challenge
* @param monthDuration the number of months to consider for the leaderboard
* @param limit the limit on the number of users returned
* @param offset the number of users to skip before starting to return results (for pagination)
* @return Top-ranked users with scores based on task completion activity
*/
def getChallengeLeaderboard(
id: Int,
monthDuration: Int,
limit: Int,
offset: Int
): Action[AnyContent] = Action.async { implicit request =>
this.sessionManager.userAwareRequest { implicit user =>
Ok(Json.toJson(this.service.getChallengeLeaderboard(id, monthDuration, limit, offset)))
}
}

/**
* 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
* and ranking within the project.
*
* @param id the ID of the project
* @param monthDuration the number of months to consider for the leaderboard
* @param limit the maximum number of users to return
* @param offset the number of users to skip before starting to return results (for pagination)
* @return List of top-ranked users with scores and rankings for the specified project
*/
def getProjectLeaderboard(
id: Int,
monthDuration: Int,
limit: Int,
offset: Int
): Action[AnyContent] = Action.async { implicit request =>
this.sessionManager.userAwareRequest { implicit user =>
Ok(Json.toJson(this.service.getProjectLeaderboard(id, monthDuration, limit, offset)))
}
}

// 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
4 changes: 2 additions & 2 deletions app/org/maproulette/framework/model/Leaderboard.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ case class LeaderboardUser(
avatarURL: String,
score: Int,
rank: Int,
completedTasks: Int,
avgTimeSpent: Long,
completedTasks: Option[Int],
avgTimeSpent: Option[Long],
created: DateTime = new DateTime(),
topChallenges: List[LeaderboardChallenge],
reviewsApproved: Option[Int],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ class LeaderboardRepository @Inject() (override val db: Database) extends Reposi
get[Int]("user_score") ~
get[Int]("user_ranking") ~
get[DateTime]("created").? ~
get[Int]("completed_tasks") ~
get[Long]("avg_time_spent") ~
get[Int]("completed_tasks").? ~
get[Long]("avg_time_spent").? ~
get[Int]("reviews_approved").? ~
get[Int]("reviews_assisted").? ~
get[Int]("reviews_rejected").? ~
Expand Down Expand Up @@ -149,6 +149,100 @@ class LeaderboardRepository @Inject() (override val db: Database) extends Reposi
}
}

/**
* Queries the user_top_challenges table to retrieve leaderboard data for a specific challenge
*
* @param query The query object containing parameters for filtering and sorting
* @return List of LeaderboardUsers representing the challenge leaderboard
*/
def queryChallengeLeaderboard(
query: Query
): List[LeaderboardUser] = {
withMRConnection { implicit c =>
query
.build(
"""
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
"""
)
.as(this.userLeaderboardParser(fetchedUserId => List()).*)
}
}

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
*
* @param query The query object containing parameters for filtering and sorting
* @return List of LeaderboardUsers representing the project leaderboard
*/
def queryProjectLeaderboard(query: Query): List[LeaderboardUser] = {
withMRConnection { implicit c =>
query
.build(
s"""
SELECT
u.id AS user_id,
u.name AS user_name,
u.avatar_url AS user_avatar_url,
SUM(utc.activity) AS user_score,
ROW_NUMBER() OVER (ORDER BY SUM(utc.activity) DESC) AS user_ranking
FROM
users u
JOIN
user_top_challenges utc ON u.id = utc.user_id
JOIN
challenges c ON c.id = utc.challenge_id
JOIN
projects p ON p.id = c.parent_id
"""
)
.as(this.userLeaderboardParser(fetchedUserId => List()).*)
}
}

/**
* Queries the user_leaderboard table with ranking sql
*
Expand Down
Loading