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
39 changes: 39 additions & 0 deletions htdocs/js/ProblemGrader/problemgrader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,45 @@
'use strict';

(() => {
const setPointInputValue = (pointInput, score) =>
(pointInput.value = parseFloat(
(Math.round((score * pointInput.max) / 100 / pointInput.step) * pointInput.step).toFixed(2)
));

// Update problem score if point value changes and is a valid value.
for (const pointInput of document.querySelectorAll('.problem-points')) {
pointInput.addEventListener('input', () => {
const userId = pointInput.id.replace(/\.points$/, '');
if (pointInput.checkValidity()) {
const scoreInput = document.getElementById(`${userId}.score`);
if (scoreInput) {
scoreInput.classList.remove('is-invalid');
scoreInput.value = Math.round((100 * pointInput.value) / pointInput.max);
}
pointInput.classList.remove('is-invalid');
} else {
pointInput.classList.add('is-invalid');
}
});
}

// Update problem points if score changes and is a valid value.
for (const scoreInput of document.querySelectorAll('.problem-score')) {
scoreInput.addEventListener('input', () => {
const userId = scoreInput.id.replace(/\.score$/, '');
if (scoreInput.checkValidity()) {
const pointInput = document.getElementById(`${userId}.points`);
if (pointInput) {
pointInput.classList.remove('is-invalid');
pointInput.value = setPointInputValue(pointInput, scoreInput.value);
}
scoreInput.classList.remove('is-invalid');
} else {
scoreInput.classList.add('is-invalid');
}
});
}

const userSelect = document.getElementById('student_selector');
if (!userSelect) return;

Expand Down
10 changes: 5 additions & 5 deletions lib/WeBWorK/ConfigValues.pm
Original file line number Diff line number Diff line change
Expand Up @@ -776,12 +776,12 @@ sub getConfigValues ($ce) {
},
{
var => 'problemGraderScore',
doc => x('Method to enter problem scores in the single problem manual grader'),
doc => x('Method to enter problem scores in the manual problem graders'),
doc2 => x(
'This configures if the single problem manual grader has inputs to enter problem scores as '
. 'a percent, a point value, or both. Note, the problem score is always saved as a '
. 'percent, so when using a point value, the problem score will be rounded to the '
. 'nearest whole percent.'
'This configures if the manual problem grader or single problem grader has inputs to enter '
. 'problem scores as a percent, a point value, or both. Note, the problem score is always '
. 'saved as a percent, so when using a point value, the problem score will be rounded to '
. 'the nearest whole percent.'
),
values => [qw(Percent Point Both)],
type => 'popuplist'
Expand Down
9 changes: 5 additions & 4 deletions lib/WeBWorK/ContentGenerator/Instructor/ProblemGrader.pm
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ async sub initialize ($c) {
my $userID = $c->param('user');

# Make sure these are defined for the template.
$c->stash->{set} = $db->getGlobalSet($setID);
$c->stash->{problem} = $db->getGlobalProblem($setID, $problemID);
$c->stash->{users} = [];
$c->stash->{haveSections} = 0;
$c->stash->{set} = $db->getGlobalSet($setID);
$c->stash->{problem} = $db->getGlobalProblem($setID, $problemID);
$c->stash->{users} = [];
$c->stash->{haveSections} = 0;
$c->stash->{problem_value} = $c->stash->{problem}->value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't exactly see the point of adding this stash value. The problem is in the stash, and so the value can be accessed from there anywhere already. Stashing the problem value additionally is unnecessary.

Although, there is perhaps a bigger issue with this in the pull request in general. This is using the same problem value for all students. Yes, usually this is the same for all students, but it does not have to be. The problem value can be set per student. It is probably bad teaching practice to do so, but it is a supported override setting. So the merged user problem should be used for this instead. The single problem grader does use the user's specific value for the problem.

This probably means you cannot show the Points (0 - n) in the header for the column since n is not necessarily the same for all students. Furthermore, the stepsize for the number input will need to be set for each student separately.


return
unless $c->stash->{set}
Expand Down
40 changes: 40 additions & 0 deletions lib/WeBWorK/Utils.pm
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ our @EXPORT_OK = qw(
generateURLs
formatEmailSubject
getAssetURL
points_stepsize
round_nearest_stepsize
x
);

Expand Down Expand Up @@ -533,6 +535,28 @@ sub getAssetURL ($ce, $file, $isThemeFile = 0) {
return "$ce->{webworkURLs}{htdocs}/$file";
}

sub points_stepsize ($points) {
my $stepsize;
if ($points == 1) {
$stepsize = 0.01;
} elsif ($points <= 5) {
$stepsize = 0.05;
} elsif ($points <= 10) {
$stepsize = 0.1;
} elsif ($points <= 25) {
$stepsize = 0.25;
} elsif ($points <= 50) {
$stepsize = 0.5;
} else {
$stepsize = int(($points - 1) / 100) + 1;
}
return $stepsize;
}

sub round_nearest_stepsize ($score, $stepsize) {
return wwRound(2, wwRound(0, $score / $stepsize) * $stepsize);
}

sub x (@args) { return @args }

1;
Expand Down Expand Up @@ -763,6 +787,22 @@ Returns the URL for the asset specified in C<$file>. If C<$isThemeFile> is
true, then the asset will be assumed to be located in a theme directory. The
parameter C<$ce> must be a valid C<WeBWorK::CourseEnvironment> object.

=head2 points_stepsize

Usage: C<points_stepsize($points)>

Returns a reasonable stepsize that best converts between a whole percent and
a point value. The stepsize is the point value that is close to but greater
than or equal to 1% per step. This is done by first using preset values of
0.01, 0.05, 0.1, 0.25, or 0.5, then using only whole point values, such that
the stepsize is greater than or equal to 1% of total points.

=head2 round_nearest_stepsize

Usage: C<round_nearest_stepsize($score, $stepsize)>

Returns the value of the score rounded to the nearest stepsize.

=head2 x

Usage: C<x(@args)>
Expand Down
44 changes: 34 additions & 10 deletions templates/ContentGenerator/Instructor/ProblemGrader.html.ep
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
% use WeBWorK::Utils qw(wwRound getAssetURL);
% use WeBWorK::Utils qw(wwRound getAssetURL points_stepsize round_nearest_stepsize);
% require WeBWorK::PG;
%
% content_for js => begin
Expand Down Expand Up @@ -122,11 +122,17 @@
<%= check_box 'select-all' => 'on', id => 'select-all', class => 'select-all form-check-input',
data => { select_group => 'mark_correct' } =%>
</th>
<th id="score-header"><%= maketext('Score (%)') %></th>
% unless ($ce->{problemGraderScore} eq 'Point') {
<th id="score-header"><%= maketext('Score (%)') %></th>
% }
% unless ($ce->{problemGraderScore} eq 'Percent') {
<th id="point-header"><%= maketext('Points (0 - [_1])', $problem_value) %></th>
% }
<th id="comment-header"><%= maketext('Comment') %></th>
</tr>
</thead>
<tbody>
% my $stepSize = points_stepsize($problem_value);
% for my $user (@$users) {
% my $userID = $user->user_id;
%
Expand Down Expand Up @@ -206,14 +212,32 @@
class => 'mark_correct form-check-input',
'aria-labelledby' => 'mark-all-correct-header' =%>
</td>
<td class="restricted-width-col">
% param("$userID.$versionID.score", undef);
<%= number_field "$userID.$versionID.score" =>
wwRound(0, $_->{problem}->status * 100),
class => 'score-selector form-control form-control-sm restricted-width-col',
style => 'width:6.5rem;', min => 0, max => 100, autocomplete => 'off',
'aria-labelledby' => 'score-header' =%>
</td>
% unless ($ce->{problemGraderScore} eq 'Point') {
<td class="restricted-width-col">
% param("$userID.$versionID.score", undef);
<%= number_field "$userID.$versionID.score" => wwRound(0, $_->{problem}->status * 100),
id => "$userID.$versionID.score",
class => 'problem-score form-control form-control-sm restricted-width-col',
style => 'width:6.5rem;', min => 0, max => 100, step => 1,
autocomplete => 'off', 'aria-labelledby' => 'score-header' =%>
</td>
% }
% unless ($ce->{problemGraderScore} eq 'Percent') {
<td class="restricted-width-col">
% if ($ce->{problemGraderScore} eq 'Point') {
% param("$userID.$versionID.score", undef);
<%= hidden_field "$userID.$versionID.score" => wwRound(0, $_->{problem}->status * 100),
id => "$userID.$versionID.score" %>
% }
% param("$userID.$versionID.points", undef);
<%= number_field "$userID.$versionID.points" =>
round_nearest_stepsize($_->{problem}->status * $problem_value, $stepSize),
id => "$userID.$versionID.points",
class => 'problem-points form-control form-control-sm restricted-width-col',
style => 'width:6.5rem;', min => 0, max => $problem_value, step => $stepSize,
autocomplete => 'off', 'aria-labelledby' => 'point-header' =%>
</td>
% }
<td class="grader-comment-column">
% if (defined $_->{past_answer}) {
<%= text_area "$userID.$versionID.comment" => $_->{past_answer}->comment_string,
Expand Down
25 changes: 4 additions & 21 deletions templates/HTML/SingleProblemGrader/grader.html.ep
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
% use WeBWorK::Utils 'wwRound';
% use WeBWorK::Utils qw(wwRound points_stepsize round_nearest_stepsize);
%
% if (!stash->{jsInserted}) {
% stash->{jsInserted} = 1;
Expand Down Expand Up @@ -82,28 +82,11 @@
%
% # Total point value. Show only if configured to.
% unless ($ce->{problemGraderScore} eq 'Percent') {
% # Compute a reasonable step size based on point value.
% # First use some preset nice values, then only use whole
% # point values, such that the step size >= 1% of total.
% my $stepSize;
% if ($grader->{problem_value} == 1) {
% $stepSize = 0.01;
% } elsif ($grader->{problem_value} <= 5) {
% $stepSize = 0.05;
% } elsif ($grader->{problem_value} <= 10) {
% $stepSize = 0.1;
% } elsif ($grader->{problem_value} <= 25) {
% $stepSize = 0.25;
% } elsif ($grader->{problem_value} <= 50) {
% $stepSize = 0.5;
% } else {
% $stepSize = int(($grader->{problem_value} - 1) / 100) + 1;
% }
% # Round point score to the nearest $stepSize.
% my $stepSize = points_stepsize($grader->{problem_value});
% my $recordedPoints =
% wwRound(2, wwRound(0, $grader->{recorded_score} * $grader->{problem_value} / $stepSize) * $stepSize);
% round_nearest_stepsize($grader->{recorded_score} * $grader->{problem_value}, $stepSize);
% my $currentPoints =
% wwRound(2, wwRound(0, $rawCurrentScore / 100 * $grader->{problem_value} / $stepSize) * $stepSize);
% round_nearest_stepsize($rawCurrentScore / 100 * $grader->{problem_value}, $stepSize);
% param('grader-problem-points', $recordedPoints);
<div class="row align-items-center mb-2">
<%= label_for "score_problem$grader->{problem_id}_points",
Expand Down