Skip to content

Commit 6ab3acd

Browse files
authored
Add challenge and project leaderboard endpoints (#1152)
1 parent 2e5d946 commit 6ab3acd

File tree

5 files changed

+514
-4
lines changed

5 files changed

+514
-4
lines changed

app/org/maproulette/framework/controller/LeaderboardController.scala

+109
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,115 @@ class LeaderboardController @Inject() (
5252
}
5353
}
5454

55+
/**
56+
* Gets the top scoring users, based on task completion, over the given
57+
* number of months (or using start and end dates). Included with each user is their top challenges
58+
* (by amount of activity).
59+
*
60+
* @param id the ID of the challenge
61+
* @param monthDuration the number of months to consider for the leaderboard
62+
* @param limit the limit on the number of users returned
63+
* @param offset the number of users to skip before starting to return results (for pagination)
64+
* @return Top-ranked users with scores based on task completion activity
65+
*/
66+
def getChallengeLeaderboard(
67+
id: Int,
68+
monthDuration: Int,
69+
limit: Int,
70+
offset: Int
71+
): Action[AnyContent] = Action.async { implicit request =>
72+
this.sessionManager.userAwareRequest { implicit user =>
73+
Ok(Json.toJson(this.service.getChallengeLeaderboard(id, monthDuration, limit, offset)))
74+
}
75+
}
76+
77+
/**
78+
* Gets the leaderboard ranking for a user on a challenge, based on task completion, over
79+
* the given number of months (or start and end dates). Included with the user is their top challenges
80+
* (by amount of activity). Also a bracketing number of users above and below
81+
* the user in the rankings.
82+
*
83+
* @param userId user Id for user
84+
* @param bracket the number of users to return above and below the given user (0 returns just the user)
85+
* @return User with score and ranking based on task completion activity
86+
*/
87+
def getChallengeLeaderboardForUser(
88+
userId: Int,
89+
challengeId: Int,
90+
monthDuration: Int,
91+
bracket: Int
92+
): Action[AnyContent] = Action.async { implicit request =>
93+
this.sessionManager.userAwareRequest { implicit user =>
94+
SearchParameters.withSearch { implicit params =>
95+
Ok(
96+
Json.toJson(
97+
this.service.getChallengeLeaderboardForUser(
98+
userId,
99+
challengeId,
100+
monthDuration,
101+
bracket
102+
)
103+
)
104+
)
105+
}
106+
}
107+
}
108+
109+
/**
110+
* Gets the top scoring users for a specific project, based on task completion,
111+
* over the given number of months. Included with each user is their score
112+
* and ranking within the project.
113+
*
114+
* @param id the ID of the project
115+
* @param monthDuration the number of months to consider for the leaderboard
116+
* @param limit the maximum number of users to return
117+
* @param offset the number of users to skip before starting to return results (for pagination)
118+
* @return List of top-ranked users with scores and rankings for the specified project
119+
*/
120+
def getProjectLeaderboard(
121+
id: Int,
122+
monthDuration: Int,
123+
limit: Int,
124+
offset: Int
125+
): Action[AnyContent] = Action.async { implicit request =>
126+
this.sessionManager.userAwareRequest { implicit user =>
127+
Ok(Json.toJson(this.service.getProjectLeaderboard(id, monthDuration, limit, offset)))
128+
}
129+
}
130+
131+
// TODO: make this work for projects
132+
/**
133+
* Gets the leaderboard ranking for a user on a project, based on task completion, over
134+
* the given number of months (or start and end dates). Included with the user is their top challenges
135+
* (by amount of activity). Also a bracketing number of users above and below
136+
* the user in the rankings.
137+
*
138+
* @param userId user Id for user
139+
* @param bracket the number of users to return above and below the given user (0 returns just the user)
140+
* @return User with score and ranking based on task completion activity
141+
*/
142+
def getProjectLeaderboardForUser(
143+
userId: Int,
144+
projectId: Int,
145+
monthDuration: Int,
146+
bracket: Int
147+
): Action[AnyContent] = Action.async { implicit request =>
148+
this.sessionManager.userAwareRequest { implicit user =>
149+
SearchParameters.withSearch { implicit params =>
150+
Ok(
151+
Json.toJson(
152+
this.service.getChallengeLeaderboardForUser(
153+
userId,
154+
projectId,
155+
monthDuration,
156+
bracket
157+
)
158+
)
159+
)
160+
}
161+
}
162+
}
163+
55164
/**
56165
* Gets the leaderboard ranking for a user, based on task completion, over
57166
* the given number of months (or start and end dates). Included with the user is their top challenges

app/org/maproulette/framework/model/Leaderboard.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ case class LeaderboardUser(
2020
avatarURL: String,
2121
score: Int,
2222
rank: Int,
23-
completedTasks: Int,
24-
avgTimeSpent: Long,
23+
completedTasks: Option[Int],
24+
avgTimeSpent: Option[Long],
2525
created: DateTime = new DateTime(),
2626
topChallenges: List[LeaderboardChallenge],
2727
reviewsApproved: Option[Int],

app/org/maproulette/framework/repository/LeaderboardRepository.scala

+96-2
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ class LeaderboardRepository @Inject() (override val db: Database) extends Reposi
4848
get[Int]("user_score") ~
4949
get[Int]("user_ranking") ~
5050
get[DateTime]("created").? ~
51-
get[Int]("completed_tasks") ~
52-
get[Long]("avg_time_spent") ~
51+
get[Int]("completed_tasks").? ~
52+
get[Long]("avg_time_spent").? ~
5353
get[Int]("reviews_approved").? ~
5454
get[Int]("reviews_assisted").? ~
5555
get[Int]("reviews_rejected").? ~
@@ -149,6 +149,100 @@ class LeaderboardRepository @Inject() (override val db: Database) extends Reposi
149149
}
150150
}
151151

152+
/**
153+
* Queries the user_top_challenges table to retrieve leaderboard data for a specific challenge
154+
*
155+
* @param query The query object containing parameters for filtering and sorting
156+
* @return List of LeaderboardUsers representing the challenge leaderboard
157+
*/
158+
def queryChallengeLeaderboard(
159+
query: Query
160+
): List[LeaderboardUser] = {
161+
withMRConnection { implicit c =>
162+
query
163+
.build(
164+
"""
165+
SELECT
166+
utc.user_id,
167+
u.name AS user_name,
168+
u.avatar_url AS user_avatar_url,
169+
utc.activity AS user_score,
170+
ROW_NUMBER() OVER(ORDER BY utc.activity DESC) AS user_ranking
171+
FROM
172+
user_top_challenges utc
173+
JOIN
174+
users u ON u.id = utc.user_id
175+
"""
176+
)
177+
.as(this.userLeaderboardParser(fetchedUserId => List()).*)
178+
}
179+
}
180+
181+
def queryUserChallengeLeaderboardWithRank(
182+
userId: Int,
183+
query: Query,
184+
rankQuery: Query
185+
)(implicit c: Option[Connection] = None): List[LeaderboardUser] = {
186+
withMRConnection { implicit c =>
187+
query.build(s"""
188+
WITH ranked AS (
189+
SELECT
190+
utc.user_id,
191+
u.name AS user_name,
192+
u.avatar_url AS user_avatar_url,
193+
utc.activity AS user_score,
194+
ROW_NUMBER() OVER (ORDER BY utc.activity DESC) AS user_ranking
195+
FROM user_top_challenges utc
196+
JOIN users u ON u.id = utc.user_id
197+
${rankQuery.sql()}
198+
),
199+
user_rank AS (
200+
SELECT user_ranking
201+
FROM ranked
202+
WHERE user_id = ${userId}
203+
)
204+
SELECT
205+
r.user_id as user_id,
206+
r.user_name AS user_name,
207+
r.user_avatar_url AS user_avatar_url,
208+
r.user_score AS user_score,
209+
r.user_ranking AS user_ranking
210+
FROM ranked r
211+
""").as(this.userLeaderboardParser(fetchedUserId => List()).*)
212+
}
213+
}
214+
215+
/**
216+
* Queries the user_top_challenges table to retrieve leaderboard data for a specific project
217+
*
218+
* @param query The query object containing parameters for filtering and sorting
219+
* @return List of LeaderboardUsers representing the project leaderboard
220+
*/
221+
def queryProjectLeaderboard(query: Query): List[LeaderboardUser] = {
222+
withMRConnection { implicit c =>
223+
query
224+
.build(
225+
s"""
226+
SELECT
227+
u.id AS user_id,
228+
u.name AS user_name,
229+
u.avatar_url AS user_avatar_url,
230+
SUM(utc.activity) AS user_score,
231+
ROW_NUMBER() OVER (ORDER BY SUM(utc.activity) DESC) AS user_ranking
232+
FROM
233+
users u
234+
JOIN
235+
user_top_challenges utc ON u.id = utc.user_id
236+
JOIN
237+
challenges c ON c.id = utc.challenge_id
238+
JOIN
239+
projects p ON p.id = c.parent_id
240+
"""
241+
)
242+
.as(this.userLeaderboardParser(fetchedUserId => List()).*)
243+
}
244+
}
245+
152246
/**
153247
* Queries the user_leaderboard table with ranking sql
154248
*

0 commit comments

Comments
 (0)