Skip to content
Draft
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
266 changes: 264 additions & 2 deletions lib/WeBWorK/ContentGenerator/Grades.pm
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,32 @@ WeBWorK::ContentGenerator::Grades - Display statistics by user.
=cut

use WeBWorK::Utils qw(wwRound);
use WeBWorK::Utils::DateTime qw(after);
use WeBWorK::Utils::DateTime qw(after before);
use WeBWorK::Utils::JITAR qw(jitar_id_to_seq);
use WeBWorK::Utils::Sets qw(grade_set format_set_name_display);
use WeBWorK::Utils::Sets qw(grade_set format_set_name_display restricted_set_message);
use WeBWorK::Utils::ProblemProcessing qw(compute_unreduced_score);
use WeBWorK::HTML::StudentNav qw(studentNav);
use WeBWorK::Localize;

use constant TWO_DAYS => 172800;

sub initialize ($c) {
$c->{studentID} = $c->param('effectiveUser') // $c->param('user');
return;
}

sub nav ($c, $args) {
return '' unless $c->authz->hasPermissions($c->param('user'), 'become_student');

return $c->tag(
'div',
class => 'row sticky-nav',
role => 'navigation',
'aria-label' => 'student grades navigation',
studentNav($c, undef)
);
}

sub scoring_info ($c) {
my $db = $c->db;
my $ce = $c->ce;
Expand Down Expand Up @@ -450,4 +465,251 @@ sub displayStudentStats ($c, $studentID) {
);
}

# Determine if the grade can be improved by testing if the unreduced score
# less than 1 and there are more attempts available.
sub can_improve_score ($c, $set, $problem_record) {
my $unreduced_score = compute_unreduced_score($c->ce, $problem_record, $set);
return $unreduced_score < 1
&& ($problem_record->max_attempts < 0
|| $problem_record->num_correct + $problem_record->num_incorrect < $problem_record->max_attempts);
}

