From d2141f332b4a1e8f738ce3835ab01c2a2170a77e Mon Sep 17 00:00:00 2001 From: Jaimos Skriletz Date: Tue, 24 Sep 2024 07:41:58 -0600 Subject: [PATCH] Add leaderboard for achievements. 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. --- conf/defaults.config | 2 + lib/WeBWorK/ConfigValues.pm | 20 ++++ lib/WeBWorK/ContentGenerator/Leaderboard.pm | 101 ++++++++++++++++++ lib/WeBWorK/Utils/Routes.pm | 8 ++ templates/ContentGenerator/Base/links.html.ep | 3 + .../ContentGenerator/Leaderboard.html.ep | 66 ++++++++++++ templates/HelpFiles/Leaderboard.html.ep | 27 +++++ 7 files changed, 227 insertions(+) create mode 100644 lib/WeBWorK/ContentGenerator/Leaderboard.pm create mode 100644 templates/ContentGenerator/Leaderboard.html.ep create mode 100644 templates/HelpFiles/Leaderboard.html.ep diff --git a/conf/defaults.config b/conf/defaults.config index f9087301e8..cfc22f7607 100644 --- a/conf/defaults.config +++ b/conf/defaults.config @@ -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", diff --git a/lib/WeBWorK/ConfigValues.pm b/lib/WeBWorK/ConfigValues.pm index 24cb77116c..9b8f731483 100644 --- a/lib/WeBWorK/ConfigValues.pm +++ b/lib/WeBWorK/ConfigValues.pm @@ -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'), diff --git a/lib/WeBWorK/ContentGenerator/Leaderboard.pm b/lib/WeBWorK/ContentGenerator/Leaderboard.pm new file mode 100644 index 0000000000..8feb1552df --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/Leaderboard.pm @@ -0,0 +1,101 @@ +################################################################################ +# 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 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; diff --git a/lib/WeBWorK/Utils/Routes.pm b/lib/WeBWorK/Utils/Routes.pm index 3197229372..68e9092be5 100644 --- a/lib/WeBWorK/Utils/Routes.pm +++ b/lib/WeBWorK/Utils/Routes.pm @@ -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 @@ -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', diff --git a/templates/ContentGenerator/Base/links.html.ep b/templates/ContentGenerator/Base/links.html.ep index ed64aee118..f45f2f9b2e 100644 --- a/templates/ContentGenerator/Base/links.html.ep +++ b/templates/ContentGenerator/Base/links.html.ep @@ -85,6 +85,9 @@ % % if ($ce->{achievementsEnabled}) { + % if ($authz->hasPermissions($userID, 'view_leaderboard')) { + + % } % } % % if ($authz->hasPermissions($userID, 'access_instructor_tools')) { diff --git a/templates/ContentGenerator/Leaderboard.html.ep b/templates/ContentGenerator/Leaderboard.html.ep new file mode 100644 index 0000000000..d806a75d2d --- /dev/null +++ b/templates/ContentGenerator/Leaderboard.html.ep @@ -0,0 +1,66 @@ +% unless ($c->{leaderBoardRows}) { +
+ <%= maketext('Leaderboard is unavailable.') %> +
+ % last; +% } +% +% if ($c->{currentRank}) { +

<%= maketext('You are currently rank [_1] out of [_2].', $c->{currentRank}, $c->{maxRank}) %>

+% } +% + + + + + + % if ($c->{showLevels}) { + + % } + % if ($c->{showUserNames}) { + + % } + + + + + % for (@{ $c->{leaderBoardRows} }) { + + % my ($rank, $points, $level, $user, $badges) = @$_; + + + % if ($c->{showLevels}) { + + % } + % if ($c->{showUserNames}) { + + % } + + + % } + +
<%= maketext('Rank') %><%= maketext('Points') %><%= maketext('Level') %><%= maketext('Name') %><%= maketext('Badges') %>
<%= $rank %><%= $points %> + % if ($level) { + <%= $level->{name} %> +
+ <%= image $level->{icon} + ? "$ce->{courseURLs}{achievements}/$level->{icon}" + : "$ce->{webworkURLs}{htdocs}/images/defaulticon.png", + alt => maketext('[_1] Icon', $level->{name}), + height => 75 %> + % } +
+ <%= $user->first_name %>
<%= $user->last_name %> +
+ % for my $badge (@$badges) { + + <%= image $badge->{icon} + ? "$ce->{courseURLs}{achievements}/$badge->{icon}" + : "$ce->{webworkURLs}{htdocs}/images/defaulticon.png", + alt => $c->maketext('[_1] Icon', $badge->{name}), + width => 50 %> + + % } +
diff --git a/templates/HelpFiles/Leaderboard.html.ep b/templates/HelpFiles/Leaderboard.html.ep new file mode 100644 index 0000000000..a2d8eac4a3 --- /dev/null +++ b/templates/HelpFiles/Leaderboard.html.ep @@ -0,0 +1,27 @@ +%################################################################################ +%# 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. +%################################################################################ +% +% layout 'help_macro'; +% title maketext('Leaderboard Help'); +% +

+ <%= 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.') %> +

+

+ <%= 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.') %> +