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 leaderboard for achievements. #2579

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from

Conversation

somiaj
Copy link
Contributor

@somiaj somiaj commented Sep 24, 2024

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.

@somiaj somiaj force-pushed the achievement-leader-board branch 4 times, most recently from 6ebf2c3 to 66f2ceb Compare September 29, 2024 03:37
Copy link
Member

@drgrice1 drgrice1 left a 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.

lib/WeBWorK/Utils/Routes.pm Outdated Show resolved Hide resolved
templates/HelpFiles/Leaderboard.html.ep Outdated Show resolved Hide resolved
lib/WeBWorK/ContentGenerator/Leaderboard.pm Outdated Show resolved Hide resolved

=cut

use WeBWorK::Utils qw(sortAchievements);
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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:%' } });
Copy link
Member

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.

Copy link
Contributor Author

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.

lib/WeBWorK/ContentGenerator/Leaderboard.pm Outdated Show resolved Hide resolved
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);
Copy link
Member

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

Suggested change
my $globalData = $db->getGlobalUserAchievement($user->user_id);
my $globalData = $globalUserAchievements{ $user->user_id };

lib/WeBWorK/ContentGenerator/Leaderboard.pm Outdated Show resolved Hide resolved
Comment on lines 63 to 70
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;
}
Copy link
Member

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.

Suggested change
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;
}

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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 && .....

@drgrice1
Copy link
Member

Also, to take the "Leaderboard" to "Achievement Leaderboard" comments further, I think you should rename Leaderboard.pm and both Leaderboard.html.ep files to start with "Achievement". This makes it clear that these are associated with achievements for developers. Who knows, there may be some other kind of leaderboard created for something else in the future.

@somiaj
Copy link
Contributor Author

somiaj commented Nov 14, 2024

@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.

@drgrice1
Copy link
Member

Yeah, I did note that "Achievements Leaderboard" does become the longest name in the site navigation. But it seems to still fit okay.

@somiaj
Copy link
Contributor Author

somiaj commented Nov 14, 2024

@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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants