file.
diff --git a/lib/WeBWorK/Authz.pm b/lib/WeBWorK/Authz.pm
index a238f3fb67..784d97c8fb 100644
--- a/lib/WeBWorK/Authz.pm
+++ b/lib/WeBWorK/Authz.pm
@@ -46,7 +46,7 @@ use warnings;
use Carp qw/croak/;
use WeBWorK::Utils::DateTime qw(before);
-use WeBWorK::Utils::Sets qw(is_restricted);
+use WeBWorK::Utils::Sets qw(restricted_set_message);
use WeBWorK::Authen::Proctor;
use Net::IP;
use Scalar::Util qw(weaken);
@@ -414,34 +414,35 @@ sub checkSet {
# Cache the set for future use as needed. This should probably be more sophisticated than this.
$self->{merged_set} = $set;
+ # Save restricted set messages to show to instructors if they exist.
+ my $canViewUnopened = $self->hasPermissions($userName, "view_unopened_sets");
+ my @restrictedSetMessages;
+
# Now we know that the set is assigned to the appropriate user.
- # Check to see if the user is trying to access a set that is not open.
- if (
- before($set->open_date)
- && !$self->hasPermissions($userName, "view_unopened_sets")
- && !(
- defined $set->assignment_type
- && $set->assignment_type =~ /gateway/
- && $node_name eq 'problem_list'
- && $db->countSetVersions($effectiveUserName, $set->set_id)
- )
- )
- {
- return $c->maketext("Requested set '[_1]' is not yet open.", $setName);
- }
+ # $c->{viewSetCheck} is used to configure what is shown on ProblemSet page.
# Check to make sure that the set is visible, and that the user is allowed to view hidden sets.
my $visible = $set && $set->visible ne '0' && $set->visible ne '1' ? 1 : $set->visible;
if (!$visible && !$self->hasPermissions($userName, "view_hidden_sets")) {
+ $c->{viewSetCheck} = 'hidden';
+ return $c->maketext("Requested set '[_1]' is not available.", $setName);
+ }
+
+ # Check to see if the user is trying to access a set that is not open.
+ if (before($set->open_date) && !$canViewUnopened) {
+ $c->{viewSetCheck} = 'not-open';
return $c->maketext("Requested set '[_1]' is not available yet.", $setName);
}
# Check to see if conditional release conditions have been met.
- if ($ce->{options}{enableConditionalRelease}
- && is_restricted($db, $set, $effectiveUserName)
- && !$self->hasPermissions($userName, "view_unopened_sets"))
- {
- return $c->maketext("The prerequisite conditions have not been met for set '[_1]'.", $setName);
+ my $conditional_msg = restricted_set_message($c, $set, 'conditional');
+ if ($conditional_msg) {
+ if ($canViewUnopened) {
+ push(@restrictedSetMessages, $conditional_msg);
+ } else {
+ $c->{viewSetCheck} = 'restricted';
+ return $conditional_msg;
+ }
}
# Check to be sure that gateways are being sent to the correct content generator.
@@ -474,25 +475,27 @@ sub checkSet {
# Check for ip restrictions.
my $badIP = $self->invalidIPAddress($set);
- return $badIP if $badIP;
-
- # If LTI grade passback is enabled and set to 'homework' mode then we need to make sure that there is a sourcedid
- # for this set before students access it.
- my $LTIGradeMode = $ce->{LTIGradeMode} // '';
-
- if ($LTIGradeMode eq 'homework' && !$self->hasPermissions($userName, "view_unopened_sets")) {
- my $LMS =
- $ce->{LTI}{ $ce->{LTIVersion} }{LMS_url}
- ? $c->link_to($ce->{LTI}{ $ce->{LTIVersion} }{LMS_name} => $ce->{LTI}{ $ce->{LTIVersion} }{LMS_url})
- : $ce->{LTI}{ $ce->{LTIVersion} }{LMS_name};
- return $c->b($c->maketext(
- 'You must use your Learning Management System ([_1]) to access this set. '
- . 'Try logging in to the Learning Management System and visiting the set from there.',
- $LMS
- ))
- unless $set->lis_source_did || ($ce->{LTIVersion} eq 'v1p3' && $ce->{LTI}{v1p3}{ignoreMissingSourcedID});
+ if ($badIP) {
+ if ($self->hasPermissions($userName, 'view_ip_restricted_sets')) {
+ push(@restrictedSetMessages, $badIP);
+ } else {
+ $c->{viewSetCheck} = 'restricted';
+ return $badIP;
+ }
+ }
+
+ # Check for lis_source_did if LTI grade passback is 'homework'.
+ my $lti_msg = restricted_set_message($c, $set, 'lti');
+ if ($lti_msg) {
+ if ($canViewUnopened) {
+ push(@restrictedSetMessages, $lti_msg);
+ } else {
+ $c->{viewSetCheck} = 'restricted';
+ return $lti_msg;
+ }
}
+ $c->{restrictedSetMessages} = \@restrictedSetMessages if @restrictedSetMessages;
return 0;
}
@@ -514,8 +517,7 @@ sub invalidIPAddress {
return 0
if (!defined($set->restrict_ip)
|| $set->restrict_ip eq ''
- || $set->restrict_ip eq 'No'
- || $self->hasPermissions($userName, 'view_ip_restricted_sets'));
+ || $set->restrict_ip eq 'No');
my $clientIP = new Net::IP($c->tx->remote_address);
@@ -530,7 +532,9 @@ sub invalidIPAddress {
# if there are no addresses in the locations, return an error that
# says this
return $c->maketext(
- "Client ip address [_1] is not allowed to work this assignment, because the assignment has ip address restrictions and there are no allowed locations associated with the restriction. Contact your professor to have this problem resolved.",
+ 'Client ip address [_1] is not allowed to work this assignment, because the assignment has ip address '
+ . 'restrictions and there are no allowed locations associated with the restriction. Contact your '
+ . 'professor to have this problem resolved.',
$clientIP->ip()
) if (!@restrictAddresses);
@@ -552,17 +556,13 @@ sub invalidIPAddress {
# this is slightly complicated by having to check relax_restrict_ip
my $badIP = '';
if ($restrictType eq 'RestrictTo' && !$inRestrict) {
- $badIP =
- "Client ip address "
- . $clientIP->ip()
- . " is not in the list of addresses from "
- . "which this assignment may be worked.";
+ $badIP = $c->maketext(
+ 'Client ip address [_1] is not in the list of addresses from which this assignment may be worked.',
+ $clientIP->ip());
} elsif ($restrictType eq 'DenyFrom' && $inRestrict) {
- $badIP =
- "Client ip address "
- . $clientIP->ip()
- . " is in the list of addresses from "
- . "which this assignment may not be worked.";
+ $badIP = $c->maketext(
+ 'Client ip address [_1] is in the list of addresses from which this assignment may not be worked.',
+ $clientIP->ip());
} else {
return 0;
}
diff --git a/lib/WeBWorK/ConfigValues.pm b/lib/WeBWorK/ConfigValues.pm
index 411be4426c..5a61a7e860 100644
--- a/lib/WeBWorK/ConfigValues.pm
+++ b/lib/WeBWorK/ConfigValues.pm
@@ -213,7 +213,7 @@ sub getConfigValues ($ce) {
{
var => 'hardcopyThemePGEditor',
doc => x('Hardcopy Theme for Problem Editor'),
- doc2 => x('Choose a layout/styling theme for PDF hardcopy production from the Prooblem Editor.'),
+ doc2 => x('Choose a layout/styling theme for PDF hardcopy production from the Problem Editor.'),
values => [qw(empty.xml)],
type => 'popuplist',
hashVar => '{hardcopyThemePGEditor}'
@@ -278,8 +278,8 @@ sub getConfigValues ($ce) {
"Achievements are a way to gamify WeBWorK. In parallel to a student's regular scores on "
. 'assignments, they earn "achievement points" for (a) answering an exercise correctly, and '
. '(b) earning badges. Badges can be for tasks like earning 100% on three assignments, '
- . 'answering five questions correclty on the first attempt, etc. As students earn achivement '
- . 'points, they can "level up" as well. An instructor can manage Achievents using the '
+ . 'answering five questions correctly on the first attempt, etc. As students earn achievement '
+ . 'points, they can "level up" as well. An instructor can manage Achievements using the '
. 'Achievements Manager tool.'
),
type => 'boolean'
@@ -545,8 +545,8 @@ sub getConfigValues ($ce) {
var => 'permissionLevels{report_bugs}',
doc => x('Can report bugs'),
doc2 => x(
- 'Users with at least this permission level get a link in the left panel for reporting bugs to the '
- . 'bug tracking system at bugs.webwork.maa.org.'
+ 'Users with at least this permission level get a link in the left panel for reporting issues at '
+ . 'github.com/openwebwork/webwork2.'
),
type => 'permission'
},
@@ -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'
@@ -796,7 +796,7 @@ sub getConfigValues ($ce) {
. 'the "Check Answers" button. Or if that button is also not present, it will activate '
. 'the "Preview My Answers" button. A third option is "conservative". In this case, the '
. 'enter key behaves like "preview" when the "Submit" button is available and there are '
- . 'only finitely many attempts allowed. Otherise the enter key behaves like "submit". '
+ . 'only finitely many attempts allowed. Otherwise the enter key behaves like "submit". '
. 'Note that this is only affects homework problem pages, not test/quiz pages, and not '
. 'instructor pages like the PG Editor and the Library Browser.'
),
@@ -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'
}
@@ -829,13 +830,15 @@ sub getConfigValues ($ce) {
x('E-Mail'),
{
var => 'mail{feedbackSubjectFormat}',
- doc => x('Format for the subject line in feedback emails'),
+ doc => x('Format for the subject of feedback emails'),
doc2 => x(
- 'When students click the Email Instructor button to send feedback, WeBWorK fills in the '
- . 'subject line. Here you can set the subject line. In it, you can have various bits of '
- . 'information filled in with the following escape sequences.- %c = course ID
'
+ 'When students click the Email Instructor button to send feedback, WeBWorK fills in '
+ . 'the subject line. Here you can set the subject line. In it, you can have various bits of '
+ . 'information filled in with the following escape sequences.
- %c = course ID
'
. '- %u = user ID
- %s = set ID
- %p = problem ID
- %x = section
'
- . '- %r = recitation
- %% = literal percent sign
'
+ . '- %r = recitation
- %% = literal percent sign
If content is between '
+ . "a brace pair, like '{ rec:%r}', then it will only be included in the subject line if all "
+ . 'substitutions within the double brace pair are defined and nonempty.'
),
width => 45,
type => 'text'
@@ -1152,13 +1155,15 @@ sub getConfigValues ($ce) {
};
# Get the list of theme folders in the theme directory.
- my $themes = eval { path($ce->{webworkDirs}{themes})->list({ dir => 1 })->map('basename')->sort; };
+ my $themes = eval {
+ path($ce->{webworkDirs}{themes})->list({ dir => 1 })->grep(sub {-d})->map('basename')->sort;
+ };
die "can't opendir $ce->{webworkDirs}{themes}: $@" if $@;
# Get the list of all site hardcopy theme files.
my $hardcopyThemesSite =
eval { path($ce->{webworkDirs}{hardcopyThemes})->list->grep(qr/\.xml$/)->map('basename')->sort };
- die "Unabled to list files in $ce->{webworkDirs}{hardcopyThemes}: $@" if $@;
+ die "Unable to list files in $ce->{webworkDirs}{hardcopyThemes}: $@" if $@;
my $hardcopyThemesCourse = eval {
path($ce->{courseDirs}{hardcopyThemes})->list->grep(sub {
diff --git a/lib/WeBWorK/ContentGenerator.pm b/lib/WeBWorK/ContentGenerator.pm
index 3e069f2a03..2bd7756686 100644
--- a/lib/WeBWorK/ContentGenerator.pm
+++ b/lib/WeBWorK/ContentGenerator.pm
@@ -32,6 +32,7 @@ use MIME::Base64;
use Scalar::Util qw(weaken);
use HTML::Entities;
use Encode;
+use Mojo::JSON qw(encode_json true);
use WeBWorK::File::Scoring qw(parse_scoring_file);
use WeBWorK::Localize;
@@ -94,7 +95,7 @@ The method content() is called to send the page content to client.
async sub go ($c) {
my $ce = $c->ce;
- # If grades are being passed back to the lti, then peroidically update all of the
+ # If grades are being passed back to the lti, then periodically update all of the
# grades because things can get out of sync if instructors add or modify sets.
massUpdate($c) if $c->stash('courseID') && ref($c->db) && $ce->{LTIGradeMode};
@@ -109,8 +110,6 @@ async sub go ($c) {
my $tx = $c->render_later->tx;
- $c->stash->{footerWidthClass} = $c->can('info') ? 'col-md-8' : 'col-12';
-
if ($c->can('pre_header_initialize')) {
my $pre_header_initialize = $c->pre_header_initialize;
await $pre_header_initialize
@@ -132,6 +131,8 @@ async sub go ($c) {
await $initialize if ref $initialize eq 'Future' || ref $initialize eq 'Mojo::Promise';
}
+ $c->stash->{footerWidthClass} //= $c->can('info') ? 'col-md-8' : 'col-12';
+
$c->content;
# All content generator modules must have rendered at this point unless there was an error in which case an error
@@ -646,7 +647,7 @@ sub timestamp ($c) {
Defined in this package.
Print any messages (error or non-error) resulting from the last form submission.
-This could be used to give Sucess and Failure messages after an action is performed by a module.
+This could be used to give Success and Failure messages after an action is performed by a module.
The implementation in this package outputs the value of the field
$c->{status_message}, if it is present.
@@ -682,20 +683,19 @@ sub page_title ($c) {
return route_title($c, $c->current_route, 1);
}
-=item webwork_url
-
-Defined in this package.
-
-Outputs the $webwork_url defined in site.conf, unless $webwork_url is equal to
-"/", in which case this outputs the empty string.
+=item webwork_js_config
-This is used to set a value in a global webworkConfig javascript variable,
-that can be accessed in javascript files.
+Outputs the webwork2 JavaScript configuration. This configuration can be
+accessed by JavaScript files to obtain various webwork2 settings.
=cut
-sub webwork_url ($c) {
- return $c->location;
+sub webwork_js_config ($c, $showMathJaxErrors = 0) {
+ return encode_json({
+ webwork_url => $c->location,
+ mathJaxBSColorSchemeUrl => getAssetURL($c->ce, 'js/MathJaxConfig/bs-color-scheme.js'),
+ $showMathJaxErrors ? (showMathJaxErrors => true) : ()
+ });
}
=item warnings()
@@ -813,7 +813,7 @@ there are pg errors.
=cut
sub have_warnings ($c) {
- return $c->stash('warnings') || $c->{pgerrors};
+ return $c->stash('warnings');
}
=item exists_theme_file
@@ -1122,7 +1122,7 @@ object from which the base path will be taken. %options can consist of:
Can be either a reference to an array or a reference to a hash.
-If it is a reference to a hash, it maps parmaeter names to values. These
+If it is a reference to a hash, it maps parameter names to values. These
parameters will be included in the generated link. If a value is an arrayref,
the values of the array referenced will be used. If a value is undefined, the
value from the current request will be used.
@@ -1220,8 +1220,8 @@ Used to display a generic warning message at the top of the page
=cut
sub warningMessage ($c) {
- return $c->maketext('Warning: There may be something wrong with this question. '
- . 'Please inform your instructor including the warning messages below.');
+ return $c->maketext('Warning: WeBWorK has encountered warnings while processing your request. '
+ . 'See the warning messages below for details.');
}
=item $string = formatDateTime($date_time, $format_string, $timezone, $locale)
@@ -1238,7 +1238,7 @@ If C<$locale> is provided, the string returned will be in the format of that
locale. If C<$locale> is not provided, Perl defaults to using C.
Note that the defaults for C<$timezone> and C<$locale> should almost never be
-overriden when this method is used.
+overridden when this method is used.
=cut
diff --git a/lib/WeBWorK/ContentGenerator/CourseAdmin.pm b/lib/WeBWorK/ContentGenerator/CourseAdmin.pm
index 02f1467544..12225ae105 100644
--- a/lib/WeBWorK/ContentGenerator/CourseAdmin.pm
+++ b/lib/WeBWorK/ContentGenerator/CourseAdmin.pm
@@ -707,9 +707,10 @@ sub do_rename_course ($c) {
eval {
renameCourse(
- courseID => $rename_oldCourseID,
- ce => WeBWorK::CourseEnvironment->new({ courseName => $rename_oldCourseID }),
- newCourseID => $rename_newCourseID,
+ courseID => $rename_oldCourseID,
+ ce => WeBWorK::CourseEnvironment->new({ courseName => $rename_oldCourseID }),
+ newCourseID => $rename_newCourseID,
+ updateLTICourseMap => 1,
%optional_arguments
);
};
diff --git a/lib/WeBWorK/ContentGenerator/Feedback.pm b/lib/WeBWorK/ContentGenerator/Feedback.pm
index 336e5fc127..335caff765 100644
--- a/lib/WeBWorK/ContentGenerator/Feedback.pm
+++ b/lib/WeBWorK/ContentGenerator/Feedback.pm
@@ -12,7 +12,7 @@ use Email::Stuffer;
use Try::Tiny;
use WeBWorK::Upload;
-use WeBWorK::Utils qw(createEmailSenderTransportSMTP fetchEmailRecipients);
+use WeBWorK::Utils qw(createEmailSenderTransportSMTP fetchEmailRecipients formatEmailSubject);
# request paramaters used
#
@@ -108,18 +108,15 @@ sub initialize ($c) {
}
}
- my %subject_map = (
- 'c' => $courseID,
- 'u' => $user ? $user->user_id : undef,
- 's' => $set ? $set->set_id : undef,
- 'p' => $problem ? $problem->problem_id : undef,
- 'x' => $user ? $user->section : undef,
- 'r' => $user ? $user->recitation : undef,
- '%' => '%',
+ my $subject = formatEmailSubject(
+ $ce->{mail}{feedbackSubjectFormat},
+ $courseID,
+ $user ? $user->user_id : '',
+ $set ? $set->set_id : '',
+ $problem ? $problem->problem_id : '',
+ $user ? $user->section : '',
+ $user ? $user->recitation : ''
);
- my $chars = join('', keys %subject_map);
- my $subject = $ce->{mail}{feedbackSubjectFormat} || 'WeBWorK question from %c: %u set %s/prob %p';
- $subject =~ s/%([$chars])/defined $subject_map{$1} ? $subject_map{$1} : ''/eg;
my %data = (
user => $user,
diff --git a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm
index f97712269f..246ab07820 100644
--- a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm
+++ b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm
@@ -274,14 +274,15 @@ sub get_instructor_comment ($c, $problem) {
async sub pre_header_initialize ($c) {
# Make sure these are defined for the templates.
- $c->stash->{problems} = [];
- $c->stash->{pg_results} = [];
- $c->stash->{startProb} = 0;
- $c->stash->{endProb} = 0;
- $c->stash->{numPages} = 0;
- $c->stash->{pageNumber} = 0;
- $c->stash->{problem_numbers} = [];
- $c->stash->{probOrder} = [];
+ $c->stash->{problems} = [];
+ $c->stash->{pg_results} = [];
+ $c->stash->{startProb} = 0;
+ $c->stash->{endProb} = 0;
+ $c->stash->{numPages} = 0;
+ $c->stash->{pageNumber} = 0;
+ $c->stash->{problem_numbers} = [];
+ $c->stash->{probOrder} = [];
+ $c->stash->{haveProblemWarnings} = 0;
# If authz->checkSet has failed, then this set is invalid. No need to proceeded.
return if $c->{invalidSet};
@@ -502,7 +503,6 @@ async sub pre_header_initialize ($c) {
my $maxAttemptsPerVersion = $tmplSet->attempts_per_version || 0;
my $timeInterval = $tmplSet->time_interval || 0;
my $versionsPerInterval = $tmplSet->versions_per_interval || 0;
- my $timeLimit = $tmplSet->version_time_limit || 0;
# What happens if someone didn't set one of these? Perhaps this can happen if we're handed a malformed set, where
# the values in the database are null.
@@ -588,7 +588,8 @@ async sub pre_header_initialize ($c) {
$set = $db->getMergedSetVersion($effectiveUserID, $setID, $setVersionNumber);
$set->visible(1);
- # If there is a cap on problems per page, make sure that is respected in case something higher snuck in.
+ # If there is a cap on problems per page, make sure that is respected
+ # in case something higher snuck in.
if (
$ce->{test}{maxProblemsPerPage}
&& ($tmplSet->problems_per_page == 0
@@ -603,6 +604,8 @@ async sub pre_header_initialize ($c) {
# Convert the floating point value from Time::HiRes to an integer for use below. Truncate toward 0.
my $timeNowInt = int($c->submitTime);
+ my $timeLimit = ($tmplSet->version_time_limit || 0) * $effectiveUser->accommodation_time_factor;
+
# Set up creation time, and open and due dates.
my $ansOffset = $set->answer_date - $set->due_date;
$set->version_creation_time($timeNowInt);
@@ -625,7 +628,7 @@ async sub pre_header_initialize ($c) {
$cleanSet->due_date($set->due_date);
$cleanSet->answer_date($set->answer_date);
$cleanSet->version_last_attempt_time($set->version_last_attempt_time);
- $cleanSet->version_time_limit($set->version_time_limit);
+ $cleanSet->version_time_limit($set->version_time_limit * $effectiveUser->accommodation_time_factor);
$cleanSet->attempts_per_version($set->attempts_per_version);
$cleanSet->assignment_type($set->assignment_type);
$db->putSetVersion($cleanSet);
@@ -780,14 +783,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'),
@@ -887,9 +887,6 @@ async sub pre_header_initialize ($c) {
my @problems;
my @pg_results;
- # pg errors are stored here.
- $c->{errors} = [];
-
# Process the problems as needed.
my @mergedProblems;
if ($setID eq 'Undefined_Set') {
@@ -1313,7 +1310,8 @@ async sub pre_header_initialize ($c) {
} elsif ($endTime > $set->due_date) {
$c->{exceededAllowedTime} = 1;
}
- $c->{elapsedTime} = int(($endTime - $set->open_date) / 0.6 + 0.5) / 100;
+ $c->{elapsedTime} = int(($endTime - $set->open_date) / 0.6 + 0.5) / 100;
+ $c->{completedTime} = $c->formatDateTime($endTime, $ce->{studentDateDisplayFormat});
# Get the number of attempts and number of remaining attempts.
$c->{attemptNumber} =
@@ -1346,7 +1344,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 => ''
@@ -1362,7 +1360,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';
@@ -1371,81 +1369,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
- );
}
-}
-
-sub warningMessage ($c) {
- return $c->maketext('Warning: There may be something wrong with a question in this test. '
- . 'Please inform your instructor including the warning messages below.');
+ return '';
}
# Evaluation utility
@@ -1494,10 +1494,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}),
@@ -1507,27 +1506,14 @@ async sub getProblemHTML ($c, $effectiveUser, $set, $formFields, $mergedProblem)
},
);
- # Warnings in the renderPG subprocess will not be caught by the global warning handler of this process.
- # So rewarn them and let the global warning handler take care of it.
- warn $pg->{warnings} if $pg->{warnings};
-
- if ($pg->{flags}{error_flag}) {
- push @{ $c->{errors} },
- {
- set => $set->set_id . ',v' . $set->version_id,
- problem => $mergedProblem->problem_id,
- message => $pg->{errors},
- context => $pg->{body_text},
- };
- $pg->{body_text} = undef;
- }
-
# If the user can check answers and either this is not an answer submission or the problem_data form
# parameter was previously set, then set or update the problem_data form parameter.
$c->param('problem_data_' . $mergedProblem->problem_id => encode_json($pg->{PERSISTENCE_HASH} || '{}'))
if $c->{can}{checkAnswers}
&& (!$c->{submitAnswers} || defined $c->param('problem_data_' . $mergedProblem->problem_id));
+ $c->stash->{haveProblemWarnings} = 1 if $pg->{warnings} || @{ $pg->{pgwarning} // [] };
+
return $pg;
}
diff --git a/lib/WeBWorK/ContentGenerator/Hardcopy.pm b/lib/WeBWorK/ContentGenerator/Hardcopy.pm
index fdd35ed8ca..7e7809f389 100644
--- a/lib/WeBWorK/ContentGenerator/Hardcopy.pm
+++ b/lib/WeBWorK/ContentGenerator/Hardcopy.pm
@@ -10,6 +10,7 @@ problem sets.
use File::Temp qw/tempdir/;
use Mojo::File;
+use Mojo::Util qw(xml_escape);
use String::ShellQuote;
use Archive::Zip qw(:ERROR_CODES);
use XML::LibXML;
@@ -130,14 +131,16 @@ async sub pre_header_initialize ($c) {
# Make sure the format is valid.
unless (grep { $_ eq $hardcopy_format } keys %HC_FORMATS) {
- $c->addbadmessage(qq{"$hardcopy_format" is not a valid hardcopy format.});
+ $c->addbadmessage($c->maketext('"[_1]" is not a valid hardcopy format.', xml_escape($hardcopy_format)));
$validation_failed = 1;
}
# Make sure we are allowed to generate hardcopy in this format.
unless ($authz->hasPermissions($userID, "download_hardcopy_format_$hardcopy_format")) {
- $c->addbadmessage(
- $c->maketext('You do not have permission to generate hardcopy in [_1] format.', $hardcopy_format));
+ $c->addbadmessage($c->maketext(
+ 'You do not have permission to generate hardcopy in [_1] format.',
+ xml_escape($hardcopy_format)
+ ));
$validation_failed = 1;
}
@@ -284,13 +287,14 @@ async sub pre_header_initialize ($c) {
my $fullFilePath = "$ce->{webworkDirs}{tmp}/$courseID/hardcopy/$userID/$tempFile";
unless (-e $fullFilePath) {
- $c->addbadmessage($c->maketext('The requested file "[_1]" does not exist on the server.', $tempFile));
+ $c->addbadmessage(
+ $c->maketext('The requested file "[_1]" does not exist on the server.', xml_escape($tempFile)));
return;
}
unless ($baseName =~ /\.$userID\./ || $authz->hasPermissions($userID, 'download_hardcopy_multiuser')) {
$c->addbadmessage($c->maketext('You do not have permission to access the requested file "[_1]".'),
- $tempFile);
+ xml_escape($tempFile));
return;
}
@@ -670,7 +674,7 @@ sub generate_hardcopy_tex ($c, $temp_dir_path, $final_file_basename) {
);
}
}
- for (qw{pg.sty PGML.tex CAPA.tex}) {
+ for (qw{pg.sty PGML.tex}) {
eval { Mojo::File->new("$ce->{pg}{directories}{assetsTex}/$_")->copy_to($bundle_path) };
if ($@) {
$c->add_error(
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/AchievementNotificationEditor.pm b/lib/WeBWorK/ContentGenerator/Instructor/AchievementNotificationEditor.pm
index 906845efa4..353e3f06ea 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/AchievementNotificationEditor.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/AchievementNotificationEditor.pm
@@ -212,7 +212,7 @@ sub save_as_handler ($c) {
));
} else {
$c->addbadmessage($c->maketext(
- 'Unable to change the achievement notification template for achivement [_1]. Unknown error.',
+ 'Unable to change the achievement notification template for achievement [_1]. Unknown error.',
$achievementName
));
}
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/Index.pm b/lib/WeBWorK/ContentGenerator/Instructor/Index.pm
index 2ac5e5e94e..ac0f00f2be 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/Index.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/Index.pm
@@ -80,13 +80,6 @@ sub pre_header_initialize ($c) {
} else {
push @error, E_ONE_SET;
}
- } elsif (defined $c->param('user_stats')) {
- if ($nusers == 1) {
- $route = 'instructor_user_statistics';
- $args{userID} = $firstUserID;
- } else {
- push @error, E_ONE_USER;
- }
} elsif (defined $c->param('set_stats')) {
if ($nsets == 1) {
$route = 'instructor_set_statistics';
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm b/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm
index c7736812de..6e208d6cd9 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm
@@ -90,7 +90,7 @@ the submit button pressed (the action).
Requested actions and aliases
View/Reload action = view
Generate Hardcopy: action = hardcopy
- Format Code: action = format_code
+ Code Maintenance: action = code_maintenance
Save: action = save
Save as: action = save_as
Append: action = add_problem
@@ -108,25 +108,25 @@ not exist. The path to the actual file being edited is stored in inputFilePath.
use Mojo::File;
use XML::LibXML;
-use WeBWorK::Utils qw(not_blank x max);
-use WeBWorK::Utils::Files qw(surePathToFile readFile path_is_subdir);
-use WeBWorK::Utils::Instructor qw(assignProblemToAllSetUsers addProblemToSet);
-use WeBWorK::Utils::JITAR qw(seq_to_jitar_id jitar_id_to_seq);
-use WeBWorK::Utils::Sets qw(format_set_name_display);
-use SampleProblemParser qw(getSampleProblemCode generateMetadata);
+use WeBWorK::Utils qw(not_blank x max);
+use WeBWorK::Utils::Files qw(surePathToFile readFile path_is_subdir);
+use WeBWorK::Utils::Instructor qw(assignProblemToAllSetUsers addProblemToSet);
+use WeBWorK::Utils::JITAR qw(seq_to_jitar_id jitar_id_to_seq);
+use WeBWorK::Utils::Sets qw(format_set_name_display);
+use WeBWorK::PG::SampleProblemParser qw(getSampleProblemCode generateMetadata);
use constant DEFAULT_SEED => 123456;
# Editor tabs
-use constant ACTION_FORMS => [qw(view hardcopy format_code save save_as add_problem revert)];
+use constant ACTION_FORMS => [qw(view hardcopy code_maintenance save save_as add_problem revert)];
use constant ACTION_FORM_TITLES => {
- view => x('View/Reload'),
- hardcopy => x('Generate Hardcopy'),
- format_code => x('Format Code'),
- save => x('Save'),
- save_as => x('Save As'),
- add_problem => x('Append'),
- revert => x('Revert'),
+ view => x('View/Reload'),
+ hardcopy => x('Generate Hardcopy'),
+ code_maintenance => x('Code Maintenance'),
+ save => x('Save'),
+ save_as => x('Save As'),
+ add_problem => x('Append'),
+ revert => x('Revert'),
};
my $BLANKPROBLEM = 'newProblem.pg';
@@ -847,9 +847,9 @@ sub view_handler ($c) {
return;
}
-# The format_code action is handled by javascript. This is provided just in case
+# The code_maintenance action is handled by javascript. This is provided just in case
# something goes wrong and the handler is called.
-sub format_code_handler { }
+sub code_maintenance_handler { }
sub hardcopy_handler ($c) {
# Redirect to problem editor page.
@@ -1278,7 +1278,7 @@ sub save_as_handler ($c) {
$new_file_type = $file_type;
} else {
$c->addbadmessage($c->maketext(
- 'Please use radio buttons to choose the method for saving this file. Uknown saveMode: [_1].', $saveMode
+ 'Please use radio buttons to choose the method for saving this file. Unknown saveMode: [_1].', $saveMode
));
return;
}
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemGrader.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemGrader.pm
index 7bf2c9ed78..dae595e356 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemGrader.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemGrader.pm
@@ -13,6 +13,7 @@ use HTML::Entities;
use WeBWorK::Utils::JITAR qw(jitar_id_to_seq);
use WeBWorK::Utils::Rendering qw(renderPG);
use WeBWorK::Utils::Sets qw(get_test_problem_position format_set_name_display);
+use WeBWorK::Utils::DateTime qw(before);
async sub initialize ($c) {
my $authz = $c->authz;
@@ -96,6 +97,17 @@ async sub initialize ($c) {
if ($c->param('assignGrades')) {
$c->addgoodmessage($c->maketext('Grades have been saved for all current users.'));
+ # Get all of the merged user sets for this set. These are needed to determine if the problem sub_status also
+ # needs to be set. The sub_status must be set if reduced scoring is not enabled for the course or set or if it
+ # is before the reduced scoring date.
+ my %mergedSets;
+ if ($c->stash->{set}->assignment_type =~ /gateway/) {
+ $mergedSets{ $_->user_id }{ $_->version_id } = $_
+ for $db->getMergedSetVersionsWhere({ set_id => { like => "$setID,v\%" } });
+ } else {
+ %mergedSets = map { $_->user_id => { 0 => $_ } } $db->getMergedSetsWhere({ set_id => $setID });
+ }
+
for my $user (@{ $c->stash->{users} }) {
my $userID = $user->user_id;
for (@{ $user->{data} }) {
@@ -115,9 +127,16 @@ async sub initialize ($c) {
$_->{problem}{flags} =~ s/:needs_grading$//;
if ($c->param("$userID.$versionID.mark_correct")) {
$_->{problem}->status(1);
+ $_->{problem}->sub_status(1);
} elsif (defined $c->param("$userID.$versionID.score")) {
my $newscore = $c->param("$userID.$versionID.score") / 100;
- if ($newscore != $_->{problem}->status) { $_->{problem}->status($newscore); }
+ if ($newscore != $_->{problem}->status) {
+ $_->{problem}->status($newscore);
+ $_->{problem}->sub_status($newscore)
+ if !$ce->{pg}{ansEvalDefaults}{enableReducedScoring}
+ || !$mergedSets{$userID}{$versionID}->enable_reduced_scoring
+ || before($mergedSets{$userID}{$versionID}->reduced_scoring_date);
+ }
}
if ($versionID) { $db->putProblemVersion($_->{problem}); }
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
index eba2d2aa82..19c2c557aa 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
@@ -29,20 +29,15 @@ use constant SET_FIELDS => [
];
use constant PROBLEM_FIELDS =>
[qw(source_file value max_attempts showMeAnother showHintsAfter prPeriod att_to_open_children counts_parent_grade)];
-use constant USER_PROBLEM_FIELDS => [qw(problem_seed status num_correct num_incorrect)];
+use constant USER_PROBLEM_FIELDS => [qw(problem_seed status)];
# These constants determine what order those fields should be displayed in.
-use constant HEADER_ORDER => [qw(set_header hardcopy_header)];
-use constant PROBLEM_FIELD_ORDER => [
- qw(problem_seed status value max_attempts showMeAnother showHintsAfter prPeriod attempted last_answer num_correct
- num_incorrect)
-];
-# For gateway sets, don't allow changing max_attempts on a per problem basis.
-use constant GATEWAY_PROBLEM_FIELD_ORDER =>
- [qw(problem_seed status value attempted last_answer num_correct num_incorrect)];
+use constant HEADER_ORDER => [qw(set_header hardcopy_header)];
+use constant PROBLEM_FIELD_ORDER => [qw(problem_seed status value max_attempts showMeAnother showHintsAfter prPeriod)];
+use constant GATEWAY_PROBLEM_FIELD_ORDER => [qw(problem_seed status value)];
use constant JITAR_PROBLEM_FIELD_ORDER => [
qw(problem_seed status value max_attempts showMeAnother showHintsAfter prPeriod att_to_open_children
- counts_parent_grade attempted last_answer num_correct num_incorrect)
+ counts_parent_grade)
];
# Exclude the gateway set fields from the set field order, because they are only displayed for sets that are gateways.
@@ -70,14 +65,28 @@ use constant JITAR_SET_FIELD_ORDER => [qw(restrict_prob_progression email_instru
# [min, max, step] will introduce validation, so should not be used on just any
# input where we expect numbers
# size => "50", # size of the edit box (if any)
-# override => "none", # none, one, any, all - defines for whom this data can/must be overidden
+# override => "all", # none, one, any, all - defines for whom this data can be overidden
# module => "problem_list", # WeBWorK module
# default => 0 # if a field cannot default to undefined/empty what should it default to
-# labels => { # special values can be hashed to display labels
-# 1 => x('Yes'),
-# 0 => x('No'),
+# labels => { # Display labels for type "choose" or type "[min, max, step]".
+# 1 => x('Yes'), # This is required for type "choose", is optional for type "[min, max, step]",
+# 0 => x('No'), # and should never be used for any other type.
# },
+# choices => [ qw(0 1) ] # Order of the labels above. This must be given if labels is.
# convertby => 60, # divide incoming database field values by this, and multiply when saving
+#
+# Note that if "type" is "[min, max, step]" and "labels" is defined, then a select will be shown before the numeric
+# input with the labels as options. The label values must not overlap with the numeric values (i.e., min must be
+# greater than all defined label values), and must be numeric. The labels must also include a "numeric" label. This
+# label will be shown when the number input value is used. It is impomrtant that the choices should not include the
+# special "numeric" value, and that all other choices have numeric values.
+
+# FIXME: The override "none" case needs to be revisited if it is ever used again. It is definitely not implemented
+# correctly, or it is pointless. If override "none" is to mean it can't be changed at all, then why have it? But then
+# again, with the current implementation fields set to override "none" are not shown when editing for more than one
+# user, but are shown when editing for one user (although they still can't be edited). That doesn't make sense. The
+# previous fields that used it were also of type "hidden", and it turns out that type "hidden" and override "none"
+# really means don't use at all, and so listing them was pointless.
use constant BLANKPROBLEM => 'newProblem.pg';
@@ -281,7 +290,9 @@ use constant FIELD_PROPERTIES => {
'This sets a number of minutes for each version of a test, once it is started. Use "0" to indicate no '
. 'time limit. If there is a time limit, then there will be an indication that this is a timed '
. 'test on the main "Assignments" page. Additionally the student will be sent to a confirmation '
- . 'page beefore they can begin.'
+ . 'page before they can begin. Note that the actual time a student will have to complete a timed test '
+ . 'is the product of this time limit and the accommodation time factor set for the student in the '
+ . 'accounts manager.'
)
},
time_limit_cap => {
@@ -479,65 +490,57 @@ use constant FIELD_PROPERTIES => {
)
},
max_attempts => {
- name => x('Max Attempts'),
- type => 'edit',
- size => 6,
- override => 'any',
- default => '-1',
- labels => {
- '-1' => x('Unlimited'),
- },
+ name => x('Max Attempts'),
+ type => [ 0, undef, 1 ],
+ size => 6,
+ override => 'any',
+ default => '-1',
+ choices => [qw(-1)],
+ labels => { '-1' => x('Unlimited'), numeric => x('Limit to') },
help_text => x(
- 'You may cap the number of attempts a student can use for the problem. Use -1 to indicate unlimited attempts.'
+ 'You may cap the number of attempts a student can use for the problem. '
+ . 'Select "Unlimited" to allow an unlimited number of attempts.'
)
},
showMeAnother => {
- name => x('Show Me Another'),
- type => 'edit',
- size => '6',
- override => 'any',
- default => '-2',
- labels => {
- '-1' => x('Never'),
- '-2' => x('Course Default'),
- },
+ name => x('Show Me Another'),
+ type => [ 0, undef, 1 ],
+ size => '6',
+ override => 'any',
+ default => '-2',
+ choices => [qw(-2 -1)],
+ labels => { '-2' => x('Course Default'), '-1' => x('Never'), numeric => x('After number of attempts is') },
help_text => x(
'When a student has more attempts than is specified here they will be able to view another '
- . 'version of this problem. If set to -1 the feature is disabled and if set to -2 '
- . 'the course default is used.'
+ . 'version of this problem. The "Show Me Another" feature is is disabled if "Never" is selected.'
)
},
showHintsAfter => {
- name => x('Show Hints After'),
- type => 'edit',
- size => '6',
- override => 'any',
- default => '-2',
- labels => {
- '-2' => x('Course Default'),
- '-1' => x('Never'),
- },
+ name => x('Show Hints'),
+ type => [ 0, undef, 1 ],
+ size => '6',
+ override => 'any',
+ default => '-2',
+ choices => [qw(-2 -1)],
+ labels => { '-2' => x('Course Default'), '-1' => x('Never'), numeric => x('After number of attempts is') },
help_text => x(
'This specifies the number of attempts before hints are shown to students. '
- . 'The value of -2 uses the default from course configuration. '
- . 'The value of -1 disables hints. '
+ . 'If "Never" is selected, then hints are disabled. '
. 'Note that this will only have an effect if the problem has a hint.'
),
},
prPeriod => {
- name => x('Rerandomize After'),
- type => 'edit',
- size => '6',
- override => 'any',
- default => '-1',
- labels => {
- '-1' => x('Course Default'),
- '0' => x('Never'),
- },
+ name => x('Rerandomize'),
+ type => [ 1, undef, 1 ],
+ size => '6',
+ override => 'any',
+ default => '-1',
+ choices => [qw(-1 0)],
+ labels => { '-1' => x('Course Default'), '0' => x('Never'), numeric => x('After number of attempts is') },
help_text => x(
'This specifies the rerandomization period: the number of attempts before a new version of '
- . 'the problem is generated by changing the Seed value. The value of -1 uses the '
- . 'default from course configuration. The value of 0 disables rerandomization.'
+ . 'the problem is generated by changing the Seed value. '
+ . 'Randomization is disabled if "Never" is selected.'
),
},
problem_seed => {
@@ -561,34 +564,6 @@ use constant FIELD_PROPERTIES => {
. 'to 1 to manually award full credit on this problem.'
)
},
- attempted => {
- name => x('Attempted'),
- type => 'hidden',
- override => 'none',
- choices => [qw(0 1)],
- labels => {
- 1 => x('Yes'),
- 0 => x('No'),
- },
- default => '0',
- },
- last_answer => {
- name => x('Last Answer'),
- type => 'hidden',
- override => 'none',
- },
- num_correct => {
- name => x('Correct'),
- type => 'hidden',
- override => 'none',
- default => '0',
- },
- num_incorrect => {
- name => x('Incorrect'),
- type => 'hidden',
- override => 'none',
- default => '0',
- },
hide_hint => {
name => x('Hide Hints from Students'),
type => 'choose',
@@ -605,19 +580,18 @@ use constant FIELD_PROPERTIES => {
)
},
att_to_open_children => {
- name => x('Attempt Threshold for Children'),
- type => 'edit',
- size => 6,
- override => 'any',
- default => '0',
- labels => {
- '-1' => x('max'),
- },
+ name => x('Attempt Threshold for Children'),
+ type => [ 0, undef, 1 ],
+ size => 6,
+ override => 'any',
+ default => '0',
+ choices => [qw(-1)],
+ labels => { '-1' => x('No Attempts Remaining'), numeric => x('Number incorrect is') },
help_text => x(
'The child problems for this problem will become visible to the student when they either have more '
. 'incorrect attempts than is specified here, or when they run out of attempts, whichever comes '
- . 'first. Use -1 to indicate that child problems should only be available after a student '
- . 'runs out of attempts.'
+ . 'first. Select "No Attempts Remaining" to indicate that child problems should only be available '
+ . 'after a student runs out of attempts.'
),
},
counts_parent_grade => {
@@ -638,13 +612,6 @@ use constant FIELD_PROPERTIES => {
},
};
-use constant FIELD_PROPERTIES_GWQUIZ => {
- max_attempts => {
- type => 'hidden',
- override => 'any',
- }
-};
-
# Create a table of fields for the given parameters, one row for each db field.
# If only the setID is included, it creates a table of set information.
# If the problemID is included, it creates a table of problem information.
@@ -724,13 +691,7 @@ sub fieldTable ($c, $userID, $setID, $problemID, $globalRecord, $userRecord = un
}
for my $field (@fieldOrder) {
- my %properties;
-
- if ($isGWset && defined(FIELD_PROPERTIES_GWQUIZ->{$field})) {
- %properties = %{ FIELD_PROPERTIES_GWQUIZ->{$field} };
- } else {
- %properties = %{ FIELD_PROPERTIES()->{$field} };
- }
+ my %properties = %{ FIELD_PROPERTIES()->{$field} };
# Don't show fields if that option isn't enabled.
if (!$ce->{options}{enableConditionalRelease}
@@ -822,9 +783,8 @@ sub fieldHTML ($c, $userID, $setID, $problemID, $globalRecord, $userRecord, $fie
my %properties = %{ FIELD_PROPERTIES()->{$field} };
if ($field eq 'problems_per_page') {
- if ($c->ce->{test}{maxProblemsPerPage} == 1) {
- $properties{override} = 'none';
- } elsif ($c->ce->{test}{maxProblemsPerPage} > 1) {
+ return '' if $c->ce->{test}{maxProblemsPerPage} == 1;
+ if ($c->ce->{test}{maxProblemsPerPage} > 1) {
my $max = $c->ce->{test}{maxProblemsPerPage};
$properties{type} = [ 1, $max, 1 ];
$properties{help_text} =
@@ -841,10 +801,6 @@ sub fieldHTML ($c, $userID, $setID, $problemID, $globalRecord, $userRecord, $fie
return '' if $properties{override} eq 'none' && !$forOneUser;
return '' if $properties{override} eq 'all' && $forUsers;
- my $edit = $properties{type} eq 'edit' && $properties{override} ne 'none';
- my $number = ref($properties{type}) eq 'ARRAY' && $properties{override} ne 'none';
- my $choose = $properties{type} eq 'choose' && $properties{override} ne 'none';
-
my ($globalValue, $userValue, $blankField) = (undef, undef, '');
if ($field =~ /:/) {
# This allows one "select" to set multiple database fields.
@@ -882,106 +838,141 @@ sub fieldHTML ($c, $userID, $setID, $problemID, $globalRecord, $userRecord, $fie
# Determine if this is a set record or problem record.
my ($recordType, $recordID) = defined $problemID ? ('problem', $problemID) : ('set', $setID);
- my %labels = (map { $_ => $c->maketext($properties{labels}{$_}) } keys %{ $properties{labels} });
+ my %labels = map { $_ => $c->maketext($properties{labels}{$_}) } keys %{ $properties{labels} // {} };
# This contains either a text input or a select for changing a given database field.
my $input = '';
- if ($edit || $number) {
- if ($field =~ /_date/) {
- $input = $c->tag(
- 'div',
- class => 'input-group input-group-sm flatpickr',
- $c->c(
- $c->text_field(
- "$recordType.$recordID.$field",
- $forUsers ? $userValue : $globalValue,
- id => "$recordType.$recordID.${field}_id",
- class => 'form-control form-control-sm'
- . ($field eq 'open_date' ? ' datepicker-group' : ''),
- placeholder => (
- $forUsers && $canOverride ? $c->maketext('Set Default') : $c->maketext('None Specified')
- ),
- data => {
- input => undef,
- done_text => $c->maketext('Done'),
- today_text => $c->maketext('Today'),
- now_text => $c->maketext('Now'),
- locale => $c->ce->{language},
- timezone => $c->ce->{siteDefaults}{timezone}
- }
- ),
- $c->tag(
- 'a',
- class => 'btn btn-secondary btn-sm',
- data => { toggle => undef },
- role => 'button',
- tabindex => 0,
- 'aria-label' => $c->maketext('Pick date and time'),
- $c->tag('i', class => 'fas fa-calendar-alt', 'aria-hidden' => 'true', '')
- )
- )->join('')
- );
- } else {
- my $value = $forUsers ? ($labels{$userValue} || $userValue) : ($labels{$globalValue} || $globalValue);
- $value = $c->ce->{test}{maxProblemsPerPage}
- if ($field eq 'problems_per_page'
- && $c->ce->{test}{maxProblemsPerPage}
- && ($value == 0 || $value > $c->ce->{test}{maxProblemsPerPage}));
- $value = format_set_name_display($value =~ s/\s*,\s*/,/gr) if $field eq 'restricted_release';
-
- my @field_args = (
- "$recordType.$recordID.$field", $value,
- id => "$recordType.$recordID.${field}_id",
- class => 'form-control form-control-sm',
- $field eq 'restricted_release' || $field eq 'source_file' ? (dir => 'ltr') : ()
- );
- if ($field eq 'problem_seed') {
- # Insert a randomization button
+ if ($properties{override} ne 'none') {
+ if ($properties{type} eq 'edit' || ref($properties{type}) eq 'ARRAY') {
+ if ($field =~ /_date/) {
$input = $c->tag(
'div',
- class => 'input-group input-group-sm',
- style => 'min-width: 7rem',
+ class => 'input-group input-group-sm flatpickr',
$c->c(
- $c->number_field(@field_args, min => 0),
+ $c->text_field(
+ "$recordType.$recordID.$field",
+ $forUsers ? $userValue : $globalValue,
+ id => "$recordType.$recordID.${field}_id",
+ class => 'form-control form-control-sm'
+ . ($field eq 'open_date' ? ' datepicker-group' : ''),
+ placeholder => (
+ $forUsers
+ && $canOverride ? $c->maketext('Set Default') : $c->maketext('None Specified')
+ ),
+ data => {
+ input => undef,
+ done_text => $c->maketext('Done'),
+ today_text => $c->maketext('Today'),
+ now_text => $c->maketext('Now'),
+ locale => $c->ce->{language},
+ timezone => $c->ce->{siteDefaults}{timezone}
+ }
+ ),
$c->tag(
- 'button',
- type => 'button',
- class => 'randomize-seed-btn btn btn-sm btn-secondary',
- title => 'randomize',
- data => {
- seed_input => "$recordType.$recordID.problem_seed_id",
- status_input => "$recordType.$recordID.status_id"
- },
- $c->tag('i', class => 'fa-solid fa-shuffle')
+ 'a',
+ class => 'btn btn-secondary btn-sm',
+ data => { toggle => undef },
+ role => 'button',
+ tabindex => 0,
+ 'aria-label' => $c->maketext('Pick date and time'),
+ $c->tag('i', class => 'fas fa-calendar-alt', 'aria-hidden' => 'true', '')
)
)->join('')
);
- } elsif ($number) {
- $input = $c->number_field(
- @field_args,
- min => ($properties{type}[0] || 0),
- max => ($properties{type}[1] || undef),
- step => ($properties{type}[2] || 1),
- $forUsers && $canOverride ? (placeholder => $c->maketext('Set Default')) : ()
- );
} else {
- $input = $c->text_field(@field_args,
- $forUsers && $canOverride ? (placeholder => $c->maketext('Set Default')) : ());
+ my $value = $forUsers ? $userValue : $globalValue;
+ $value = $c->ce->{test}{maxProblemsPerPage}
+ if ($field eq 'problems_per_page'
+ && $c->ce->{test}{maxProblemsPerPage}
+ && ($value == 0 || $value > $c->ce->{test}{maxProblemsPerPage}));
+ $value = format_set_name_display($value =~ s/\s*,\s*/,/gr) if $field eq 'restricted_release';
+
+ my @field_args = (
+ id => "$recordType.$recordID.${field}_id",
+ class => 'form-control form-control-sm',
+ $field eq 'restricted_release' || $field eq 'source_file' ? (dir => 'ltr') : ()
+ );
+ if ($field eq 'problem_seed') {
+ # Insert a randomization button
+ $input = $c->tag(
+ 'div',
+ class => 'input-group input-group-sm',
+ style => 'min-width: 7rem',
+ $c->c(
+ $c->number_field("$recordType.$recordID.$field", $value, @field_args, min => 0),
+ $c->tag(
+ 'button',
+ type => 'button',
+ class => 'randomize-seed-btn btn btn-sm btn-secondary',
+ title => 'randomize',
+ data => {
+ seed_input => "$recordType.$recordID.problem_seed_id",
+ status_input => "$recordType.$recordID.status_id"
+ },
+ $c->tag('i', class => 'fa-solid fa-shuffle')
+ )
+ )->join('')
+ );
+ } elsif (ref($properties{type}) eq 'ARRAY') {
+ if (ref $properties{labels} eq 'HASH') {
+ $input = $c->tag(
+ 'div',
+ class => 'input-group input-group-sm mixed-numeric-select',
+ style => 'min-width: 7rem',
+ $c->c(
+ $c->select_field(
+ "$recordType.$recordID.$field",
+ [
+ $forUsers && $canOverride ? [ $c->maketext('Set Default') => '' ] : (),
+ [
+ $labels{numeric} => 'numeric',
+ $value ne '' && !defined $labels{$value} ? (selected => undef) : ()
+ ],
+ map { [ $labels{$_} => $_, $_ eq $value ? (selected => undef) : () ] }
+ @{ $properties{choices} }
+ ],
+ class => 'form-select form-select-sm'
+ ),
+ $c->number_field(
+ "$recordType.$recordID.$field", defined $labels{$value} ? '' : $value,
+ @field_args,
+ min => $properties{type}[0],
+ $properties{type}[1] ? (max => $properties{type}[1]) : (),
+ step => $properties{type}[2] || 1,
+ style => 'max-width: 4rem'
+ )
+ )->join('')
+ );
+ } else {
+ $input = $c->number_field(
+ "$recordType.$recordID.$field", $value,
+ @field_args,
+ min => $properties{type}[0],
+ $properties{type}[1] ? (max => $properties{type}[1]) : (),
+ step => $properties{type}[2] || 1,
+ $forUsers && $canOverride ? (placeholder => $c->maketext('Set Default')) : (),
+ );
+ }
+ } else {
+ $input = $c->text_field("$recordType.$recordID.$field",
+ $value, @field_args,
+ $forUsers && $canOverride ? (placeholder => $c->maketext('Set Default')) : ());
+ }
}
+ } elsif ($properties{type} eq 'choose') {
+ my $value = $forUsers ? $userValue : $globalValue;
+
+ $input = $c->select_field(
+ "$recordType.$recordID.$field",
+ [
+ $forUsers && $userRecord ? [ $c->maketext('Set Default') => '' ] : (),
+ map { [ $labels{$_} => $_, $_ eq $value ? (selected => undef) : () ] } @{ $properties{choices} }
+ ],
+ id => "$recordType.$recordID.${field}_id",
+ class => 'form-select form-select-sm'
+ );
}
- } elsif ($choose) {
- my $value = $forUsers ? $userValue : $globalValue;
-
- $input = $c->select_field(
- "$recordType.$recordID.$field",
- [
- $forUsers && $userRecord ? [ $c->maketext('Set Default') => '' ] : (),
- map { [ $labels{$_} => $_, $_ eq $value ? (selected => undef) : () ] } @{ $properties{choices} }
- ],
- id => "$recordType.$recordID.${field}_id",
- class => 'form-select form-select-sm'
- );
}
my $globalDisplayValue =
@@ -1342,18 +1333,11 @@ sub initialize ($c) {
my $forOneUser = $forUsers == 1;
$c->stash->{forOneUser} = $forOneUser;
- # If editing a versioned set, it only makes sense edit it for one user.
+ # If editing a versioned set, it only makes sense to edit it for one user.
return if ($editingSetVersion && !$forOneUser);
my %properties = %{ FIELD_PROPERTIES() };
- # Invert the labels hashes.
- my %undoLabels;
- for my $key (keys %properties) {
- %{ $undoLabels{$key} } =
- map { $c->maketext($properties{$key}{labels}{$_}) => $_ } keys %{ $properties{$key}{labels} };
- }
-
my $error = 0;
if ($c->param('submit_changes')) {
my @names = ('open_date', 'due_date', 'answer_date', 'reduced_scoring_date');
@@ -1405,7 +1389,6 @@ sub initialize ($c) {
$c->addbadmessage($c->maketext('No changes were saved!')) if $error;
if ($c->param('submit_changes') && !$error) {
-
my $oldAssignmentType = $setRecord->assignment_type();
# Save general set information (including headers)
@@ -1428,9 +1411,9 @@ sub initialize ($c) {
for my $field (@{ SET_FIELDS() }) {
next unless canChange($forUsers, $field);
- my $param = $c->param("set.$setID.$field");
+ my @paramValues = $c->param("set.$setID.$field");
+ my $param = @paramValues > 1 && $paramValues[0] eq 'numeric' ? $paramValues[1] : $paramValues[0];
if ($param && $param ne '') {
- $param = $undoLabels{$field}{$param} if defined $undoLabels{$field}{$param};
$param = $param * $properties{$field}->{convertby} if $properties{$field}{convertby};
# Special case: Does field fill in multiple values?
@@ -1502,11 +1485,10 @@ sub initialize ($c) {
foreach my $field (@{ SET_FIELDS() }) {
next unless canChange($forUsers, $field);
- my $param = $c->param("set.$setID.$field");
- $param = defined $properties{$field}->{default} ? $properties{$field}->{default} : ""
- unless defined $param && $param ne "";
- my $unlabel = $undoLabels{$field}->{$param};
- $param = $unlabel if defined $unlabel;
+ my @paramValues = $c->param("set.$setID.$field");
+ my $param = @paramValues > 1 && $paramValues[0] eq 'numeric' ? $paramValues[1] : $paramValues[0];
+ $param = defined $properties{$field}{default} ? $properties{$field}{default} : ''
+ unless defined $param && $param ne '';
if ($field =~ /restricted_release/ && $param) {
$param = format_set_name_internal($param =~ s/\s*,\s*/,/gr);
$c->check_sets($db, $param);
@@ -1655,10 +1637,10 @@ sub initialize ($c) {
for my $field (@{ PROBLEM_FIELDS() }) {
next unless canChange($forUsers, $field);
- my $param = $c->param("problem.$problemID.$field");
+ my @paramValues = $c->param("problem.$problemID.$field");
+ my $param =
+ @paramValues > 1 && $paramValues[0] eq 'numeric' ? $paramValues[1] : $paramValues[0];
if (defined $param && $param ne '') {
- $param = $undoLabels{$field}{$param} if defined $undoLabels{$field}{$param};
-
# Protect exploits with source_file
if ($field eq 'source_file') {
if ($param =~ /\.\./ || $param =~ /^\//) {
@@ -1682,11 +1664,12 @@ sub initialize ($c) {
for my $field (@{ USER_PROBLEM_FIELDS() }) {
next unless canChange($forUsers, $field);
- my $param = $c->param("problem.$problemID.$field");
- $param = defined $properties{$field}->{default} ? $properties{$field}->{default} : ""
- unless defined $param && $param ne "";
- my $unlabel = $undoLabels{$field}->{$param};
- $param = $unlabel if defined $unlabel;
+ my @paramValues = $c->param("problem.$problemID.$field");
+ my $param =
+ @paramValues > 1 && $paramValues[0] eq 'numeric' ? $paramValues[1] : $paramValues[0];
+ $param = defined $properties{$field}{default} ? $properties{$field}{default} : ''
+ unless defined $param && $param ne '';
+
# Protect exploits with source_file
if ($field eq 'source_file') {
if ($param =~ /\.\./ || $param =~ /^\//) {
@@ -1719,11 +1702,10 @@ sub initialize ($c) {
foreach my $field (@{ PROBLEM_FIELDS() }) {
next unless canChange($forUsers, $field);
- my $param = $c->param("problem.$problemID.$field");
- $param = defined $properties{$field}->{default} ? $properties{$field}->{default} : ""
- unless defined $param && $param ne "";
- my $unlabel = $undoLabels{$field}->{$param};
- $param = $unlabel if defined $unlabel;
+ my @paramValues = $c->param("problem.$problemID.$field");
+ my $param = @paramValues > 1 && $paramValues[0] eq 'numeric' ? $paramValues[1] : $paramValues[0];
+ $param = defined $properties{$field}{default} ? $properties{$field}{default} : ''
+ unless defined $param && $param ne '';
# Protect exploits with source_file
if ($field eq 'source_file') {
@@ -1759,11 +1741,11 @@ sub initialize ($c) {
foreach my $field (keys %useful) {
next unless canChange($forUsers, $field);
- my $param = $c->param("problem.$problemID.$field");
- $param = defined $properties{$field}->{default} ? $properties{$field}->{default} : ""
- unless defined $param && $param ne "";
- my $unlabel = $undoLabels{$field}->{$param};
- $param = $unlabel if defined $unlabel;
+ my @paramValues = $c->param("problem.$problemID.$field");
+ my $param =
+ @paramValues > 1 && $paramValues[0] eq 'numeric' ? $paramValues[1] : $paramValues[0];
+ $param = defined $properties{$field}{default} ? $properties{$field}{default} : ''
+ unless defined $param && $param ne '';
$changed ||= changed($record->$field, $param);
$record->$field($param);
}
@@ -2048,13 +2030,11 @@ sub initialize ($c) {
}
# Helper method for checking if two values are different.
-# The return values will usually be thrown away, but they could be useful for debugging.
sub changed ($first, $second) {
- return "def/undef" if defined $first && !defined $second;
- return "undef/def" if !defined $first && defined $second;
- return "" if !defined $first && !defined $second;
- return "ne" if $first ne $second;
- return "";
+ return 0 if !defined $first && !defined $second;
+ return 1 if !defined $first || !defined $second;
+ return 1 if $first ne $second;
+ return 0;
}
# Helper method that determines for how many users at a time a field can be changed.
@@ -2071,7 +2051,7 @@ sub canChange ($forUsers, $field) {
return 1 if $howManyCan eq "any";
return 1 if $howManyCan eq "one" && $forOneUser;
return 1 if $howManyCan eq "all" && !$forUsers;
- return 0; # FIXME: maybe it should default to 1?
+ return 0;
}
# Helper method that determines if a file is valid and returns a pretty error message.
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm
index 6a65b99ce0..5e33ee8369 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm
@@ -643,17 +643,6 @@ sub save_edit_handler ($c) {
}
}
- # make sure the dates are not more than 10 years in the future
- my $curr_time = time;
- my $seconds_per_year = 31_556_926;
- my $cutoff = $curr_time + $seconds_per_year * 10;
- return (0, $c->maketext('Error: Open date cannot be more than 10 years from now in set [_1].', $setID))
- if $Set->open_date > $cutoff;
- return (0, $c->maketext('Error: Close date cannot be more than 10 years from now in set [_1].', $setID))
- if $Set->due_date > $cutoff;
- return (0, $c->maketext('Error: Answer date cannot be more than 10 years from now in set [_1].', $setID))
- if $Set->answer_date > $cutoff;
-
# Check that the open, due and answer dates are in increasing order.
# Bail if this is not correct.
if ($Set->open_date > $Set->due_date) {
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm b/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm
index 63ef67a7bb..33b86fb40b 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm
@@ -22,30 +22,7 @@ sub initialize ($c) {
# Check permissions
return unless $c->authz->hasPermissions($user, 'access_instructor_tools');
- # Cache a list of all users except set level proctors and practice users, and restrict to the sections or
- # recitations that are allowed for the user if such restrictions are defined. This list is sorted by last_name,
- # then first_name, then user_id. This is used in multiple places in this module, and is guaranteed to be used at
- # least once. So it is done here to prevent extra database access.
- $c->{student_records} = [
- $db->getUsersWhere(
- {
- user_id => [ -and => { not_like => 'set_id:%' }, { not_like => "$ce->{practiceUserPrefix}\%" } ],
- $ce->{viewable_sections}{$user} || $ce->{viewable_recitations}{$user}
- ? (
- -or => [
- $ce->{viewable_sections}{$user} ? (section => $ce->{viewable_sections}{$user}) : (),
- $ce->{viewable_recitations}{$user} ? (recitation => $ce->{viewable_recitations}{$user}) : ()
- ]
- )
- : ()
- },
- [qw/last_name first_name user_id/]
- )
- ];
-
- if ($c->current_route eq 'instructor_user_statistics') {
- $c->{studentID} = $c->stash('userID');
- } elsif ($c->current_route =~ /^instructor_(set|problem)_statistics$/) {
+ if ($c->current_route =~ /^instructor_(set|problem)_statistics$/) {
my $setRecord = $db->getGlobalSet($c->stash('setID'));
return unless $setRecord;
$c->{setRecord} = $setRecord;
@@ -57,6 +34,30 @@ sub initialize ($c) {
return unless $problemRecord;
$c->{problemRecord} = $problemRecord;
}
+
+ # Cache a list of all users except set level proctors and practice users, and restrict to the sections
+ # or recitations that are allowed for the user if such restrictions are defined. This list is sorted by
+ # last_name, then first_name, then user_id. This is used in multiple places in this module, and is used
+ # on every page except the main page, so it is done here to prevent extra database access.
+ $c->{student_records} = [
+ $db->getUsersWhere(
+ {
+ user_id =>
+ [ -and => { not_like => 'set_id:%' }, { not_like => "$ce->{practiceUserPrefix}\%" } ],
+ $ce->{viewable_sections}{$user} || $ce->{viewable_recitations}{$user}
+ ? (
+ -or => [
+ $ce->{viewable_sections}{$user} ? (section => $ce->{viewable_sections}{$user}) : (),
+ $ce->{viewable_recitations}{$user}
+ ? (recitation => $ce->{viewable_recitations}{$user})
+ : ()
+ ]
+ )
+ : ()
+ },
+ [qw/last_name first_name user_id/]
+ )
+ ];
}
return;
@@ -67,9 +68,7 @@ sub page_title ($c) {
my $setID = $c->stash('setID') || '';
- if ($c->current_route eq 'instructor_user_statistics') {
- return $c->maketext('Statistics for student [_1]', $c->{studentID});
- } elsif ($c->current_route eq 'instructor_set_statistics') {
+ if ($c->current_route eq 'instructor_set_statistics') {
return $c->maketext('Statistics for [_1]', $c->tag('span', dir => 'ltr', format_set_name_display($setID)));
} elsif ($c->current_route eq 'instructor_problem_statistics') {
return $c->maketext(
@@ -79,12 +78,11 @@ sub page_title ($c) {
);
}
- return $c->maketext('Statistics');
+ return $c->maketext('Set Statistics');
}
sub siblings ($c) {
- # Stats and StudentProgress share this template.
- return $c->include('ContentGenerator/Instructor/Stats/siblings', header => $c->maketext('Statistics'));
+ return $c->include('ContentGenerator/Instructor/Stats/siblings');
}
# Apply the currently selected filter to the student records, and return a reference to the
@@ -567,6 +565,7 @@ sub build_bar_chart ($c, $data, %options) {
viewbox => '-2 -2 ' . ($imageWidth + 3) . ' ' . ($imageHeight + 3),
'aria-labelledby' => "bar_graph_title_$id",
role => 'img',
+ class => 'stats-image',
-nocredits => 1
);
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/StudentProgress.pm b/lib/WeBWorK/ContentGenerator/Instructor/StudentProgress.pm
index 6acc9cc579..0871948c44 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/StudentProgress.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/StudentProgress.pm
@@ -68,8 +68,7 @@ sub page_title ($c) {
}
sub siblings ($c) {
- # Stats and StudentProgress share this template.
- return $c->include('ContentGenerator/Instructor/Stats/siblings', header => $c->maketext('Student Progress'));
+ return $c->include('ContentGenerator/Instructor/StudentProgress/siblings');
}
# Display student progress table
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm b/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm
index a3eff9d7b6..ca6db7ed6b 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm
@@ -93,24 +93,47 @@ use constant SORT_SUBS => {
};
use constant FIELDS => [
- 'user_id', 'first_name', 'last_name', 'email_address', 'student_id', 'status',
- 'section', 'recitation', 'comment', 'permission', 'password'
+ 'user_id', 'first_name', 'last_name', 'email_address',
+ 'student_id', 'status', 'accommodation_time_factor', 'section',
+ 'recitation', 'comment', 'permission', 'password'
];
-# Note that only the editable fields need a type (i.e. all but user_id),
-# and only the text fields need a size.
+# Note that only the editable fields need a type (i.e. all but user_id).
+# The fields of type text or number may also include optional attributes for the HTML input.
+# Any field may also contain a perlValidate method that will be called to validate user input. If provided, it should be
+# a subroutine that takes the parameter value as its only argument, and returns a translatable error string if the
+# parameter value is not valid for the field, and 0 otherwise.
use constant FIELD_PROPERTIES => {
- user_id => { name => x('Login Name') },
- first_name => { name => x('First Name'), type => 'text', size => 10 },
- last_name => { name => x('Last Name'), type => 'text', size => 10 },
- email_address => { name => x('Email Address'), type => 'text', size => 20 },
- student_id => { name => x('Student ID'), type => 'text', size => 11 },
- status => { name => x('Enrollment Status'), type => 'status' },
- section => { name => x('Section'), type => 'text', size => 3 },
- recitation => { name => x('Recitation'), type => 'text', size => 3 },
- comment => { name => x('Comment'), type => 'text', size => 20 },
- permission => { name => x('Permission Level'), type => 'permission' },
- password => { name => x('Password'), type => 'password' },
+ user_id => { name => x('Login Name') },
+ first_name => { name => x('First Name'), type => 'text', attributes => { size => 10 } },
+ last_name => { name => x('Last Name'), type => 'text', attributes => { size => 10 } },
+ email_address => { name => x('Email Address'), type => 'text', attributes => { size => 20 } },
+ student_id => { name => x('Student ID'), type => 'text', attributes => { size => 11 } },
+ status => { name => x('Enrollment Status'), type => 'status' },
+ accommodation_time_factor => {
+ name => x('Accommodation Time Factor'),
+ type => 'number',
+ attributes => {
+ size => 5,
+ min => 1,
+ step => 'any',
+ title => 'Enter a decimal number that is greater than or equal to 1.'
+ },
+ perlValidate => sub {
+ my $value = shift;
+ return $value !~ /^(\d+(\.\d*)?|\.\d+)$/ || $value <= 0
+ ? (x(
+ 'Accomodation time factor for [_1] unchanged. '
+ . 'A value was given that is not a decimal number or is not greater than or equal to 1.'
+ ))[0]
+ : 0;
+ }
+ },
+ section => { name => x('Section'), type => 'text', attributes => { size => 3 } },
+ recitation => { name => x('Recitation'), type => 'text', attributes => { size => 3 } },
+ comment => { name => x('Comment'), type => 'text', attributes => { size => 20 } },
+ permission => { name => x('Permission Level'), type => 'permission' },
+ password => { name => x('Password'), type => 'password' },
};
sub pre_header_initialize ($c) {
@@ -517,7 +540,14 @@ sub save_edit_handler ($c) {
for my $field ($User->NONKEYFIELDS()) {
my $newValue = $c->param("user.$userID.$field");
- $User->$field($newValue) if defined $newValue;
+ next unless defined $newValue;
+ if (ref(FIELD_PROPERTIES()->{$field}{perlValidate}) eq 'CODE'
+ && (my $error = FIELD_PROPERTIES()->{$field}{perlValidate}->($newValue)))
+ {
+ $c->addbadmessage($c->maketext($error, $userID));
+ next;
+ }
+ $User->$field($newValue);
}
$db->putUser($User);
diff --git a/lib/WeBWorK/ContentGenerator/LTIAdvantage.pm b/lib/WeBWorK/ContentGenerator/LTIAdvantage.pm
index 78139db160..75b58e5d6c 100644
--- a/lib/WeBWorK/ContentGenerator/LTIAdvantage.pm
+++ b/lib/WeBWorK/ContentGenerator/LTIAdvantage.pm
@@ -1,11 +1,13 @@
package WeBWorK::ContentGenerator::LTIAdvantage;
-use Mojo::Base 'WeBWorK::ContentGenerator', -signatures;
+use Mojo::Base 'WeBWorK::ContentGenerator', -signatures, -async_await;
use Mojo::UserAgent;
+use Mojo::URL;
use Mojo::JSON qw(decode_json);
use Crypt::JWT qw(decode_jwt encode_jwt);
use Math::Random::Secure qw(irand);
use Digest::SHA qw(sha256_hex);
+use Mojo::File qw(tempfile);
use WeBWorK::Debug qw(debug);
use WeBWorK::Authen::LTIAdvantage::SubmitGrade;
@@ -175,9 +177,11 @@ sub launch ($c) {
return $c->redirect_to($c->systemLink(
$c->url_for($c->stash->{LTILaunchRedirect}),
- $c->stash->{isContentSelection}
- ? (
- params => {
+ params => {
+ %{ Mojo::URL->new($c->stash->{LTILaunchRedirect})->query->to_hash },
+ $c->stash->{isContentSelection}
+ ? (
+
courseID => $c->stash->{courseID},
initial_request => 1,
accept_multiple =>
@@ -191,9 +195,9 @@ sub launch ($c) {
? (data => $c->stash->{lti_jwt_claims}
{'https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings'}{data})
: ()
- }
- )
- : ()
+ )
+ : ()
+ }
));
}
@@ -425,4 +429,120 @@ sub purge_expired_lti_data ($c, $ce, $db) {
return;
}
+async sub registration ($c) {
+ return $c->render(json => { error => 'invalid configuration request' }, status => 400)
+ unless defined $c->req->param('openid_configuration') && defined $c->req->param('registration_token');
+
+ # If we want to allow options in the configuration such as whether grade passback is enabled or to allow the LMS
+ # administrator to choose a tool name, then this should render a form that the LMS will be presented in an iframe
+ # allowing the LMS administrator to select the options. When that form is submitted, then the code below should be
+ # executed taking those options into consideration. However, at this point this is a simplistic approach that will
+ # work in most cases.
+
+ $c->render_later;
+
+ my $configurationResult = (await Mojo::UserAgent->new->get_p($c->req->param('openid_configuration')))->result;
+ return $c->render(json => { error => 'unabled to obtain openid configuration' }, status => 400)
+ unless $configurationResult->is_success;
+ my $lmsConfiguration = $configurationResult->json;
+
+ return $c->render(json => { error => 'invalid openid configuration received' }, status => 400)
+ unless defined $lmsConfiguration->{registration_endpoint}
+ && defined $lmsConfiguration->{issuer}
+ && defined $lmsConfiguration->{jwks_uri}
+ && defined $lmsConfiguration->{token_endpoint}
+ && defined $lmsConfiguration->{authorization_endpoint}
+ && defined $lmsConfiguration->{'https://purl.imsglobal.org/spec/lti-platform-configuration'}
+ {product_family_code};
+
+ # FIXME: This should also probably check that the token_endpoint_auth_method is private_key_jwt, the
+ # id_token_signing_alg_values_supported is RS256, and that the scopes_supported is an array and contains all of the
+ # scopes listed below. There are perhaps some other configuration values that should be checked as well. However,
+ # most of the time these are all going to be fine.
+
+ my $rootURL = $c->url_for('root')->to_abs;
+
+ my $registrationResult = (await Mojo::UserAgent->new->post_p(
+ $lmsConfiguration->{registration_endpoint},
+ {
+ Authorization => 'Bearer ' . $c->req->param('registration_token'),
+ 'Content-Type' => 'application/json'
+ },
+ json => {
+ application_type => 'web',
+ response_types => ['id_token'],
+ grant_types => [ 'implicit', 'client_credentials' ],
+ client_name => 'WeBWorK at ' . $rootURL->host_port,
+ client_uri => $rootURL->to_string,
+ initiate_login_uri => $c->url_for('ltiadvantage_login')->to_abs->to_string,
+ redirect_uris => [ $c->url_for('ltiadvantage_launch')->to_abs->to_string ],
+ jwks_uri => $c->url_for('ltiadvantage_keys')->to_abs->to_string,
+ token_endpoint_auth_method => 'private_key_jwt',
+ scope => join(' ',
+ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem',
+ 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly',
+ 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly',
+ 'https://purl.imsglobal.org/spec/lti-ags/scope/score'),
+ 'https://purl.imsglobal.org/spec/lti-tool-configuration' => {
+ domain => $rootURL->host_port,
+ target_link_uri => $rootURL->to_string,
+ claims => [ 'iss', 'sub', 'name', 'given_name', 'family_name', 'email' ],
+ messages => [ {
+ type => 'LtiDeepLinkingRequest',
+ target_link_uri => $c->url_for('ltiadvantage_content_selection')->to_abs->to_string,
+ # Placements are specific to the LMS. The following placements are needed for Canvas, and Moodle
+ # completely ignores this parameter. Does D2L need any? What about Blackboard?
+ placements => [ 'assignment_selection', 'course_assignments_menu' ]
+ } ]
+ }
+ }
+ ))->result;
+ unless ($registrationResult->is_success) {
+ $c->log->error('Invalid regististration response: ' . $registrationResult->message);
+ return $c->render(json => { error => 'invalid registration response' }, status => 400);
+ }
+ return $c->render(json => { error => 'invalid registration received' }, status => 400)
+ unless defined $registrationResult->json->{client_id};
+
+ my $configuration = <<~ "END_CONFIG";
+ \$LTI{v1p3}{PlatformID} = '$lmsConfiguration->{issuer}';
+ \$LTI{v1p3}{ClientID} = '${\($registrationResult->json->{client_id})}';
+ \$LTI{v1p3}{DeploymentID} = '${
+ \($registrationResult->json->{'https://purl.imsglobal.org/spec/lti-tool-configuration'}{deployment_id}
+ // 'obtain from LMS administrator')
+ }';
+ \$LTI{v1p3}{PublicKeysetURL} = '$lmsConfiguration->{jwks_uri}';
+ \$LTI{v1p3}{AccessTokenURL} = '$lmsConfiguration->{token_endpoint}';
+ \$LTI{v1p3}{AccessTokenAUD} = '${
+ \($lmsConfiguration->{authorization_server}
+ // $lmsConfiguration->{token_endpoint})
+ }';
+ \$LTI{v1p3}{AuthReqURL} = '$lmsConfiguration->{authorization_endpoint}';
+ END_CONFIG
+
+ my $registrationDir = Mojo::File->new($c->ce->{webworkDirs}{DATA})->child('LTIRegistrationRequests');
+ if (!-d $registrationDir) {
+ eval { $registrationDir->make_path };
+ if ($@) {
+ $c->log->error("Failed to create directory for saving LTI registrations: $@");
+ return $c->render(json => { error => 'internal server error' }, status => 400);
+ }
+ }
+
+ my $registrationFile = tempfile(
+ TEMPLATE =>
+ $lmsConfiguration->{'https://purl.imsglobal.org/spec/lti-platform-configuration'}{product_family_code}
+ . '-XXXX',
+ DIR => $registrationDir,
+ SUFFIX => '.conf',
+ UNLINK => 0
+ );
+ $registrationFile->spew($configuration, 'UTF-8');
+
+ # This tells the LMS that registration is complete and it can close its dialog.
+ return $c->render(data => '');
+}
+
1;
diff --git a/lib/WeBWorK/ContentGenerator/Problem.pm b/lib/WeBWorK/ContentGenerator/Problem.pm
index e2ef7bd20a..ac08c1ff0e 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},
@@ -531,18 +529,16 @@ async sub pre_header_initialize ($c) {
# requiring another answer submission.
my $showReturningFeedback = 0;
- # Sticky answers
- if (!($c->{submitAnswers} || $previewAnswers || $checkAnswers) && $will{showOldAnswers}) {
+ # Reinsert sticky answers. Do this only if new answers are NOT being submitted,
+ # and a new problem version is NOT being opened.
+ if (!($prEnabled && !$problem->{prCount})
+ && !($c->{submitAnswers} || $previewAnswers || $checkAnswers)
+ && $will{showOldAnswers})
+ {
my %oldAnswers = decodeAnswers($problem->last_answer);
- # Do this only if new answers are NOT being submitted
- if ($prEnabled && !$problem->{prCount}) {
- # Clear answers if this is a new problem version
- delete $formFields->{$_} for keys %oldAnswers;
- } else {
- $formFields->{$_} = $oldAnswers{$_} for (keys %oldAnswers);
- $showReturningFeedback = 1
- if $ce->{pg}{options}{automaticAnswerFeedback} && $problem->num_correct + $problem->num_incorrect > 0;
- }
+ $formFields->{$_} = $oldAnswers{$_} for (keys %oldAnswers);
+ $showReturningFeedback = 1
+ if $ce->{pg}{options}{automaticAnswerFeedback} && $problem->num_correct + $problem->num_incorrect > 0;
}
my $showOnlyCorrectAnswers = $c->param('showCorrectAnswers') && $will{showCorrectAnswers};
@@ -555,7 +551,9 @@ async sub pre_header_initialize ($c) {
$c->{set},
$problem,
$c->{set}->psvn,
- $formFields,
+ $prEnabled
+ && !$problem->{prCount}
+ && !($c->{submitAnswers} || $previewAnswers || $checkAnswers || $showOnlyCorrectAnswers) ? {} : $formFields,
{
displayMode => $displayMode,
showHints => $will{showHints},
@@ -581,22 +579,21 @@ 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),
- $can{checkAnswers}
- && defined $formFields->{problem_data} ? (problemData => $formFields->{problem_data}) : ()
+ $prEnabled && !$problem->{prCount}
+ ? (problemData => '{}')
+ : ($can{checkAnswers} && defined $formFields->{problem_data})
+ ? (problemData => $formFields->{problem_data})
+ : ()
}
);
- # Warnings in the renderPG subprocess will not be caught by the global warning handler of this process.
- # So rewarn them and let the global warning handler take care of it.
- warn $pg->{warnings} if $pg->{warnings};
-
debug('end pg processing');
$pg->{body_text} .= $c->hidden_field(
@@ -608,20 +605,6 @@ async sub pre_header_initialize ($c) {
$can{showHints} &&= $pg->{flags}{hintExists};
$can{showSolutions} &&= $pg->{flags}{solutionExists};
- # Record errors
- $c->{pgdebug} = $pg->{debug_messages} if ref $pg->{debug_messages} eq 'ARRAY';
- $c->{pgwarning} = $pg->{warning_messages} if ref $pg->{warning_messages} eq 'ARRAY';
- $c->{pginternalerrors} = $pg->{internal_debug_messages} if ref $pg->{internal_debug_messages} eq 'ARRAY';
- # $c->{pgerrors} is defined if any of the above are defined, and is nonzero if any are non-empty.
- $c->{pgerrors} = @{ $c->{pgdebug} // [] } || @{ $c->{pgwarning} // [] } || @{ $c->{pginternalerrors} // [] }
- if defined $c->{pgdebug} || defined $c->{pgwarning} || defined $c->{pginternalerrors};
-
- # If $c->{pgerrors} is not defined, then the PG messages arrays were not defined,
- # which means $pg->{pgcore} was not defined and the translator died.
- warn 'Processing of this PG problem was not completed. Probably because of a syntax error. '
- . 'The translator died prematurely and no PG warning messages were transmitted.'
- unless defined $c->{pgerrors};
-
# Store fields
$c->{want} = \%want;
$c->{can} = \%can;
@@ -634,53 +617,6 @@ async sub pre_header_initialize ($c) {
return;
}
-sub warnings ($c) {
- my $output = $c->c;
-
- # Display warning messages
- if (!defined $c->{pgerrors}) {
- push(
- @$output,
- $c->tag(
- 'div',
- $c->c(
- $c->tag('h3', style => 'color:red;', $c->maketext('PG question failed to render')),
- $c->tag('p', $c->maketext('Unable to obtain error messages from within the PG question.'))
- )->join('')
- )
- );
- } elsif ($c->{pgerrors} > 0) {
- my @pgdebug = @{ $c->{pgdebug} // [] };
- my @pgwarning = @{ $c->{pgwarning} // [] };
- my @pginternalerrors = @{ $c->{pginternalerrors} // [] };
- push(
- @$output,
- $c->tag(
- 'div',
- $c->c(
- $c->tag('h2', $c->maketext('PG question processing error messages')),
- @pgdebug ? $c->c(
- $c->tag('h3', $c->maketext('PG debug messages')),
- $c->tag('p', $c->c(@pgdebug)->join($c->tag('br')))
- )->join('') : '',
- @pgwarning ? $c->c(
- $c->tag('h3', $c->maketext('PG warning messages')),
- $c->tag('p', $c->c(@pgwarning)->join($c->tag('br')))
- )->join('') : '',
- @pginternalerrors ? $c->c(
- $c->tag('h3', $c->maketext('PG internal errors')),
- $c->tag('p', $c->c(@pginternalerrors)->join($c->tag('br')))
- )->join('') : ''
- )->join('')
- )
- );
- }
-
- push(@$output, $c->SUPER::warnings());
-
- return $output->join('');
-}
-
sub head ($c) {
return '' if ($c->{invalidSet});
return $c->{pg}{head_text} if $c->{pg}{head_text};
@@ -722,9 +658,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 +728,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 +739,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 +775,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 +835,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 +845,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('')
);
}
@@ -1113,7 +977,7 @@ sub output_problem_body ($c) {
} else {
# For students render the body text of the problem with a message about error details.
return $c->c(
- $c->tag('div', id => 'output_problem_body', $c->b($c->{pg}{body_text})),
+ $c->tag('div', $c->b($c->{pg}{body_text})),
$c->include(
'ContentGenerator/Base/error_output',
error => $c->{pg}{errors},
@@ -1123,7 +987,14 @@ sub output_problem_body ($c) {
}
}
- return $c->tag('div', id => 'output_problem_body', $c->b($c->{pg}{body_text}));
+ return $c->tag(
+ 'div',
+ id => 'output_problem_body',
+ class => 'text-dark',
+ style => 'color-scheme: light',
+ data => { bs_theme => 'light' },
+ $c->b($c->{pg}{body_text})
+ );
}
# Output messages about the problem
@@ -1133,10 +1004,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}, $c->{set})->insertGrader
+ if $c->{will}{showProblemGrader};
return '';
}
@@ -1444,7 +1313,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(
@@ -1480,7 +1349,7 @@ sub output_summary ($c) {
'div',
class => 'alert alert-danger d-inline-block mb-2 p-1',
$c->maketext(
- 'ATTEMPT NOT ACCEPTED -- Please submit answers again (or request new version if neccessary).')
+ 'ATTEMPT NOT ACCEPTED -- Please submit answers again (or request new version if necessary).')
)
) if $c->{resubmitDetected};
@@ -1549,7 +1418,7 @@ sub output_achievement_message ($c) {
&& $c->{submitAnswers}
&& $c->{problem}->set_id ne 'Undefined_Set')
{
- return checkForAchievements($c->{problem}, $c->{pg}, $c);
+ return checkForAchievements($c->{problem}, $c);
}
return '';
@@ -1593,7 +1462,8 @@ sub output_past_answer_button ($c) {
$c->hidden_field(selected_sets => $c->{problem}->set_id),
$c->hidden_field(selected_users => $c->{problem}->user_id),
$c->tag(
- 'p',
+ 'div',
+ class => 'mb-3',
$c->submit_button(
$c->maketext('Show Past Answers'),
name => 'action',
diff --git a/lib/WeBWorK/ContentGenerator/ProblemSet.pm b/lib/WeBWorK/ContentGenerator/ProblemSet.pm
index ba77939ec5..d20438d94d 100644
--- a/lib/WeBWorK/ContentGenerator/ProblemSet.pm
+++ b/lib/WeBWorK/ContentGenerator/ProblemSet.pm
@@ -17,19 +17,25 @@ 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);
+
+sub can ($c, $arg) {
+ if ($arg eq 'info') {
+ return $c->{pg} ? 1 : 0;
+ }
+ return $c->SUPER::can($arg);
+}
async sub initialize ($c) {
my $db = $c->db;
my $ce = $c->ce;
my $authz = $c->authz;
- # $c->{invalidSet} is set in checkSet which is called by ContentGenerator.pm
- return
- if $c->{invalidSet}
- && ($c->{invalidSet} !~ /^Client ip address .* is not in the list of addresses/
- || $authz->{merged_set}->assignment_type !~ /gateway/);
+ # $c->{invalidSet} is set in checkSet which is called by ContentGenerator.pm.
+ # If $c->{viewSetCheck} is also set, we want to view some information unless the set is hidden.
+ return if $c->{invalidSet} && (!$c->{viewSetCheck} || $c->{viewSetCheck} eq 'hidden');
- # This will all be valid if checkSet did not set $c->{invalidSet}.
+ # This will all be valid if the above check passes.
my $userID = $c->param('user');
my $eUserID = $c->param('effectiveUser');
@@ -105,6 +111,7 @@ async sub initialize ($c) {
$c->{pg} =
await renderPG($c, $effectiveUser, $c->{set}, $problem, $c->{set}->psvn, {}, { displayMode => $displayMode });
+ $c->{pg} = '' unless $c->{pg}{body_text} =~ /\S/;
return;
}
@@ -113,17 +120,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('')
);
}
@@ -161,10 +175,8 @@ sub siblings ($c) {
return $c->include('ContentGenerator/ProblemSet/siblings', setIDs => \@setIDs);
}
-sub info {
- my ($c) = @_;
- return '' unless $c->{pg};
- return $c->include('ContentGenerator/ProblemSet/info');
+sub info ($c) {
+ return $c->{pg} ? $c->include('ContentGenerator/ProblemSet/info') : '';
}
# This is called by the ContentGenerator/ProblemSet/body template for a regular homework set.
@@ -180,12 +192,14 @@ sub gateway_body ($c) {
my $ce = $c->ce;
my $db = $c->db;
- my $set = $c->{set};
- my $effectiveUser = $c->param('effectiveUser');
- my $user = $c->param('user');
+ my $set = $c->{set};
+ my $effectiveUserID = $c->param('effectiveUser');
+ my $userID = $c->param('user');
+
+ my $effectiveUser = $db->getUser($effectiveUserID);
my $timeNow = time;
- my $timeLimit = $set->version_time_limit || 0;
+ my $timeLimit = ($set->version_time_limit || 0) * $effectiveUser->accommodation_time_factor;
# Compute how many versions have been launched within timeInterval to determine if a new version can be created,
# if a version can be continued, and the date a next version can be started. If there is an open version that
@@ -206,8 +220,9 @@ sub gateway_body ($c) {
}
# Get a problem to determine how many submits have been made.
- my @ProblemNums = $db->listUserProblems($effectiveUser, $set->set_id);
- my $Problem = $db->getMergedProblemVersion($effectiveUser, $set->set_id, $verSet->version_id, $ProblemNums[0]);
+ my @ProblemNums = $db->listUserProblems($effectiveUserID, $set->set_id);
+ my $Problem =
+ $db->getMergedProblemVersion($effectiveUserID, $set->set_id, $verSet->version_id, $ProblemNums[0]);
my $verSubmits = defined $Problem ? $Problem->num_correct + $Problem->num_incorrect : 0;
my $maxSubmits = $verSet->attempts_per_version || 0;
@@ -292,11 +307,11 @@ sub gateway_body ($c) {
$data->{score} = '';
# Only show score if user has permission and assignment has at least one submit.
- if ($authz->hasPermissions($user, 'view_hidden_work')
+ if ($authz->hasPermissions($userID, 'view_hidden_work')
|| ($verSet->hide_score eq 'N' && $verSubmits >= 1)
|| ($verSet->hide_score eq 'BeforeAnswerDate' && $timeNow > $set->answer_date))
{
- my ($total, $possible) = grade_set($db, $verSet, $effectiveUser, 1);
+ my ($total, $possible) = grade_set($db, $verSet, $effectiveUserID, 1);
$total = wwRound(2, $total);
$data->{score} = "$total/$possible";
}
diff --git a/lib/WeBWorK/ContentGenerator/ProblemSets.pm b/lib/WeBWorK/ContentGenerator/ProblemSets.pm
index 3eeb4fb4c3..2c02a51ceb 100644
--- a/lib/WeBWorK/ContentGenerator/ProblemSets.pm
+++ b/lib/WeBWorK/ContentGenerator/ProblemSets.pm
@@ -11,7 +11,7 @@ use WeBWorK::Debug;
use WeBWorK::Utils qw(sortByName);
use WeBWorK::Utils::DateTime qw(after);
use WeBWorK::Utils::Files qw(readFile path_is_subdir);
-use WeBWorK::Utils::Sets qw(is_restricted format_set_name_display);
+use WeBWorK::Utils::Sets qw(restricted_set_message);
use WeBWorK::Localize;
# The "default" data in the course_info.txt file.
@@ -114,15 +114,11 @@ sub info ($c) {
}
sub getSetStatus ($c, $set) {
- my $ce = $c->ce;
- my $db = $c->db;
- my $authz = $c->authz;
- my $effectiveUser = $c->param('effectiveUser') || $c->param('user');
- my $canViewUnopened = $authz->hasPermissions($c->param('user'), 'view_unopened_sets');
-
- my @restricted = $ce->{options}{enableConditionalRelease} ? is_restricted($db, $set, $effectiveUser) : ();
-
- my $link_is_active = 1;
+ my $ce = $c->ce;
+ my $db = $c->db;
+ my $authz = $c->authz;
+ my $effectiveUser = $c->param('effectiveUser') || $c->param('user');
+ my $restricted_msg = restricted_set_message($c, $set, 'conditional') || restricted_set_message($c, $set, 'lti');
# Determine set status.
my $status_msg;
@@ -132,11 +128,7 @@ sub getSetStatus ($c, $set) {
$status = 'not-open';
$status_msg =
$c->maketext('Will open on [_1].', $c->formatDateTime($set->open_date, $ce->{studentDateDisplayFormat}));
- push(@$other_messages, $c->restricted_progression_msg(1, $set->restricted_status * 100, @restricted))
- if @restricted;
- $link_is_active = 0
- unless $canViewUnopened
- || ($set->assignment_type =~ /gateway/ && $db->countSetVersions($effectiveUser, $set->set_id));
+ push(@$other_messages, $restricted_msg) if $restricted_msg;
} elsif ($c->submitTime < $set->due_date) {
$status = 'open';
@@ -172,31 +164,8 @@ sub getSetStatus ($c, $set) {
$c->maketext('Open. Due [_1].', $c->formatDateTime($set->due_date, $ce->{studentDateDisplayFormat}));
}
- if (@restricted) {
- $link_is_active = 0 unless $canViewUnopened;
- push(@$other_messages, $c->restricted_progression_msg(0, $set->restricted_status * 100, @restricted));
- } elsif (!$canViewUnopened
- && $ce->{LTIVersion}
- && ($ce->{LTIVersion} ne 'v1p3' || !$ce->{LTI}{v1p3}{ignoreMissingSourcedID})
- && defined $ce->{LTIGradeMode}
- && $ce->{LTIGradeMode} eq 'homework'
- && !$set->lis_source_did)
- {
- # The set shouldn't be shown if LTI grade mode is set to homework and a
- # sourced_id is not available to use to send back grades
- # (unless we are using LTI 1.3 and $LTI{v1p3}{ignoreMissingSourcedID} is set)
- push(
- @$other_messages,
- $c->maketext(
- 'You must log into this set via your Learning Management System ([_1]).',
- $ce->{LTI}{ $ce->{LTIVersion} }{LMS_url}
- ? $c->link_to(
- $ce->{LTI}{ $ce->{LTIVersion} }{LMS_name} => $ce->{LTI}{ $ce->{LTIVersion} }{LMS_url}
- )
- : $ce->{LTI}{ $ce->{LTIVersion} }{LMS_name}
- )
- );
- $link_is_active = 0;
+ if ($restricted_msg) {
+ push(@$other_messages, $restricted_msg);
}
} elsif ($c->submitTime < $set->answer_date) {
$status_msg = $c->maketext('Answers available for review on [_1].',
@@ -209,8 +178,7 @@ sub getSetStatus ($c, $set) {
status => $status,
status_msg => $status_msg,
other_messages => $other_messages,
- link_is_active => $link_is_active,
- is_restricted => scalar(@restricted)
+ is_restricted => $restricted_msg ? 1 : 0
);
}
@@ -232,20 +200,4 @@ sub byUrgency {
return $a->set_id cmp $b->set_id;
}
-sub restricted_progression_msg ($c, $open, $restriction, @restricted) {
- if (@restricted == 1) {
- return $c->maketext(
- 'To access this set you must score at least [_1]% on set [_2].',
- sprintf('%.0f', $restriction),
- $c->tag('span', dir => 'ltr', format_set_name_display($restricted[0]))
- );
- } else {
- return $c->maketext(
- 'To access this set you must score at least [_1]% on the following sets: [_2].',
- sprintf('%.0f', $restriction),
- join(', ', map { $c->tag('span', dir => 'ltr', format_set_name_display($_)) } @restricted)
- );
- }
-}
-
1;
diff --git a/lib/WeBWorK/ContentGenerator/RenderViaRPC.pm b/lib/WeBWorK/ContentGenerator/RenderViaRPC.pm
index 09e719d91b..f5d95d9e6e 100644
--- a/lib/WeBWorK/ContentGenerator/RenderViaRPC.pm
+++ b/lib/WeBWorK/ContentGenerator/RenderViaRPC.pm
@@ -21,14 +21,34 @@ use WebworkWebservice;
sub initializeRoute ($c, $routeCaptures) {
$c->{rpc} = 1;
+ my $allow_unsecured_rpc = $c->config('allow_unsecured_rpc');
+ my $disable_cookies = 0;
+
+ if ($allow_unsecured_rpc) {
+ if (ref($allow_unsecured_rpc) eq 'HASH') {
+ my $courseID = $c->param('courseID');
+ if ($courseID && $allow_unsecured_rpc->{$courseID}) {
+ if (ref($allow_unsecured_rpc->{$courseID}) eq 'HASH') {
+ my $userID = $c->param('user');
+ if ($userID && $allow_unsecured_rpc->{$courseID}{$userID}) {
+ $disable_cookies = 1;
+ }
+ } else {
+ $disable_cookies = 1;
+ }
+ }
+ } else {
+ $disable_cookies = 1;
+ }
+ }
$c->stash(disable_cookies => 1)
- if $c->current_route eq 'render_rpc' && $c->param('disableCookies') && $c->config('allow_unsecured_rpc');
+ if $c->current_route eq 'render_rpc' && $c->param('disableCookies') && $disable_cookies;
# This provides compatibility for legacy html2xml parameters.
# This should be deleted when the html2xml endpoint is removed.
if ($c->current_route eq 'html2xml') {
- $c->stash(disable_cookies => 1) if $c->config('allow_unsecured_rpc');
+ $c->stash(disable_cookies => 1) if $disable_cookies;
for ([ 'userID', 'user' ], [ 'course_password', 'passwd' ], [ 'session_key', 'key' ]) {
$c->param($_->[1], $c->param($_->[0])) if defined $c->param($_->[0]) && !defined $c->param($_->[1]);
}
diff --git a/lib/WeBWorK/ContentGenerator/SampleProblemViewer.pm b/lib/WeBWorK/ContentGenerator/SampleProblemViewer.pm
index 3ee248c2b0..d4d4ef44a0 100644
--- a/lib/WeBWorK/ContentGenerator/SampleProblemViewer.pm
+++ b/lib/WeBWorK/ContentGenerator/SampleProblemViewer.pm
@@ -2,14 +2,10 @@ package WeBWorK::ContentGenerator::SampleProblemViewer;
use Mojo::Base 'WeBWorK::ContentGenerator', -signatures;
use File::Basename qw(basename);
-use Mojo::File;
-use Mojo::JSON qw(decode_json encode_json);
-use File::Find;
use Pod::Simple::Search;
-use Pod::Simple::SimpleTree;
-use WeBWorK::Utils::Files qw(path_is_subdir);
-use SampleProblemParser qw(parseSampleProblem generateMetadata getSampleProblemCode);
+use WeBWorK::Utils::Files qw(path_is_subdir);
+use WeBWorK::PG::SampleProblemParser qw(parseSampleProblem generateMetadata getSampleProblemCode getSearchData);
=head1 NAME
@@ -92,10 +88,10 @@ sub renderSampleProblem ($c) {
%{
parseSampleProblem(
$problemFile,
- metadata => $metadata,
- pod_root => $c->url_for('pod_viewer', filePath => 'macros'),
- pg_doc_home => $c->url_for('sample_problem_index'),
- macro_locations => \%macro_locations,
+ metadata => $metadata,
+ pod_base_url => $c->url_for('pod_viewer', filePath => 'macros'),
+ sample_problem_base_url => $c->url_for('sample_problem_index'),
+ macro_locations => \%macro_locations,
)
},
metadata => $metadata,
@@ -106,195 +102,7 @@ sub renderSampleProblem ($c) {
}
sub searchData ($c) {
- my $sampleProblemDir = $c->ce->{pg_dir} . '/tutorial/sample-problems';
-
- my $searchDataFile = Mojo::File->new($c->ce->{webworkDirs}{DATA})->child('sample-problem-search-data.json');
- my %files = map { $_->{filename} => $_ } @{ (eval { decode_json($searchDataFile->slurp('UTF-8')) } // []) };
- my @updatedFiles;
-
- # Process the sample problems in the sample problem directory.
- find(
- {
- wanted => sub {
- return unless $_ =~ /\.pg$/;
-
- my $file = Mojo::File->new($File::Find::name);
- my $lastModified = $file->stat->mtime;
-
- if ($files{$_}) {
- push(@updatedFiles, $files{$_});
- return if $files{$_}{lastModified} >= $lastModified;
- }
-
- my @fileContents = eval { split("\n", $file->slurp('UTF-8')) };
- return if $@;
-
- if (!$files{$_}) {
- $files{$_} = {
- type => 'sample problem',
- filename => $_,
- dir => $file->dirname->basename
- };
- push(@updatedFiles, $files{$_});
- }
- $files{$_}{lastModified} = $lastModified;
-
- my (%words, @kw, @macros, @subjects, $description);
-
- while (@fileContents) {
- my $line = shift @fileContents;
- if ($line =~ /^#:%\s*(\w+)\s*=\s*(.*)\s*$/) {
- # Store the name and subjects.
- $files{$_}{name} = $2 if $1 eq 'name';
- if ($1 eq 'subject') {
- @subjects = split(',\s*', $2 =~ s/\[(.*)\]/$1/r);
- }
- } elsif ($line =~ /^#:\s*(.*)?/) {
- my @newWords = $c->processLine($1);
- @words{@newWords} = (1) x @newWords if @newWords;
- } elsif ($line =~ /loadMacros\(/) {
- my $macros = $line;
- while ($line && $line !~ /\);\s*$/) {
- $line = shift @fileContents;
- $macros .= $line;
- }
- my @usedMacros =
- map {s/['"\s]//gr} split(/\s*,\s*/, $macros =~ s/loadMacros\((.*)\)\;$/$1/r);
-
- # Get the macros other than PGML.pl, PGstandard.pl, and PGcourse.pl.
- for my $m (@usedMacros) {
- push(@macros, $m) unless $m =~ /^(PGML|PGstandard|PGcourse)\.pl$/;
- }
- } elsif ($line =~ /##\s*KEYWORDS\((.*)\)/) {
- @kw = map {s/^'(.*)'$/$1/r} split(/,\s*/, $1);
- } elsif ($line =~ /^##\s*DESCRIPTION/) {
- $line = shift(@fileContents);
- while ($line && $line !~ /^##\s*ENDDESCRIPTION/) {
- $description .= ($line =~ s/^##\s+//r) . ' ';
- $line = shift(@fileContents);
- }
- $description =~ s/\s+$//;
- }
- }
-
- $files{$_}{description} = $description;
- $files{$_}{subjects} = \@subjects;
- $files{$_}{terms} = [ keys %words ];
- $files{$_}{keywords} = \@kw;
- $files{$_}{macros} = \@macros;
-
- return;
- }
- },
- $sampleProblemDir
- );
-
- # Process the POD in macros in the macros dir.
- (undef, my $macro_files) = Pod::Simple::Search->new->inc(0)->survey($c->ce->{pg_dir} . "/macros");
- for my $macroFile (sort keys %$macro_files) {
- next if $macroFile =~ /deprecated/;
-
- my $file = Mojo::File->new($macroFile);
- my $fileName = $file->basename;
- my $lastModified = $file->stat->mtime;
-
- if ($files{$fileName}) {
- push(@updatedFiles, $files{$fileName});
- next if $files{$fileName}{lastModified} >= $lastModified;
- }
-
- if (!$files{$fileName}) {
- $files{$fileName} = {
- type => 'macro',
- id => scalar(keys %files) + 1,
- filename => $fileName,
- dir => $file->dirname->to_rel($c->ce->{pg_dir})->to_string
- };
- push(@updatedFiles, $files{$fileName});
- }
- $files{$fileName}{lastModified} = $lastModified;
-
- my $root = Pod::Simple::SimpleTree->new->parse_file($file->to_string)->root;
-
- $files{$fileName}{terms} = $c->extractHeaders($root);
-
- if (my $nameDescription = extractHeadText($root, 'NAME')) {
- (undef, my $description) = split(/\s*-\s*/, $nameDescription, 2);
- $files{$fileName}{description} = $description if $description;
- }
- }
-
- # Redindex in case files were added or removed.
- my $count = 0;
- $_->{id} = ++$count for @updatedFiles;
-
- $searchDataFile->spew(encode_json(\@updatedFiles), 'UTF-8');
-
- return $c->render(json => \@updatedFiles);
-}
-
-# Get the stop words. The stop words file is loaded the first time this method is called,
-# and is stashed and returned in later calls.
-sub stopWords ($c) {
- return $c->stash->{stopWords} if $c->stash->{stopWords};
- $c->stash->{stopWords} = {};
-
- my $contents = eval { $c->app->home->child('assets', 'stop-words-en.txt')->slurp('UTF-8') };
- return $c->stash->{stopWords} if $@;
-
- for my $line (split("\n", $contents)) {
- chomp $line;
- next if $line =~ /^#/ || !$line;
- $c->stash->{stopWords}{$line} = 1;
- }
-
- return $c->stash->{stopWords};
-}
-
-sub processLine ($c, $line) {
- my %words;
-
- # Extract linked macros and problems.
- my @linkedFiles = $line =~ /(?:PODLINK|PROBLINK)\('([\w.]+)'\)/g;
- $words{$_} = 1 for @linkedFiles;
-
- # Replace any non-word characters with spaces.
- $line =~ s/\W/ /g;
-
- for my $word (split(/\s+/, $line)) {
- next if $word =~ /^\d*$/;
- $word = lc($word);
- $words{$word} = 1 if !$c->stopWords->{$word};
- }
- return keys %words;
-}
-
-# Extract the text for a section from the given POD with a section header title.
-sub extractHeadText ($root, $title) {
- my @index = grep { ref($root->[$_]) eq 'ARRAY' && $root->[$_][2] eq $title } 0 .. $#$root;
- return unless @index == 1;
-
- my $node = $root->[ $index[0] + 1 ];
- my $str = '';
- for (2 .. $#$node) {
- $str .= ref($node->[$_]) eq 'ARRAY' ? $node->[$_][2] : $node->[$_];
- }
- return $str;
-}
-
-# Extract terms form POD headers.
-sub extractHeaders ($c, $root) {
- my %terms =
- map { $_ => 1 }
- grep { $_ && !$c->stopWords->{$_} }
- map { split(/\s+/, $_) }
- map { lc($_) =~ s/\W/ /gr }
- map {
- grep { !ref($_) }
- @$_[ 2 .. $#$_ ]
- }
- grep { ref($_) eq 'ARRAY' && $_->[0] =~ /^head\d+$/ } @$root;
- return [ keys %terms ];
+ return $c->render(json => getSearchData($c->ce->{webworkDirs}{DATA} . '/sample-problem-search-data.json'));
}
1;
diff --git a/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm b/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm
index 39b164d4ab..3dc0c76b31 100644
--- a/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm
+++ b/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm
@@ -78,6 +78,7 @@ async sub pre_header_initialize ($c) {
effectivePermissionLevel => $db->getPermissionLevel($c->{effectiveUserID})->permission,
useMathQuill => $c->{will}{useMathQuill},
useMathView => $c->{will}{useMathView},
+ problemData => '{}'
},
);
@@ -116,6 +117,7 @@ async sub pre_header_initialize ($c) {
effectivePermissionLevel => $db->getPermissionLevel($c->{effectiveUserID})->permission,
useMathQuill => $c->{will}{useMathQuill},
useMathView => $c->{will}{useMathView},
+ problemData => '{}'
},
);
@@ -155,10 +157,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} =
@@ -206,34 +207,17 @@ async sub pre_header_initialize ($c) {
showMessages => !$showOnlyCorrectAnswers,
showCorrectAnswers => $showOnlyCorrectAnswers
|| ($c->{will}{checkAnswers} && $c->{will}{showCorrectAnswers}) ? 1 : 0,
- debuggingOptions => getTranslatorDebuggingOptions($c->authz, $c->{userID})
+ debuggingOptions => getTranslatorDebuggingOptions($c->authz, $c->{userID}),
+ problemData => $c->{formFields}{problem_data} || '{}'
}
);
- # Warnings in the renderPG subprocess will not be caught by the global warning handler of this process.
- # So rewarn them and let the global warning handler take care of it.
- warn $pg->{warnings} if $pg->{warnings};
-
debug('end pg processing');
# Update and fix hint/solution options after PG processing
$c->{can}{showHints} &&= $pg->{flags}{hintExists};
$c->{can}{showSolutions} &&= $pg->{flags}{solutionExists};
- # Record errors
- $c->{pgdebug} = $pg->{debug_messages} if ref $pg->{debug_messages} eq 'ARRAY';
- $c->{pgwarning} = $pg->{warning_messages} if ref $pg->{warning_messages} eq 'ARRAY';
- $c->{pginternalerrors} = $pg->{internal_debug_messages} if ref $pg->{internal_debug_messages} eq 'ARRAY';
- # $c->{pgerrors} is defined if any of the above are defined, and is nonzero if any are non-empty.
- $c->{pgerrors} = @{ $c->{pgdebug} // [] } || @{ $c->{pgwarning} // [] } || @{ $c->{pginternalerrors} // [] }
- if defined $c->{pgdebug} || defined $c->{pgwarning} || defined $c->{pginternalerrors};
-
- # If $c->{pgerrors} is not defined, then the PG messages arrays were not defined,
- # which means $pg->{pgcore} was not defined and the translator died.
- warn 'Processing of this PG problem was not completed. Probably because of a syntax error. '
- . 'The translator died prematurely and no PG warning messages were transmitted.'
- unless defined $c->{pgerrors};
-
$c->{pg} = $pg;
return;
diff --git a/lib/WeBWorK/DB.pm b/lib/WeBWorK/DB.pm
index 5ccf5e0eba..2aa588e0d9 100644
--- a/lib/WeBWorK/DB.pm
+++ b/lib/WeBWorK/DB.pm
@@ -436,13 +436,19 @@ sub abort_transaction {
BEGIN {
*User = gen_schema_accessor("user");
- *newUser = gen_new("user");
*countUsersWhere = gen_count_where("user");
*existsUserWhere = gen_exists_where("user");
*listUsersWhere = gen_list_where("user");
*getUsersWhere = gen_get_records_where("user");
}
+sub newUser {
+ my ($self, @data) = @_;
+ my $user = $self->{user}{record}->new(@data);
+ $user->accommodation_time_factor(1) unless defined $user->accommodation_time_factor;
+ return $user;
+}
+
sub countUsers { return scalar shift->listUsers(@_) }
# Note: This returns a list of user_ids for all users except set level proctors.
diff --git a/lib/WeBWorK/DB/Record/User.pm b/lib/WeBWorK/DB/Record/User.pm
index b712f3473d..5eeea5780e 100644
--- a/lib/WeBWorK/DB/Record/User.pm
+++ b/lib/WeBWorK/DB/Record/User.pm
@@ -12,20 +12,21 @@ use warnings;
BEGIN {
__PACKAGE__->_fields(
- user_id => { type => "VARCHAR(100) NOT NULL", key => 1 },
- first_name => { type => "TEXT" },
- last_name => { type => "TEXT" },
- email_address => { type => "TEXT" },
- student_id => { type => "TEXT" },
- status => { type => "TEXT" },
- section => { type => "TEXT" },
- recitation => { type => "TEXT" },
- comment => { type => "TEXT" },
- displayMode => { type => "TEXT" },
- showOldAnswers => { type => "INT" },
- useMathView => { type => "INT" },
- useMathQuill => { type => "INT" },
- lis_source_did => { type => "TEXT" },
+ user_id => { type => "VARCHAR(100) NOT NULL", key => 1 },
+ first_name => { type => "TEXT" },
+ last_name => { type => "TEXT" },
+ email_address => { type => "TEXT" },
+ student_id => { type => "TEXT" },
+ status => { type => "TEXT" },
+ accommodation_time_factor => { type => "FLOAT NOT NULL DEFAULT 1" },
+ section => { type => "TEXT" },
+ recitation => { type => "TEXT" },
+ comment => { type => "TEXT" },
+ displayMode => { type => "TEXT" },
+ showOldAnswers => { type => "INT" },
+ useMathView => { type => "INT" },
+ useMathQuill => { type => "INT" },
+ lis_source_did => { type => "TEXT" },
);
}
diff --git a/lib/WeBWorK/File/SetDef.pm b/lib/WeBWorK/File/SetDef.pm
index e137ddf544..1599b25848 100644
--- a/lib/WeBWorK/File/SetDef.pm
+++ b/lib/WeBWorK/File/SetDef.pm
@@ -197,8 +197,8 @@ sub importSetsFromDef ($ce, $db, $setDefFiles, $existingSets = undef, $assign =
showMeAnother => $rh_problem->{showMeAnother},
showHintsAfter => $rh_problem->{showHintsAfter},
prPeriod => $rh_problem->{prPeriod},
- attToOpenChildren => $rh_problem->{attToOpenChildren},
- countsParentGrade => $rh_problem->{countsParentGrade}
+ attToOpenChildren => $rh_problem->{att_to_open_children},
+ countsParentGrade => $rh_problem->{counts_parent_grade}
);
}
diff --git a/lib/WeBWorK/HTML/SingleProblemGrader.pm b/lib/WeBWorK/HTML/SingleProblemGrader.pm
index d10de75633..fa7bd24304 100644
--- a/lib/WeBWorK/HTML/SingleProblemGrader.pm
+++ b/lib/WeBWorK/HTML/SingleProblemGrader.pm
@@ -11,8 +11,9 @@ as a student.
use WeBWorK::Localize;
use WeBWorK::Utils 'wwRound';
+use WeBWorK::Utils::DateTime qw(before);
-sub new ($class, $c, $pg, $userProblem) {
+sub new ($class, $c, $pg, $userProblem, $mergedSet) {
$class = ref($class) || $class;
my $db = $c->db;
@@ -43,7 +44,12 @@ sub new ($class, $c, $pg, $userProblem) {
recorded_score => $recordedScore,
past_answer_id => $userPastAnswerID // 0,
comment_string => $comment,
- c => $c
+ c => $c,
+ # The grader needs to also save the sub_status if reduced scoring is not enabled,
+ # or if it is but it is before the reduced scoring date.
+ save_sub_status => !$c->ce->{pg}{ansEvalDefaults}{enableReducedScoring}
+ || !$mergedSet->enable_reduced_scoring
+ || before($mergedSet->reduced_scoring_date)
};
bless $self, $class;
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/Localize/webwork2.pot b/lib/WeBWorK/Localize/webwork2.pot
index 0d3860417c..79054b475e 100644
--- a/lib/WeBWorK/Localize/webwork2.pot
+++ b/lib/WeBWorK/Localize/webwork2.pot
@@ -19,7 +19,7 @@ msgstr ""
msgid " (version %1)"
msgstr ""
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/ProblemSet.pm:263 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/ProblemSet.pm:282
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/ProblemSet.pm:278 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/ProblemSet.pm:297
msgid " Answers Available."
msgstr ""
@@ -38,6 +38,11 @@ msgstr ""
msgid "\"%1\" contains invalid characters."
msgstr ""
+#. (xml_escape($hardcopy_format)
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:134
+msgid "\"%1\" is not a valid hardcopy format."
+msgstr ""
+
#: /opt/webwork/webwork2/templates/HelpFiles/InstructorUserList.html.ep:127
msgid "\"Act as\" a student"
msgstr ""
@@ -74,7 +79,7 @@ msgstr ""
msgid "% Score with Review"
msgstr ""
-#: /opt/webwork/webwork2/templates/ContentGenerator/GatewayQuiz.html.ep:501
+#: /opt/webwork/webwork2/templates/ContentGenerator/GatewayQuiz.html.ep:506
msgid "% Score:"
msgstr ""
@@ -83,13 +88,28 @@ msgstr ""
msgid "%1 (%2 remaining)"
msgstr ""
+#. ($user->full_name, $user->user_id)
+#: /opt/webwork/webwork2/templates/ContentGenerator/Feedback/feedback_email.txt.ep:21
+msgid "%1 (%2) wrote:"
+msgstr ""
+
+#. ($user->status)
+#: /opt/webwork/webwork2/templates/ContentGenerator/Feedback/feedback_email.html.ep:165 /opt/webwork/webwork2/templates/ContentGenerator/Feedback/feedback_email.txt.ep:97
+msgid "%1 (unknown status abbreviation)"
+msgstr ""
+
#. ($c->maketext($self->name)
#: /opt/webwork/webwork2/lib/WeBWorK/AchievementItems.pm:121
msgid "%1 (unlimited reusability)"
msgstr ""
+#. ($_->{displayName}, $_->{setVersion})
+#: /opt/webwork/webwork2/templates/ContentGenerator/GatewayQuiz/nav.html.ep:50
+msgid "%1 (version %2)"
+msgstr ""
+
#. ($properties{name})
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm:1018
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm:1008
msgid "%1 Help"
msgstr ""
@@ -100,12 +120,12 @@ msgid "%1 Icon"
msgstr ""
#. ($total)
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/CourseAdmin.pm:2552
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/CourseAdmin.pm:2553
msgid "%1 OTP secrets copied."
msgstr ""
#. ($total)
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/CourseAdmin.pm:2527
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/CourseAdmin.pm:2528
msgid "%1 OTP secrets reset."
msgstr ""
@@ -120,22 +140,22 @@ msgid "%1 Problems:"
msgstr ""
#. ('templates', 'html')
-#: /opt/webwork/webwork2/templates/ContentGenerator/CourseAdmin/add_course_form.html.ep:180
+#: /opt/webwork/webwork2/templates/ContentGenerator/CourseAdmin/add_course_form.html.ep:181
msgid "%1 and %2 folders"
msgstr ""
#. ($achievementID =~ s/_/ /gr)
-#: /opt/webwork/webwork2/templates/ContentGenerator/Base/links.html.ep:333
+#: /opt/webwork/webwork2/templates/ContentGenerator/Base/links.html.ep:309
msgid "%1 evaluator"
msgstr ""
#. ($achievementID =~ s/_/ /gr)
-#: /opt/webwork/webwork2/templates/ContentGenerator/Base/links.html.ep:340
+#: /opt/webwork/webwork2/templates/ContentGenerator/Base/links.html.ep:316
msgid "%1 notifications"
msgstr ""
#. ($count)
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm:2141
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm:2120
msgid "%1 sets"
msgstr ""
@@ -145,13 +165,13 @@ msgid "%1 setting"
msgstr ""
#. ($count, $numUsers)
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm:2127
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm:2106
msgid "%1 students out of %2"
msgstr ""
#. ($achievementItem->name, $message)
#: /opt/webwork/webwork2/lib/WeBWorK/AchievementItems.pm:93
-msgid "%1 successfuly used. %2"
+msgid "%1 successfully used. %2"
msgstr ""
#. ($rename_oldCourseID, $rename_oldCourseTitle, $rename_newCourseTitle, $rename_oldCourseInstitution, $rename_newCourseInstitution)
@@ -160,17 +180,17 @@ msgid "%1 title and institution changed from %2 to %3 and from %4 to %5"
msgstr ""
#. ($achievementID =~ s/_/ /gr)
-#: /opt/webwork/webwork2/templates/ContentGenerator/Base/links.html.ep:347
+#: /opt/webwork/webwork2/templates/ContentGenerator/Base/links.html.ep:323
msgid "%1 users"
msgstr ""
#. (scalar @userIDsToExport, "$dir/$fileName")
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm:458
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm:481
msgid "%1 users exported to file %2"
msgstr ""
#. ($numReplaced, $numAdded, $numSkipped, join(', ', @$skipped)
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm:432
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm:455
msgid "%1 users replaced, %2 users added, %3 users skipped. Skipped users: (%4)"
msgstr ""
@@ -219,12 +239,12 @@ msgstr ""
#. ($c->tag('span', dir => 'ltr', format_set_name_display($setID)
#. ($c->tag('span', dir => 'ltr', $prettySetID)
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Login.pm:27 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Problem.pm:1028 /opt/webwork/webwork2/templates/ContentGenerator/Base/links.html.ep:162
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Login.pm:27 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Problem.pm:957 /opt/webwork/webwork2/templates/ContentGenerator/Base/links.html.ep:162
msgid "%1: Problem %2"
msgstr ""
#. ($c->tag('span', dir => 'ltr', format_set_name_display($c->stash('setID')
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm:252
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm:254
msgid "%1: Problem %2 Show Me Another"
msgstr ""
@@ -251,7 +271,7 @@ msgstr ""
#. (sprintf('%3.1f', $testTime)
#. ($timeLeft)
#. ($minutes)
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/LTIUpdate.pm:93 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/StudentProgress.pm:146 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/StudentProgress.pm:164
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/LTIUpdate.pm:93 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/StudentProgress.pm:145 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/StudentProgress.pm:163
msgid "%quant(%1,minute)"
msgstr ""
@@ -291,43 +311,43 @@ msgid "(%quant(%1,item))"
msgstr ""
#. ($problemValue)
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Problem.pm:1040
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Problem.pm:969
msgid "(%quant(%1,point))"
msgstr ""
-#: /opt/webwork/webwork2/templates/ContentGenerator/Instructor/ProblemSetDetail.html.ep:713
+#: /opt/webwork/webwork2/templates/ContentGenerator/Instructor/ProblemSetDetail.html.ep:714
msgid "(Any unsaved changes will be lost.)"
msgstr ""
-#: /opt/webwork/webwork2/templates/HelpFiles/InstructorAchievementNotificationEditor.html.ep:11 /opt/webwork/webwork2/templates/HelpFiles/InstructorPGProblemEditor.html.ep:137
+#: /opt/webwork/webwork2/templates/HelpFiles/InstructorAchievementNotificationEditor.html.ep:11 /opt/webwork/webwork2/templates/HelpFiles/InstructorPGProblemEditor.html.ep:123
msgid "(If an action cannot be executed it will not appear.)"
msgstr ""
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Problem.pm:1221
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Problem.pm:1162
msgid "(This problem will not count toward your grade.)"
msgstr ""
-#: /opt/webwork/webwork2/templates/ContentGenerator/ProblemSets/set_list_row.html.ep:34
+#: /opt/webwork/webwork2/templates/ContentGenerator/ProblemSets/set_list_row.html.ep:33
msgid "(This set is hidden from students.)"
msgstr ""
-#: /opt/webwork/webwork2/templates/ContentGenerator/GatewayQuiz.html.ep:359
+#: /opt/webwork/webwork2/templates/ContentGenerator/GatewayQuiz.html.ep:342
msgid "(This test is overtime because it was not submitted in the allowed time.)"
msgstr ""
# $testNoun is either "test" or "submission"
#. ($testNoun, $c->formatDateTime($c->{set}->answer_date, $ce->{studentDateDisplayFormat})
-#: /opt/webwork/webwork2/templates/ContentGenerator/GatewayQuiz.html.ep:170
+#: /opt/webwork/webwork2/templates/ContentGenerator/GatewayQuiz.html.ep:151
msgid "(Your score on this %1 is not available until %2.)"
msgstr ""
# $testNoun is either "test" or "submission"
#. ($testNoun)
-#: /opt/webwork/webwork2/templates/ContentGenerator/GatewayQuiz.html.ep:172
+#: /opt/webwork/webwork2/templates/ContentGenerator/GatewayQuiz.html.ep:153
msgid "(Your score on this %1 is not available.)"
msgstr ""
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1276 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1286
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1280 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1290
msgid "(correct)"
msgstr ""
@@ -335,24 +355,28 @@ msgstr ""
msgid "(in target set)"
msgstr ""
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1278 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1288
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1282 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1292
msgid "(incorrect)"
msgstr ""
#. ($pgScore)
#. ($recScore)
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1280 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1290
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1284 /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm:1294
msgid "(score %1)"
msgstr ""
+#: /opt/webwork/webwork2/templates/ContentGenerator/Instructor/ProblemSetList/import_form.html.ep:37
+msgid "(taken from filenames)"
+msgstr ""
+
#. ($versionID)
-#: /opt/webwork/webwork2/templates/ContentGenerator/Instructor/ProblemGrader.html.ep:164
+#: /opt/webwork/webwork2/templates/ContentGenerator/Instructor/ProblemGrader.html.ep:160
msgid "(version %1)"
msgstr ""
#. ($display_sort_method_name{$secondary_sort_method})
#. ($display_sort_method_name{$ternary_sort_method})
-#: /opt/webwork/webwork2/templates/ContentGenerator/Instructor/StudentProgress/set_progress.html.ep:141 /opt/webwork/webwork2/templates/ContentGenerator/Instructor/StudentProgress/set_progress.html.ep:144
+#: /opt/webwork/webwork2/templates/ContentGenerator/Instructor/StudentProgress/set_progress.html.ep:142 /opt/webwork/webwork2/templates/ContentGenerator/Instructor/StudentProgress/set_progress.html.ep:145
msgid ", then by %1"
msgstr ""
@@ -364,11 +388,11 @@ msgstr ""
msgid "0 seconds"
msgstr ""
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm:2137
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm:2116
msgid "1 set"
msgstr ""
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm:2123
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm:2102
msgid "1 student"
msgstr ""
@@ -402,11 +426,11 @@ msgstr ""
msgid "Some servers handle courses taking place in different timezones. If this course is not showing the correct timezone, enter the correct value here. The format consists of unix times, such as \"America/New_York\", \"America/Chicago\", \"America/Denver\", \"America/Phoenix\" or \"America/Los_Angeles\".
Complete list: TimeZoneFiles"
msgstr ""
-#: /opt/webwork/webwork2/lib/WeBWorK/ConfigValues.pm:982
+#: /opt/webwork/webwork2/lib/WeBWorK/ConfigValues.pm:985
msgid "This can be set to one of the dates associated with assignments, or \"Never\". For each assignment, if this setting is \"After the ... \" then if it is after the indicated date, WeBWorK will send scores. If this setting is \"Never\" then there is no date that will force WeBWorK to send scores and only the $LTISendGradesEarlyThreshold can cause scores to be sent. If scores are sent:
- For 'course' grade passback mode, the assignment will be included in the overall course score calculation.
- For 'homework' grade passback mode, the assignment's score itself will be sent.
If $LTISendScoresAfterDate is set to \"After the reduced scoring date\" and an assignment has no reduced scoring date or reduced scoring is disabled, the fallback is to use the close date.
For a given assignment, WeBWorK will still send a score to the LMS if the $LTISendGradesEarlyThreshold has been met, regardless of how $LTISendScoresAfterDate is set.
"
msgstr ""
-#: /opt/webwork/webwork2/lib/WeBWorK/ConfigValues.pm:1008
+#: /opt/webwork/webwork2/lib/WeBWorK/ConfigValues.pm:1011
msgid "This can either be set to a score or set to Attempted. When something triggers a potential grade passback, if it is earlier than $LTISendScoresAfterDate, the condition described by this variable must be met or else no score will be sent.
If this variable is a score, then the set will need to have a score that reaches or exceeds this score for its score to be sent to the LMS (or included in the 'course' score calculation). If this variable is set to Attempted, then the set needs to have been attempted for its score to be sent to the LMS (or included in the 'course' score calculation).
For a regular or jitar set, 'attempted' means that at least one exercise was attempted. For a test, 'attempted' means that either multiple versions exist or there is one version with a graded submission.
"
msgstr ""
@@ -414,7 +438,11 @@ msgstr ""
msgid "This sets whether the Reduced Scoring system will be enabled. If enabled you will need to set the default length of the reduced scoring period and the value of work done in the reduced scoring period below.
To use this, you also have to enable Reduced Scoring for individual assignments and set their Reduced Scoring Dates by editing the set data.
This works with the avg_problem_grader (which is the default grader) and the std_problem_grader (the all or nothing grader). It will work with custom graders if they are written appropriately.
"
msgstr ""
-#: /opt/webwork/webwork2/lib/WeBWorK/ConfigValues.pm:948
+#: /opt/webwork/webwork2/lib/WeBWorK/ConfigValues.pm:835
+msgid "When students click the Email Instructor button to send feedback, WeBWorK fills in the subject line. Here you can set the subject line. In it, you can have various bits of information filled in with the following escape sequences.
- %c = course ID
- %u = user ID
- %s = set ID
- %p = problem ID
- %x = section
- %r = recitation
- %% = literal percent sign
If content is between a brace pair, like '{ rec:%r}', then it will only be included in the subject line if all substitutions within the double brace pair are defined and nonempty."
+msgstr ""
+
+#: /opt/webwork/webwork2/lib/WeBWorK/ConfigValues.pm:951
msgid "
When this is true, any time WeBWorK is about to send a score to the LMS, it will first request from the LMS what that score currently is. Then if there is no significant difference between the LMS score and the WeBWorK score, WeBWorK will not follow through with updating the LMS score. This is to avoid frequent insignificant updates to a student score in the LMS. With some LMSs, students may receive notifications each time a score is updated, and setting this variable will prevent too many notifications for them. This does create a two-step process, first querying the current score from the LMS and then actually updating the score (if there is a significant difference). Additional details:
- If the LMS score is not 100%, but the WeBWorK score is, then even if the LMS score is only insignificantly less than 100%, it will be updated anyway.
- If the LMS score is not set and the WeBWorK score is 0, this is considered a significant difference and the LMS score will updated to 0. However, the constraints of the $LTISendScoresAfterDate and the $LTISendGradesEarlyThreshold variables (described below) might apply, and the score may still not be updated in this case.
- \"Significant\" means an absolute difference of 0.001, or 0.1%. At this time this is not configurable.
"
msgstr ""
@@ -422,7 +450,7 @@ msgstr ""
msgid "When viewing a problem, users may choose different methods of rendering formulas via an options box in the left panel. Here, you can adjust what display modes are listed.
The display modes are
- plainText: shows the raw LaTeX strings for formulas.
- images: produces images using the external programs LaTeX and dvipng.
- MathJax: uses javascript to render mathematics.
You must use at least one display mode. If you select only one, then the options box will not give a choice of modes (since there will only be one active).
"
msgstr ""
-#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm:1447
+#: /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm:1451
msgid "Warning: There may be something wrong with a question in this test. Please inform your instructor including the warning messages below."
msgstr ""
@@ -451,7 +479,7 @@ msgid "