# Note, this is meant to be a student view. Instructors will see the same information
# as the student they are acting as. For an instructor to see hidden grades, they
# can use the student progress report in instructor tools.
sub displayStudentGrades ($c, $studentID) {
my $db = $c->db;
my $ce = $c->ce;
my $authz = $c->authz;

my $studentRecord = $db->getUser($studentID);
unless ($studentRecord) {
$c->addbadmessage($c->maketext('Record for user [_1] not found.', $studentID));
return '';
}
my $effectiveUser = $studentRecord->user_id;

my $courseName = $ce->{courseName};

# First get all merged sets for this user ordered by set_id.
my @sets = $db->getMergedSetsWhere({ user_id => $studentID }, 'set_id');
# To be able to find the set objects later, make a handy hash of set ids to set objects.
my %setsByID = (map { $_->set_id => $_ } @sets);

# Before going through the table generating loop, find all the set versions for the sets in our list.
my %setVersionsCount;
my @allSetIDs;
for my $set (@sets) {
# Don't show hidden sets.
next unless $set->visible;

my $setID = $set->set_id;

# FIXME: Here, as in many other locations, we assume that there is a one-to-one matching between versioned sets
# and gateways. We really should have two flags, $set->assignment_type and $set->versioned. I'm not adding
# that yet, however, so this will continue to use assignment_type.
if (defined $set->assignment_type && $set->assignment_type =~ /gateway/) {
# We have to have the merged set versions to know what each of their assignment types are
# (because proctoring can change this).
my @setVersions =
$db->getMergedSetVersionsWhere({ user_id => $studentID, set_id => { like => "$setID,v\%" } });

# Add the set versions to our list of sets.
$setsByID{ $_->set_id . ',v' . $_->version_id } = $_ for (@setVersions);

# Flag the existence of set versions for this set.
$setVersionsCount{$setID} = scalar @setVersions;

# Save the set names for display.
push(@allSetIDs, $setID);
push(@allSetIDs, map { $_->set_id . ',v' . $_->version_id } @setVersions);
} else {
push(@allSetIDs, $setID);
}
}

# Set groups.
my (@notOpen, @open, @reduced, @recentClosed, @closed, %allItems);

for my $setID (@allSetIDs) {
my $set = $setsByID{$setID};

# Determine if set is a test and if it is a test template or version.
my $setIsTest = defined $set->assignment_type && $set->assignment_type =~ /gateway/;
my $setIsVersioned = $setIsTest && !defined $setVersionsCount{$setID};
my $setTemplateID = $setID =~ s/,v\d+$//r;

# Initialize set item. Define link here. It will be adjusted for versioned tests later.
my $item = {
name => format_set_name_display($setTemplateID),
grade => 0,
grade_total => 0,
grade_total_right => 0,
is_test => $setIsTest,
link => $c->systemLink(
$c->url_for('problem_list', setID => $setID),
params => { effectiveUser => $effectiveUser }
)
};
$allItems{$setID} = $item;

# Determine which group to put set in. Test versions are added to test template.
unless ($setIsVersioned) {
my $enable_reduced_scoring =
$ce->{pg}{ansEvalDefaults}{enableReducedScoring}
&& $set->enable_reduced_scoring
&& $set->reduced_scoring_date;
if (before($set->open_date)) {
push(@notOpen, $item);
$item->{message} = $c->maketext('Will open on [_1].',
$c->formatDateTime($set->open_date, $ce->{studentDateDisplayFormat}));
next;
} elsif (($enable_reduced_scoring && before($set->reduced_scoring_date)) || before($set->due_date)) {
push(@open, $item);
} elsif ($enable_reduced_scoring && before($set->due_date)) {
push(@reduced, $item);
} elsif ($ce->{achievementsEnabled} && $ce->{achievementItemsEnabled} && before($set->due_date + TWO_DAYS))
{
push(@recentClosed, $item);
} else {
push(@closed, $item);
}
}

# Tests need their link updated. Along with template sets need to add a version list.
# Also determines if grade and test problems should be shown.
if ($setIsTest) {
my $act_as_student_test_url = '';
if ($set->assignment_type eq 'proctored_gateway') {
$act_as_student_test_url = $item->{link} =~ s/($courseName)\//$1\/proctored_test_mode\//r;
} else {
$act_as_student_test_url = $item->{link} =~ s/($courseName)\//$1\/test_mode\//r;
}

# If this is a template gateway set, determine if there are any versions, then move on.
unless ($setIsVersioned) {
# Remove version from set url
$item->{link} =~ s/,v\d+//;
if ($setVersionsCount{$setID}) {
$item->{versions} = [];
# Hide score initially unless there is a version the score can be seen.
$item->{hide_score} = 1;
} else {
$item->{message} = $c->maketext('No versions of this test have been taken.');
}
next;
}

# This is a versioned test, add it to the appropriate template item.
push(@{ $allItems{$setTemplateID}{versions} }, $item);
$item->{name} = $c->maketext('Version [_1]', $set->version_id);

# Only add link if the problems can be seen.
if ($set->hide_work eq 'N'
|| ($set->hide_work eq 'BeforeAnswerDate' && time >= $set->answer_date))
{
if ($set->assignment_type eq 'proctored_gateway') {
$item->{link} =~ s/($courseName)\//$1\/proctored_test_mode\//;
} else {
$item->{link} =~ s/($courseName)\//$1\/test_mode\//;
}
} else {
$item->{link} = '';
}

# If the set has hide_score set, then nothing left to do.
if (defined $set->hide_score && $set->hide_score eq 'Y'
|| ($set->hide_score eq 'BeforeAnswerDate' && time < $set->answer_date))
{
$item->{hide_score} = 1;
$item->{message} = $c->maketext('Display of scores for this test is not allowed.');
next;
}
# This is a test version, and the scores can be shown, so also show score of template set.
$allItems{$setTemplateID}{hide_score} = 0;
} else {
# For a regular set, start out assuming it is complete until a problem says otherwise.
$item->{completed} = 1;
}

my ($total_right, $total, $problem_scores, $problem_incorrect_attempts, $problem_records) =
grade_set($db, $set, $studentID, $setIsVersioned, 1);
$total_right = wwRound(2, $total_right);

# Save set grades.
$item->{grade_total} = $total;
$item->{grade_total_right} = $total_right;
$item->{grade} = 100 * wwRound(2, $total ? $total_right / $total : 0);

# Only show problem scores if allowed.
unless (defined $set->hide_score_by_problem && $set->hide_score_by_problem eq 'Y') {
$item->{problems} = [];

# Create a direct link to the problems unless the set is a test, or there is a set
# restriction preventing the student from accessing the set problems.
my $noProblemLink =
$setIsTest
|| restricted_set_message($c, $set, 'lti')
|| restricted_set_message($c, $set, 'conditional')
|| $authz->invalidIPAddress($set);

for my $i (0 .. $#$problem_scores) {
my $score = $problem_scores->[$i];
my $problem_id = $setIsVersioned ? $i + 1 : $problem_records->[$i]{problem_id};
my $problem_link =
$noProblemLink
? ''
: $c->systemLink($c->url_for('problem_detail', setID => $setID, problemID => $problem_id),
params => { effectiveUser => $effectiveUser });
$score = 0 unless $score =~ /^\d+$/;
# For jitar sets we only display grades for top level problems.
if ($set->assignment_type eq 'jitar') {
my @seq = jitar_id_to_seq($problem_id);
if ($#seq == 0) {
push(@{ $item->{problems} }, { id => $seq[0], score => $score, link => $problem_link });
$item->{completed} = 0 if $c->can_improve_score($set, $problem_records->[$i]);
}
} else {
push(@{ $item->{problems} }, { id => $problem_id, score => $score, link => $problem_link });
$item->{completed} = 0 if !$setIsTest && $c->can_improve_score($set, $problem_records->[$i]);
}
}
}

# If this is a test version, update template set to the best grade a student hand.
if ($setIsVersioned) {
# Compare the score to the template set and update as needed.
my $templateItem = $allItems{$setTemplateID};
if ($item->{grade} > $templateItem->{grade}) {
for ('grade', 'grade_total', 'grade_total_right') {
$templateItem->{$_} = $item->{$_};
}
}
}
}

# Compute total course grade if requested.
my $courseTotal = 0;
my $totalRight = 0;
if ($ce->{showCourseHomeworkTotals}) {
for (@open, @reduced, @recentClosed, @closed) {
$courseTotal += $_->{grade_total};
$totalRight += $_->{grade_total_right};
}
}

return $c->include(
'ContentGenerator/Grades/student_grades',
effectiveUser => $effectiveUser,
fullName => join(' ', $studentRecord->first_name, $studentRecord->last_name),
notOpen => \@notOpen,
open => \@open,
reduced => \@reduced,
recentClosed => \@recentClosed,
closed => \@closed,
courseTotal => $courseTotal,
totalRight => $totalRight
);
}

