From 42d5fa58319e521e3e69aaa9ac01af1fe6e39677 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Tue, 9 Sep 2025 06:42:55 -0500 Subject: [PATCH 1/3] Add an "accessibility time factor" to provide for extra time on timed tests. This accesibility time factor is a user property and is set when editing a student on the "Accounts Manager" page. The time that a student will have to complete any timed test is the product of the "Test Time Limit" set for the test on the "Set Detail" page, and this accesibility time factor for the student taking the test. By default the accesibility time factor for each student is 1, but can be set to something like 1.5 to determine that a student is allowed time and a half to complete timed tests. The point of this is that it is a bit tedious to need to go through all timed tests and change the test time limit for all of the students that need to be given extra time for accesibility accomodations. With this you only need to set one number, and it is rather convenient to do so for all of the students in the class that need it from one page. Since this is a per user setting, this requires the addition of a new column to the user table in the database. --- lib/WeBWorK/ContentGenerator/GatewayQuiz.pm | 8 +++-- .../Instructor/ProblemSetDetail.pm | 4 ++- .../ContentGenerator/Instructor/UserList.pm | 28 +++++++++--------- lib/WeBWorK/ContentGenerator/ProblemSet.pm | 19 +++++++----- lib/WeBWorK/DB/Record/User.pm | 29 ++++++++++--------- .../Instructor/UserList/user_list.html.ep | 1 + .../ProblemSet/version_list.html.ep | 2 +- .../HelpFiles/InstructorUserList.html.ep | 15 ++++++++-- 8 files changed, 63 insertions(+), 43 deletions(-) diff --git a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm index e4e827d069..a6d946bf1d 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->accessibility_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->accessibility_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..2174da34de 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 accessibility 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..f8b6651bc1 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm @@ -93,24 +93,26 @@ 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', 'accessibility_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. 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', 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' }, + accessibility_time_factor => { name => x('Accessibility Time Factor'), type => 'text', size => 5 }, + 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' }, }; sub pre_header_initialize ($c) { diff --git a/lib/WeBWorK/ContentGenerator/ProblemSet.pm b/lib/WeBWorK/ContentGenerator/ProblemSet.pm index ba77939ec5..d25a8a62a9 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->accessibility_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..b7dfea59fc 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" }, + accessibility_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/user_list.html.ep b/templates/ContentGenerator/Instructor/UserList/user_list.html.ep index 3abe3e2bc6..0ec940e0ca 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('Accessibility Time Factor') %>
<%= link_to maketext('Section') => '#', class => 'sort-header', 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..5e13139d03 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, accessibility 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,15 @@ . '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 "Accesibility Time Factor" to the desired multiplier for each student selected. 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 "Accessibility 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 } From 261c64524e4f09862651172ea89b08edba25b32d Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 19 Dec 2025 05:55:04 -0600 Subject: [PATCH 2/3] Change from "Accessibility Time Factor" to "Accommodation Time Factor". --- lib/WeBWorK/ContentGenerator/GatewayQuiz.pm | 4 ++-- lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm | 2 +- lib/WeBWorK/ContentGenerator/Instructor/UserList.pm | 4 ++-- lib/WeBWorK/ContentGenerator/ProblemSet.pm | 2 +- lib/WeBWorK/DB/Record/User.pm | 2 +- .../ContentGenerator/Instructor/UserList/user_list.html.ep | 2 +- templates/HelpFiles/InstructorUserList.html.ep | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm index a6d946bf1d..bad45efc5a 100644 --- a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm +++ b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm @@ -603,7 +603,7 @@ 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->accessibility_time_factor; + 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; @@ -627,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 * $effectiveUser->accessibility_time_factor); + $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 2174da34de..9b5d279b9a 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm @@ -282,7 +282,7 @@ use constant FIELD_PROPERTIES => { . '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 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 accessibility time factor set for the student in the ' + . 'is the product of this time limit and the accommodation time factor set for the student in the ' . 'accounts manager.' ) }, diff --git a/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm b/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm index f8b6651bc1..0666199f81 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm @@ -94,7 +94,7 @@ use constant SORT_SUBS => { use constant FIELDS => [ 'user_id', 'first_name', 'last_name', 'email_address', - 'student_id', 'status', 'accessibility_time_factor', 'section', + 'student_id', 'status', 'accommodation_time_factor', 'section', 'recitation', 'comment', 'permission', 'password' ]; @@ -107,7 +107,7 @@ use constant FIELD_PROPERTIES => { 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' }, - accessibility_time_factor => { name => x('Accessibility Time Factor'), type => 'text', size => 5 }, + accommodation_time_factor => { name => x('Accommodation Time Factor'), type => 'text', size => 5 }, section => { name => x('Section'), type => 'text', size => 3 }, recitation => { name => x('Recitation'), type => 'text', size => 3 }, comment => { name => x('Comment'), type => 'text', size => 20 }, diff --git a/lib/WeBWorK/ContentGenerator/ProblemSet.pm b/lib/WeBWorK/ContentGenerator/ProblemSet.pm index d25a8a62a9..1bb6c88548 100644 --- a/lib/WeBWorK/ContentGenerator/ProblemSet.pm +++ b/lib/WeBWorK/ContentGenerator/ProblemSet.pm @@ -187,7 +187,7 @@ sub gateway_body ($c) { my $effectiveUser = $db->getUser($effectiveUserID); my $timeNow = time; - my $timeLimit = ($set->version_time_limit || 0) * $effectiveUser->accessibility_time_factor; + 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 diff --git a/lib/WeBWorK/DB/Record/User.pm b/lib/WeBWorK/DB/Record/User.pm index b7dfea59fc..5eeea5780e 100644 --- a/lib/WeBWorK/DB/Record/User.pm +++ b/lib/WeBWorK/DB/Record/User.pm @@ -18,7 +18,7 @@ BEGIN { email_address => { type => "TEXT" }, student_id => { type => "TEXT" }, status => { type => "TEXT" }, - accessibility_time_factor => { type => "FLOAT NOT NULL DEFAULT 1" }, + accommodation_time_factor => { type => "FLOAT NOT NULL DEFAULT 1" }, section => { type => "TEXT" }, recitation => { type => "TEXT" }, comment => { type => "TEXT" }, diff --git a/templates/ContentGenerator/Instructor/UserList/user_list.html.ep b/templates/ContentGenerator/Instructor/UserList/user_list.html.ep index 0ec940e0ca..4e99bb6e7a 100644 --- a/templates/ContentGenerator/Instructor/UserList/user_list.html.ep +++ b/templates/ContentGenerator/Instructor/UserList/user_list.html.ep @@ -52,7 +52,7 @@ <%= include 'ContentGenerator/Instructor/UserList/sort_button', field => 'status' =%>
- <%= maketext('Accessibility Time Factor') %> + <%= maketext('Accommodation Time Factor') %>
<%= link_to maketext('Section') => '#', class => 'sort-header', diff --git a/templates/HelpFiles/InstructorUserList.html.ep b/templates/HelpFiles/InstructorUserList.html.ep index 5e13139d03..d9443bd282 100644 --- a/templates/HelpFiles/InstructorUserList.html.ep +++ b/templates/HelpFiles/InstructorUserList.html.ep @@ -3,7 +3,7 @@ %

<%== maketext('From this page you can add new students, edit user data ' - . '(name, email address, student ID, enrollment status, accessibility time factor, section, recitation, ' + . '(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.') =%>

@@ -148,7 +148,7 @@ . 'assigned, click on the radio button for editing selected users and then click the "Edit" button .' . 'Set the "Accesibility Time Factor" to the desired multiplier for each student selected. 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 "Accessibility Time Factor" set here.') =%> + . '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.') %>
From b7e5cfeeb9696ff10a494f57d120726769c47056 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 2 Jan 2026 07:59:17 -0600 Subject: [PATCH 3/3] Add validation of the accomodation time factor. --- htdocs/js/ActionTabs/actiontabs.js | 1 + htdocs/js/UserList/userlist.js | 2 +- .../ContentGenerator/Instructor/UserList.pm | 56 ++++++++++++++----- .../Instructor/UserList.html.ep | 9 ++- .../UserList/user_list_field.html.ep | 7 ++- .../HelpFiles/InstructorUserList.html.ep | 9 +-- 6 files changed, 60 insertions(+), 24 deletions(-) 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/Instructor/UserList.pm b/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm index 0666199f81..ca6db7ed6b 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm @@ -98,21 +98,42 @@ use constant FIELDS => [ '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' }, - accommodation_time_factor => { name => x('Accommodation Time Factor'), type => 'text', size => 5 }, - 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' }, + 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) { @@ -519,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/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_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/HelpFiles/InstructorUserList.html.ep b/templates/HelpFiles/InstructorUserList.html.ep index d9443bd282..f343ae7d42 100644 --- a/templates/HelpFiles/InstructorUserList.html.ep +++ b/templates/HelpFiles/InstructorUserList.html.ep @@ -145,10 +145,11 @@
<%= 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 "Accesibility Time Factor" to the desired multiplier for each student selected. 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.') =%> + . '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.') %>