diff --git a/htdocs/js/ActionTabs/actiontabs.js b/htdocs/js/ActionTabs/actiontabs.js index 50c43da164..0662a455c7 100644 --- a/htdocs/js/ActionTabs/actiontabs.js +++ b/htdocs/js/ActionTabs/actiontabs.js @@ -6,6 +6,7 @@ actionLink.addEventListener('show.bs.tab', () => { if (takeAction) takeAction.value = actionLink.textContent; if (currentAction) currentAction.value = actionLink.dataset.action; + takeAction.formNoValidate = actionLink.dataset.noValidate ? true : false; }); }); diff --git a/htdocs/js/UserList/userlist.js b/htdocs/js/UserList/userlist.js index 617594371f..483dbc1e19 100644 --- a/htdocs/js/UserList/userlist.js +++ b/htdocs/js/UserList/userlist.js @@ -58,7 +58,7 @@ } } else { element?.classList.remove('is-invalid'); - if (element.id in event_listeners) { + if (element && element.id in event_listeners) { element?.removeEventListener('change', event_listeners[element.id]); delete event_listeners[element.id]; } diff --git a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm index e4e827d069..bad45efc5a 100644 --- a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm +++ b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm @@ -502,7 +502,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 +587,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 +603,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 +627,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); diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm index eba2d2aa82..9b5d279b9a 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm @@ -281,7 +281,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 => { 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/ProblemSet.pm b/lib/WeBWorK/ContentGenerator/ProblemSet.pm index ba77939ec5..1bb6c88548 100644 --- a/lib/WeBWorK/ContentGenerator/ProblemSet.pm +++ b/lib/WeBWorK/ContentGenerator/ProblemSet.pm @@ -180,12 +180,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 +208,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 +295,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/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/templates/ContentGenerator/Instructor/UserList.html.ep b/templates/ContentGenerator/Instructor/UserList.html.ep index 1f441230e2..424b0c2d9b 100644 --- a/templates/ContentGenerator/Instructor/UserList.html.ep +++ b/templates/ContentGenerator/Instructor/UserList.html.ep @@ -59,10 +59,15 @@ <%= link_to maketext($formTitles->{$actionID}) => "#$actionID", class => "nav-link action-link$active$disabled", id => "$actionID-tab", - data => { action => $actionID, bs_toggle => 'tab', bs_target => "#$actionID" }, + data => { + action => $actionID, + bs_toggle => 'tab', + bs_target => "#$actionID", + $actionID eq 'cancel_edit' ? (no_validate => 1) : () + }, role => 'tab', 'aria-controls' => $actionID, - 'aria-selected' => $active ? 'true' : 'false' =%> + 'aria-selected' => $active ? 'true' : 'false', =%> % end % content_for 'tab-content' => begin diff --git a/templates/ContentGenerator/Instructor/UserList/user_list.html.ep b/templates/ContentGenerator/Instructor/UserList/user_list.html.ep index 3abe3e2bc6..4e99bb6e7a 100644 --- a/templates/ContentGenerator/Instructor/UserList/user_list.html.ep +++ b/templates/ContentGenerator/Instructor/UserList/user_list.html.ep @@ -52,6 +52,7 @@ <%= include 'ContentGenerator/Instructor/UserList/sort_button', field => 'status' =%> + <%= maketext('Accommodation Time Factor') %>
<%= link_to maketext('Section') => '#', class => 'sort-header', diff --git a/templates/ContentGenerator/Instructor/UserList/user_list_field.html.ep b/templates/ContentGenerator/Instructor/UserList/user_list_field.html.ep index c0a842ee0a..e7e3831480 100644 --- a/templates/ContentGenerator/Instructor/UserList/user_list_field.html.ep +++ b/templates/ContentGenerator/Instructor/UserList/user_list_field.html.ep @@ -1,12 +1,13 @@ % my $fieldName = 'user.' . $user->user_id . '.' . $field; % my $properties = $fieldProperties->{$field}; % -% if ($properties->{type} eq 'text') { +% if ($properties->{type} eq 'text' || $properties->{type} eq 'number') { % my $value = $user->$field; % if ($c->{editMode}) { - <%= text_field $fieldName => $value, id => $fieldName . '_id', size => $properties->{size}, + % my $field_method = "$properties->{type}_field"; + <%= $c->$field_method($fieldName => $value, id => $fieldName . '_id', %{$properties->{attributes} // {}}, class => 'form-control form-control-sm d-inline w-auto', - 'aria-labelledby' => ($fieldName =~ s/^.*\.([^.]*)$/$1/r) . '_header' =%> + 'aria-labelledby' => ($fieldName =~ s/^.*\.([^.]*)$/$1/r) . '_header') =%> % } else { % if ($field eq 'email_address') { % if ($value =~ /\S/) { diff --git a/templates/ContentGenerator/ProblemSet/version_list.html.ep b/templates/ContentGenerator/ProblemSet/version_list.html.ep index 0d332c749e..f5eaa58597 100644 --- a/templates/ContentGenerator/ProblemSet/version_list.html.ep +++ b/templates/ContentGenerator/ProblemSet/version_list.html.ep @@ -15,7 +15,7 @@
<%= $c->{invalidSet} %>
% } elsif ($continueVersion) { % # Display information about the current test and a continue open test button. - % if ($timeLimit > 0) { + % if ($continueVersion->version_time_limit > 0) { % if ($timeNow >= $continueVersion->due_date) { % # If the currently open test is in the grace period, display a mesage stating this.
diff --git a/templates/HelpFiles/InstructorUserList.html.ep b/templates/HelpFiles/InstructorUserList.html.ep index 57ed8676a7..f343ae7d42 100644 --- a/templates/HelpFiles/InstructorUserList.html.ep +++ b/templates/HelpFiles/InstructorUserList.html.ep @@ -3,9 +3,9 @@ %

<%== maketext('From this page you can add new students, edit user data ' - . '(name, email address, recitation, section, permission level, enrollment status, and password), ' - . 'and export (save) class lists for back-up or use in another course. ' - . 'You can also delete students from the class roster, but this cannot be undone.') =%> + . '(name, email address, student ID, enrollment status, accommodation time factor, section, recitation, ' + . 'comment, permission level, and password), and export (save) class lists for back-up or use ' + . 'in another course. You can also delete students from the class roster, but this cannot be undone.') =%>

<%= maketext('This page gives access to information about the student, independent of the assignments ' @@ -142,6 +142,16 @@ . 'grade for each problem is listed as "status" on this third page).') =%> +

<%= maketext('Give one student or several students additional time for all timed tests.') %>
+
+ <%= maketext('Click on the "Select" checkbox next to the names of the students that additional time is to be ' + . 'assigned, click on the radio button for editing selected users, and then click the "Edit" button. ' + . 'Set the "Accommodation Time Factor" to the desired multiplier for each student selected (this must be a ' + . 'decimal number that is greater than or equal to 1). The time that a student will have to complete a ' + . 'timed test will be the product of the "Test Time Limit" for the test set in the "Sets Manager" and ' + . 'the "Accommodation Time Factor" set here.') =%> +
+
<%= maketext('Extend the number of attempts allowed a student on a given problem.') %>
<%= maketext(q{Click first in the "Assigned Sets" column in the student's row. This will take you to a new }