From 2ed9374bae55ad3e371c9a6c692089efee20f827 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Mon, 29 Dec 2025 17:42:16 -0600 Subject: [PATCH 1/2] Rework the single problem grader interface. The problem grader is now always visible for users that have the permission to use it and in the case that they are acting for another user. This does mean that there is no way to open the problem grader when viewing your own problem. However, the problem grader is in a collapse. The state of the collapse is stored in local storage, and whenever you open another problem or change effective users, the collapse goes back to the state that it was in the last time that you had a page open that showed the problem grader. Correct answers in feedback are now always shown with the reveal button, even when the problem grader is on the page. However, the reveal button is removed by JavaScript behind the scenes while the problem grader is expanded, and put back if the feedback button is not opened while the problem grader is open. So if you open a feedback button while the problem grader is open, the reveal button is not shown, and the correct answer is immediately visible. To summarize the reveal button visibility, the reveal button will not be shown anytime that a feedback button is opened while the problem grader is open, and in that case will never return until the page reloads, but any feedback button that is not opened while the problem grader is open will still show the reveal button, and as usual once the reveal button is used, it will never come back until the page is reloaded. The problem grader is now below the problem in homework sets as it is in tests. With the collapse and the grader always in the page, I really do not want it above the problem as it currently is. The original reason for the problem grader being on top was so that it would be close to the old results table with the answers. With that gone, that reason no longer applies. Also remove the code for the `output_hidden_info` method in the `Problem.html.ep` template. This is because the answer to the question `$c->can('output_hidden_info')` is `$c` can't. There is no such method anywhere in the code anymore. --- htdocs/js/GatewayQuiz/gateway.scss | 2 +- .../js/ProblemGrader/singleproblemgrader.js | 137 ++++++++++++++++++ htdocs/js/System/system.scss | 11 ++ lib/WeBWorK/ConfigValues.pm | 3 +- lib/WeBWorK/ContentGenerator/GatewayQuiz.pm | 10 +- lib/WeBWorK/ContentGenerator/Problem.pm | 39 ++--- lib/WeBWorK/ContentGenerator/ShowMeAnother.pm | 7 +- lib/WeBWorK/Utils.pm | 1 - .../ContentGenerator/GatewayQuiz.html.ep | 21 +-- .../ContentGenerator/GatewayQuiz/nav.html.ep | 28 ++-- .../Instructor/ProblemGrader.html.ep | 8 +- templates/ContentGenerator/Problem.html.ep | 16 +- .../Problem/instructor_buttons.html.ep | 18 +-- .../Problem/student_nav.html.ep | 31 +--- .../HTML/SingleProblemGrader/grader.html.ep | 5 +- 15 files changed, 209 insertions(+), 128 deletions(-) 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..e4088251ce 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'), @@ -1491,10 +1488,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..f18266f55d 100644 --- a/lib/WeBWorK/ContentGenerator/Problem.pm +++ b/lib/WeBWorK/ContentGenerator/Problem.pm @@ -431,20 +431,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 +578,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 +719,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 +789,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 +800,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) ) @@ -970,10 +964,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', @@ -1133,10 +1126,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 +1435,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/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/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.
" tabindex="-1"><%= $recordMessage %>
% # Output the problem header. -

<%= maketext('Problem [_1].', $i + 1) %>

+

<%= maketext('Problem [_1].', $i + 1) %>

