Skip to content

Commit 930e69f

Browse files
committed
Initial work on new student grades page.
This replaces the Grades page for students with a new layout designed for students. The old grades table is still available to instructors under "Student Progress". Being a grade page for students, instructors see the same info a student would (no hidden sets or grades are shown for instructors acting as a student). The only difference for an instructor is the student navigation menu is shown to switch which student to act as. The assignments are split into categories. Open assignments, reduced scoring assignments (if reduced scoring is enabled), recently closed assignments (if achievement items are enabled and these are assignments closed less than two days ago in which an extension item could be used on), and closed assignments. Currently assignments are all ordered alphabetically in each category (this could be changed by sorting the list, but has not been done at this time). The total grade, if configured to be shown, is shown at the top of the page for all sets that are past the open date. All open, reduced scoring, and recently closed assignments have their grade marked as either complete, the grade can no longer be improved due to no more attempts left or the student has answered all the questions correctly, or incomplete. This is so students can identify which assignments they can improve the grade or use an achievement item on to improve the grade if recently closed. Currently tests do not show this information (this could be added). Each assignment is a list item which shows the total score (for tests it shows the best test version score if the student can see the score). Then for assignments it shows a table which includes the total score and status for each problem in the set. For just in time, only top level problems are shown. For tests, each test version is shown, and then each test version has a table showing the score and status of each problem. This is only the initial work to create a new grades page, there is still work to do in terms of formatting of the page, what is shown, ordering of the page, and anything else that comes up during review of the new page.
1 parent 42893ad commit 930e69f

8 files changed

Lines changed: 448 additions & 10 deletions

File tree

lib/WeBWorK/ContentGenerator/Grades.pm

Lines changed: 252 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,32 @@ WeBWorK::ContentGenerator::Grades - Display statistics by user.
88
=cut
99

1010
use WeBWorK::Utils qw(wwRound);
11-
use WeBWorK::Utils::DateTime qw(after);
11+
use WeBWorK::Utils::DateTime qw(after before);
1212
use WeBWorK::Utils::JITAR qw(jitar_id_to_seq);
1313
use WeBWorK::Utils::Sets qw(grade_set format_set_name_display);
1414
use WeBWorK::Utils::ProblemProcessing qw(compute_unreduced_score);
15+
use WeBWorK::HTML::StudentNav qw(studentNav);
1516
use WeBWorK::Localize;
1617

18+
use constant TWO_DAYS => 172800;
19+
1720
sub initialize ($c) {
1821
$c->{studentID} = $c->param('effectiveUser') // $c->param('user');
1922
return;
2023
}
2124

25+
sub nav ($c, $args) {
26+
return '' unless $c->authz->hasPermissions($c->param('user'), 'become_student');
27+
28+
return $c->tag(
29+
'div',
30+
class => 'row sticky-nav',
31+
role => 'navigation',
32+
'aria-label' => 'student grades navigation',
33+
studentNav($c, undef)
34+
);
35+
}
36+
2237
sub scoring_info ($c) {
2338
my $db = $c->db;
2439
my $ce = $c->ce;
@@ -450,4 +465,240 @@ sub displayStudentStats ($c, $studentID) {
450465
);
451466
}
452467

