Skip to content

Commit d4bf011

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. The old Grades page code is moved into StudentProgress.pm where it is still used. 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 3567f60 commit d4bf011

11 files changed

Lines changed: 727 additions & 271 deletions

File tree

lib/WeBWorK/ContentGenerator/Grades.pm

Lines changed: 191 additions & 257 deletions
Large diffs are not rendered by default.

lib/WeBWorK/ContentGenerator/Instructor/StudentProgress.pm

Lines changed: 335 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ WeBWorK::ContentGenerator::Instructor::StudentProgress - Display Student Progres
77
88
=cut
99

10-
use WeBWorK::Utils qw(wwRound);
11-
use WeBWorK::Utils::FilterRecords qw(getFiltersForClass filterRecords);
12-
use WeBWorK::Utils::JITAR qw(jitar_id_to_seq);
13-
use WeBWorK::Utils::Sets qw(grade_set list_set_versions format_set_name_display);
10+
use WeBWorK::Utils qw(wwRound);
11+
use WeBWorK::Utils::DateTime qw(after);
12+
use WeBWorK::Utils::FilterRecords qw(getFiltersForClass filterRecords);
13+
use WeBWorK::Utils::JITAR qw(jitar_id_to_seq);
14+
use WeBWorK::Utils::Sets qw(grade_set list_set_versions format_set_name_display);
15+
use WeBWorK::Utils::ProblemProcessing qw(compute_unreduced_score);
1416

1517
sub initialize ($c) {
1618
my $db = $c->db;
@@ -278,4 +280,333 @@ sub displaySets ($c) {
278280
);
279281
}
280282