% if ($c->{can}{showTemplateIds}) { <%= '(' @@ -738,20 +739,10 @@

<%= maketext('Note: grading the test grades all problems, not just those on this page.') %>

% } % - % if ($c->{can}{showProblemGrader}) { + % if ($c->{can}{showCorrectAnswers}) {
- % if ($c->{can}{showCorrectAnswers}) { - <%= submit_button maketext('Show Correct Answers'), name => 'showCorrectAnswers', - class => 'btn btn-primary mb-1' =%> - % } - % if ($c->{will}{showProblemGrader}) { - <%= submit_button maketext('Hide Problem Graders'), name => 'hideProblemGrader', - class => 'btn btn-primary mb-1' =%> - <%= hidden_field showProblemGrader => 1 =%> - % } else { - <%= submit_button maketext('Show Problem Graders'), name => 'showProblemGrader', - class => 'btn btn-primary mb-1' =%> - % } + <%= submit_button maketext('Show Correct Answers'), name => 'showCorrectAnswers', + class => 'btn btn-primary mb-1' =%>
% } % diff --git a/templates/ContentGenerator/GatewayQuiz/nav.html.ep b/templates/ContentGenerator/GatewayQuiz/nav.html.ep index f4b83b096c..51eb9de764 100644 --- a/templates/ContentGenerator/GatewayQuiz/nav.html.ep +++ b/templates/ContentGenerator/GatewayQuiz/nav.html.ep @@ -13,9 +13,8 @@ <%= link_to $c->systemLink( url_for('gateway_quiz', setID => "$setID,v$prevTest->{setVersion}"), params => { - effectiveUser => $prevTest->user_id, - currentPage => $pageNumber, - showProblemGrader => $c->{will}{showProblemGrader}, + effectiveUser => $prevTest->user_id, + currentPage => $pageNumber, $filter ? (studentNavFilter => $filter) : () } ), @@ -42,9 +41,8 @@ <%= link_to "$_->{displayName} (version $_->{setVersion})" => $c->systemLink( url_for('gateway_quiz', setID => "$setID,v$_->{setVersion}"), params => { - effectiveUser => $_->user_id, - currentPage => $pageNumber, - showProblemGrader => $c->{will}{showProblemGrader}, + effectiveUser => $_->user_id, + currentPage => $pageNumber, $filter ? (studentNavFilter => $filter) : () } ), @@ -59,9 +57,8 @@ <%= link_to $c->systemLink( url_for('gateway_quiz', setID => "$setID,v$nextTest->{setVersion}"), params => { - effectiveUser => $nextTest->user_id, - currentPage => $pageNumber, - showProblemGrader => $c->{will}{showProblemGrader}, + effectiveUser => $nextTest->user_id, + currentPage => $pageNumber, $filter ? (studentNavFilter => $filter) : () } ), @@ -90,11 +87,7 @@
  • <%= link_to maketext('Show all tests') => $c->systemLink( url_for('gateway_quiz', setID => "$setID,v$setVersion"), - params => { - effectiveUser => param('effectiveUser'), - currentPage => $pageNumber, - showProblemGrader => $c->{will}{showProblemGrader} - } + params => { effectiveUser => param('effectiveUser'), currentPage => $pageNumber } ), class => 'dropdown-item' =%>
  • @@ -104,10 +97,9 @@ <%= link_to $filters->{$_}[0] => $c->systemLink( url_for('gateway_quiz', setID => "$setID,v$filters->{$_}[2]"), params => { - effectiveUser => $filters->{$_}[1], - currentPage => $pageNumber, - showProblemGrader => $c->{will}{showProblemGrader}, - studentNavFilter => $_ + effectiveUser => $filters->{$_}[1], + currentPage => $pageNumber, + studentNavFilter => $_ } ), class => 'dropdown-item', diff --git a/templates/ContentGenerator/Instructor/ProblemGrader.html.ep b/templates/ContentGenerator/Instructor/ProblemGrader.html.ep index d6e4c804ab..920182f8a7 100644 --- a/templates/ContentGenerator/Instructor/ProblemGrader.html.ep +++ b/templates/ContentGenerator/Instructor/ProblemGrader.html.ep @@ -138,16 +138,12 @@ % if ($versionID) { % $problemLink = $c->systemLink( % url_for('gateway_quiz', setID => "$setID,v$versionID"), - % params => { - % effectiveUser => $userID, - % currentPage => $_->{pageNumber}, - % showProblemGrader => 1 - % } + % params => { effectiveUser => $userID, currentPage => $_->{pageNumber} } % )->fragment("prob$_->{problemNumber}"); % } else { % $problemLink = $c->systemLink( % url_for('problem_detail'), - % params => { effectiveUser => $userID, showProblemGrader => 1 } + % params => { effectiveUser => $userID } % )->fragment('problem_body'); % } diff --git a/templates/ContentGenerator/Problem.html.ep b/templates/ContentGenerator/Problem.html.ep index 83b7773faa..c36a3a68e0 100644 --- a/templates/ContentGenerator/Problem.html.ep +++ b/templates/ContentGenerator/Problem.html.ep @@ -71,23 +71,19 @@
    <%= $c->output_achievement_message %>
    <%= $c->output_comments %>
    -
    <%= $c->output_grader %>
    <%= form_for current_route, method => 'POST', name => 'problemMainForm', id => 'problemMainForm', class => 'problem-main-form', begin =%> <%= $c->hidden_authen_fields =%> <%= hidden_field(startTime => param('startTime') || time) =%> - <% if ($c->can('output_hidden_info')) { - <%= $c->output_hidden_info =%> - % } -
    -
    output_problem_lang_and_dir %>> - <%= $c->output_problem_body =%> -
    - <%= $c->output_message =%> + <%= hidden_field(courseID => $c->ce->{courseName}) =%> +
    output_problem_lang_and_dir %>> + <%= $c->output_problem_body =%>
    + <%= $c->output_message =%> + <%= $c->output_grader %> <%= $c->output_checkboxes %>
    <%= $c->output_submit_buttons %>
    <%= include 'ContentGenerator/Problem/instructor_buttons' %> diff --git a/templates/ContentGenerator/Problem/instructor_buttons.html.ep b/templates/ContentGenerator/Problem/instructor_buttons.html.ep index 1462957186..c336324f87 100644 --- a/templates/ContentGenerator/Problem/instructor_buttons.html.ep +++ b/templates/ContentGenerator/Problem/instructor_buttons.html.ep @@ -1,18 +1,8 @@ % last unless $authz->hasPermissions(param('user'), 'access_instructor_tools'); % -
    - % if ($c->{can}{showCorrectAnswers}) { +% if ($c->{can}{showCorrectAnswers}) { +
    <%= submit_button maketext('Show Correct Answers'), name => 'showCorrectAnswers', class => 'btn btn-primary mb-1' =%> - % } - % if ($c->{can}{showProblemGrader} && !$c->{will}{showMeAnother}) { - % if ($c->{will}{showProblemGrader}) { - <%= submit_button maketext('Hide Problem Grader'), name => 'hideProblemGrader', - class => 'btn btn-primary mb-1' =%> - <%= hidden_field showProblemGrader => 1 =%> - % } else { - <%= submit_button maketext('Show Problem Grader'), name => 'showProblemGrader', - class => 'btn btn-primary mb-1' =%> - % } - % } -
    +
    +% } diff --git a/templates/ContentGenerator/Problem/student_nav.html.ep b/templates/ContentGenerator/Problem/student_nav.html.ep index 5d4c09c79c..d12b078a97 100644 --- a/templates/ContentGenerator/Problem/student_nav.html.ep +++ b/templates/ContentGenerator/Problem/student_nav.html.ep @@ -9,11 +9,7 @@ % if ($prevUser) { <%= link_to $c->systemLink( $problemPage, - params => { - effectiveUser => $prevUser->user_id, - showProblemGrader => $c->{will}{showProblemGrader}, - $filter ? (studentNavFilter => $filter) : () - } + params => { effectiveUser => $prevUser->user_id, $filter ? (studentNavFilter => $filter) : () } ), data => { bs_toggle => 'tooltip', bs_placement => 'top' }, title => $prevUser->{displayName}, @@ -33,11 +29,7 @@
  • <%= link_to $_->{displayName} => $c->systemLink( $problemPage, - params => { - effectiveUser => $_->user_id, - showProblemGrader => $c->{will}{showProblemGrader}, - $filter ? (studentNavFilter => $filter) : () - } + params => { effectiveUser => $_->user_id, $filter ? (studentNavFilter => $filter) : () } ), $_->{currentUser} ? (style => 'background-color:#8F8') : (), class => 'dropdown-item' =%> @@ -49,8 +41,7 @@ <%= link_to $c->systemLink( $problemPage, params => { - effectiveUser => $nextUser->user_id, - showProblemGrader => $c->{will}{showProblemGrader}, + effectiveUser => $nextUser->user_id, $filter ? (studentNavFilter => $filter) : () } ), @@ -74,24 +65,16 @@ % # If a filter is currently in use, then add an item that will remove that filter. % if ($filter) {
  • - <%= link_to maketext('Show all students') => $c->systemLink( - $problemPage, - params => { - effectiveUser => $eUserID, - showProblemGrader => $c->{will}{showProblemGrader} - } - ), class => 'dropdown-item' =%> + <%= link_to maketext('Show all students') => + $c->systemLink($problemPage, params => { effectiveUser => $eUserID }), + class => 'dropdown-item' =%>
  • % } % for (sort keys %$filters) {
  • <%= link_to $filters->{$_}[0] => $c->systemLink( $problemPage, - params => { - effectiveUser => $filters->{$_}[1], - showProblemGrader => $c->{will}{showProblemGrader}, - studentNavFilter => $_ - } + params => { effectiveUser => $filters->{$_}[1], studentNavFilter => $_ } ), ($filter || '') eq $_ ? (style => 'background-color:#8F8') : (), class => 'dropdown-item' =%> diff --git a/templates/HTML/SingleProblemGrader/grader.html.ep b/templates/HTML/SingleProblemGrader/grader.html.ep index b5caad01ba..464c3bfb5c 100644 --- a/templates/HTML/SingleProblemGrader/grader.html.ep +++ b/templates/HTML/SingleProblemGrader/grader.html.ep @@ -7,8 +7,8 @@ % end % } % -
    -
    +
    % my $currentScore = 0; % my $rawCurrentScore = 0; @@ -280,5 +280,4 @@
    -
  • From 1510b6ad63214bc2aa8155b0aa734678619d8e05 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Wed, 14 Jan 2026 21:17:09 -0600 Subject: [PATCH 2/2] Make it much easier to act as a student. This is achieved by displaying the student nav on the problem set page for users that have the permission to act as a user as well as always showing the student nav in problems and in tests. If you are acting as a user, then the student nav looks the same as before with the next and previous buttons and the name of the user that is currently being acted as shown on the button. However, if you are not currently acting as another user, then the next and previous buttons are not shown and the button says "Select Student to Act As" (or in a test it says "Select a Test to Review"). Also fix the breadcrumb in a test when the set is not valid, but the setID does have the `setID,v?` format. Currently if you are acting as a student user and reviewing a test version, say "test,v1", but you have not worked the test, and you click the "Stop Acting" button, then the message stating that the selected test is not valid for the user is shown (it would be nice to not show this even) and the breadcrumb ends with the inactive link "test,v1", and you can only go back to the assignments page. Now, the "setID" link is shown and works, and the inactive "v1" link is at the end. This is built on top of #2875 since it would conflict rather heavily with that pull request if it were not. --- lib/WeBWorK/ContentGenerator/GatewayQuiz.pm | 139 +++++++++--------- lib/WeBWorK/ContentGenerator/Problem.pm | 73 +-------- lib/WeBWorK/ContentGenerator/ProblemSet.pm | 22 ++- lib/WeBWorK/HTML/StudentNav.pm | 83 +++++++++++ .../ContentGenerator/GatewayQuiz/nav.html.ep | 88 ++++++----- .../StudentNav}/student_nav.html.ep | 84 ++++++----- 6 files changed, 271 insertions(+), 218 deletions(-) create mode 100644 lib/WeBWorK/HTML/StudentNav.pm rename templates/{ContentGenerator/Problem => HTML/StudentNav}/student_nav.html.ep (52%) diff --git a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm index e4088251ce..147d4bda71 100644 --- a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm +++ b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm @@ -1340,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 => '' @@ -1356,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'; @@ -1365,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) { diff --git a/lib/WeBWorK/ContentGenerator/Problem.pm b/lib/WeBWorK/ContentGenerator/Problem.pm index f18266f55d..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 # @@ -836,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); @@ -974,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('') ); } 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/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/templates/ContentGenerator/GatewayQuiz/nav.html.ep b/templates/ContentGenerator/GatewayQuiz/nav.html.ep index 51eb9de764..3a3f7aca81 100644 --- a/templates/ContentGenerator/GatewayQuiz/nav.html.ep +++ b/templates/ContentGenerator/GatewayQuiz/nav.html.ep @@ -9,27 +9,36 @@