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 58955fc34e..bf362915be 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/AchievementsLeaderboard.pm b/lib/WeBWorK/ContentGenerator/AchievementsLeaderboard.pm new file mode 100644 index 0000000000..2c2ba971d4 --- /dev/null +++ b/lib/WeBWorK/ContentGenerator/AchievementsLeaderboard.pm @@ -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; diff --git a/lib/WeBWorK/Utils/Routes.pm b/lib/WeBWorK/Utils/Routes.pm index 9132730552..57cf125a9c 100644 --- a/lib/WeBWorK/Utils/Routes.pm +++ b/lib/WeBWorK/Utils/Routes.pm @@ -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 @@ -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', diff --git a/templates/ContentGenerator/AchievementsLeaderboard.html.ep b/templates/ContentGenerator/AchievementsLeaderboard.html.ep new file mode 100644 index 0000000000..a2ede47d6e --- /dev/null +++ b/templates/ContentGenerator/AchievementsLeaderboard.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) { + + % } +
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/HelpFiles/AchievementsLeaderboard.html.ep b/templates/HelpFiles/AchievementsLeaderboard.html.ep new file mode 100644 index 0000000000..0cf76cac48 --- /dev/null +++ b/templates/HelpFiles/AchievementsLeaderboard.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('Achievements 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.') %> +