From 6f1fe2ee65b870fbfaaad4e9c5496d3a1554b29c Mon Sep 17 00:00:00 2001 From: Jaimos Skriletz Date: Sun, 31 Dec 2023 14:27:10 -0700 Subject: [PATCH] Remove action scope option. On the UserList, ProblemSetList, and AchievementList managers, remove the scope option that helps determine which items to act on, instead users will always select which items to act on and can use filters to change the list of items to select from. This address #1991. In addition add javascript form validation that will create a modal popup if the form is missing information, such as no items are selected, a text string is not provided, a valid file is not selected, and so on. Last, make the import tabs not create a form if no valid files are found to import from. --- htdocs/js/AchievementList/achievementlist.js | 48 +++++++ htdocs/js/ActionTabs/actiontabs.js | 53 ++++++++ htdocs/js/ProblemSetList/problemsetlist.js | 65 +++++++-- htdocs/js/UserList/userlist.js | 59 +++++++++ .../Instructor/AchievementList.pm | 77 +++-------- .../Instructor/ProblemSetList.pm | 102 +++----------- .../ContentGenerator/Instructor/UserList.pm | 66 +++------ .../Instructor/AchievementList.html.ep | 16 ++- .../AchievementList/assign_form.html.ep | 13 +- .../AchievementList/delete_form.html.ep | 8 +- .../AchievementList/edit_form.html.ep | 11 +- .../AchievementList/export_form.html.ep | 11 +- .../AchievementList/import_form.html.ep | 50 ++++--- .../AchievementList/score_form.html.ep | 12 +- .../Instructor/ProblemSetList.html.ep | 12 +- .../ProblemSetList/delete_form.html.ep | 8 +- .../ProblemSetList/edit_form.html.ep | 10 +- .../ProblemSetList/export_form.html.ep | 12 +- .../ProblemSetList/filter_form.html.ep | 3 - .../ProblemSetList/import_form.html.ep | 125 +++++++++--------- .../ProblemSetList/publish_form.html.ep | 15 +-- .../ProblemSetList/score_form.html.ep | 11 +- .../Instructor/UserList.html.ep | 12 +- .../Instructor/UserList/delete_form.html.ep | 8 +- .../Instructor/UserList/edit_form.html.ep | 10 +- .../Instructor/UserList/export_form.html.ep | 12 -- .../Instructor/UserList/import_form.html.ep | 65 +++++---- .../Instructor/UserList/password_form.html.ep | 11 +- 28 files changed, 452 insertions(+), 453 deletions(-) create mode 100644 htdocs/js/AchievementList/achievementlist.js diff --git a/htdocs/js/AchievementList/achievementlist.js b/htdocs/js/AchievementList/achievementlist.js new file mode 100644 index 0000000000..9ace0f64d3 --- /dev/null +++ b/htdocs/js/AchievementList/achievementlist.js @@ -0,0 +1,48 @@ +(() => { + // Action form validation. + const is_achievement_selected = () => { + const users = document.getElementsByName('selected_achievements'); + for (let i = 0; i < users.length; i++) { + if (users[i].checked) { return true; } + } + return false; + }; + + const action_form = document.getElementById('achievement-list'); + action_form.addEventListener('submit', e => { + const action = document.getElementById('current_action'); + if (['edit', 'assign', 'export', 'score'].includes(action.value)) { + if (!is_achievement_selected()) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.selectText, action.dataset.okayText); + } + } else if (action.value == 'import') { + if (!document.getElementById('import_file_select').value.endsWith('.axp')) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.importText, action.dataset.okayText); + } + } else if (action.value == 'create') { + if (document.getElementById('create_text')?.value == '') { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.createText, action.dataset.okayText); + } else if (document.getElementById('create_select')?.selectedIndex == 1 && !is_achievement_selected()) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.selectText, action.dataset.okayText); + } + } else if (action.value == 'delete') { + if (!is_achievement_selected()) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.selectText, action.dataset.okayText); + } else if (document.getElementById('delete_select').value != 'yes') { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.deleteText, action.dataset.okayText); + } + } + }); +})(); diff --git a/htdocs/js/ActionTabs/actiontabs.js b/htdocs/js/ActionTabs/actiontabs.js index 50c43da164..5cfde4534b 100644 --- a/htdocs/js/ActionTabs/actiontabs.js +++ b/htdocs/js/ActionTabs/actiontabs.js @@ -45,4 +45,57 @@ }); } } + + // Error modal popup added to window so it can be accessed from other modules. + window.takeActionErrorModal = (titleText, errorText, okayText) => { + const modal = document.createElement('div'); + modal.classList.add('modal'); + modal.tabIndex = -1; + modal.setAttribute('aria-labelledby', 'take-action-error-dialog'); + modal.setAttribute('aria-hidden', 'true'); + + const modalDialog = document.createElement('div'); + modalDialog.classList.add('modal-dialog', 'modal-dialog-centered'); + const modalContent = document.createElement('div'); + modalContent.classList.add('modal-content'); + + const modalHeader = document.createElement('div'); + modalHeader.classList.add('modal-header'); + + const title = document.createElement('h5'); + title.id = 'take-action-error-dialog'; + title.textContent = titleText; + + const closeButton = document.createElement('button'); + closeButton.type = 'button'; + closeButton.classList.add('btn-close'); + closeButton.dataset.bsDismiss = 'modal'; + closeButton.setAttribute('aria-label', 'close'); + + modalHeader.append(title, closeButton); + + const modalBody = document.createElement('div'); + modalBody.classList.add('modal-body'); + const modalBodyContent = document.createElement('div'); + + modalBodyContent.textContent = errorText; + modalBody.append(modalBodyContent); + + const modalFooter = document.createElement('div'); + modalFooter.classList.add('modal-footer'); + + const okButton = document.createElement('button'); + okButton.classList.add('btn', 'btn-primary'); + okButton.textContent = okayText; + okButton.addEventListener('click', () => { bsModal.hide(); }); + + modalFooter.append(okButton); + modalContent.append(modalHeader, modalBody, modalFooter); + modalDialog.append(modalContent); + modal.append(modalDialog); + + const bsModal = new bootstrap.Modal(modal); + bsModal.show(); + } + })(); diff --git a/htdocs/js/ProblemSetList/problemsetlist.js b/htdocs/js/ProblemSetList/problemsetlist.js index 2053823014..e3d11c4ca9 100644 --- a/htdocs/js/ProblemSetList/problemsetlist.js +++ b/htdocs/js/ProblemSetList/problemsetlist.js @@ -1,20 +1,61 @@ (() => { - // Show the filter error message if the 'Filter' button is clicked when matching set IDs without having entered - // a text to filter on. - document.getElementById('take_action')?.addEventListener('click', - (e) => { - const filter_err_msg = document.getElementById('filter_err_msg'); + // Action form validation. + const is_set_selected = () => { + const users = document.getElementsByName('selected_sets'); + for (let i = 0; i < users.length; i++) { + if (users[i].checked) { return true; } + } + return false; + }; - if (filter_err_msg && - document.getElementById('current_action')?.value === 'filter' && - document.getElementById('filter_select')?.selectedIndex === 3 && - document.getElementById('filter_text')?.value === '') { - filter_err_msg.classList.remove('d-none'); + const action_form = document.getElementById('problemsetlist'); + action_form.addEventListener('submit', e => { + const action = document.getElementById('current_action'); + if (action.value == 'filter') { + const filter = document.getElementById('filter_select')?.selectedIndex || 0; + if (filter === 2 && !is_set_selected()) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.selectSetText, action.dataset.okayText); + } else if (filter === 3 && document.getElementById('filter_text')?.value === '') { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.filterText, action.dataset.okayText); + } + } else if (['edit', 'publish', 'export', 'save_export', 'score'].includes(action.value)) { + if (!is_set_selected()) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.selectSetText, action.dataset.okayText); + } + } else if (action.value == 'import') { + if (!document.getElementById('import_source_select').value.endsWith('.def')) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.importSetText, action.dataset.okayText); + } + } else if (action.value == 'create') { + if (document.getElementById('create_text')?.value == '') { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.createFileText, action.dataset.okayText); + } else if (document.getElementById('create_select')?.selectedIndex == 1 && !is_set_selected()) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.selectSetText, action.dataset.okayText); + } + } else if (action.value == 'delete') { + if (!is_set_selected()) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.selectSetText, action.dataset.okayText); + } else if (document.getElementById('delete_select').value != 'yes') { e.preventDefault(); - e.stopPropagation(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.delSetText, action.dataset.okayText); } } - ); + }); // Toggle the display of the filter elements as the filter select changes. const filter_select = document.getElementById('filter_select'); diff --git a/htdocs/js/UserList/userlist.js b/htdocs/js/UserList/userlist.js index cbda5ab719..d24ca3ea79 100644 --- a/htdocs/js/UserList/userlist.js +++ b/htdocs/js/UserList/userlist.js @@ -24,4 +24,63 @@ export_select_target.addEventListener('change', classlist_add_export_elements); classlist_add_export_elements(); } + + // Action form validation. + const is_user_selected = () => { + const users = document.getElementsByName('selected_users'); + for (let i = 0; i < users.length; i++) { + if (users[i].checked) { return true; } + } + return false; + }; + + const action_form = document.getElementById('user-list-form'); + action_form.addEventListener('submit', e => { + const action = document.getElementById('current_action'); + if (action.value == 'filter') { + const filter = document.getElementById('filter_select')?.selectedIndex || 0; + if (filter === 2 && !is_user_selected()) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.selectUserText, action.dataset.okayText); + } else if (filter === 3 && document.getElementById('filter_text')?.value === '') { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.filterText, action.dataset.okayText); + } + } else if (action.value == 'edit' || action.value == 'password') { + if (!is_user_selected()) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.selectUserText, action.dataset.okayText); + } + } else if (action.value == 'import') { + if (!document.getElementById('action.import.source').value.endsWith('.lst')) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.importFileText, action.dataset.okayText); + } + } else if (action.value == 'export') { + if (!is_user_selected()) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.selectUserText, action.dataset.okayText); + } else if (document.getElementById('export_select_target').value == 'new' && + document.getElementById('export_filename').value == '') { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.exportFileText, action.dataset.okayText); + } + } else if (action.value == 'delete') { + if (!is_user_selected()) { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.selectUserText, action.dataset.okayText); + } else if (document.getElementById('delete_select').value != 'yes') { + e.preventDefault(); + window.takeActionErrorModal( + action.dataset.errorText, action.dataset.delUserText, action.dataset.okayText); + } + } + }); })(); diff --git a/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm b/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm index 43aec79032..c5ce580757 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm @@ -146,36 +146,17 @@ sub initialize ($c) { # Handler for editing achievements. Just changes the view mode. sub edit_handler ($c) { - my $result; - - my $scope = $c->param('action.edit.scope'); - if ($scope eq "all") { - $c->{selectedAchievementIDs} = $c->{allAchievementIDs}; - $result = $c->maketext('Editing all achievements.'); - } elsif ($scope eq "selected") { - $result = $c->maketext('Editing selected achievements.'); - } $c->{editMode} = 1; - return (1, $result); + return (1, $c->maketext('Editing selected achievements.')); } # Handler for assigning achievements to users sub assign_handler ($c) { - my $db = $c->db; - my $ce = $c->ce; - - my $scope = $c->param('action.assign.scope'); - my $overwrite = $c->param('action.assign.overwrite') eq 'everything'; - - my @achievementIDs; - my @users = $db->listUsers; - - if ($scope eq "all") { - @achievementIDs = @{ $c->{allAchievementIDs} }; - } else { - @achievementIDs = @{ $c->{selectedAchievementIDs} }; - } + my $db = $c->db; + my $overwrite = $c->param('action.assign.overwrite') eq 'everything'; + my @achievementIDs = @{ $c->{selectedAchievementIDs} }; + my @users = $db->listUsers; # Enable all achievements my @achievements = $db->getAchievements(@achievementIDs); @@ -222,20 +203,10 @@ sub assign_handler ($c) { # Handler for scoring sub score_handler ($c) { - my $ce = $c->ce; - my $db = $c->db; - my $courseName = $c->stash('courseID'); - - my $scope = $c->param('action.score.scope'); - my @achievementsToScore; - - if ($scope eq "none") { - @achievementsToScore = (); - } elsif ($scope eq "all") { - @achievementsToScore = @{ $c->{allAchievementIDs} }; - } elsif ($scope eq "selected") { - @achievementsToScore = $c->param('selected_achievements'); - } + my $ce = $c->ce; + my $db = $c->db; + my $courseName = $c->stash('courseID'); + my @achievementsToScore = $c->param('selected_achievements'); # Define file name my $scoreFileName = $courseName . "_achievement_scores.csv"; @@ -323,16 +294,12 @@ sub score_handler ($c) { # Handler for delete action sub delete_handler ($c) { - my $db = $c->db; + my $db = $c->db; + my $confirm = $c->param('action.delete.confirm'); - my $scope = $c->param('action.delete.scope'); - - my @achievementIDsToDelete = (); - - if ($scope eq "selected") { - @achievementIDsToDelete = @{ $c->{selectedAchievementIDs} }; - } + return (1, $c->maketext('Deleted [quant,_1,achievement].', 0)) unless ($confirm eq 'yes'); + my @achievementIDsToDelete = @{ $c->{selectedAchievementIDs} }; my %allAchievementIDs = map { $_ => 1 } @{ $c->{allAchievementIDs} }; my %selectedAchievementIDs = map { $_ => 1 } @{ $c->{selectedAchievementIDs} }; @@ -348,8 +315,7 @@ sub delete_handler ($c) { $c->{allAchievementIDs} = [ keys %allAchievementIDs ]; $c->{selectedAchievementIDs} = [ keys %selectedAchievementIDs ]; - my $num = @achievementIDsToDelete; - return (1, $c->maketext('Deleted [quant,_1,achievement].', $num)); + return (1, $c->maketext('Deleted [quant,_1,achievement].', scalar @achievementIDsToDelete)); } # Handler for creating an ahcievement @@ -472,19 +438,10 @@ sub import_handler ($c) { # Export handler # This does not actually export any files, rather it sends us to a new page in order to export the files. sub export_handler ($c) { - my $result; - - my $scope = $c->param('action.export.scope'); - if ($scope eq "all") { - $result = $c->maketext('Exporting all achievements.'); - $c->{selectedAchievementIDs} = $c->{allAchievementIDs}; - } elsif ($scope eq "selected") { - $result = $c->maketext('Exporting selected achievements.'); - $c->{selectedAchievementIDs} = [ $c->param('selected_achievements') ]; - } - $c->{exportMode} = 1; + $c->{selectedAchievementIDs} = [ $c->param('selected_achievements') ]; + $c->{exportMode} = 1; - return (1, $result); + return (1, $c->maketext('Exporting selected achievements.')); } # Handler for leaving the export page. diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm index 7a7c05c855..feeb11775b 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm @@ -158,18 +158,7 @@ sub pre_header_initialize ($c) { $c->{totalUsers} = $db->countUsers; if (defined $c->param('action') && $c->param('action') eq 'score' && $authz->hasPermissions($user, 'score_sets')) { - my $scope = $c->param('action.score.scope'); - my @setsToScore; - - if ($scope eq 'none') { - return; - } elsif ($scope eq 'all') { - @setsToScore = @{ $c->{allSetIDs} }; - } elsif ($scope eq 'visible') { - @setsToScore = @{ $c->param('visibleSetIDs') }; - } elsif ($scope eq 'selected') { - @setsToScore = $c->param('selected_sets'); - } + my @setsToScore = $c->param('selected_sets'); return unless @setsToScore; @@ -373,62 +362,24 @@ sub sort_handler ($c) { } sub edit_handler ($c) { - my $result; + $c->{visibleSetIDs} = [ $c->param('selected_sets') ]; + $c->{editMode} = 1; - my $scope = $c->param('action.edit.scope'); - if ($scope eq "all") { - $result = $c->maketext('Editing all sets.'); - $c->{visibleSetIDs} = $c->{allSetIDs}; - } elsif ($scope eq "visible") { - $result = $c->maketext('Editing listed sets.'); - # leave visibleSetIDs alone - } elsif ($scope eq "selected") { - $result = $c->maketext('Editing selected sets.'); - $c->{visibleSetIDs} = [ $c->param('selected_sets') ]; - } - $c->{editMode} = 1; - - return (1, $result); + return (1, $c->maketext('Editing selected sets.')); } sub publish_handler ($c) { - my $db = $c->db; - - my @result; - - my $scope = $c->param('action.publish.scope'); - my $value = $c->param('action.publish.value'); - - my @setIDs; - - if ($scope eq "none") { - @setIDs = (); - @result = (0, $c->maketext('No change made to any set.')); - } elsif ($scope eq "all") { - @setIDs = @{ $c->{allSetIDs} }; - @result = - $value - ? (1, $c->maketext('All sets made visible for all students.')) - : (1, $c->maketext('All sets hidden from all students.')); - } elsif ($scope eq "visible") { - @setIDs = @{ $c->{visibleSetIDs} }; - @result = - $value - ? (1, $c->maketext('All listed sets were made visible for all the students.')) - : (1, $c->maketext('All listed sets were hidden from all the students.')); - } elsif ($scope eq "selected") { - @setIDs = $c->param('selected_sets'); - @result = - $value - ? (1, $c->maketext('All selected sets made visible for all students.')) - : (1, $c->maketext('All selected sets hidden from all students.')); - } + my $db = $c->db; + my $value = $c->param('action.publish.value'); + my @setIDs = $c->param('selected_sets'); # Can we use UPDATE here, instead of fetch/change/store? my @sets = $db->getGlobalSets(@setIDs); map { $_->visible($value); $db->putGlobalSet($_); } @sets; - return @result; + return $value + ? (1, $c->maketext('All selected sets made visible for all students.')) + : (1, $c->maketext('All selected sets hidden from all students.')); } sub score_handler ($c) { @@ -438,16 +389,12 @@ sub score_handler ($c) { } sub delete_handler ($c) { - my $db = $c->db; + my $db = $c->db; + my $confirm = $c->param('action.delete.confirm'); - my $scope = $c->param('action.delete.scope'); - - my @setIDsToDelete = (); - - if ($scope eq "selected") { - @setIDsToDelete = @{ $c->{selectedSetIDs} }; - } + return (1, $c->maketext('Deleted [_1] sets.', 0)) unless ($confirm eq 'yes'); + my @setIDsToDelete = @{ $c->{selectedSetIDs} }; my %allSetIDs = map { $_ => 1 } @{ $c->{allSetIDs} }; my %visibleSetIDs = map { $_ => 1 } @{ $c->{visibleSetIDs} }; my %selectedSetIDs = map { $_ => 1 } @{ $c->{selectedSetIDs} }; @@ -463,8 +410,7 @@ sub delete_handler ($c) { $c->{visibleSetIDs} = [ keys %visibleSetIDs ]; $c->{selectedSetIDs} = [ keys %selectedSetIDs ]; - my $num = @setIDsToDelete; - return (1, $c->maketext('Deleted [_1] sets.', $num)); + return (1, $c->maketext('Deleted [_1] sets.', scalar @setIDsToDelete)); } sub create_handler ($c) { @@ -595,22 +541,10 @@ sub import_handler ($c) { # this does not actually export any files, rather it sends us to a new page in order to export the files sub export_handler ($c) { - my $result; + $c->{selectedSetIDs} = $c->{visibleSetIDs} = [ $c->param('selected_sets') ]; + $c->{exportMode} = 1; - my $scope = $c->param('action.export.scope'); - if ($scope eq "all") { - $result = $c->maketext("All sets were selected for export."); - $c->{selectedSetIDs} = $c->{visibleSetIDs} = $c->{allSetIDs}; - } elsif ($scope eq "visible") { - $result = $c->maketext("Visible sets were selected for export."); - $c->{selectedSetIDs} = $c->{visibleSetIDs}; - } elsif ($scope eq "selected") { - $result = $c->maketext("Sets were selected for export."); - $c->{selectedSetIDs} = $c->{visibleSetIDs} = [ $c->param('selected_sets') ]; - } - $c->{exportMode} = 1; - - return (1, $result); + return (1, $c->maketext('Selected sets were exported.')); } sub cancel_export_handler ($c) { diff --git a/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm b/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm index 4138304db3..acf6fcc942 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm @@ -348,54 +348,28 @@ sub sort_handler ($c) { } sub edit_handler ($c) { - my $result; - my @usersToEdit; - - my $scope = $c->param('action.edit.scope'); - if ($scope eq 'all') { - $result = $c->maketext('Editing all users.'); - @usersToEdit = grep { $c->{userIsEditable}{$_} } @{ $c->{allUserIDs} }; - } elsif ($scope eq 'visible') { - $result = $c->maketext('Editing visible users.'); - @usersToEdit = grep { $c->{userIsEditable}{$_} } (keys %{ $c->{visibleUserIDs} }); - } elsif ($scope eq 'selected') { - $result = $c->maketext('Editing selected users.'); - @usersToEdit = grep { $c->{userIsEditable}{$_} } (keys %{ $c->{selectedUserIDs} }); - } + my @usersToEdit = grep { $c->{userIsEditable}{$_} } (keys %{ $c->{selectedUserIDs} }); $c->{visibleUserIDs} = { map { $_ => 1 } @usersToEdit }; $c->{editMode} = 1; - return $result; + return $c->maketext('Editing selected users.'); } sub password_handler ($c) { - my $result; - my @usersToEdit; - - my $scope = $c->param('action.password.scope'); - if ($scope eq 'all') { - $result = $c->maketext('Giving new passwords to all users.'); - @usersToEdit = grep { $c->{userIsEditable}{$_} } @{ $c->{allUserIDs} }; - } elsif ($scope eq 'visible') { - $result = $c->maketext('Giving new passwords to visible users.'); - @usersToEdit = grep { $c->{userIsEditable}{$_} } (keys %{ $c->{visibleUserIDs} }); - } elsif ($scope eq 'selected') { - $result = $c->maketext('Giving new passwords to selected users.'); - @usersToEdit = grep { $c->{userIsEditable}{$_} } (keys %{ $c->{selectedUserIDs} }); - } + my @usersToEdit = grep { $c->{userIsEditable}{$_} } (keys %{ $c->{selectedUserIDs} }); $c->{visibleUserIDs} = { map { $_ => 1 } @usersToEdit }; $c->{passwordMode} = 1; - return $result; + return $c->maketext('Giving new passwords to selected users.'); } sub delete_handler ($c) { - my $db = $c->db; - my $user = $c->param('user'); - my $scope = $c->param('action.delete.scope'); - my $num = 0; + my $db = $c->db; + my $user = $c->param('user'); + my $confirm = $c->param('action.delete.confirm'); + my $num = 0; - return $c->maketext('Deleted [_1] users.', $num) if ($scope eq 'none'); + return $c->maketext('Deleted [_1] users.', $num) unless ($confirm eq 'yes'); # grep on userIsEditable would still enforce permissions, but no UI feedback my @userIDsToDelete = keys %{ $c->{selectedUserIDs} }; @@ -429,11 +403,14 @@ sub add_handler ($c) { } sub import_handler ($c) { - my $source = $c->param('action.import.source'); - my $add = $c->param('action.import.add'); - my $replace = $c->param('action.import.replace'); + my $fileName = $c->param('action.import.source'); + my $add = $c->param('action.import.add'); + my $replace = $c->param('action.import.replace'); - my $fileName = $source; + unless (defined($fileName) and $fileName =~ /\.lst$/) { + $c->addbadmessage($c->maketext('No class list file provided.')); + return $c->maketext('No users added.'); + } my $createNew = $add eq 'any'; my $replaceExisting; my @replaceList; @@ -475,7 +452,6 @@ sub export_handler ($c) { my $ce = $c->ce; my $dir = $ce->{courseDirs}{templates}; - my $scope = $c->param('action.export.scope'); my $target = $c->param('action.export.target'); my $new = $c->param('action.export.new'); @@ -491,15 +467,7 @@ sub export_handler ($c) { $fileName .= '.lst' unless $fileName =~ m/\.lst$/; - my @userIDsToExport; - if ($scope eq 'all') { - @userIDsToExport = @{ $c->{allUserIDs} }; - } elsif ($scope eq 'visible') { - @userIDsToExport = keys %{ $c->{visibleUserIDs} }; - } elsif ($scope eq 'selected') { - @userIDsToExport = keys %{ $c->{selectedUserIDs} }; - } - + my @userIDsToExport = keys %{ $c->{selectedUserIDs} }; $c->exportUsersToCSV($fileName, @userIDsToExport); return $c->maketext('[_1] users exported to file [_2]', scalar @userIDsToExport, "$dir/$fileName"); diff --git a/templates/ContentGenerator/Instructor/AchievementList.html.ep b/templates/ContentGenerator/Instructor/AchievementList.html.ep index 6b9c8218b2..1bb91dfb28 100644 --- a/templates/ContentGenerator/Instructor/AchievementList.html.ep +++ b/templates/ContentGenerator/Instructor/AchievementList.html.ep @@ -1,8 +1,9 @@ % use WeBWorK::Utils qw(getAssetURL); % % content_for js => begin - <%= javascript getAssetURL($ce, 'js/ActionTabs/actiontabs.js'), defer => undef =%> - <%= javascript getAssetURL($ce, 'js/SelectAll/selectall.js'), defer => undef =%> + <%= javascript getAssetURL($ce, 'js/ActionTabs/actiontabs.js'), defer => undef =%> + <%= javascript getAssetURL($ce, 'js/AchievementList/achievementlist.js'), defer => undef =%> + <%= javascript getAssetURL($ce, 'js/SelectAll/selectall.js'), defer => undef =%> % end % % unless ($authz->hasPermissions(param('user'), 'edit_achievements')) { @@ -20,7 +21,16 @@

<%= maketext('Any changes made below will be reflected in the achievement for ALL students.') %>

% } % - <%= hidden_field action => $formsToShow->[0], id => 'current_action' =%> + <%= hidden_field action => $formsToShow->[0], + id => 'current_action', + data => { + error_text => maketext('Input error'), + okay_text => maketext('Okay'), + select_text => maketext('No achievements selected.'), + import_text => maketext('Select achievements to import.'), + create_text => maketext('Enter in achievement ID to create.'), + delete_text => maketext('Confirm it is okay to delete achievements. This cannot be undone.'), + } =%>