Skip to content

Commit

Permalink
Add leaderboard for achievements.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
somiaj committed Sep 24, 2024
1 parent 87dce1d commit 27f4d91
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 0 deletions.
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
99 changes: 99 additions & 0 deletions lib/WeBWorK/ContentGenerator/Leaderboard.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
################################################################################
# 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::Leaderboard;
use Mojo::Base 'WeBWorK::ContentGenerator', -signatures;

=head1 NAME
WeBWorK::ContentGenerator::Leaderboard - 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 @allUsers = $db->getUsersWhere({ 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 (@allUsers) {
# 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 = $db->getGlobalUserAchievement($user->user_id);
next unless $globalData;

my $level = $globalData->level_achievement_id ? $db->getAchievement($globalData->level_achievement_id) : '';

my @badges;
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;
}

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

# Sort rows 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] } @rows;
for my $row (@rows) {
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 @@ -51,6 +51,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 @@ -280,10 +281,17 @@ my %routeParameters = (
},
achievements => {
title => x('Achievements'),
children => [qw(achievements_leaderboard)],
module => 'Achievements',
path => '/achievements',
unrestricted => 1
},
achievements_leaderboard => {
title => x('Leaderboard'),
module => 'Leaderboard',
path => '/leaderboard',
unrestricted => 1
},
equation_display => {
title => x('Equation Display'),
module => 'EquationDisplay',
Expand Down
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
66 changes: 66 additions & 0 deletions templates/ContentGenerator/Leaderboard.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-sm">
<thead>
<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 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) {
<a class="help-popup" role="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 %>
</a>
% }
</td>
</tr>
% }
</tbody>
</table>
27 changes: 27 additions & 0 deletions templates/HelpFiles/Leaderboard.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('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>

0 comments on commit 27f4d91

Please sign in to comment.