283+
sub displayStudentStats ($c) {
284+
my $db = $c->db;
285+
my $ce = $c->ce;
286+
my $authz = $c->authz;
287+
288+
my $studentID = $c->{studentID};
289+
my $studentRecord = $db->getUser($studentID);
290+
unless ($studentRecord) {
291+
$c->addbadmessage($c->maketext('Record for user [_1] not found.', $studentID));
292+
return '';
293+
}
294+
295+
my $courseName = $ce->{courseName};
296+
297+
# First get all merged sets for this user ordered by set_id.
298+
my @sets = $db->getMergedSetsWhere({ user_id => $studentID }, 'set_id');
299+
# To be able to find the set objects later, make a handy hash of set ids to set objects.
300+
my %setsByID = (map { $_->set_id => $_ } @sets);
301+
302+
# Before going through the table generating loop, find all the set versions for the sets in our list.
303+
my %setVersionsCount;
304+
my @allSetIDs;
305+
for my $set (@sets) {
306+
# Don't show hidden sets unless user has appropriate permissions.
307+
next unless ($set->visible || $authz->hasPermissions($c->param('user'), 'view_hidden_sets'));
308+
309+
my $setID = $set->set_id();
310+
311+
# FIXME: Here, as in many other locations, we assume that there is a one-to-one matching between versioned sets
312+
# and gateways. We really should have two flags, $set->assignment_type and $set->versioned. I'm not adding
313+
# that yet, however, so this will continue to use assignment_type.
314+
if (defined $set->assignment_type && $set->assignment_type =~ /gateway/) {
315+
# We have to have the merged set versions to know what each of their assignment types are
316+
# (because proctoring can change this).
317+
my @setVersions =
318+
$db->getMergedSetVersionsWhere({ user_id => $studentID, set_id => { like => "$setID,v\%" } });
319+
320+
# Add the set versions to our list of sets.
321+
$setsByID{ $_->set_id . ',v' . $_->version_id } = $_ for (@setVersions);
322+
323+
# Flag the existence of set versions for this set.
324+
$setVersionsCount{$setID} = scalar @setVersions;
325+
326+
# Save the set names for display.
327+
push(@allSetIDs, $setID);
328+
push(@allSetIDs, map { $_->set_id . ',v' . $_->version_id } @setVersions);
329+
330+
} else {
331+
push(@allSetIDs, $setID);
332+
}
333+
}
334+
335+
my $fullName = join(' ', $studentRecord->first_name, $studentRecord->last_name);
336+
my $effectiveUser = $studentRecord->user_id();
337+
338+
my $max_problems = 0;
339+
my $courseTotal = 0;
340+
my $courseTotalRight = 0;
341+
342+
for my $setID (@allSetIDs) {
343+
my $set = $db->getGlobalSet($setID);
344+
my $num_of_problems;
345+
# For jitar sets we only display grades for top level problems, so we need to count how many there are.
346+
if ($set && $set->assignment_type() eq 'jitar') {
347+
my @problemIDs = $db->listGlobalProblems($setID);
348+
for my $problemID (@problemIDs) {
349+
my @seq = jitar_id_to_seq($problemID);
350+
$num_of_problems++ if ($#seq == 0);
351+
}
352+
} else {
353+
# For other sets we just count the number of problems.
354+
$num_of_problems = $db->countGlobalProblems($setID);
355+
}
356+
$max_problems =
357+
$set && after($set->open_date) && $max_problems < $num_of_problems ? $num_of_problems : $max_problems;
358+
}
359+
360+
# Variables to help compute gateway scores.
361+
my $numGatewayVersions = 0;
362+
my $bestGatewayScore = 0;
363+
364+
my $rows = $c->c;
365+
for my $setID (@allSetIDs) {
366+
my $act_as_student_set_url =
367+
$c->systemLink($c->url_for('problem_list', setID => $setID), params => { effectiveUser => $effectiveUser });
368+
my $set = $setsByID{$setID};
369+
370+
# Determine if set is a test and create the test url.
371+
my $setIsVersioned = 0;
372+
my $act_as_student_test_url = '';
373+
if (defined $set->assignment_type && $set->assignment_type =~ /gateway/) {
374+
$setIsVersioned = 1;
375+
if ($set->assignment_type eq 'proctored_gateway') {
376+
$act_as_student_test_url = $act_as_student_set_url =~ s/($courseName)\//$1\/proctored_test_mode\//r;
377+
} else {
378+
$act_as_student_test_url = $act_as_student_set_url =~ s/($courseName)\//$1\/test_mode\//r;
379+
}
380+
# Remove version from set url
381+
$act_as_student_set_url =~ s/,v\d+//;
382+
}
383+
384+
# Format set name based on set visibility.
385+
my $setName = $c->tag(
386+
'span',
387+
class => $set->visible ? 'font-visible' : 'font-hidden',
388+
format_set_name_display($setID =~ s/,v\d+$//r)
389+
);
390+
391+
# If the set is a template gateway set and there are no versions, we acknowledge that the set exists and the
392+
# student hasn't attempted it. Otherwise, we skip it and let the versions speak for themselves.
393+
if (defined $setVersionsCount{$setID}) {
394+
next if $setVersionsCount{$setID};
395+
push @$rows,
396+
$c->tag(
397+
'tr',
398+
$c->c(
399+
$c->tag(
400+
'th',
401+
dir => 'ltr',
402+
(after($set->open_date) || $authz->hasPermissions($c->param('user'), 'view_unopened_sets'))
403+
? $c->link_to($setName => $act_as_student_set_url)
404+
: $setName
405+
),
406+
$c->tag(
407+
'td',
408+
colspan => $max_problems + 3,
409+
$c->tag(
410+
'em',
411+
after($set->open_date) ? $c->maketext('No versions of this test have been taken.')
412+
: $c->maketext(
413+
'Will open on [_1].',
414+
$c->formatDateTime($set->open_date, $ce->{studentDateDisplayFormat})
415+
)
416+
)
417+
)
418+
)->join('')
419+
);
420+
next;
421+
}
422+
423+
# If the set has hide_score set, then we need to skip printing the score as well.
424+
if (
425+
defined $set->assignment_type
426+
&& $set->assignment_type =~ /gateway/
427+
&& defined $set->hide_score
428+
&& (
429+
!$authz->hasPermissions($c->param('user'), 'view_hidden_work')
430+
&& ($set->hide_score eq 'Y' || ($set->hide_score eq 'BeforeAnswerDate' && time < $set->answer_date))
431+
)
432+
)
433+
{
434+
# Add a link to the test version if the problems can be seen.
435+
my $thisSetName =
436+
$c->link_to($setName => $act_as_student_set_url) . ' ('
437+
. (
438+
(
439+
$set->hide_work eq 'N'
440+
|| ($set->hide_work eq 'BeforeAnswerDate' && time >= $set->answer_date)
441+
|| $authz->hasPermissions($c->param('user'), 'view_unopened_sets')
442+
)
443+
? $c->link_to($c->maketext('version [_1]', $set->version_id) => $act_as_student_test_url)
444+
: $c->maketext('version [_1]', $set->version_id)
445+
) . ')';
446+
push(
447+
@$rows,
448+
$c->tag(
449+
'tr',
450+
$c->c(
451+
$c->tag(
452+
'th',
453+
dir => 'ltr',
454+
sub {$thisSetName}
455+
),
456+
$c->tag(
457+
'td',
458+
colspan => $max_problems + 3,
459+
$c->tag('em', $c->maketext('Display of scores for this test is not allowed.'))
460+
)
461+
)->join('')
462+
)
463+
);
464+
next;
465+
}
466+
467+
my ($totalRight, $total, $problem_scores, $problem_incorrect_attempts, $problem_records) =
468+
grade_set($db, $set, $studentID, $setIsVersioned, 1);
469+
$totalRight = wwRound(2, $totalRight);
470+
471+
my @html_prob_scores;
472+
473+
my $show_problem_scores = 1;
474+
475+
if (defined $set->hide_score_by_problem
476+
&& !$authz->hasPermissions($c->param('user'), 'view_hidden_work')
477+
&& $set->hide_score_by_problem eq 'Y')
478+
{
479+
$show_problem_scores = 0;
480+
}
481+
482+
for my $i (0 .. $max_problems - 1) {
483+
my $score = defined $problem_scores->[$i] && $show_problem_scores ? $problem_scores->[$i] : '';
484+
my $is_correct = $score =~ /^\d+$/ && compute_unreduced_score($ce, $problem_records->[$i], $set) == 1;
485+
push(
486+
@html_prob_scores,
487+
$c->tag(
488+
'td',
489+
class => 'problem-data',
490+
$c->c(
491+
$c->tag(
492+
'span',
493+
class => $is_correct ? 'correct' : $score eq '&nbsp;.&nbsp;' ? 'unattempted' : '',
494+
$c->b($score)
495+
),
496+
$c->tag('br'),
497+
(defined $problem_incorrect_attempts->[$i] && $show_problem_scores)
498+
? $problem_incorrect_attempts->[$i]
499+
: $c->b('&nbsp;')
500+
)->join('')
501+
)
502+
);
503+
}
504+
505+
# Get percentage correct.
506+
my $totalRightPercent = 100 * wwRound(2, $total ? $totalRight / $total : 0);
507+
my $class = '';
508+
if ($totalRightPercent == 0) {
509+
$class = 'unattempted';
510+
} elsif ($totalRightPercent == 100) {
511+
$class = 'correct';
512+
}
513+
514+
# If its a gateway set, then in order to mimic the scoring done in Scoring Tools we need to use the best score a
515+
# student had. Otherwise we just add the set to the running course total.
516+
if ($setIsVersioned) {
517+
$setID =~ /(.+),v(\d+)$/;
518+
my $gatewayName = $1;
519+
my $currentVersion = $2;
520+
521+
# If we are just starting a new gateway then set variables to look for the max.
522+
if ($currentVersion == 1) {
523+
$numGatewayVersions = $db->countSetVersions($studentID, $gatewayName);
524+
}
525+
526+
if ($totalRight > $bestGatewayScore) {
527+
$bestGatewayScore = $totalRight;
528+
}
529+
530+
# If its the last version then add the max to the course totals and reset variables;
531+
if ($currentVersion == $numGatewayVersions) {
532+
if (after($set->open_date())) {
533+
$courseTotal += $total;
534+
$courseTotalRight += $bestGatewayScore;
535+
}
536+
$bestGatewayScore = 0;
537+
}
538+
} else {
539+
if (after($set->open_date())) {
540+
$courseTotal += $total;
541+
$courseTotalRight += $totalRight;
542+
}
543+
}
544+
545+
# Only show scores for open sets, and don't link to non open sets.
546+
if (after($set->open_date) || $authz->hasPermissions($c->param('user'), 'view_unopened_sets')) {
547+
# Set the set name and link. If a test, don't link to the version unless the problems can be seen.
548+
my $thisSetName = $setIsVersioned
549+
? $c->link_to($setName => $act_as_student_set_url) . ' ('
550+
. (
551+
(
552+
$set->hide_work eq 'N'
553+
|| ($set->hide_work eq 'BeforeAnswerDate' && time >= $set->answer_date)
554+
|| $authz->hasPermissions($c->param('user'), 'view_unopened_sets')
555+
)
556+
? $c->link_to($c->maketext('version [_1]', $set->version_id) => $act_as_student_test_url)
557+
: $c->maketext('version [_1]', $set->version_id)
558+
)
559+
. ')'
560+
: $c->link_to($setName => $act_as_student_set_url);
561+
push @$rows, $c->tag(
562+
'tr',
563+
$c->c(
564+
$c->tag(
565+
'th',
566+
scope => 'row',
567+
dir => 'ltr',
568+
sub {$thisSetName}
569+
),
570+
$c->tag('td', $c->tag('span', class => $class, $totalRightPercent . '%')),
571+
$c->tag('td', sprintf('%0.2f', $totalRight)), # score
572+
$c->tag('td', $total), # out of
573+
@html_prob_scores # problems
574+
)->join('')
575+
);
576+
} else {
577+
push @$rows,
578+
$c->tag(
579+
'tr',
580+
$c->c(
581+
$c->tag(
582+
'th',
583+
dir => 'ltr',
584+
$setName
585+
),
586+
$c->tag(
587+
'td',
588+
colspan => $max_problems + 3,
589+
$c->tag(
590+
'em',
591+
$c->maketext(
592+
'Will open on [_1].',
593+
$c->formatDateTime($set->open_date, $ce->{studentDateDisplayFormat})
594+
)
595+
)
596+
)
597+
)->join('')
598+
);
599+
}
600+
}
601+
602+
return $c->include(
603+
'ContentGenerator/Instructor/StudentProgress/student_stats',
604+
fullName => $fullName,
605+
max_problems => $max_problems,
606+
rows => $rows->join(''),
607+
courseTotal => $courseTotal,
608+
courseTotalRight => $courseTotalRight
609+
);
610+
}
611+
281612
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+
% }

0 commit comments

Comments
 (0)