Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion htdocs/js/GatewayQuiz/gateway.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
137 changes: 137 additions & 0 deletions htdocs/js/ProblemGrader/singleproblemgrader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
});
}
})();
11 changes: 11 additions & 0 deletions htdocs/js/System/system.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion lib/WeBWorK/ConfigValues.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand Down
149 changes: 76 additions & 73 deletions lib/WeBWorK/ContentGenerator/GatewayQuiz.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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 => ''
Expand All @@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -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}),
Expand Down
Loading