468+
# Determine if the grade can be improved by testing if the unreduced score
469+
# less than 1 and there are more attempts available.
470+
sub can_improve_score ($c, $set, $problem_record) {
471+
my $unreduced_score = compute_unreduced_score($c->ce, $problem_record, $set);
472+
return $unreduced_score < 1
473+
&& ($problem_record->max_attempts < 0
474+
|| $problem_record->num_correct + $problem_record->num_incorrect < $problem_record->max_attempts);
475+
}
476+
477+
# Note, this is meant to be a student view. Instructors will see the same information
478+
# as the student they are acting as. For an instructor to see hidden grades, they
479+
# can use the student progress report in instructor tools.
480+
sub displayStudentGrades ($c, $studentID) {
481+
my $db = $c->db;
482+
my $ce = $c->ce;
483+
my $authz = $c->authz;
484+
485+
my $studentRecord = $db->getUser($studentID);
486+
unless ($studentRecord) {
487+
$c->addbadmessage($c->maketext('Record for user [_1] not found.', $studentID));
488+
return '';
489+
}
490+
my $effectiveUser = $studentRecord->user_id;
491+
492+
my $courseName = $ce->{courseName};
493+
494+
# First get all merged sets for this user ordered by set_id.
495+
my @sets = $db->getMergedSetsWhere({ user_id => $studentID }, 'set_id');
496+
# To be able to find the set objects later, make a handy hash of set ids to set objects.
497+
my %setsByID = (map { $_->set_id => $_ } @sets);
498+
499+
# Before going through the table generating loop, find all the set versions for the sets in our list.
500+
my %setVersionsCount;
501+
my @allSetIDs;
502+
for my $set (@sets) {
503+
# Don't show hidden sets.
504+
next unless $set->visible;
505+
506+
my $setID = $set->set_id;
507+
508+
# FIXME: Here, as in many other locations, we assume that there is a one-to-one matching between versioned sets
509+
# and gateways. We really should have two flags, $set->assignment_type and $set->versioned. I'm not adding
510+
# that yet, however, so this will continue to use assignment_type.
511+
if (defined $set->assignment_type && $set->assignment_type =~ /gateway/) {
512+
# We have to have the merged set versions to know what each of their assignment types are
513+
# (because proctoring can change this).
514+
my @setVersions =
515+
$db->getMergedSetVersionsWhere({ user_id => $studentID, set_id => { like => "$setID,v\%" } });
516+
517+
# Add the set versions to our list of sets.
518+
$setsByID{ $_->set_id . ',v' . $_->version_id } = $_ for (@setVersions);
519+
520+
# Flag the existence of set versions for this set.
521+
$setVersionsCount{$setID} = scalar @setVersions;
522+
523+
# Save the set names for display.
524+
push(@allSetIDs, $setID);
525+
push(@allSetIDs, map { $_->set_id . ',v' . $_->version_id } @setVersions);
526+
} else {
527+
push(@allSetIDs, $setID);
528+
}
529+
}
530+
531+
# Set groups.
532+
my (@notOpen, @open, @reduced, @recentClosed, @closed, %allItems);
533+
534+
for my $setID (@allSetIDs) {
535+
my $set = $setsByID{$setID};
536+
537+
# Determine if set is a test and if it is a test template or version.
538+
my $setIsTest = defined $set->assignment_type && $set->assignment_type =~ /gateway/;
539+
my $setIsVersioned = $setIsTest && !defined $setVersionsCount{$setID};
540+
my $setTemplateID = $setID =~ s/,v\d+$//r;
541+
542+
# Initialize set item. Define link here. It will be adjusted for versioned tests later.
543+
my $item = {
544+
name => format_set_name_display($setTemplateID),
545+
grade => 0,
546+
grade_total => 0,
547+
grade_total_right => 0,
548+
is_test => $setIsTest
549+
};
550+
$allItems{$setID} = $item;
551+
$item->{link} =
552+
$c->systemLink($c->url_for('problem_list', setID => $setID), params => { effectiveUser => $effectiveUser });
553+
554+
# Determine which group to put set in. Test versions are added to test template.
555+
unless ($setIsVersioned) {
556+
my $enable_reduced_scoring =
557+
$ce->{pg}{ansEvalDefaults}{enableReducedScoring}
558+
&& $set->enable_reduced_scoring
559+
&& $set->reduced_scoring_date;
560+
if (before($set->open_date)) {
561+
push(@notOpen, $item);
562+
$item->{message} = $c->maketext('Will open on [_1].',
563+
$c->formatDateTime($set->open_date, $ce->{studentDateDisplayFormat}));
564+
next;
565+
} elsif (($enable_reduced_scoring && before($set->reduced_scoring_date)) || before($set->due_date)) {
566+
push(@open, $item);
567+
} elsif ($enable_reduced_scoring && before($set->due_date)) {
568+
push(@reduced, $item);
569+
} elsif ($ce->{achievementsEnabled} && $ce->{achievementItemsEnabled} && before($set->due_date + TWO_DAYS))
570+
{
571+
push(@recentClosed, $item);
572+
} else {
573+
push(@closed, $item);
574+
}
575+
}
576+
577+
# Tests need their link updated. Along with template sets need to add a version list.
578+
# Also determines if grade and test problems should be shown.
579+
if ($setIsTest) {
580+
my $act_as_student_test_url = '';
581+
if ($set->assignment_type eq 'proctored_gateway') {
582+
$act_as_student_test_url = $item->{link} =~ s/($courseName)\//$1\/proctored_test_mode\//r;
583+
} else {
584+
$act_as_student_test_url = $item->{link} =~ s/($courseName)\//$1\/test_mode\//r;
585+
}
586+
587+
# If this is a template gateway set, determine if there are any versions, then move on.
588+
unless ($setIsVersioned) {
589+
# Remove version from set url
590+
$item->{link} =~ s/,v\d+//;
591+
if ($setVersionsCount{$setID}) {
592+
$item->{versions} = [];
593+
# Hide score initially unless there is a version the score can be seen.
594+
$item->{hide_score} = 1;
595+
} else {
596+
$item->{message} = $c->maketext('No versions of this test have been taken.');
597+
}
598+
next;
599+
}
600+
601+
# This is a versioned test, add it to the appropriate template item.
602+
push(@{ $allItems{$setTemplateID}{versions} }, $item);
603+
$item->{name} = $c->maketext('Version [_1]', $set->version_id);
604+
605+
# Only add link if the problems can be seen.
606+
if ($set->hide_work eq 'N'
607+
|| ($set->hide_work eq 'BeforeAnswerDate' && time >= $set->answer_date))
608+
{
609+
if ($set->assignment_type eq 'proctored_gateway') {
610+
$item->{link} =~ s/($courseName)\//$1\/proctored_test_mode\//;
611+
} else {
612+
$item->{link} =~ s/($courseName)\//$1\/test_mode\//;
613+
}
614+
} else {
615+
$item->{link} = '';
616+
}
617+
618+
# If the set has hide_score set, then nothing left to do.
619+
if (defined $set->hide_score && $set->hide_score eq 'Y'
620+
|| ($set->hide_score eq 'BeforeAnswerDate' && time < $set->answer_date))
621+
{
622+
$item->{hide_score} = 1;
623+
$item->{message} = $c->maketext('Display of scores for this test is not allowed.');
624+
next;
625+
}
626+
# This is a test version, and the scores can be shown, so also show score of template set.
627+
$allItems{$setTemplateID}{hide_score} = 0;
628+
} else {
629+
# For a regular set, start out assuming it is complete until a problem says otherwise.
630+
$item->{completed} = 1;
631+
}
632+
633+
my ($total_right, $total, $problem_scores, $problem_incorrect_attempts, $problem_records) =
634+
grade_set($db, $set, $studentID, $setIsVersioned, 1);
635+
$total_right = wwRound(2, $total_right);
636+
637+
# Save set grades.
638+
$item->{grade_total} = $total;
639+
$item->{grade_total_right} = $total_right;
640+
$item->{grade} = 100 * wwRound(2, $total ? $total_right / $total : 0);
641+
642+
# Only show problem scores if allowed.
643+
unless (defined $set->hide_score_by_problem && $set->hide_score_by_problem eq 'Y') {
644+
$item->{problems} = [];
645+
for my $i (0 .. $#$problem_scores) {
646+
my $score = $problem_scores->[$i];
647+
my $problem_id = $setIsVersioned ? $i + 1 : $problem_records->[$i]{problem_id};
648+
my $problem_link =
649+
$setIsTest
650+
? ''
651+
: $c->systemLink($c->url_for('problem_detail', setID => $setID, problemID => $problem_id),
652+
params => { effectiveUser => $effectiveUser });
653+
$score = 0 unless $score =~ /^\d+$/;
654+
# For jitar sets we only display grades for top level problems.
655+
if ($set->assignment_type eq 'jitar') {
656+
my @seq = jitar_id_to_seq($problem_id);
657+
if ($#seq == 0) {
658+
push(@{ $item->{problems} }, { id => $seq[0], score => $score, link => $problem_link });
659+
$item->{completed} = 0 if $c->can_improve_score($set, $problem_records->[$i]);
660+
}
661+
} else {
662+
push(@{ $item->{problems} }, { id => $problem_id, score => $score, link => $problem_link });
663+
$item->{completed} = 0 if !$setIsTest && $c->can_improve_score($set, $problem_records->[$i]);
664+
}
665+
}
666+
}
667+
668+
# If this is a test version, update template set to the best grade a student hand.
669+
if ($setIsVersioned) {
670+
# Compare the score to the template set and update as needed.
671+
my $templateItem = $allItems{$setTemplateID};
672+
if ($item->{grade} > $templateItem->{grade}) {
673+
for ('grade', 'grade_total', 'grade_total_right') {
674+
$templateItem->{$_} = $item->{$_};
675+
}
676+
}
677+
}
678+
}
679+
680+
# Compute total course grade if requested.
681+
my $courseTotal = 0;
682+
my $totalRight = 0;
683+
if ($ce->{showCourseHomeworkTotals}) {
684+
for (@open, @reduced, @closed) {
685+
$courseTotal += $_->{grade_total};
686+
$totalRight += $_->{grade_total_right};
687+
}
688+
}
689+
690+
return $c->include(
691+
'ContentGenerator/Grades/student_grades',
692+
effectiveUser => $effectiveUser,
693+
fullName => join(' ', $studentRecord->first_name, $studentRecord->last_name),
694+
notOpen => \@notOpen,
695+
open => \@open,
696+
reduced => \@reduced,
697+
recentClosed => \@recentClosed,
698+
closed => \@closed,
699+
courseTotal => $courseTotal,
700+
totalRight => $totalRight
701+
);
702+
}
703+
453704
1;

