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 5 commits into
base: develop
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions conf/defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,8 @@ $authen{admin_module} = ['WeBWorK::Authen::Basic_TheLastOption'];
view_hidden_sets => "ta",
view_answers => "ta",
view_ip_restricted_sets => "ta",
view_leaderboard => "professor",
view_leaderboard_usernames => "professor",

become_student => "professor",
access_instructor_tools => "ta",
Expand Down
20 changes: 20 additions & 0 deletions lib/WeBWorK/ConfigValues.pm
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,26 @@ sub getConfigValues ($ce) {
),
type => 'permission'
},
{
var => 'permissionLevels{view_leaderboard}',
doc => x('Allowed to view achievements leaderboard'),
doc2 => x(
'The permission level to view the achievements leaderboard, if achievements are enabled. '
. 'Consider that achievement points can be closely tied to student grades before '
. 'showing the leaderboard to students.'
),
type => 'permission'
},
{
var => 'permissionLevels{view_leaderboard_usernames}',
doc => x('Allowed to view usernames on the achievements leaderboard'),
doc2 => x(
'The permission level to view usernames on the achievements leaderboard. '
. 'Consider that achievement points can be closely tied to student grades before '
. 'showing user names to students.'
),
type => 'permission'
},
],
[
x('Problem Display/Answer Checking'),
Expand Down
111 changes: 111 additions & 0 deletions lib/WeBWorK/ContentGenerator/AchievementsLeaderboard.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
################################################################################
# WeBWorK Online Homework Delivery System
# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of either: (a) the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version, or (b) the "Artistic License" which comes with this package.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
# Artistic License for more details.
################################################################################

# Leader board for achievements.
package WeBWorK::ContentGenerator::AchievementsLeaderboard;
use Mojo::Base 'WeBWorK::ContentGenerator', -signatures;

=head1 NAME

WeBWorK::ContentGenerator::AchievementsLeaderboard - Leaderboard for achievements,
which lists the total number of achievement points, level, and badges
earned for each user with the 'include_in_stats' status.

Only users with the 'view_leaderboard' permission can see the Leaderboard, and only
users with the 'view_leaderboard_usernames' permission can see user names.

=cut

use WeBWorK::Utils qw(sortAchievements);

sub initialize ($c) {
my $db = $c->db;
my $ce = $c->ce;

# Get user Data
$c->{userName} = $c->param('user');
$c->{studentName} = $c->param('effectiveUser') // $c->{userName};

return unless $c->authz->hasPermissions($c->{userName}, 'view_leaderboard');

# Get list of all users (except set-level proctors) and achievements.
my @achievements = sortAchievements($db->getAchievementsWhere);
my %achievementsById = map { $_->achievement_id => $_ } @achievements;
my %globalUserAchievements =
map { $_->user_id => $_ } $db->getGlobalUserAchievementsWhere({ user_id => { not_like => 'set_id:%' } });

my @allBadgeIDs = $db->listAchievements;
my @allBadges = @allBadgeIDs ? sortAchievements($db->getAchievements(@allBadgeIDs)) : ();

$c->{showUserNames} = $c->authz->hasPermissions($c->{userName}, 'view_leaderboard_usernames');
$c->{showLevels} = 0; # Hide level column unless at least one user has a level achievement.

my @rows;
for my $user ($db->getUsersWhere({ user_id => { not_like => 'set_id:%' } })) {
# Only include users who can be shown in stats.
next unless $ce->status_abbrev_has_behavior($user->status, 'include_in_stats');

# Skip unless user has achievement data.
my $globalData = $globalUserAchievements{ $user->user_id };
next unless $globalData;

my $level = $globalData->level_achievement_id ? $achievementsById{ $globalData->level_achievement_id } : '';

my %userAchievements = map { $_->achievement_id => $_ } $db->getUserAchievementsWhere({
user_id => $user->user_id,
achievement_id => [ map { $_->achievement_id } grep { $_->category ne 'level' } @achievements ],
});

my @badges;
for my $achievement (@achievements) {
# Skip level achievements and only show earned achievements.
last if $achievement->category eq 'level';

push(@badges, $achievement)
if $userAchievements{ $achievement->achievement_id }
&& $achievement->enabled
&& $userAchievements{ $achievement->achievement_id }->earned;
}

push(@rows, [ $globalData->achievement_points || 0, $level, $user, \@badges ]);
}

# Sort rows descending by achievement points (or number of badges if achievement points are equal)
# then loop over them to compute rank and determine rank of effective student user.
my $rank = 0;
my $prev_points = -1;
my $skip = 1;
@rows = sort { $b->[0] <=> $a->[0] || scalar(@{ $b->[3] }) <=> scalar(@{ $a->[3] }) } @rows;
for my $row (@rows) {
# All users with an equal number of achievement points have the same rank.
if ($row->[0] == $prev_points) {
$skip++;
} else {
$rank += $skip;
$skip = 1;
}
$prev_points = $row->[0];
unshift(@$row, $rank);

$c->{showLevels} = 1 if $row->[2];
$c->{currentRank} = $rank if $c->{studentName} eq $row->[3]->user_id;
}
$c->{maxRank} = $rank;
$c->{leaderBoardRows} = \@rows;

return;
}

1;
8 changes: 8 additions & 0 deletions lib/WeBWorK/Utils/Routes.pm
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ PLEASE FOR THE LOVE OF GOD UPDATE THIS IF YOU CHANGE THE ROUTES BELOW!!!
options /$courseID/options
grades /$courseID/grades
achievements /$courseID/achievements
achievements_leaderboard /$courseID/achievements/leaderboard
equation_display /$courseID/equation
feedback /$courseID/feedback
gateway_quiz /$courseID/test_mode/$setID
Expand Down Expand Up @@ -316,10 +317,17 @@ my %routeParameters = (
},
achievements => {
title => x('Achievements'),
children => [qw(achievements_leaderboard)],
module => 'Achievements',
path => '/achievements',
unrestricted => 1
},
achievements_leaderboard => {
title => x('Achievements Leaderboard'),
module => 'AchievementsLeaderboard',
path => '/leaderboard',
unrestricted => 1
},
equation_display => {
title => x('Equation Display'),
module => 'EquationDisplay',
Expand Down
66 changes: 66 additions & 0 deletions templates/ContentGenerator/AchievementsLeaderboard.html.ep
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
% unless ($c->{leaderBoardRows}) {
<div class="alert alert-danger p-1 mb-0">
<%= maketext('Leaderboard is unavailable.') %>
</div>
% last;
% }
%
% if ($c->{currentRank}) {
<p><%= maketext('You are currently rank [_1] out of [_2].', $c->{currentRank}, $c->{maxRank}) %></p>
% }
%
<table class="table table-bordered table-hover table-sm">
<thead class="table-primary">
<tr>
<th><%= maketext('Rank') %></th>
<th><%= maketext('Points') %></th>
% if ($c->{showLevels}) {
<th><%= maketext('Level') %></th>
% }
% if ($c->{showUserNames}) {
<th><%= maketext('Name') %></th>
% }
<th><%= maketext('Badges') %></th>
</tr>
</thead>
<tbody>
% for (@{ $c->{leaderBoardRows} }) {
<tr>
% my ($rank, $points, $level, $user, $badges) = @$_;
<td><%= $rank %></td>
<td><%= $points %></td>
% if ($c->{showLevels}) {
<td class="text-center" style="white-space: nowrap;">
% if ($level) {
<%= $level->{name} %>
<br>
<%= image $level->{icon}
? "$ce->{courseURLs}{achievements}/$level->{icon}"
: "$ce->{webworkURLs}{htdocs}/images/defaulticon.png",
alt => maketext('[_1] Icon', $level->{name}),
height => 75 %>
% }
</td>
% }
% if ($c->{showUserNames}) {
<td style="white-space: nowrap;">
<%= $user->first_name %><br><%= $user->last_name %>
</td>
% }
<td>
% for my $badge (@$badges) {
<button class="btn btn-sm btn-link help-popup p-0" type="button" tabindex="0"
data-bs-placement="top" data-bs-toggle="popover" data-bs-html="true"
data-bs-content="<strong><%= $badge->{name} %></strong><br><%= $badge->{description} %>">
<%= image $badge->{icon}
? "$ce->{courseURLs}{achievements}/$badge->{icon}"
: "$ce->{webworkURLs}{htdocs}/images/defaulticon.png",
alt => $c->maketext('[_1] Icon', $badge->{name}),
width => 50 %>
</button>
% }
</td>
</tr>
% }
</tbody>
</table>
3 changes: 3 additions & 0 deletions templates/ContentGenerator/Base/links.html.ep
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@
%
% if ($ce->{achievementsEnabled}) {
<li class="list-group-item nav-item"><%= $makelink->('achievements') %></li>
% if ($authz->hasPermissions($userID, 'view_leaderboard')) {
<li class="list-group-item nav-item"><%= $makelink->('achievements_leaderboard') %></li>
% }
% }
%
% if ($authz->hasPermissions($userID, 'access_instructor_tools')) {
Expand Down
27 changes: 27 additions & 0 deletions templates/HelpFiles/AchievementsLeaderboard.html.ep
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
%################################################################################
%# WeBWorK Online Homework Delivery System
%# Copyright &copy; 2000-2024 The WeBWorK Project, https://github.com/openwebwork
%#
%# This program is free software; you can redistribute it and/or modify it under
%# the terms of either: (a) the GNU General Public License as published by the
%# Free Software Foundation; either version 2, or (at your option) any later
%# version, or (b) the "Artistic License" which comes with this package.
%#
%# This program is distributed in the hope that it will be useful, but WITHOUT
%# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
%# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
%# Artistic License for more details.
%################################################################################
%
% layout 'help_macro';
% title maketext('Achievements Leaderboard Help');
%
<p>
<%= maketext('The leaderboard orders the achievement points earned from the greatest to the least. '
. 'The rank of each user is determined by their position on the leader board. All users with the '
. 'same number of achievement points have the same rank.') %>
</p>
<p>
<%= maketext(q(Achievement badges are shown for each earned achievement. Mousing over the badge's )
. 'icon will give the name and description of the achievement.') %>
</p>