1;
8 changes: 6 additions & 2 deletions lib/WeBWorK/HTML/StudentNav.pm
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ sub studentNav ($c, $setID) {
return '' unless $c->authz->hasPermissions($userID, 'become_student');

# Find all users for the given set (except the current user) sorted by last_name, then first_name, then user_id.
# If $setID is undefined, list all users except the current user instead.
my @allUserRecords = $c->db->getUsersWhere(
{
user_id =>
[ map { $_->[0] } $c->db->listUserSetsWhere({ set_id => $setID, user_id => { '!=' => $userID } }) ]
user_id => [
map { $_->[0] } $c->db->listUserSetsWhere(
{ defined $setID ? (set_id => $setID) : (), user_id => { '!=' => $userID } }
)
]
},
[qw/last_name first_name user_id/]
);
Expand Down
9 changes: 7 additions & 2 deletions templates/ContentGenerator/Grades.html.ep
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
<%= $c->displayStudentStats($c->{studentID}) =%>
<%= $c->scoring_info =%>
<%= $c->displayStudentGrades($c->{studentID}) =%>
%
% my $scoring_info = $c->scoring_info;
% if ($scoring_info) {
<h2><%= maketext('Additional Grade Information') %></h2>
<%= $scoring_info =%>
% }
54 changes: 54 additions & 0 deletions templates/ContentGenerator/Grades/grade_items.html.ep
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<ul class="list-group">
% my $n = 0;
% for my $item (@$items) {
% $n++;
<li class="list-group-item d-flex align-items-center justify-content-between">
% if ($item->{hide_score}) {
<div style="width: 50px;"></div>
% } else {
% my $class = '';
% my $data = '';
% if ($showCompleted && defined $item->{completed}) {
% $data = ' data-bs-placement="top" data-bs-toggle="tooltip" ';
% $class = ' set-id-tooltip text-white rounded-3 ';
% if ($item->{completed}) {
% $class .= 'bg-success';
% $data .= 'data-bs-title="'
% . maketext('This assignment is complete and the grade can no longer be improved.')
% . '"';
% } else {
% $class .= 'bg-info';
% $data .= 'data-bs-title="'
% . maketext('This assignment is not complete and the grade could be improved.')
% . '"';
% }
% }
<div class="fw-bold font-lg text-center<%= $class %> p-2" style="width: 50px;"<%== $data =%>>
<%= $item->{grade} %>%
</div>
% }
<div class="ms-3 me-auto table-responsive">
<div>
% if ($item->{link}) {
<%= link_to $item->{name} => $item->{link}, class => "fw-bold" %>
% } else {
<span class="fw-bold"><%= $item->{name} %></span>
% }
</div>
% if ($item->{message}) {
<div><em><%= $item->{message} %></em></div>
% }
% if ($item->{problems}) {
<%= include 'ContentGenerator/Grades/problem_table',
problems => $item->{problems},
set_name => $item->{name},
total => $item->{grade_total},
total_right => $item->{grade_total_right} %>
% } elsif ($item->{versions}) {
<%= include 'ContentGenerator/Grades/version_list', versions => $item->{versions} %>
% }
</div>
</li>
% }
</ul>

Loading