lib/WeBWorK/HTML/StudentNav.pm

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ sub studentNav ($c, $setID) {
1515
return '' unless $c->authz->hasPermissions($userID, 'become_student');
1616

1717
# Find all users for the given set (except the current user) sorted by last_name, then first_name, then user_id.
18+
# If $setID is undefined, list all users except the current user instead.
1819
my @allUserRecords = $c->db->getUsersWhere(
1920
{
20-
user_id =>
21-
[ map { $_->[0] } $c->db->listUserSetsWhere({ set_id => $setID, user_id => { '!=' => $userID } }) ]
21+
user_id => [
22+
map { $_->[0] } $c->db->listUserSetsWhere(
23+
{ defined $setID ? (set_id => $setID) : (), user_id => { '!=' => $userID } }
24+
)
25+
]
2226
},
2327
[qw/last_name first_name user_id/]
2428
);
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1-
<%= $c->displayStudentStats($c->{studentID}) =%>
2-
<%= $c->scoring_info =%>
1+
<%= $c->displayStudentGrades($c->{studentID}) =%>
2+
%
3+
% my $scoring_info = $c->scoring_info;
4+
% if ($scoring_info) {
5+
<h2><%= maketext('Additional Grade Information') %></h2>
6+
<%= $scoring_info =%>
7+
% }
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<ul class="list-group">
2+
% my $n = 0;
3+
% for my $item (@$items) {
4+
% $n++;
5+
<li class="list-group-item d-flex align-items-center justify-content-between">
6+
% if ($item->{hide_score}) {
7+
<div style="width: 50px;"></div>
8+
% } else {
9+
% my $class = '';
10+
% my $data = '';
11+
% if ($showCompleted && defined $item->{completed}) {
12+
% $data = ' data-bs-placement="top" data-bs-toggle="tooltip" ';
13+
% $class = ' set-id-tooltip text-white rounded-3 ';
14+
% if ($item->{completed}) {
15+
% $class .= 'bg-success';
16+
% $data .= 'data-bs-title="'
17+
% . maketext('This assignment is complete and the grade can no longer be improved.')
18+
% . '"';
19+
% } else {
20+
% $class .= 'bg-info';
21+
% $data .= 'data-bs-title="'
22+
% . maketext('This assignment is not complete and the grade could be improved.')
23+
% . '"';
24+
% }
25+
% }
26+
<div class="fw-bold font-lg text-center<%= $class %> p-2" style="width: 50px;"<%== $data =%>>
27+
<%= $item->{grade} %>%
28+
</div>
29+
% }
30+
<div class="ms-3 me-auto table-responsive">
31+
<div>
32+
% if ($item->{link}) {
33+
<%= link_to $item->{name} => $item->{link}, class => "fw-bold" %>
34+
% } else {
35+
<span class="fw-bold"><%= $item->{name} %></span>
36+
% }
37+
</div>
38+
% if ($item->{message}) {
39+
<div><em><%= $item->{message} %></em></div>
40+
% }
41+
% if ($item->{problems}) {
42+
<%= include 'ContentGenerator/Grades/problem_table',
43+
problems => $item->{problems},
44+
total => $item->{grade_total},
45+
total_right => $item->{grade_total_right} %>
46+
% } elsif ($item->{versions}) {
47+
<%= include 'ContentGenerator/Grades/version_list', versions => $item->{versions} %>
48+
% }
49+
</div>
50+
</li>
51+
% }
52+
</ul>
53+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<div class="ms-3">
2+
<table class="table table-sm table-bordered font-sm text-center w-auto mb-0">
3+
<tbody>
4+
<tr>
5+
<th class="text-start"><%= maketext('Score') %></th>
6+
<td class="text-start" colspan="<%= @$problems %>">
7+
<%= maketext('[_1] out of [_2]', $total_right, $total) %>
8+
</td>
9+
<tr>
10+
<th class="text-start"><%= maketext('Problem') %></th>
11+
% for (@$problems) {
12+
<td>
13+
% if ($_->{link}) {
14+
<%= link_to $_->{id} => $_->{link}, class => "fw-bold" %>
15+
% } else {
16+
<%= $_->{id} %>
17+
% }
18+
</td>
19+
% }
20+
</tr>
21+
<tr>
22+
<th class="text-start"><%= maketext('Status') %></th>
23+
% for (@$problems) {
24+
<td><%= $_->{score} %>%</td>
25+
% }
26+
</tr>
27+
</tbody>
28+
</table>
29+
</div>
30+

0 commit comments

Comments
 (0)