-
-
Notifications
You must be signed in to change notification settings - Fork 165
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 leaderboard for achievements. #2579
base: develop
Are you sure you want to change the base?
Conversation
6ebf2c3
to
66f2ceb
Compare
66f2ceb
to
d2141f3
Compare
d2141f3
to
95843d5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are several changes that are needed here. I also suggested some changes that should improve loading the needed data from the database some.
|
||
=cut | ||
|
||
use WeBWorK::Utils qw(sortAchievements); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This imported method is not used in this file. Although it probably should be. The achievements should be sorted for the display of badges so that they are shown in the same order that they are shown on the achievements page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was used on line 46 below, I think you found it, but might have not removed this comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was no usage of the sortAchievements
method in the code that I was looking at.
return unless $c->authz->hasPermissions($c->{userName}, 'view_leaderboard'); | ||
|
||
# Get list of all users (except set-level proctors) and achievements. | ||
my @allUsers = $db->getUsersWhere({ user_id => { not_like => 'set_id:%' } }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no need for all
in @allUsers
. Just call the variable @users
as is typically done elsewhere for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems I only used this in one place too, I will just call the database there in the start of the for loop instead of storing it in a variable.
next unless $ce->status_abbrev_has_behavior($user->status, 'include_in_stats'); | ||
|
||
# Skip unless user has achievement data. | ||
my $globalData = $db->getGlobalUserAchievement($user->user_id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where the %globalUserAchievements
from my last comment comes in. You are getting the global user achievements for all users in this loop with a separate database query. That is very expensive. You can just get all global user achievements as in my last comment, and then look them up here with
my $globalData = $db->getGlobalUserAchievement($user->user_id); | |
my $globalData = $globalUserAchievements{ $user->user_id }; |
for my $badge (@allBadges) { | ||
# Skip level achievements and only show earned achievements. | ||
last if $badge->category eq 'level'; | ||
next unless $db->existsUserAchievement($user->user_id, $badge->achievement_id); | ||
|
||
my $userBadge = $db->getUserAchievement($user->user_id, $badge->achievement_id); | ||
push(@badges, $badge) if $badge->enabled && $userBadge->earned; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no need to call existsUserAchievement
and then getUserAchievement
. In fact it is more expensive to do that since it takes two database queries, than to just call getUserAchievement
and check if that is truthy which means that the achievement exists.
So change this as suggested below. This also incorporates the other suggested variable changes. I left the local @badges
for now, but those are also not badges. They are user achievement database records.
Note that this still leaves the getUserAchievement
database call in the for loop. You could get all user achievements before the for loop and then just look them up here, but there is a trade off in this case that didn't occur in the others. That is that in this case you don't actually need all of the user achievements. You only need those that aren't of the 'level' category. So there is the trade off of loading unneeded data into memory versus the inefficiency of querying the database for every user and non-level achievement. It would be possible to go to a lower level in the database layer and use a join on the achievement and achievement_user tables to accomplish this, but that is messy to do here and I guess it is okay to leave this one database query repeated in the loop for now. It is still expensive to do this though.
for my $badge (@allBadges) { | |
# Skip level achievements and only show earned achievements. | |
last if $badge->category eq 'level'; | |
next unless $db->existsUserAchievement($user->user_id, $badge->achievement_id); | |
my $userBadge = $db->getUserAchievement($user->user_id, $badge->achievement_id); | |
push(@badges, $badge) if $badge->enabled && $userBadge->earned; | |
} | |
for my $achievement (@achievements) { | |
# Skip level achievements and only show earned achievements. | |
last if $achievement->category eq 'level'; | |
my $userBadge = $db->getUserAchievement($user->user_id, $achievement->achievement_id); | |
next unless $userBadge; | |
push(@badges, $achievement) if $achievement->enabled && $userBadge->earned; | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The next unless $userBadge
line doesn't seem needed here, I just put the check in the next if statement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The next unless $userBadge
is needed. If the achievement has not been assigned to a user, then it will be undefined, and then the $userBadge->earned
will cause an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is, I put the check in the next if statement, push(@badges, $achievement) if $userBadge && ....
.
Also, to take the "Leaderboard" to "Achievement Leaderboard" comments further, I think you should rename |
@drgrice1 thanks for your suggestions. I'll add them later. I did initially call this the "Achievement Leaderboard", but felt the name looked to long in the menu, and then I put "Leaderboard" indented under the "Achievements" link, but felt the offset looked odd, and ended up just calling it "Leaderboard" with no offset. I'll go change the name back, just sharing my thought process. |
Yeah, I did note that "Achievements Leaderboard" does become the longest name in the site navigation. But it seems to still fit okay. |
95843d5
to
1ce79e9
Compare
@drgrice1 thanks again, your suggestions improve load speed quite a bit here. Still takes a couple of seconds, but 3 seconds is faster than the 15 or so seconds I was getting. |
This adds a leaderboard for achievements, which ranks users from the greatest to the least number of achievement points along with showing the badges of all earned achievements. The default use of this is to provide a summary page for professors to see how many achievement points students have earned along with which badges they have earned. The default permission level to view the leaderboard and see usernames on the leaderboard is professor. The permission level for viewing the leaderboard and viewing names on the leaderboard can be changed under course configuration to allow students to see the leaderboard. It is noted that since achievement points are often closely related to grades, that this should be considered before allowing students access.
Rename "Leaderboard" to "AchievementsLeaderboard". Implement the code and database improvements to make the page load faster.
1ce79e9
to
30586c1
Compare
This adds a leaderboard for achievements, which ranks users from the greatest to the least number of achievement points along with showing the badges of all earned achievements.
The default use of this is to provide a summary page for professors to see how many achievement points students have earned along with which badges they have earned. The default permission level to view the leaderboard and see usernames on the leaderboard is professor.
The permission level for viewing the leaderboard and viewing names on the leaderboard can be changed under course configuration to allow students to see the leaderboard. It is noted that since achievement points are often closely related to grades, that this should be considered before allowing students access.
Note, this is currently a little slow since it loops over all students, and for each student loops over all achievements making multiple database calls to get the user achievement records. In my class of 40 students with 80 achievements, it takes about 5 seconds to generate the page, and could be slower for much larger classes. I would like to speed this up, but unsure on how to more efficiently make database calls to get all the achievement data needed to build the leaderboard.