diff --git a/htdocs/js/GatewayQuiz/gateway.scss b/htdocs/js/GatewayQuiz/gateway.scss index f7d450dff6..6d4b7ead5e 100644 --- a/htdocs/js/GatewayQuiz/gateway.scss +++ b/htdocs/js/GatewayQuiz/gateway.scss @@ -60,7 +60,7 @@ table.attemptResults { border: 1px solid #ddd; border-radius: 3px; - h2 { + h2.gw-problem-number { display: inline-block; font-size: 16px; margin-right: 5px; diff --git a/htdocs/js/ProblemGrader/singleproblemgrader.js b/htdocs/js/ProblemGrader/singleproblemgrader.js index 002c5b7f0d..b0212689a8 100644 --- a/htdocs/js/ProblemGrader/singleproblemgrader.js +++ b/htdocs/js/ProblemGrader/singleproblemgrader.js @@ -216,4 +216,141 @@ } }); } + + const settingStoreID = `WW.${document.getElementsByName('courseID')[0]?.value ?? 'unknownCourse'}.${ + document.getElementsByName('user')[0]?.value ?? 'unknownUser' + }.problem_grader`; + let gradersOpen = localStorage.getItem(`${settingStoreID}.open`) === 'true'; + + const graderCollapses = []; + + for (const grader of document.querySelectorAll('.problem-grader')) { + const problemId = grader.id.replace('problem-grader-'); + + grader.classList.add('accordion'); + + const accordionItem = document.createElement('div'); + accordionItem.classList.add('accordion-item'); + + const accordionHeader = document.createElement('h2'); + accordionHeader.classList.add('accordion-header'); + + const accordionButton = document.createElement('button'); + accordionButton.classList.add('accordion-button'); + accordionButton.type = 'button'; + accordionButton.textContent = grader.dataset.graderTitle ?? 'Problem Grader'; + accordionButton.dataset.bsToggle = 'collapse'; + accordionButton.dataset.bsTarget = `#problem-grader-collapse-${problemId}`; + accordionButton.setAttribute('aria-controls', `#problem-grader-collapse-${problemId}`); + accordionButton.setAttribute('aria-expanded', gradersOpen); + if (!gradersOpen) accordionButton.classList.add('collapsed'); + + accordionHeader.append(accordionButton); + + const accordionCollapse = document.createElement('div'); + accordionCollapse.classList.add('accordion-collapse', 'collapse'); + accordionCollapse.id = `problem-grader-collapse-${problemId}`; + accordionCollapse.dataset.bsParent = `problem-grader-${problemId}`; + if (gradersOpen) accordionCollapse.classList.add('show'); + + const accordionBody = grader.querySelector('.problem-grader-table'); + accordionBody.classList.add('accordion-body'); + accordionCollapse.append(accordionBody); + + accordionItem.append(accordionHeader, accordionCollapse); + grader.append(accordionItem); + + const graderCollapse = new bootstrap.Collapse(accordionCollapse, { toggle: false }); + graderCollapses.push(graderCollapse); + + grader.classList.remove('d-none'); + + // Expand or collapse all problem graders on the page when any one of them is expanded or collapsed. + let transitioning = false; + accordionCollapse.addEventListener('show.bs.collapse', () => { + if (transitioning) return; + transitioning = true; + for (const grader of graderCollapses) { + if (grader !== graderCollapse) grader.show(); + } + transitioning = false; + }); + accordionCollapse.addEventListener('hide.bs.collapse', () => { + if (transitioning) return; + transitioning = true; + for (const grader of graderCollapses) { + if (grader !== graderCollapse) grader.hide(); + } + transitioning = false; + }); + + // Make sure that the "Reveal" button in feedback is not shown if a feedback button is used while the problem + // grader is open. However, also make sure that the "Reveal" button is shown for any feedback button that is + // not used while the problem grader is open. + + const unrevealedFeedbackBtns = []; + + for (const feedbackBtn of document.querySelectorAll('.ww-feedback-btn')) { + const container = document.createElement('div'); + container.innerHTML = feedbackBtn.dataset.bsContent; + const button = container.querySelector('.reveal-correct-btn'); + if (!button) continue; + + button.nextElementSibling?.classList.remove('d-none'); + button.remove(); + + const fragment = new DocumentFragment(); + fragment.append(container); + + unrevealedFeedbackBtns.push([feedbackBtn, fragment.firstElementChild.innerHTML]); + + const handler = () => { + const index = unrevealedFeedbackBtns.findIndex((data) => data[0] === feedbackBtn); + if (index !== -1) { + if (gradersOpen) { + unrevealedFeedbackBtns.splice(index, 1); + feedbackBtn.removeEventListener('shown.bs.popover', handler); + } else { + bootstrap.Popover.getInstance(feedbackBtn) + ?.tip?.querySelector('.reveal-correct-btn') + ?.addEventListener( + 'click', + () => { + unrevealedFeedbackBtns.splice(index, 1); + feedbackBtn.removeEventListener('shown.bs.popover', handler); + }, + { once: true } + ); + } + } + }; + + feedbackBtn.addEventListener('shown.bs.popover', handler); + } + + const removeRevealButtons = () => { + for (const data of unrevealedFeedbackBtns) { + const feedbackPopover = bootstrap.Popover.getInstance(data[0]); + feedbackPopover?.setContent({ '.popover-body': data[1] }); + } + }; + + if (gradersOpen) removeRevealButtons(); + + // In addition to removing and putting back the feedback "Reveal" buttons as needed, + // preserve the collapsed/expanded status of the problem graders in local storage. + accordionCollapse.addEventListener('shown.bs.collapse', () => { + localStorage.setItem(`${settingStoreID}.open`, 'true'); + gradersOpen = true; + removeRevealButtons(); + }); + accordionCollapse.addEventListener('hidden.bs.collapse', () => { + gradersOpen = false; + localStorage.setItem(`${settingStoreID}.open`, 'false'); + for (const data of unrevealedFeedbackBtns) { + const feedbackPopover = bootstrap.Popover.getInstance(data[0]); + feedbackPopover?.setContent({ '.popover-body': data[0].dataset.bsContent }); + } + }); + } })(); diff --git a/htdocs/js/System/system.scss b/htdocs/js/System/system.scss index 09c486b10a..340cb6c277 100644 --- a/htdocs/js/System/system.scss +++ b/htdocs/js/System/system.scss @@ -1018,6 +1018,17 @@ td.alt-source { } } +.problem-grader.accordion { + .accordion-header { + .accordion-button { + --bs-accordion-btn-padding-x: 0.75rem; + --bs-accordion-btn-padding-y: 0.375rem; + --bs-accordion-btn-bg: var(--bs-primary, #038); + --bs-accordion-btn-color: var(--ww-primary-foreground-color, white); + } + } +} + .problem-grader-table { .col-fixed { width: 11rem; diff --git a/lib/WeBWorK/ConfigValues.pm b/lib/WeBWorK/ConfigValues.pm index 209cde6df8..a57890e329 100644 --- a/lib/WeBWorK/ConfigValues.pm +++ b/lib/WeBWorK/ConfigValues.pm @@ -820,7 +820,8 @@ sub getConfigValues ($ce) { doc2 => x( 'A "Reveal" button must be clicked to make a correct answer visible any time that correct ' . 'answers for a problem are shown. Note that this is always the case for instructors ' - . 'before answers are available to students, and in "Show Me Another" problems.' + . 'before answers are available to students (except when the problem grader is open), and ' + . 'in "Show Me Another" problems.' ), type => 'boolean' } diff --git a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm index e4e827d069..147d4bda71 100644 --- a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm +++ b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm @@ -780,14 +780,11 @@ async sub pre_header_initialize ($c) { return; } - # Unset the showProblemGrader parameter if the "Hide Problem Grader" button was clicked. - $c->param(showProblemGrader => undef) if $c->param('hideProblemGrader'); - # What does the user want to do? my %want = ( showOldAnswers => $user->showOldAnswers ne '' ? $user->showOldAnswers : $ce->{pg}{options}{showOldAnswers}, showCorrectAnswers => 1, - showProblemGrader => $c->param('showProblemGrader') || 0, + showProblemGrader => $userID ne $effectiveUserID, showHints => 0, # Hints are not yet implemented in gateway quzzes. showSolutions => 1, recordAnswers => $c->{submitAnswers} && !$authz->hasPermissions($userID, 'avoid_recording_answers'), @@ -1343,7 +1340,7 @@ sub path ($c, $args) { $courseName => $navigation_allowed ? $c->url_for('set_list') : '', $setID eq 'Undefined_Set' || $c->{invalidSet} || $c->{actingCreationError} || $c->stash->{actingConfirmation} - ? ($setID => '') + ? ($setID =~ /^(.+),(v\d+)$/ ? ($1 => $c->url_for('problem_list', setID => $1), $2 => '') : ($setID => '')) : ( $c->{set}->set_id => $c->url_for('problem_list', setID => $c->{set}->set_id), 'v' . $c->{set}->version_id => '' @@ -1359,7 +1356,7 @@ sub nav ($c, $args) { return '' if $c->{invalidSet} || $c->{actingCreationError} || $c->stash->{actingConfirmation}; # Set up and display a student navigation for those that have permission to act as a student. - if ($c->authz->hasPermissions($userID, 'become_student') && $effectiveUserID ne $userID) { + if ($c->authz->hasPermissions($userID, 'become_student')) { my $setID = $c->{set}->set_id; return '' if $setID eq 'Undefined_Set'; @@ -1368,76 +1365,83 @@ sub nav ($c, $args) { # Find all versions of this set that have been taken (excluding those taken by the current user). my @users = - $db->listSetVersionsWhere({ user_id => { not_like => $userID }, set_id => { like => "$setID,v\%" } }); + $db->listSetVersionsWhere({ user_id => { '!=' => $userID }, set_id => { like => "$setID,v\%" } }); my @allUserRecords = $db->getUsers(map { $_->[0] } @users); - my $filter = $c->param('studentNavFilter'); - - # Format the student names for display, and associate the users with the test versions. - my %filters; - my @userRecords; - for (0 .. $#allUserRecords) { - # Add to the sections and recitations if defined. Also store the first user found in that section or - # recitation. This user will be switched to when the filter is selected. - my $section = $allUserRecords[$_]->section; - $filters{"section:$section"} = - [ $c->maketext('Filter by section [_1]', $section), $allUserRecords[$_]->user_id, $users[$_][2] ] - if $section && !$filters{"section:$section"}; - my $recitation = $allUserRecords[$_]->recitation; - $filters{"recitation:$recitation"} = - [ $c->maketext('Filter by recitation [_1]', $recitation), $allUserRecords[$_]->user_id, $users[$_][2] ] - if $recitation && !$filters{"recitation:$recitation"}; - - # Only keep this user if it satisfies the selected filter if a filter was selected. - next - unless !$filter - || ($filter =~ /^section:(.*)$/ && $allUserRecords[$_]->section eq $1) - || ($filter =~ /^recitation:(.*)$/ && $allUserRecords[$_]->recitation eq $1); - - my $addRecord = $allUserRecords[$_]; - push @userRecords, $addRecord; - - $addRecord->{displayName} = - ($addRecord->last_name || $addRecord->first_name - ? $addRecord->last_name . ', ' . $addRecord->first_name - : $addRecord->user_id); - $addRecord->{setVersion} = $users[$_][2]; - } + if (@allUserRecords) { + my $filter = $c->param('studentNavFilter'); + + # Format the student names for display, and associate the users with the test versions. + my %filters; + my @userRecords; + for (0 .. $#allUserRecords) { + # Add to the sections and recitations if defined. Also store the first user found in that section or + # recitation. This user will be switched to when the filter is selected. + my $section = $allUserRecords[$_]->section; + $filters{"section:$section"} = + [ $c->maketext('Filter by section [_1]', $section), $allUserRecords[$_]->user_id, $users[$_][2] ] + if $section && !$filters{"section:$section"}; + my $recitation = $allUserRecords[$_]->recitation; + $filters{"recitation:$recitation"} = [ + $c->maketext('Filter by recitation [_1]', $recitation), $allUserRecords[$_]->user_id, + $users[$_][2] + ] + if $recitation && !$filters{"recitation:$recitation"}; + + # Only keep this user if it satisfies the selected filter if a filter was selected. + next + unless !$filter + || ($filter =~ /^section:(.*)$/ && $allUserRecords[$_]->section eq $1) + || ($filter =~ /^recitation:(.*)$/ && $allUserRecords[$_]->recitation eq $1); + + my $addRecord = $allUserRecords[$_]; + push @userRecords, $addRecord; + + $addRecord->{displayName} = + ($addRecord->last_name || $addRecord->first_name + ? $addRecord->last_name . ', ' . $addRecord->first_name + : $addRecord->user_id); + $addRecord->{setVersion} = $users[$_][2]; + } - # Sort by last name, then first name, then user_id, then set version. - @userRecords = sort { - lc($a->last_name) cmp lc($b->last_name) - || lc($a->first_name) cmp lc($b->first_name) - || lc($a->user_id) cmp lc($b->user_id) - || lc($a->{setVersion}) <=> lc($b->{setVersion}) - } @userRecords; - - # Find the previous, current, and next test. - my $currentTestIndex = 0; - for (0 .. $#userRecords) { - if ($userRecords[$_]->user_id eq $effectiveUserID && $userRecords[$_]->{setVersion} == $setVersion) { - $currentTestIndex = $_; - last; + # Sort by last name, then first name, then user_id, then set version. + @userRecords = sort { + lc($a->last_name) cmp lc($b->last_name) + || lc($a->first_name) cmp lc($b->first_name) + || lc($a->user_id) cmp lc($b->user_id) + || lc($a->{setVersion}) <=> lc($b->{setVersion}) + } @userRecords; + + # Find the previous, current, and next test. + my $currentTestIndex = 0; + for (0 .. $#userRecords) { + if ($userRecords[$_]->user_id eq $effectiveUserID && $userRecords[$_]->{setVersion} == $setVersion) { + $currentTestIndex = $_; + last; + } } + my $prevTest = $currentTestIndex > 0 ? $userRecords[ $currentTestIndex - 1 ] : 0; + my $nextTest = $currentTestIndex < $#userRecords ? $userRecords[ $currentTestIndex + 1 ] : 0; + + # Mark the current test. + $userRecords[$currentTestIndex]{currentTest} = 1; + + # Show the student nav. + return $c->include( + 'ContentGenerator/GatewayQuiz/nav', + userID => $userID, + eUserID => $effectiveUserID, + userRecords => \@userRecords, + setVersion => $setVersion, + prevTest => $prevTest, + nextTest => $nextTest, + currentTestIndex => $currentTestIndex, + filters => \%filters, + filter => $filter + ); } - my $prevTest = $currentTestIndex > 0 ? $userRecords[ $currentTestIndex - 1 ] : 0; - my $nextTest = $currentTestIndex < $#userRecords ? $userRecords[ $currentTestIndex + 1 ] : 0; - - # Mark the current test. - $userRecords[$currentTestIndex]{currentTest} = 1; - - # Show the student nav. - return $c->include( - 'ContentGenerator/GatewayQuiz/nav', - userRecords => \@userRecords, - setVersion => $setVersion, - prevTest => $prevTest, - nextTest => $nextTest, - currentTestIndex => $currentTestIndex, - filters => \%filters, - filter => $filter - ); } + return ''; } sub warningMessage ($c) { @@ -1491,10 +1495,9 @@ async sub getProblemHTML ($c, $effectiveUser, $set, $formFields, $mergedProblem) && $c->can_showCorrectAnswersForAll($set, $c->{problem}, $c->{tmplSet})), showMessages => !$showOnlyCorrectAnswers, showCorrectAnswers => ( - $c->{will}{showProblemGrader} ? 2 - : !$c->{previewAnswers} && $c->can_showCorrectAnswersForAll($set, $c->{problem}, $c->{tmplSet}) + !$c->{previewAnswers} && $c->can_showCorrectAnswersForAll($set, $c->{problem}, $c->{tmplSet}) ? ($c->ce->{pg}{options}{correctRevealBtnAlways} ? 1 : 2) - : !$c->{previewAnswers} && $c->{will}{showCorrectAnswers} ? 1 + : $c->{will}{showProblemGrader} || (!$c->{previewAnswers} && $c->{will}{showCorrectAnswers}) ? 1 : 0 ), debuggingOptions => getTranslatorDebuggingOptions($c->authz, $c->{userID}), diff --git a/lib/WeBWorK/ContentGenerator/Problem.pm b/lib/WeBWorK/ContentGenerator/Problem.pm index af44e7afab..b64c70ca3e 100644 --- a/lib/WeBWorK/ContentGenerator/Problem.pm +++ b/lib/WeBWorK/ContentGenerator/Problem.pm @@ -7,7 +7,6 @@ WeBWorK::ContentGenerator::Problem - Allow a student to interact with a problem. =cut -use WeBWorK::HTML::SingleProblemGrader; use WeBWorK::Debug; use WeBWorK::Utils qw(decodeAnswers wwRound); use WeBWorK::Utils::DateTime qw(before between after); @@ -23,6 +22,8 @@ use WeBWorK::AchievementEvaluator qw(checkForAchievements); use WeBWorK::DB::Utils qw(global2user fake_set fake_problem); use WeBWorK::Localize; use WeBWorK::AchievementEvaluator; +use WeBWorK::HTML::SingleProblemGrader; +use WeBWorK::HTML::StudentNav qw(studentNav); # GET/POST Parameters for this module # @@ -431,20 +432,17 @@ async sub pre_header_initialize ($c) { Count => $problem->{showMeAnotherCount}, }; - # Unset the showProblemGrader parameter if the "Hide Problem Grader" button was clicked. - $c->param(showProblemGrader => undef) if $c->param('hideProblemGrader'); - # Permissions # What does the user want to do? my %want = ( showOldAnswers => $user->showOldAnswers ne '' ? $user->showOldAnswers : $ce->{pg}{options}{showOldAnswers}, showCorrectAnswers => 1, - showProblemGrader => $c->param('showProblemGrader') || 0, - showAnsGroupInfo => $c->param('showAnsGroupInfo') || $ce->{pg}{options}{showAnsGroupInfo}, - showAnsHashInfo => $c->param('showAnsHashInfo') || $ce->{pg}{options}{showAnsHashInfo}, - showPGInfo => $c->param('showPGInfo') || $ce->{pg}{options}{showPGInfo}, - showResourceInfo => $c->param('showResourceInfo') || $ce->{pg}{options}{showResourceInfo}, + showProblemGrader => $userID ne $effectiveUserID, + showAnsGroupInfo => $c->param('showAnsGroupInfo') || $ce->{pg}{options}{showAnsGroupInfo}, + showAnsHashInfo => $c->param('showAnsHashInfo') || $ce->{pg}{options}{showAnsHashInfo}, + showPGInfo => $c->param('showPGInfo') || $ce->{pg}{options}{showPGInfo}, + showResourceInfo => $c->param('showResourceInfo') || $ce->{pg}{options}{showResourceInfo}, showHints => 1, showSolutions => 1, useMathView => $user->useMathView ne '' ? $user->useMathView : $ce->{pg}{options}{useMathView}, @@ -581,10 +579,10 @@ async sub pre_header_initialize ($c) { && after($c->{set}->answer_date, $c->submitTime)), showMessages => !$showOnlyCorrectAnswers, showCorrectAnswers => ( - $will{showProblemGrader} || ($c->{submitAnswers} && $c->{showCorrectOnRandomize}) ? 2 + $c->{submitAnswers} && $c->{showCorrectOnRandomize} ? 2 : !$c->{previewAnswers} && after($c->{set}->answer_date, $c->submitTime) ? ($ce->{pg}{options}{correctRevealBtnAlways} ? 1 : 2) - : !$c->{previewAnswers} && $will{showCorrectAnswers} ? 1 + : $will{showProblemGrader} || (!$c->{previewAnswers} && $will{showCorrectAnswers}) ? 1 : 0 ), debuggingOptions => getTranslatorDebuggingOptions($authz, $userID), @@ -722,9 +720,6 @@ sub siblings ($c) { my @items; - # Keep the grader open when linking to problems if it is already open. - my %problemGraderLink = $c->{will}{showProblemGrader} ? (params => { showProblemGrader => 1 }) : (); - for my $problemID (@problemIDs) { if ($isJitarSet && !$authz->hasPermissions($eUserID, 'view_unopened_sets') @@ -795,7 +790,7 @@ sub siblings ($c) { @items, $c->tag( 'a', - $active ? () : (href => $c->systemLink($problemPage, %problemGraderLink)), + $active ? () : (href => $c->systemLink($problemPage)), class => $class, $c->b($c->maketext('Problem [_1]', join('.', @seq)) . $status_symbol) ) @@ -806,7 +801,7 @@ sub siblings ($c) { @items, $c->tag( 'a', - $active ? () : (href => $c->systemLink($problemPage, %problemGraderLink)), + $active ? () : (href => $c->systemLink($problemPage)), class => 'nav-link' . ($active ? ' active' : ''), $c->b($c->maketext('Problem [_1]', $problemID) . $status_symbol) ) @@ -842,74 +837,6 @@ sub nav ($c, $args) { my $mergedSet = $db->getMergedSet($eUserID, $setID); return '' if !$mergedSet; - # Set up a student navigation for those that have permission to act as a student. - my $userNav = ''; - if ($authz->hasPermissions($userID, 'become_student') && $eUserID ne $userID) { - # Find all users for this set (except the current user) sorted by last_name, then first_name, then user_id. - my @allUserRecords = $db->getUsersWhere( - { - user_id => [ - map { $_->[0] } $db->listUserSetsWhere({ set_id => $setID, user_id => { not_like => $userID } }) - ] - }, - [qw/last_name first_name user_id/] - ); - - my $filter = $c->param('studentNavFilter'); - - # Find the previous, current, and next users, and format the student names for display. - # Also create a hash of sections and recitations if there are any for the course. - my @userRecords; - my $currentUserIndex = 0; - my %filters; - for (@allUserRecords) { - # Add to the sections and recitations if defined. Also store the first user found in that section or - # recitation. This user will be switched to when the filter is selected. - my $section = $_->section; - $filters{"section:$section"} = [ $c->maketext('Filter by section [_1]', $section), $_->user_id ] - if $section && !$filters{"section:$section"}; - my $recitation = $_->recitation; - $filters{"recitation:$recitation"} = [ $c->maketext('Filter by recitation [_1]', $recitation), $_->user_id ] - if $recitation && !$filters{"recitation:$recitation"}; - - # Only keep this user if it satisfies the selected filter if a filter was selected. - next - unless !$filter - || ($filter =~ /^section:(.*)$/ && $_->section eq $1) - || ($filter =~ /^recitation:(.*)$/ && $_->recitation eq $1); - - my $addRecord = $_; - $currentUserIndex = @userRecords if $addRecord->user_id eq $eUserID; - push @userRecords, $addRecord; - - # Construct a display name. - $addRecord->{displayName} = - ($addRecord->last_name || $addRecord->first_name - ? $addRecord->last_name . ', ' . $addRecord->first_name - : $addRecord->user_id); - } - my $prevUser = $currentUserIndex > 0 ? $userRecords[ $currentUserIndex - 1 ] : 0; - my $nextUser = $currentUserIndex < $#userRecords ? $userRecords[ $currentUserIndex + 1 ] : 0; - - # Mark the current user. - $userRecords[$currentUserIndex]{currentUser} = 1; - - my $problemPage = $c->url_for('problem_detail', setID => $setID, problemID => $problemID); - - # Set up the student nav. - $userNav = $c->include( - 'ContentGenerator/Problem/student_nav', - eUserID => $eUserID, - problemPage => $problemPage, - userRecords => \@userRecords, - currentUserIndex => $currentUserIndex, - prevUser => $prevUser, - nextUser => $nextUser, - filter => $filter, - filters => \%filters - ); - } - my $isJitarSet = $mergedSet->assignment_type eq 'jitar'; my ($prevID, $nextID); @@ -970,10 +897,9 @@ sub nav ($c, $args) { } my %tail; - $tail{displayMode} = $c->{displayMode} if defined $c->{displayMode}; - $tail{showOldAnswers} = 1 if $c->{will}{showOldAnswers}; - $tail{showProblemGrader} = 1 if $c->{will}{showProblemGrader}; - $tail{studentNavFilter} = $c->param('studentNavFilter') if $c->param('studentNavFilter'); + $tail{displayMode} = $c->{displayMode} if defined $c->{displayMode}; + $tail{showOldAnswers} = 1 if $c->{will}{showOldAnswers}; + $tail{studentNavFilter} = $c->param('studentNavFilter') if $c->param('studentNavFilter'); return $c->tag( 'div', @@ -981,7 +907,7 @@ sub nav ($c, $args) { role => 'navigation', 'aria-label' => 'problem navigation', $c->c($c->tag('div', class => 'd-flex submit-buttons-container', $c->navMacro($args, \%tail, @links)), - $userNav)->join('') + studentNav($c, $setID))->join('') ); } @@ -1133,10 +1059,8 @@ sub output_message ($c) { # Output the problem grader if the user has permissions to grade problems sub output_grader ($c) { - if ($c->{will}{showProblemGrader}) { - return WeBWorK::HTML::SingleProblemGrader->new($c, $c->{pg}, $c->{problem})->insertGrader; - } - + return WeBWorK::HTML::SingleProblemGrader->new($c, $c->{pg}, $c->{problem})->insertGrader + if $c->{will}{showProblemGrader}; return ''; } @@ -1444,7 +1368,7 @@ sub output_summary ($c) { # Attempt summary if ($c->{submitAnswers}) { push(@$output, $c->attemptResults($pg)); - } elsif ($will{checkAnswers} || $c->{will}{showProblemGrader}) { + } elsif ($will{checkAnswers}) { push( @$output, $c->tag( diff --git a/lib/WeBWorK/ContentGenerator/ProblemSet.pm b/lib/WeBWorK/ContentGenerator/ProblemSet.pm index ba77939ec5..7eebaeb6de 100644 --- a/lib/WeBWorK/ContentGenerator/ProblemSet.pm +++ b/lib/WeBWorK/ContentGenerator/ProblemSet.pm @@ -17,6 +17,7 @@ use WeBWorK::Utils::Sets qw(is_restricted grade_set format_set_name_display use WeBWorK::DB::Utils qw(grok_versionID_from_vsetID_sql); use WeBWorK::Localize; use WeBWorK::AchievementItems; +use WeBWorK::HTML::StudentNav qw(studentNav); async sub initialize ($c) { my $db = $c->db; @@ -113,17 +114,24 @@ sub nav ($c, $args) { # Don't show the nav if the user does not have unrestricted navigation permissions. return '' unless $c->authz->hasPermissions($c->param('user'), 'navigation_allowed'); - my @links = ( - $c->maketext('Assignments'), - $c->url_for($c->app->routes->lookup($c->current_route)->parent->name), - $c->maketext('Assignments') - ); return $c->tag( 'div', class => 'row sticky-nav', role => 'navigation', - 'aria-label' => 'problem navigation', - $c->tag('div', $c->navMacro($args, {}, @links)) + 'aria-label' => 'set navigation', + $c->c( + $c->tag( + 'div', + class => 'd-flex submit-buttons-container', + $c->navMacro( + $args, {}, + $c->maketext('Assignments'), + $c->url_for($c->app->routes->lookup($c->current_route)->parent->name), + $c->maketext('Assignments') + ) + ), + $c->{set} ? studentNav($c, $c->{set}->set_id) : '' + )->join('') ); } diff --git a/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm b/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm index 39b164d4ab..7bdb85f515 100644 --- a/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm +++ b/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm @@ -155,10 +155,9 @@ async sub pre_header_initialize ($c) { } # Disable options that are not applicable for showMeAnother. - $c->{can}{recordAnswers} = 0; - $c->{can}{checkAnswers} = 0; # This is turned on if the showMeAnother conditions are met below. - $c->{can}{getSubmitButton} = 0; - $c->{can}{showProblemGrader} = 0; + $c->{can}{recordAnswers} = 0; + $c->{can}{checkAnswers} = 0; # This is turned on if the showMeAnother conditions are met below. + $c->{can}{getSubmitButton} = 0; if ($c->stash->{isPossible}) { $c->{can}{showCorrectAnswers} = diff --git a/lib/WeBWorK/HTML/StudentNav.pm b/lib/WeBWorK/HTML/StudentNav.pm new file mode 100644 index 0000000000..7591712a8d --- /dev/null +++ b/lib/WeBWorK/HTML/StudentNav.pm @@ -0,0 +1,83 @@ +package WeBWorK::HTML::StudentNav; +use Mojo::Base 'Exporter', -signatures; + +=head1 NAME + +WeBWorK::HTML::StudentNav - student navigation for all users assigned to a set. + +=cut + +our @EXPORT_OK = qw(studentNav); + +sub studentNav ($c, $setID) { + my $userID = $c->param('user'); + + 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. + my @allUserRecords = $c->db->getUsersWhere( + { + user_id => + [ map { $_->[0] } $c->db->listUserSetsWhere({ set_id => $setID, user_id => { '!=' => $userID } }) ] + }, + [qw/last_name first_name user_id/] + ); + + return '' unless @allUserRecords; + + my $eUserID = $c->param('effectiveUser'); + + my $filter = $c->param('studentNavFilter'); + + # Find the previous, current, and next users, and format the student names for display. + # Also create a hash of sections and recitations if there are any for the course. + my @userRecords; + my $currentUserIndex = 0; + my %filters; + for (@allUserRecords) { + # Add to the sections and recitations if defined. Also store the first user found in that section or + # recitation. This user will be switched to when the filter is selected. + my $section = $_->section; + $filters{"section:$section"} = [ $c->maketext('Filter by section [_1]', $section), $_->user_id ] + if $section && !$filters{"section:$section"}; + my $recitation = $_->recitation; + $filters{"recitation:$recitation"} = [ $c->maketext('Filter by recitation [_1]', $recitation), $_->user_id ] + if $recitation && !$filters{"recitation:$recitation"}; + + # Only keep this user if it satisfies the selected filter if a filter was selected. + next + unless !$filter + || ($filter =~ /^section:(.*)$/ && $_->section eq $1) + || ($filter =~ /^recitation:(.*)$/ && $_->recitation eq $1); + + my $addRecord = $_; + $currentUserIndex = @userRecords if $addRecord->user_id eq $eUserID; + push @userRecords, $addRecord; + + # Construct a display name. + $addRecord->{displayName} = + ($addRecord->last_name || $addRecord->first_name + ? $addRecord->last_name . ', ' . $addRecord->first_name + : $addRecord->user_id); + } + my $prevUser = $currentUserIndex > 0 ? $userRecords[ $currentUserIndex - 1 ] : 0; + my $nextUser = $currentUserIndex < $#userRecords ? $userRecords[ $currentUserIndex + 1 ] : 0; + + # Mark the current user. + $userRecords[$currentUserIndex]{currentUser} = 1; + + # Set up the student nav. + return $c->include( + 'HTML/StudentNav/student_nav', + userID => $userID, + eUserID => $eUserID, + userRecords => \@userRecords, + currentUserIndex => $currentUserIndex, + prevUser => $prevUser, + nextUser => $nextUser, + filter => $filter, + filters => \%filters + ); +} + +1; diff --git a/lib/WeBWorK/Utils.pm b/lib/WeBWorK/Utils.pm index a98d2f1364..e357abf272 100644 --- a/lib/WeBWorK/Utils.pm +++ b/lib/WeBWorK/Utils.pm @@ -357,7 +357,6 @@ sub generateURLs ($c, %params) { for my $name ('displayMode', 'showCorrectAnswers', 'showHints', 'showOldAnswers', 'showSolutions') { $args{$name} = [ $c->param($name) ] if defined $c->param($name) && $c->param($name) ne ''; } - $args{showProblemGrader} = 1; } else { $routePath = $c->url_for('problem_list', setID => $params{set_id}); } diff --git a/templates/ContentGenerator/GatewayQuiz.html.ep b/templates/ContentGenerator/GatewayQuiz.html.ep index 45f7d35a70..65d8f322cf 100644 --- a/templates/ContentGenerator/GatewayQuiz.html.ep +++ b/templates/ContentGenerator/GatewayQuiz.html.ep @@ -388,6 +388,7 @@ % <%= form_for $action, name => 'gwquiz', method => 'POST', class => 'problem-main-form mt-0', begin =%> <%= $c->hidden_authen_fields =%> + <%= hidden_field courseID => $ce->{courseName} =%> % % # Hacks to use a javascript link to trigger previews and jump to subsequent pages of a multipage test. <%= hidden_field pageChangeHack => '' =%> @@ -533,7 +534,7 @@ % $recordMessage = tag('div', class => 'alert alert-danger d-inline-block mb-2 p-1', % maketext('CORRECT ANSWERS SHOWN ONLY -- ANSWERS NOT RECORDED') % ); - % } elsif ($c->{will}{checkAnswers} || $c->{will}{showProblemGrader}) { + % } elsif ($c->{will}{checkAnswers}) { % $recordMessage = tag('div', class => 'alert alert-danger d-inline-block mb-2 p-1', % maketext('ANSWERS ONLY CHECKED -- ANSWERS NOT RECORDED') % ); @@ -549,7 +550,7 @@ % # Show the jump to anchor.
<%= maketext('Note: grading the test grades all problems, not just those on this page.') %>
% } % - % if ($c->{can}{showProblemGrader}) { + % if ($c->{can}{showCorrectAnswers}) { % } % diff --git a/templates/ContentGenerator/GatewayQuiz/nav.html.ep b/templates/ContentGenerator/GatewayQuiz/nav.html.ep index f4b83b096c..3a3f7aca81 100644 --- a/templates/ContentGenerator/GatewayQuiz/nav.html.ep +++ b/templates/ContentGenerator/GatewayQuiz/nav.html.ep @@ -9,28 +9,36 @@