Skip to content

Commit b28001b

Browse files
committed
Add point input to ProblemGrader.
Add a point input field to set the problem score in the ProblemGrader. This is the same as what is being used in the SingleProblemGrader, and honors the same setting to show it or not. The input uses JavaScript to update the actual score which is what is submitted when the grader is saved. If the percent score is not shown, a hidden field is used instead. This also adds a step of 1 to the percent score and validation on both the percent score and point score values.
1 parent 3567f60 commit b28001b

File tree

5 files changed

+132
-36
lines changed

5 files changed

+132
-36
lines changed

htdocs/js/ProblemGrader/problemgrader.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,45 @@
11
'use strict';
22

33
(() => {
4+
const setPointInputValue = (pointInput, score) =>
5+
(pointInput.value = parseFloat(
6+
(Math.round((score * pointInput.max) / 100 / pointInput.step) * pointInput.step).toFixed(2)
7+
));
8+
9+
// Update problem score if point value changes and is a valid value.
10+
for (const pointInput of document.querySelectorAll('.problem-points')) {
11+
pointInput.addEventListener('input', () => {
12+
const userId = pointInput.id.replace(/\.points$/, '');
13+
if (pointInput.checkValidity()) {
14+
const scoreInput = document.getElementById(`${userId}.score`);
15+
if (scoreInput) {
16+
scoreInput.classList.remove('is-invalid');
17+
scoreInput.value = Math.round((100 * pointInput.value) / pointInput.max);
18+
}
19+
pointInput.classList.remove('is-invalid');
20+
} else {
21+
pointInput.classList.add('is-invalid');
22+
}
23+
});
24+
}
25+
26+
// Update problem points if score changes and is a valid value.
27+
for (const scoreInput of document.querySelectorAll('.problem-score')) {
28+
scoreInput.addEventListener('input', () => {
29+
const userId = scoreInput.id.replace(/\.score$/, '');
30+
if (scoreInput.checkValidity()) {
31+
const pointInput = document.getElementById(`${userId}.points`);
32+
if (pointInput) {
33+
pointInput.classList.remove('is-invalid');
34+
pointInput.value = setPointInputValue(pointInput, scoreInput.value);
35+
}
36+
scoreInput.classList.remove('is-invalid');
37+
} else {
38+
scoreInput.classList.add('is-invalid');
39+
}
40+
});
41+
}
42+
443
const userSelect = document.getElementById('student_selector');
544
if (!userSelect) return;
645

lib/WeBWorK/ConfigValues.pm

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -776,12 +776,12 @@ sub getConfigValues ($ce) {
776776
},
777777
{
778778
var => 'problemGraderScore',
779-
doc => x('Method to enter problem scores in the single problem manual grader'),
779+
doc => x('Method to enter problem scores in the manual problem graders'),
780780
doc2 => x(
781-
'This configures if the single problem manual grader has inputs to enter problem scores as '
782-
. 'a percent, a point value, or both. Note, the problem score is always saved as a '
783-
. 'percent, so when using a point value, the problem score will be rounded to the '
784-
. 'nearest whole percent.'
781+
'This configures if the manual problem grader or single problem grader has inputs to enter '
782+
. 'problem scores as a percent, a point value, or both. Note, the problem score is always '
783+
. 'saved as a percent, so when using a point value, the problem score will be rounded to '
784+
. 'the nearest whole percent.'
785785
),
786786
values => [qw(Percent Point Both)],
787787
type => 'popuplist'

lib/WeBWorK/Utils.pm

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ our @EXPORT_OK = qw(
3232
generateURLs
3333
formatEmailSubject
3434
getAssetURL
35+
points_stepsize
36+
round_nearest_stepsize
3537
x
3638
);
3739

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

538+
sub points_stepsize ($points) {
539+
my $stepsize;
540+
if ($points == 1) {
541+
$stepsize = 0.01;
542+
} elsif ($points <= 5) {
543+
$stepsize = 0.05;
544+
} elsif ($points <= 10) {
545+
$stepsize = 0.1;
546+
} elsif ($points <= 25) {
547+
$stepsize = 0.25;
548+
} elsif ($points <= 50) {
549+
$stepsize = 0.5;
550+
} else {
551+
$stepsize = int(($points - 1) / 100) + 1;
552+
}
553+
return $stepsize;
554+
}
555+
556+
sub round_nearest_stepsize ($score, $stepsize) {
557+
return wwRound(2, wwRound(0, $score / $stepsize) * $stepsize);
558+
}
559+
536560
sub x (@args) { return @args }
537561

538562
1;
@@ -763,6 +787,22 @@ Returns the URL for the asset specified in C<$file>. If C<$isThemeFile> is
763787
true, then the asset will be assumed to be located in a theme directory. The
764788
parameter C<$ce> must be a valid C<WeBWorK::CourseEnvironment> object.
765789
790+
=head2 points_stepsize
791+
792+
Usage: C<points_stepsize($points)>
793+
794+
Returns a reasonable stepsize that best converts between a whole percent and
795+
a point value. The stepsize is the point value that is close to but greater
796+
than or equal to 1% per step. This is done by first using preset values of
797+
0.01, 0.05, 0.1, 0.25, or 0.5, then using only whole point values, such that
798+
the stepsize is greater than or equal to 1% of total points.
799+
800+
=head2 round_nearest_stepsize
801+
802+
Usage: C<round_nearest_stepsize($score, $stepsize)>
803+
804+
Returns the value of the score rounded to the nearest stepsize.
805+
766806
=head2 x
767807
768808
Usage: C<x(@args)>

templates/ContentGenerator/Instructor/ProblemGrader.html.ep

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
% use WeBWorK::Utils qw(wwRound getAssetURL);
1+
% use WeBWorK::Utils qw(wwRound getAssetURL points_stepsize round_nearest_stepsize);
22
% require WeBWorK::PG;
33
%
44
% content_for js => begin
@@ -122,11 +122,17 @@
122122
<%= check_box 'select-all' => 'on', id => 'select-all', class => 'select-all form-check-input',
123123
data => { select_group => 'mark_correct' } =%>
124124
</th>
125-
<th id="score-header"><%= maketext('Score (%)') %></th>
125+
% unless ($ce->{problemGraderScore} eq 'Point') {
126+
<th id="score-header"><%= maketext('Score (%)') %></th>
127+
% }
128+
% unless ($ce->{problemGraderScore} eq 'Percent') {
129+
<th id="point-header"><%= maketext('Points (0 - [_1])', $problem->value) %></th>
130+
% }
126131
<th id="comment-header"><%= maketext('Comment') %></th>
127132
</tr>
128133
</thead>
129134
<tbody>
135+
% my $stepSize = points_stepsize($problem->value);
130136
% for my $user (@$users) {
131137
% my $userID = $user->user_id;
132138
%
@@ -206,14 +212,42 @@
206212
class => 'mark_correct form-check-input',
207213
'aria-labelledby' => 'mark-all-correct-header' =%>
208214
</td>
209-
<td class="restricted-width-col">
210-
% param("$userID.$versionID.score", undef);
211-
<%= number_field "$userID.$versionID.score" =>
212-
wwRound(0, $_->{problem}->status * 100),
213-
class => 'score-selector form-control form-control-sm restricted-width-col',
214-
style => 'width:6.5rem;', min => 0, max => 100, autocomplete => 'off',
215-
'aria-labelledby' => 'score-header' =%>
216-
</td>
215+
% unless ($ce->{problemGraderScore} eq 'Point') {
216+
<td class="restricted-width-col">
217+
% param("$userID.$versionID.score", undef);
218+
<%= number_field "$userID.$versionID.score" => wwRound(0, $_->{problem}->status * 100),
219+
id => "$userID.$versionID.score",
220+
class => 'problem-score form-control form-control-sm restricted-width-col',
221+
style => 'width:6.5rem;', min => 0, max => 100, step => 1,
222+
autocomplete => 'off', 'aria-labelledby' => 'score-header' =%>
223+
</td>
224+
% }
225+
% unless ($ce->{problemGraderScore} eq 'Percent') {
226+
% my $problemValue = $_->{problem}->value || $problem->value;
227+
% my $useUserValue = $problemValue != $problem->value;
228+
% my $thisStepSize = $useUserValue ? points_stepsize($problemValue) : $stepSize;
229+
<td class="restricted-width-col">
230+
% if ($ce->{problemGraderScore} eq 'Point') {
231+
% param("$userID.$versionID.score", undef);
232+
<%= hidden_field "$userID.$versionID.score" => wwRound(0, $_->{problem}->status * 100),
233+
id => "$userID.$versionID.score" %>
234+
% }
235+
% param("$userID.$versionID.points", undef);
236+
<%= number_field "$userID.$versionID.points" => round_nearest_stepsize(
237+
$_->{problem}->status * $problemValue, $thisStepSize),
238+
id => "$userID.$versionID.points",
239+
class => 'problem-points form-control form-control-sm restricted-width-col',
240+
style => 'width:6.5rem;',
241+
min => 0,
242+
max => $problemValue,
243+
step => $thisStepSize,
244+
autocomplete => 'off',
245+
'aria-labelledby' => 'point-header' =%>
246+
% if ($useUserValue) {
247+
<strong><%= maketext('Points (0 - [_1])', $problemValue) =%></strong>
248+
% }
249+
</td>
250+
% }
217251
<td class="grader-comment-column">
218252
% if (defined $_->{past_answer}) {
219253
<%= text_area "$userID.$versionID.comment" => $_->{past_answer}->comment_string,

templates/HTML/SingleProblemGrader/grader.html.ep

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
% use WeBWorK::Utils 'wwRound';
1+
% use WeBWorK::Utils qw(wwRound points_stepsize round_nearest_stepsize);
22
%
33
% if (!stash->{jsInserted}) {
44
% stash->{jsInserted} = 1;
@@ -82,28 +82,11 @@
8282
%
8383
% # Total point value. Show only if configured to.
8484
% unless ($ce->{problemGraderScore} eq 'Percent') {
85-
% # Compute a reasonable step size based on point value.
86-
% # First use some preset nice values, then only use whole
87-
% # point values, such that the step size >= 1% of total.
88-
% my $stepSize;
89-
% if ($grader->{problem_value} == 1) {
90-
% $stepSize = 0.01;
91-
% } elsif ($grader->{problem_value} <= 5) {
92-
% $stepSize = 0.05;
93-
% } elsif ($grader->{problem_value} <= 10) {
94-
% $stepSize = 0.1;
95-
% } elsif ($grader->{problem_value} <= 25) {
96-
% $stepSize = 0.25;
97-
% } elsif ($grader->{problem_value} <= 50) {
98-
% $stepSize = 0.5;
99-
% } else {
100-
% $stepSize = int(($grader->{problem_value} - 1) / 100) + 1;
101-
% }
102-
% # Round point score to the nearest $stepSize.
85+
% my $stepSize = points_stepsize($grader->{problem_value});
10386
% my $recordedPoints =
104-
% wwRound(2, wwRound(0, $grader->{recorded_score} * $grader->{problem_value} / $stepSize) * $stepSize);
87+
% round_nearest_stepsize($grader->{recorded_score} * $grader->{problem_value}, $stepSize);
10588
% my $currentPoints =
106-
% wwRound(2, wwRound(0, $rawCurrentScore / 100 * $grader->{problem_value} / $stepSize) * $stepSize);
89+
% round_nearest_stepsize($rawCurrentScore / 100 * $grader->{problem_value}, $stepSize);
10790
% param('grader-problem-points', $recordedPoints);
10891
<div class="row align-items-center mb-2">
10992
<%= label_for "score_problem$grader->{problem_id}_points",

0 commit comments

Comments
 (0)