From 6d78f5bec9abd5982744dbe04e884fad408b9b10 Mon Sep 17 00:00:00 2001 From: Jaimos Skriletz Date: Sat, 17 Feb 2024 10:41:00 -0700 Subject: [PATCH 1/8] Add filter action to achievements manager. Achievements can now be filtered based off of selected achievements, match multiple IDs, match a single category, enabled, and disabled. This also adds the javascript form validation. --- htdocs/js/AchievementList/achievementlist.js | 84 ++++++++++++++++++- .../Instructor/AchievementList.pm | 49 ++++++++++- .../Instructor/AchievementList.html.ep | 13 +++ .../AchievementList/filter_form.html.ep | 43 ++++++++++ .../InstructorAchievementList.html.ep | 6 ++ 5 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 templates/ContentGenerator/Instructor/AchievementList/filter_form.html.ep diff --git a/htdocs/js/AchievementList/achievementlist.js b/htdocs/js/AchievementList/achievementlist.js index d07b63ee7b..8d86a75f4f 100644 --- a/htdocs/js/AchievementList/achievementlist.js +++ b/htdocs/js/AchievementList/achievementlist.js @@ -9,7 +9,7 @@ 'change', () => { document.getElementById('select_achievement_err_msg')?.classList.add('d-none'); - for (const id of ['edit_select', 'assign_select', 'export_select', 'score_select']) { + for (const id of ['filter_select', 'edit_select', 'assign_select', 'export_select', 'score_select']) { document.getElementById(id)?.classList.remove('is-invalid'); } }, @@ -20,7 +20,73 @@ document.getElementById('achievement-list')?.addEventListener('submit', (e) => { const action = document.getElementById('current_action')?.value || ''; - if (action === 'edit') { + if (action === 'filter') { + const filter_select = document.getElementById('filter_select'); + const filter = filter_select?.selectedIndex || 0; + const filter_text = document.getElementById('filter_text'); + const filter_category = document.getElementById('filter_category'); + if (filter === 1 && !is_achievement_selected()) { + e.preventDefault(); + e.stopPropagation(); + filter_select?.classList.add('is-invalid'); + filter_select?.addEventListener( + 'change', + () => { + document.getElementById('select_achievement_err_msg').classList.add('d-none'); + document.getElementById('filter_select').classList.remove('is-invalid'); + }, + { once: true } + ); + } else if (filter === 2 && filter_text?.value === '') { + e.preventDefault(); + e.stopPropagation(); + document.getElementById('filter_text_err_msg')?.classList.remove('d-none'); + filter_select?.classList.add('is-invalid'); + filter_text?.classList.add('is-invalid'); + filter_text?.addEventListener( + 'change', + () => { + document.getElementById('filter_text_err_msg')?.classList.add('d-none'); + document.getElementById('filter_text')?.classList.remove('is-invalid'); + document.getElementById('filter_select')?.classList.remove('is-invalid'); + }, + { once: true } + ); + filter_select?.addEventListener( + 'change', + () => { + document.getElementById('filter_text_err_msg')?.classList.add('d-none'); + document.getElementById('filter_text')?.classList.remove('is-invalid'); + document.getElementById('filter_select')?.classList.remove('is-invalid'); + }, + { once: true } + ); + } else if (filter === 3 && filter_category?.value === '') { + e.preventDefault(); + e.stopPropagation(); + document.getElementById('filter_category_err_msg')?.classList.remove('d-none'); + filter_select?.classList.add('is-invalid'); + filter_category?.classList.add('is-invalid'); + filter_category?.addEventListener( + 'change', + () => { + document.getElementById('filter_category_err_msg')?.classList.add('d-none'); + document.getElementById('filter_category')?.classList.remove('is-invalid'); + document.getElementById('filter_select')?.classList.remove('is-invalid'); + }, + { once: true } + ); + filter_select?.addEventListener( + 'change', + () => { + document.getElementById('filter_category_err_msg')?.classList.add('d-none'); + document.getElementById('filter_category')?.classList.remove('is-invalid'); + document.getElementById('filter_select')?.classList.remove('is-invalid'); + }, + { once: true } + ); + } + } else if (action === 'edit') { const edit_select = document.getElementById('edit_select'); if (edit_select.value === 'selected' && !is_achievement_selected()) { e.preventDefault(); @@ -149,4 +215,18 @@ } }); } + + // Toggle the display of the filter elements as the filter select changes. + const filter_select = document.getElementById('filter_select'); + const filter_text_elements = document.getElementById('filter_text_elements'); + const filter_category_elements = document.getElementById('filter_category_elements'); + const filterElementToggle = () => { + if (filter_select?.selectedIndex == 2) filter_text_elements.style.display = 'flex'; + else filter_text_elements.style.display = 'none'; + if (filter_select?.selectedIndex == 3) filter_category_elements.style.display = 'flex'; + else filter_category_elements.style.display = 'none'; + }; + + if (filter_select) filterElementToggle(); + filter_select?.addEventListener('change', filterElementToggle); })(); diff --git a/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm b/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm index cebf2f45a2..57a4360f4f 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm @@ -52,13 +52,14 @@ use WeBWorK::Utils::Instructor qw(read_dir); # Forms use constant EDIT_FORMS => [qw(save_edit cancel_edit)]; -use constant VIEW_FORMS => [qw(edit assign import export score create delete)]; +use constant VIEW_FORMS => [qw(filter edit assign import export score create delete)]; use constant EXPORT_FORMS => [qw(save_export cancel_export)]; # Prepare the tab titles for translation by maketext use constant FORM_TITLES => { save_edit => x('Save Edit'), cancel_edit => x('Cancel Edit'), + filter => x('Filter'), edit => x('Edit'), assign => x('Assign'), import => x('Import'), @@ -114,6 +115,14 @@ sub initialize ($c) { $c->{editMode} = $c->param('editMode') || 0; + if (defined $c->param('visible_achievements')) { + $c->{visibleAchievementIDs} = [ $c->param('visible_achievements') ]; + } elsif (defined $c->param('no_visible_achievements')) { + $c->{visibleAchievementIDs} = []; + } else { + $c->{visibleAchievementIDs} = $c->{allAchievementIDs}; + } + # Call action handler my $actionID = $c->param('action'); $c->{actionID} = $actionID; @@ -135,17 +144,51 @@ sub initialize ($c) { $c->stash->{axpList} = [ $c->getAxpList ] unless $c->{editMode} || $c->{exportMode}; # Get and sort achievements. Achievements are sorted by in the order they are evaluated. - $c->stash->{achievements} = [ sortAchievements($c->db->getAchievements(@{ $c->{allAchievementIDs} })) ]; + $c->stash->{achievements} = [ sortAchievements($c->db->getAchievements(@{ $c->{visibleAchievementIDs} })) ]; return; } # Actions handlers. # The forms for all of the actions are templates. -# edit, cancel_edit, and save_edit should stay with the display module and +# filter, edit, cancel_edit, and save_edit should stay with the display module and # not be real "actions". that way, all actions are shown in view mode and no # actions are shown in edit mode. +sub filter_handler ($c) { + my $db = $c->db; + my $scope = $c->param('action.filter.scope'); + my $result; + + if ($scope eq 'all') { + $result = $c->maketext('Showing all achievements.'); + $c->{visibleAchievementIDs} = $c->{allAchievementIDs}; + } elsif ($scope eq 'selected') { + $result = $c->maketext('Showing selected achievements.'); + $c->{visibleAchievementIDs} = [ $c->param('selected_achievements') ]; + } elsif ($scope eq 'match_ids') { + $result = $c->maketext('Showing matching achievements.'); + my $terms = join('|', split(/\s*,\s*/, $c->param('action.filter.achievement_ids'))); + $c->{visibleAchievementIDs} = [ grep {/$terms/i} @{ $c->{allAchievementIDs} } ]; + } elsif ($scope eq 'match_category') { + my $category = $c->param('action.filter.category') // ''; + $c->{visibleAchievementIDs} = [ map { $_->[0] } $db->listAchievementsWhere({ category => $category }) ]; + if (@{ $c->{visibleAchievementIDs} }) { + $result = $c->maketext('Showing achievements in category [_1].', $category); + } else { + $result = $c->maketext('No achievements in category [_1].', $category); + } + } elsif ($scope eq 'enabled') { + $result = $c->maketext('Showing enabled achievements.'); + $c->{visibleAchievementIDs} = [ map { $_->[0] } $db->listAchievementsWhere({ enabled => 1 }) ]; + } elsif ($scope eq 'disabled') { + $result = $c->maketext('Showing enabled achievements.'); + $c->{visibleAchievementIDs} = [ map { $_->[0] } $db->listAchievementsWhere({ enabled => 0 }) ]; + } + + return (1, $result); +} + # Handler for editing achievements. Just changes the view mode. sub edit_handler ($c) { my $result; diff --git a/templates/ContentGenerator/Instructor/AchievementList.html.ep b/templates/ContentGenerator/Instructor/AchievementList.html.ep index 8e3ca53bca..1c8a8a0fe6 100644 --- a/templates/ContentGenerator/Instructor/AchievementList.html.ep +++ b/templates/ContentGenerator/Instructor/AchievementList.html.ep @@ -17,6 +17,13 @@ <%= hidden_field editMode => $c->{editMode} =%> <%= hidden_field exportMode => $c->{exportMode} =%> % + % if (@{ $c->{visibleAchievementIDs} }) { + % for (@{ $c->{visibleAchievementIDs} }) { + <%= hidden_field visible_achievements => $_ =%> + % } + % } else { + <%= hidden_field no_visible_achievements => '1' =%> + % } % if ($c->{editMode}) {

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

% } @@ -51,6 +58,12 @@ <%= submit_button maketext($formTitles->{ $formsToShow->[0] }), id => 'take_action', class => 'btn btn-primary mb-3' =%> +

+ <%= maketext('Showing [_1] out of [_2] achievements.', + scalar @{ $c->{visibleAchievementIDs} }, + scalar @{ $c->{allAchievementIDs} } + ) =%> +

% if ($c->{exportMode}) { <%= include 'ContentGenerator/Instructor/AchievementList/export_table' =%> % } elsif ($c->{editMode}) { diff --git a/templates/ContentGenerator/Instructor/AchievementList/filter_form.html.ep b/templates/ContentGenerator/Instructor/AchievementList/filter_form.html.ep new file mode 100644 index 0000000000..c4234f0309 --- /dev/null +++ b/templates/ContentGenerator/Instructor/AchievementList/filter_form.html.ep @@ -0,0 +1,43 @@ +
+
+ <%= label_for filter_select => maketext('Show which achievements?'), + class => 'col-form-label col-form-label-sm col-sm-auto' =%> +
+ <%= select_field 'action.filter.scope' => [ + [ maketext('all course achievements') => 'all' ], + [ maketext('selected achievements') => 'selected' ], + [ maketext('enter matching achievement IDs below') => 'match_ids', selected => undef ], + [ maketext('enter matching category below') => 'match_category' ], + [ maketext('enabled achievements') => 'enabled' ], + [ maketext('disabled achievements') => 'disabled' ] + ], + id => 'filter_select', class => 'form-select form-select-sm' =%> +
+
+
+ <%= label_for 'filter_text', class => 'col-form-label col-form-label-sm col-sm-auto', begin =%> + <%= maketext('Match on what? (separate multiple IDs with commas)') =%> + * + <% end =%> +
+ <%= text_field 'action.filter.achievement_ids' => '', id => 'filter_text', 'aria-required' => 'true', + class => 'form-control form-control-sm', dir => 'ltr' =%> +
+
+
+ <%= maketext('Please enter a list of IDs to match.') %> +
+
+ <%= label_for 'filter_category', class => 'col-form-label col-form-label-sm col-sm-auto', begin =%> + <%= maketext('Match on which category? (enter in exact match)') =%> + * + <% end =%> +
+ <%= text_field 'action.filter.category' => '', id => 'filter_category', 'aria-required' => 'true', + class => 'form-control form-control-sm', dir => 'ltr' =%> +
+
+
+ <%= maketext('Please enter a category to match.') %> +
+
diff --git a/templates/HelpFiles/InstructorAchievementList.html.ep b/templates/HelpFiles/InstructorAchievementList.html.ep index da226c8160..0fdc320e81 100644 --- a/templates/HelpFiles/InstructorAchievementList.html.ep +++ b/templates/HelpFiles/InstructorAchievementList.html.ep @@ -87,6 +87,12 @@

<%= maketext('How to:') %>

+
<%= maketext('Filter achievements') %> +
+ <%= maketext('You can filter which achievements are shown by clicking the "Filter" button. Use the drop ' + . 'down menu to select the filter criteria, which allows you to filter achievements by their ID, ' + . 'category, or if they are enabled or disabled.') =%> +
<%= maketext('Edit achievement information') %>
<%= maketext('You can edit a single achievement by clicking on the pencil icon next to the achievement ID. ' From 44e7ac06ea45f8cf62cec7f971383f1bf3ba3033 Mon Sep 17 00:00:00 2001 From: Jaimos Skriletz Date: Mon, 19 Feb 2024 08:44:16 -0700 Subject: [PATCH 2/8] Fixes to achievement filter. Fix text showing disabled achievements. Fix text when no achievements are found in the filter, but the course has achievements. Fix a bug where export/edit all achievements didn't show all achievements if they were previously filtered. This is done by adding a variable to show all achievements so any previous filter remains unchanged. --- .../ContentGenerator/Instructor/AchievementList.pm | 13 +++++++++---- .../AchievementList/default_table.html.ep | 6 +++++- .../Instructor/AchievementList/edit_table.html.ep | 6 +++++- .../Instructor/AchievementList/export_table.html.ep | 6 +++++- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm b/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm index 57a4360f4f..df13f4423d 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm @@ -144,7 +144,10 @@ sub initialize ($c) { $c->stash->{axpList} = [ $c->getAxpList ] unless $c->{editMode} || $c->{exportMode}; # Get and sort achievements. Achievements are sorted by in the order they are evaluated. - $c->stash->{achievements} = [ sortAchievements($c->db->getAchievements(@{ $c->{visibleAchievementIDs} })) ]; + $c->stash->{achievements} = + $c->{showAllAchievements} + ? [ sortAchievements($c->db->getAchievements(@{ $c->{allAchievementIDs} })) ] + : [ sortAchievements($c->db->getAchievements(@{ $c->{visibleAchievementIDs} })) ]; return; } @@ -182,7 +185,7 @@ sub filter_handler ($c) { $result = $c->maketext('Showing enabled achievements.'); $c->{visibleAchievementIDs} = [ map { $_->[0] } $db->listAchievementsWhere({ enabled => 1 }) ]; } elsif ($scope eq 'disabled') { - $result = $c->maketext('Showing enabled achievements.'); + $result = $c->maketext('Showing disabled achievements.'); $c->{visibleAchievementIDs} = [ map { $_->[0] } $db->listAchievementsWhere({ enabled => 0 }) ]; } @@ -196,7 +199,8 @@ sub edit_handler ($c) { my $scope = $c->param('action.edit.scope'); if ($scope eq "all") { $c->{selectedAchievementIDs} = $c->{allAchievementIDs}; - $result = $c->maketext('Editing all achievements.'); + $result = $c->maketext('Editing all achievements.'); + $c->{showAllAchievements} = 1; } elsif ($scope eq "selected") { $result = $c->maketext('Editing selected achievements.'); } @@ -498,8 +502,9 @@ sub export_handler ($c) { my $scope = $c->param('action.export.scope'); if ($scope eq "all") { - $result = $c->maketext('Exporting all achievements.'); + $result = $c->maketext('Exporting all achievements.'); $c->{selectedAchievementIDs} = $c->{allAchievementIDs}; + $c->{showAllAchievements} = 1; } else { $result = $c->maketext('Exporting selected achievements.'); $c->{selectedAchievementIDs} = [ $c->param('selected_achievements') ]; diff --git a/templates/ContentGenerator/Instructor/AchievementList/default_table.html.ep b/templates/ContentGenerator/Instructor/AchievementList/default_table.html.ep index 6c5aaadf43..fb1ccf9b33 100644 --- a/templates/ContentGenerator/Instructor/AchievementList/default_table.html.ep +++ b/templates/ContentGenerator/Instructor/AchievementList/default_table.html.ep @@ -85,5 +85,9 @@ % % unless (@$achievements) { -

<%= maketext('No achievements shown. Create an achievement!') %>

+ % if (@{ $c->{allAchievementIDs} }) { +

<%= maketext('No achievements shown. Use the filter above to list achievements.') %>

+ % } else { +

<%= maketext('No course achievements. Create or import achievements above.') %>

+ % } % } diff --git a/templates/ContentGenerator/Instructor/AchievementList/edit_table.html.ep b/templates/ContentGenerator/Instructor/AchievementList/edit_table.html.ep index 587b47a29f..0bb0bf1d78 100644 --- a/templates/ContentGenerator/Instructor/AchievementList/edit_table.html.ep +++ b/templates/ContentGenerator/Instructor/AchievementList/edit_table.html.ep @@ -74,6 +74,10 @@ % % unless (@$achievements) { -

<%= maketext('No achievements shown. Create an achievement!') %>

+ % if (@{ $c->{allAchievementIDs} }) { +

<%= maketext('No achievements selected to edit.') %>

+ % } else { +

<%= maketext('No course achievements to edit. Create or import achievements.') %>

+ % } % } % } diff --git a/templates/ContentGenerator/Instructor/AchievementList/export_table.html.ep b/templates/ContentGenerator/Instructor/AchievementList/export_table.html.ep index 928d788f4d..8cd66dac88 100644 --- a/templates/ContentGenerator/Instructor/AchievementList/export_table.html.ep +++ b/templates/ContentGenerator/Instructor/AchievementList/export_table.html.ep @@ -41,5 +41,9 @@ % % unless (@$achievements) { -

<%= maketext('No achievements shown. Create an achievement!') %>

+ % if (@{ $c->{allAchievementIDs} }) { +

<%= maketext('No achievements selected to export.') %>

+ % } else { +

<%= maketext('No course achievements to export. Create or import achievements.') %>

+ % } % } From 6cfffb9668ea65d7b36839181c9a3f66ffcf9dfc Mon Sep 17 00:00:00 2001 From: Jaimos Skriletz Date: Mon, 19 Feb 2024 09:17:29 -0700 Subject: [PATCH 3/8] Replace selectedIndex with value statements. --- htdocs/js/AchievementList/achievementlist.js | 14 +++++++------- htdocs/js/ProblemSetList/problemsetlist.js | 10 +++++----- htdocs/js/UserList/userlist.js | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/htdocs/js/AchievementList/achievementlist.js b/htdocs/js/AchievementList/achievementlist.js index 8d86a75f4f..878a90fe8c 100644 --- a/htdocs/js/AchievementList/achievementlist.js +++ b/htdocs/js/AchievementList/achievementlist.js @@ -22,10 +22,10 @@ const action = document.getElementById('current_action')?.value || ''; if (action === 'filter') { const filter_select = document.getElementById('filter_select'); - const filter = filter_select?.selectedIndex || 0; + const filter = filter_select?.value || ''; const filter_text = document.getElementById('filter_text'); const filter_category = document.getElementById('filter_category'); - if (filter === 1 && !is_achievement_selected()) { + if (filter === 'selected' && !is_achievement_selected()) { e.preventDefault(); e.stopPropagation(); filter_select?.classList.add('is-invalid'); @@ -37,7 +37,7 @@ }, { once: true } ); - } else if (filter === 2 && filter_text?.value === '') { + } else if (filter === 'match_ids' && filter_text?.value === '') { e.preventDefault(); e.stopPropagation(); document.getElementById('filter_text_err_msg')?.classList.remove('d-none'); @@ -61,7 +61,7 @@ }, { once: true } ); - } else if (filter === 3 && filter_category?.value === '') { + } else if (filter === 'match_category' && filter_category?.value === '') { e.preventDefault(); e.stopPropagation(); document.getElementById('filter_category_err_msg')?.classList.remove('d-none'); @@ -177,7 +177,7 @@ }, { once: true } ); - } else if (document.getElementById('create_select')?.selectedIndex == 1 && !is_achievement_selected()) { + } else if (document.getElementById('create_select')?.value == 'copy' && !is_achievement_selected()) { e.preventDefault(); e.stopPropagation(); } @@ -221,9 +221,9 @@ const filter_text_elements = document.getElementById('filter_text_elements'); const filter_category_elements = document.getElementById('filter_category_elements'); const filterElementToggle = () => { - if (filter_select?.selectedIndex == 2) filter_text_elements.style.display = 'flex'; + if (filter_select?.value === 'match_ids') filter_text_elements.style.display = 'flex'; else filter_text_elements.style.display = 'none'; - if (filter_select?.selectedIndex == 3) filter_category_elements.style.display = 'flex'; + if (filter_select?.value === 'match_category' ) filter_category_elements.style.display = 'flex'; else filter_category_elements.style.display = 'none'; }; diff --git a/htdocs/js/ProblemSetList/problemsetlist.js b/htdocs/js/ProblemSetList/problemsetlist.js index e66cee6cec..bdcfd6814a 100644 --- a/htdocs/js/ProblemSetList/problemsetlist.js +++ b/htdocs/js/ProblemSetList/problemsetlist.js @@ -27,9 +27,9 @@ document.getElementById('problemsetlist')?.addEventListener('submit', (e) => { const action = document.getElementById('current_action')?.value || ''; if (action === 'filter') { - const filter = document.getElementById('filter_select')?.selectedIndex || 0; + const filter = document.getElementById('filter_select')?.value || ''; const filter_text = document.getElementById('filter_text'); - if (filter === 1 && !is_set_selected()) { + if (filter === 'selected' && !is_set_selected()) { e.preventDefault(); e.stopPropagation(); document.getElementById('filter_select')?.addEventListener( @@ -39,7 +39,7 @@ }, { once: true } ); - } else if (filter === 2 && filter_text.value === '') { + } else if (filter === 'match_ids' && filter_text.value === '') { e.preventDefault(); e.stopPropagation(); document.getElementById('filter_err_msg')?.classList.remove('d-none'); @@ -149,7 +149,7 @@ }, { once: true } ); - } else if (document.getElementById('create_select')?.selectedIndex == 1 && !is_set_selected()) { + } else if (document.getElementById('create_select')?.value == 'copy' && !is_set_selected()) { e.preventDefault(); e.stopPropagation(); } @@ -192,7 +192,7 @@ const filter_select = document.getElementById('filter_select'); const filter_elements = document.getElementById('filter_elements'); const filterElementToggle = () => { - if (filter_select?.selectedIndex == 2) filter_elements.style.display = 'flex'; + if (filter_select?.value == 'match_ids') filter_elements.style.display = 'flex'; else filter_elements.style.display = 'none'; }; diff --git a/htdocs/js/UserList/userlist.js b/htdocs/js/UserList/userlist.js index 02907d31eb..338b9e6f8f 100644 --- a/htdocs/js/UserList/userlist.js +++ b/htdocs/js/UserList/userlist.js @@ -17,7 +17,7 @@ const export_elements = document.getElementById('export_elements'); if (!export_elements) return; - if (export_select_target.selectedIndex === 0) export_elements.style.display = 'block'; + if (export_select_target.value === 'new') export_elements.style.display = 'block'; else export_elements.style.display = 'none'; }; @@ -48,9 +48,9 @@ const action = document.getElementById('current_action')?.value || ''; if (action === 'filter') { const filter_select = document.getElementById('filter_select'); - const filter = filter_select?.selectedIndex || 0; + const filter = filter_select?.value || ''; const filter_text = document.getElementById('filter_text'); - if (filter === 1 && !is_user_selected()) { + if (filter === 'selected' && !is_user_selected()) { e.preventDefault(); e.stopPropagation(); filter_select.classList.add('is-invalid'); @@ -62,7 +62,7 @@ }, { once: true } ); - } else if (filter === 2 && filter_text.value === '') { + } else if (filter === 'match_regex' && filter_text.value === '') { e.preventDefault(); e.stopPropagation(); document.getElementById('filter_err_msg')?.classList.remove('d-none'); From 71db28a5636633c11500fca389551fc17752cdf3 Mon Sep 17 00:00:00 2001 From: Jaimos Skriletz Date: Sat, 24 Feb 2024 12:54:48 -0700 Subject: [PATCH 4/8] Reduce javascript redundancy. Add helper functions to show and hide errors validation to reduce duplication of code. The helper functions also add and remove event listeners to remove the error messages. --- htdocs/js/AchievementList/achievementlist.js | 215 +++++------------- htdocs/js/ProblemSetList/problemsetlist.js | 187 +++++---------- htdocs/js/UserList/userlist.js | 159 +++++-------- .../ProblemSetList/publish_form.html.ep | 4 +- 4 files changed, 167 insertions(+), 398 deletions(-) diff --git a/htdocs/js/AchievementList/achievementlist.js b/htdocs/js/AchievementList/achievementlist.js index 878a90fe8c..ae4e8059cd 100644 --- a/htdocs/js/AchievementList/achievementlist.js +++ b/htdocs/js/AchievementList/achievementlist.js @@ -1,20 +1,48 @@ (() => { // Action form validation. + const show_errors = (ids, elements) => { + for (const id of ids) elements.push(document.getElementById(id)); + for (const element of elements) { + if (element?.id.endsWith('_err_msg')) { + element?.classList.remove('d-none'); + } else { + element?.classList.add('is-invalid'); + element?.addEventListener('change', hide_errors([], elements)); + } + } + }; + + const hide_errors = (ids, elements) => { + return () => { + for (const id of ids) elements.push(document.getElementById(id)); + for (const element of elements) { + if (element?.id.endsWith('_err_msg')) { + element?.classList.add('d-none'); + if (element.id === 'select_achievement_err_msg') + document.getElementById('achievement-table')?.removeEventListener('change', hide_errors); + } else { + element?.classList.remove('is-invalid'); + element?.removeEventListener('change', hide_errors); + } + } + }; + }; + const is_achievement_selected = () => { for (const achievement of document.getElementsByName('selected_achievements')) { if (achievement.checked) return true; } - document.getElementById('select_achievement_err_msg')?.classList.remove('d-none'); - document.getElementById('achievement-table')?.addEventListener( - 'change', - () => { - document.getElementById('select_achievement_err_msg')?.classList.add('d-none'); - for (const id of ['filter_select', 'edit_select', 'assign_select', 'export_select', 'score_select']) { - document.getElementById(id)?.classList.remove('is-invalid'); - } - }, - { once: true } - ); + const err_msg = document.getElementById('select_achievement_err_msg'); + err_msg?.classList.remove('d-none'); + document + .getElementById('achievement-table') + ?.addEventListener( + 'change', + hide_errors( + ['filter_select', 'edit_select', 'assign_select', 'export_select', 'score_select'], + [err_msg] + ) + ); return false; }; @@ -28,158 +56,41 @@ if (filter === 'selected' && !is_achievement_selected()) { e.preventDefault(); e.stopPropagation(); - filter_select?.classList.add('is-invalid'); - filter_select?.addEventListener( - 'change', - () => { - document.getElementById('select_achievement_err_msg').classList.add('d-none'); - document.getElementById('filter_select').classList.remove('is-invalid'); - }, - { once: true } - ); + show_errors(['select_achievement_err_msg'], [filter_select]); } else if (filter === 'match_ids' && filter_text?.value === '') { e.preventDefault(); e.stopPropagation(); - document.getElementById('filter_text_err_msg')?.classList.remove('d-none'); - filter_select?.classList.add('is-invalid'); - filter_text?.classList.add('is-invalid'); - filter_text?.addEventListener( - 'change', - () => { - document.getElementById('filter_text_err_msg')?.classList.add('d-none'); - document.getElementById('filter_text')?.classList.remove('is-invalid'); - document.getElementById('filter_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); - filter_select?.addEventListener( - 'change', - () => { - document.getElementById('filter_text_err_msg')?.classList.add('d-none'); - document.getElementById('filter_text')?.classList.remove('is-invalid'); - document.getElementById('filter_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); + show_errors(['filter_text_err_msg'], [filter_select, filter_text]); } else if (filter === 'match_category' && filter_category?.value === '') { e.preventDefault(); e.stopPropagation(); - document.getElementById('filter_category_err_msg')?.classList.remove('d-none'); - filter_select?.classList.add('is-invalid'); - filter_category?.classList.add('is-invalid'); - filter_category?.addEventListener( - 'change', - () => { - document.getElementById('filter_category_err_msg')?.classList.add('d-none'); - document.getElementById('filter_category')?.classList.remove('is-invalid'); - document.getElementById('filter_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); - filter_select?.addEventListener( - 'change', - () => { - document.getElementById('filter_category_err_msg')?.classList.add('d-none'); - document.getElementById('filter_category')?.classList.remove('is-invalid'); - document.getElementById('filter_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); - } - } else if (action === 'edit') { - const edit_select = document.getElementById('edit_select'); - if (edit_select.value === 'selected' && !is_achievement_selected()) { - e.preventDefault(); - e.stopPropagation(); - edit_select.classList.add('is-invalid'); - edit_select.addEventListener( - 'change', - () => { - document.getElementById('select_achievement_err_msg')?.classList.add('d-none'); - document.getElementById('edit_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); + show_errors(['filter_category_err_msg'], [filter_select, filter_category]); } - } else if (action === 'assign') { - const assign_select = document.getElementById('assign_select'); - if (assign_select.value === 'selected' && !is_achievement_selected()) { + } else if (['edit', 'assign', 'export', 'score'].includes(action)) { + const action_select = document.getElementById(`${action}_select`); + if (action_select.value === 'selected' && !is_achievement_selected()) { e.preventDefault(); e.stopPropagation(); - assign_select.classList.add('is-invalid'); - assign_select.addEventListener( - 'change', - () => { - document.getElementById('select_achievement_err_msg')?.classList.add('d-none'); - document.getElementById('assign_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); + show_errors(['select_achievement_err_msg'], [action_select]); } } else if (action === 'import') { const import_file = document.getElementById('import_file_select'); if (!import_file.value.endsWith('.axp')) { e.preventDefault(); e.stopPropagation(); - document.getElementById('import_file_err_msg')?.classList.remove('d-none'); - import_file.classList.add('is-invalid'); - import_file.addEventListener( - 'change', - () => { - document.getElementById('import_file_err_msg')?.classList.add('d-none'); - document.getElementById('import_file_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); - } - } else if (action === 'export') { - const export_select = document.getElementById('export_select'); - if (export_select.value === 'selected' && !is_achievement_selected()) { - e.preventDefault(); - e.stopPropagation(); - export_select.classList.add('is-invalid'); - export_select.addEventListener( - 'change', - () => { - document.getElementById('select_achievement_err_msg')?.classList.add('d-none'); - document.getElementById('export_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); - } - } else if (action === 'score') { - const score_select = document.getElementById('score_select'); - if (export_select.value === 'selected' && !is_achievement_selected()) { - e.preventDefault(); - e.stopPropagation(); - score_select.classList.add('is-invalid'); - score_select.addEventListener( - 'change', - () => { - document.getElementById('select_achievement_err_msg')?.classList.add('d-none'); - document.getElementById('score_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); + show_errors(['import_file_err_msg'], [import_file]); } } else if (action === 'create') { const create_text = document.getElementById('create_text'); + const create_select = document.getElementById('create_select'); if (create_text.value === '') { e.preventDefault(); e.stopPropagation(); - document.getElementById('create_file_err_msg')?.classList.remove('d-none'); - create_text.classList.add('is-invalid'); - create_text.addEventListener( - 'change', - () => { - document.getElementById('create_file_err_msg')?.classList.add('d-none'); - document.getElementById('create_text')?.classList.remove('is-invalid'); - }, - { once: true } - ); - } else if (document.getElementById('create_select')?.value == 'copy' && !is_achievement_selected()) { + show_errors(['create_file_err_msg'], [create_text]); + } else if (create_select?.value == 'copy' && !is_achievement_selected()) { e.preventDefault(); e.stopPropagation(); + show_errors(['select_achievement_err_msg'], [create_select]); } } else if (action === 'delete') { const delete_confirm = document.getElementById('delete_select'); @@ -189,16 +100,7 @@ } else if (delete_confirm.value != 'yes') { e.preventDefault(); e.stopPropagation(); - document.getElementById('delete_confirm_err_msg')?.classList.remove('d-none'); - delete_confirm.classList.add('is-invalid'); - delete_confirm.addEventListener( - 'change', - () => { - document.getElementById('delete_select')?.classList.remove('is-invalid'); - document.getElementById('delete_confirm_err_msg')?.classList.add('d-none'); - }, - { once: true } - ); + show_errors(['delete_confirm_err_msg'], [delete_confirm]); } } }); @@ -206,13 +108,10 @@ // Remove all error messages when changing tabs. for (const tab of document.querySelectorAll('a[data-bs-toggle="tab"]')) { tab.addEventListener('shown.bs.tab', () => { - const actionForm = document.getElementById('achievement-list'); - for (const err_msg of actionForm.querySelectorAll('div[id$=_err_msg]')) { - err_msg.classList.add('d-none'); - } - for (const invalid of actionForm.querySelectorAll('.is-invalid')) { - invalid.classList.remove('is-invalid'); - } + hide_errors( + [], + document.getElementById('achievement-list')?.querySelectorAll('div[id$=_err_msg], .is-invalid') + )(); }); } @@ -223,7 +122,7 @@ const filterElementToggle = () => { if (filter_select?.value === 'match_ids') filter_text_elements.style.display = 'flex'; else filter_text_elements.style.display = 'none'; - if (filter_select?.value === 'match_category' ) filter_category_elements.style.display = 'flex'; + if (filter_select?.value === 'match_category') filter_category_elements.style.display = 'flex'; else filter_category_elements.style.display = 'none'; }; diff --git a/htdocs/js/ProblemSetList/problemsetlist.js b/htdocs/js/ProblemSetList/problemsetlist.js index bdcfd6814a..7eaec0261f 100644 --- a/htdocs/js/ProblemSetList/problemsetlist.js +++ b/htdocs/js/ProblemSetList/problemsetlist.js @@ -1,157 +1,96 @@ (() => { // Action form validation. + const show_errors = (ids, elements) => { + for (const id of ids) elements.push(document.getElementById(id)); + for (const element of elements) { + if (element?.id.endsWith('_err_msg')) { + element?.classList.remove('d-none'); + } else { + element?.classList.add('is-invalid'); + element?.addEventListener('change', hide_errors([], elements)); + } + } + }; + + const hide_errors = (ids, elements) => { + return () => { + for (const id of ids) elements.push(document.getElementById(id)); + for (const element of elements) { + if (element?.id.endsWith('_err_msg')) { + element?.classList.add('d-none'); + if (element.id === 'select_set_err_msg') + document.getElementById('set_table_id')?.removeEventListener('change', hide_errors); + } else { + element?.classList.remove('is-invalid'); + element?.removeEventListener('change', hide_errors); + } + } + }; + }; + const is_set_selected = () => { for (const set of document.getElementsByName('selected_sets')) { if (set.checked) return true; } - document.getElementById('select_set_err_msg')?.classList.remove('d-none'); - document.getElementById('set_table_id')?.addEventListener( - 'change', - () => { - document.getElementById('select_set_err_msg')?.classList.add('d-none'); - for (const id of [ - 'filter_select', - 'edit_select', - 'publish_filter_select', - 'export_select', - 'score_select' - ]) { - document.getElementById(id)?.classList.remove('is-invalid'); - } - }, - { once: true } - ); + const err_msg = document.getElementById('select_set_err_msg'); + err_msg?.classList.remove('d-none'); + document + .getElementById('set_table_id') + ?.addEventListener( + 'change', + hide_errors( + ['filter_select', 'edit_select', 'publish_filter_select', 'export_select', 'score_select'], + [err_msg] + ) + ); return false; }; document.getElementById('problemsetlist')?.addEventListener('submit', (e) => { const action = document.getElementById('current_action')?.value || ''; if (action === 'filter') { - const filter = document.getElementById('filter_select')?.value || ''; + const filter_select = document.getElementById('filter_select'); + const filter = filter_select?.value || ''; const filter_text = document.getElementById('filter_text'); if (filter === 'selected' && !is_set_selected()) { e.preventDefault(); e.stopPropagation(); - document.getElementById('filter_select')?.addEventListener( - 'change', - () => { - document.getElementById('select_set_err_msg')?.classList.add('d-none'); - }, - { once: true } - ); + show_errors(['select_set_err_msg'], [filter_select]); } else if (filter === 'match_ids' && filter_text.value === '') { e.preventDefault(); e.stopPropagation(); - document.getElementById('filter_err_msg')?.classList.remove('d-none'); - filter_text.classList.add('is-invalid'); - filter_text.addEventListener( - 'change', - () => { - document.getElementById('filter_err_msg')?.classList.add('d-none'); - document.getElementById('filter_text')?.classList.remove('is-invalid'); - }, - { once: true } - ); - } - } else if (action === 'edit') { - const edit_select = document.getElementById('edit_select'); - if (edit_select.value === 'selected' && !is_set_selected()) { - e.preventDefault(); - e.stopPropagation(); - edit_select.classList.add('is-invalid'); - edit_select.addEventListener( - 'change', - () => { - document.getElementById('select_set_err_msg')?.classList.add('d-none'); - document.getElementById('edit_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); - } - } else if (action === 'publish') { - const publish_select = document.getElementById('publish_filter_select'); - if (publish_select.value === 'selected' && !is_set_selected()) { - e.preventDefault(); - e.stopPropagation(); - publish_select.classList.add('is-invalid'); - publish_select.addEventListener( - 'change', - () => { - document.getElementById('select_set_err_msg')?.classList.add('d-none'); - document.getElementById('publish_filter_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); + show_errors(['filter_err_msg'], [filter_select, filter_text]); } - } else if (action === 'export') { - const export_select = document.getElementById('export_select'); - if (export_select.value === 'selected' && !is_set_selected()) { + } else if (['edit', 'publish', 'export', 'score'].includes(action)) { + const action_select = document.getElementById(`${action}_select`); + if (action_select.value === 'selected' && !is_set_selected()) { e.preventDefault(); e.stopPropagation(); - export_select.classList.add('is-invalid'); - export_select.addEventListener( - 'change', - () => { - document.getElementById('select_set_err_msg')?.classList.add('d-none'); - document.getElementById('export_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); + show_errors(['select_set_err_msg'], [action_select]); } } else if (action === 'save_export') { if (!is_set_selected()) { e.preventDefault(); e.stopPropagation(); } - } else if (action === 'score') { - const score_select = document.getElementById('score_select'); - if (score_select.value === 'selected' && !is_set_selected()) { - e.preventDefault(); - e.stopPropagation(); - score_select.classList.add('is-invalid'); - score_select.addEventListener( - 'change', - () => { - document.getElementById('select_set_err_msg')?.classList.add('d-none'); - document.getElementById('score_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); - } } else if (action === 'import') { const import_select = document.getElementById('import_source_select'); if (!import_select.value.endsWith('.def')) { e.preventDefault(); e.stopPropagation(); - document.getElementById('import_file_err_msg')?.classList.remove('d-none'); - import_select.classList.add('is-invalid'); - import_select.addEventListener( - 'change', - () => { - document.getElementById('import_source_select')?.classList.remove('is-invalid'); - document.getElementById('import_file_err_msg')?.classList.add('d-none'); - }, - { once: true } - ); + show_errors(['import_file_err_msg'], [import_select]); } } else if (action === 'create') { const create_text = document.getElementById('create_text'); + const create_select = document.getElementById('create_select'); if (create_text.value === '') { e.preventDefault(); e.stopPropagation(); - document.getElementById('create_file_err_msg')?.classList.remove('d-none'); - create_text.classList.add('is-invalid'); - create_text.addEventListener( - 'change', - () => { - document.getElementById('create_file_err_msg')?.classList.add('d-none'); - document.getElementById('create_text')?.classList.remove('is-invalid'); - }, - { once: true } - ); - } else if (document.getElementById('create_select')?.value == 'copy' && !is_set_selected()) { + show_errors(['create_file_err_msg'], [create_text]); + } else if (create_select?.value == 'copy' && !is_set_selected()) { e.preventDefault(); e.stopPropagation(); + show_errors(['select_set_err_msg'], [create_select]); } } else if (action === 'delete') { const delete_confirm = document.getElementById('delete_select'); @@ -161,16 +100,7 @@ } else if (delete_confirm.value != 'yes') { e.preventDefault(); e.stopPropagation(); - document.getElementById('delete_confirm_err_msg')?.classList.remove('d-none'); - delete_confirm.classList.add('is-invalid'); - delete_confirm.addEventListener( - 'change', - () => { - document.getElementById('delete_select')?.classList.remove('is-invalid'); - document.getElementById('delete_confirm_err_msg')?.classList.add('d-none'); - }, - { once: true } - ); + show_errors(['delete_confirm_err_msg'], [delete_confirm]); } } }); @@ -178,13 +108,10 @@ // Remove all error messages when changing tabs. for (const tab of document.querySelectorAll('a[data-bs-toggle="tab"]')) { tab.addEventListener('shown.bs.tab', () => { - const actionForm = document.getElementById('problemsetlist'); - for (const err_msg of actionForm.querySelectorAll('div[id$=_err_msg]')) { - err_msg.classList.add('d-none'); - } - for (const invalid of actionForm.querySelectorAll('.is-invalid')) { - invalid.classList.remove('is-invalid'); - } + hide_errors( + [], + document.getElementById('problemsetlist')?.querySelectorAll('div[id$=_err_msg], .is-invalid') + )(); }); } diff --git a/htdocs/js/UserList/userlist.js b/htdocs/js/UserList/userlist.js index 338b9e6f8f..697298afff 100644 --- a/htdocs/js/UserList/userlist.js +++ b/htdocs/js/UserList/userlist.js @@ -26,21 +26,46 @@ } // Action form validation. + const show_errors = (ids, elements) => { + for (const id of ids) elements.push(document.getElementById(id)); + for (const element of elements) { + if (element?.id.endsWith('_err_msg')) { + element?.classList.remove('d-none'); + } else { + element?.classList.add('is-invalid'); + element?.addEventListener('change', hide_errors([], elements)); + } + } + }; + + const hide_errors = (ids, elements) => { + return () => { + for (const id of ids) elements.push(document.getElementById(id)); + for (const element of elements) { + if (element?.id.endsWith('_err_msg')) { + element?.classList.add('d-none'); + if (element.id === 'select_user_err_msg') + document.getElementById('classlist-table')?.removeEventListener('change', hide_errors); + } else { + element?.classList.remove('is-invalid'); + element?.removeEventListener('change', hide_errors); + } + } + }; + }; + const is_user_selected = () => { for (const user of document.getElementsByName('selected_users')) { if (user.checked) return true; } - document.getElementById('select_user_err_msg')?.classList.remove('d-none'); - document.getElementById('classlist-table')?.addEventListener( - 'change', - () => { - document.getElementById('select_user_err_msg')?.classList.add('d-none'); - for (const id of ['filter_select', 'edit_select', 'password_select', 'export_select_scope']) { - document.getElementById(id)?.classList.remove('is-invalid'); - } - }, - { once: true } - ); + const err_msg = document.getElementById('select_user_err_msg'); + err_msg?.classList.remove('d-none'); + document + .getElementById('classlist-table') + ?.addEventListener( + 'change', + hide_errors(['filter_select', 'edit_select', 'password_select', 'export_select_scope'], [err_msg]) + ); return false; }; @@ -53,101 +78,31 @@ if (filter === 'selected' && !is_user_selected()) { e.preventDefault(); e.stopPropagation(); - filter_select.classList.add('is-invalid'); - filter_select.addEventListener( - 'change', - () => { - document.getElementById('select_user_err_msg')?.classList.add('d-none'); - document.getElementById('filter_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); + show_errors(['select_user_err_msg'], [filter_select]); } else if (filter === 'match_regex' && filter_text.value === '') { e.preventDefault(); e.stopPropagation(); - document.getElementById('filter_err_msg')?.classList.remove('d-none'); - filter_text.classList.add('is-invalid'); - filter_text.addEventListener( - 'change', - () => { - document.getElementById('filter_text')?.classList.remove('is-invalid'); - document.getElementById('filter_err_msg')?.classList.add('d-none'); - }, - { once: true } - ); + show_errors(['filter_err_msg'], [filter_select, filter_text]); } - } else if (action === 'edit') { - const edit_select = document.getElementById('edit_select'); - if (edit_select.value === 'selected' && !is_user_selected()) { + } else if (['edit', 'password'].includes(action)) { + const action_select = document.getElementById(`${action}_select`); + if (action_select.value === 'selected' && !is_user_selected()) { e.preventDefault(); e.stopPropagation(); - edit_select.classList.add('is-invalid'); - edit_select.addEventListener( - 'change', - () => { - document.getElementById('select_user_err_msg')?.classList.add('d-none'); - document.getElementById('edit_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); - } - } else if (action === 'password') { - const password_select = document.getElementById('password_select'); - if (password_select.value === 'selected' && !is_user_selected()) { - e.preventDefault(); - e.stopPropagation(); - password_select.classList.add('is-invalid'); - password_select.addEventListener( - 'change', - () => { - document.getElementById('select_user_err_msg')?.classList.add('d-none'); - document.getElementById('password_select')?.classList.remove('is-invalid'); - }, - { once: true } - ); + show_errors(['select_user_err_msg'], [action_select]); } } else if (action == 'export') { const export_filename = document.getElementById('export_filename'); const export_select = document.getElementById('export_select_scope'); + const export_select_target = document.getElementById('export_select_target'); if (export_select.value === 'selected' && !is_user_selected()) { e.preventDefault(); e.stopPropagation(); - export_select.classList.add('is-invalid'); - export_select.addEventListener( - 'change', - () => { - document.getElementById('select_user_err_msg')?.classList.add('d-none'); - document.getElementById('export_select_scope')?.classList.remove('is-invalid'); - }, - { once: true } - ); - } else if ( - document.getElementById('export_select_target')?.value === 'new' && - export_filename.value === '' - ) { + show_errors(['select_user_err_msg'], [export_select]); + } else if (export_select_target?.value === 'new' && export_filename.value === '') { e.preventDefault(); e.stopPropagation(); - document.getElementById('export_file_err_msg')?.classList.remove('d-none'); - document.getElementById('export_select_target')?.classList.add('is-invalid'); - export_filename.classList.add('is-invalid'); - export_filename.addEventListener( - 'change', - () => { - document.getElementById('export_filename')?.classList.remove('is-invalid'); - document.getElementById('export_select_target')?.classList.remove('is-invalid'); - document.getElementById('export_file_err_msg')?.classList.add('d-none'); - }, - { once: true } - ); - document.getElementById('export_select_target')?.addEventListener( - 'change', - () => { - document.getElementById('export_filename')?.classList.remove('is-invalid'); - document.getElementById('export_select_target')?.classList.remove('is-invalid'); - document.getElementById('export_file_err_msg')?.classList.add('d-none'); - }, - { once: true } - ); + show_errors(['export_file_err_msg'], [export_filename, export_select_target]); } } else if (action === 'delete') { const delete_confirm = document.getElementById('delete_select'); @@ -157,16 +112,7 @@ } else if (delete_confirm.value != 'yes') { e.preventDefault(); e.stopPropagation(); - document.getElementById('delete_confirm_err_msg')?.classList.remove('d-none'); - delete_confirm.classList.add('is-invalid'); - delete_confirm.addEventListener( - 'change', - () => { - document.getElementById('delete_select')?.classList.remove('is-invalid'); - document.getElementById('delete_confirm_err_msg')?.classList.add('d-none'); - }, - { once: true } - ); + show_errors(['delete_confirm_err_msg'], [delete_confirm]); } } }); @@ -174,13 +120,10 @@ // Remove all error messages when changing tabs. for (const tab of document.querySelectorAll('a[data-bs-toggle="tab"]')) { tab.addEventListener('shown.bs.tab', () => { - const actionForm = document.getElementById('user-list-form'); - for (const err_msg of actionForm.querySelectorAll('div[id$=_err_msg]')) { - err_msg.classList.add('d-none'); - } - for (const invalid of actionForm.querySelectorAll('.is-invalid')) { - invalid.classList.remove('is-invalid'); - } + hide_errors( + [], + document.getElementById('user-list-form')?.querySelectorAll('div[id$=_err_msg], .is-invalid') + )(); }); } })(); diff --git a/templates/ContentGenerator/Instructor/ProblemSetList/publish_form.html.ep b/templates/ContentGenerator/Instructor/ProblemSetList/publish_form.html.ep index fe9b1f0c33..7314cd96a9 100644 --- a/templates/ContentGenerator/Instructor/ProblemSetList/publish_form.html.ep +++ b/templates/ContentGenerator/Instructor/ProblemSetList/publish_form.html.ep @@ -1,13 +1,13 @@
- <%= label_for publish_filter_select => maketext('Choose which sets to be affected') . ':', + <%= label_for publish_select => maketext('Choose which sets to be affected') . ':', class => 'col-form-label col-form-label-sm col-sm-auto' =%>
<%= select_field 'action.publish.scope' => [ [ maketext('all course sets') => 'all' ], [ maketext('selected sets') => 'selected', selected => undef ] ], - id => 'publish_filter_select', class => 'form-select form-select-sm' =%> + id => 'publish_select', class => 'form-select form-select-sm' =%>
From 08ce6bc0a09aa88d1fe973a9996921ffaf24e716 Mon Sep 17 00:00:00 2001 From: Jaimos Skriletz Date: Sat, 24 Feb 2024 20:06:02 -0700 Subject: [PATCH 5/8] Remove achievement transition code. --- .../Instructor/AchievementList.pm | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm b/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm index df13f4423d..7e9525688f 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm @@ -91,22 +91,6 @@ sub initialize ($c) { # Set initial values for state fields my @allAchievementIDs = $db->listAchievements; - #### Temporary Transition Code #### - # If an achievement doesn't have either a number or an assignment_type - # then its probably an old achievement in which case we should - # update its assignment_type to include 'default'. - # This whole block of code can be removed once people have had time - # to transition over. (I.E. around 2017) - - for my $achievementID (@allAchievementIDs) { - my $achievement = $db->getAchievement($achievementID); - unless ($achievement->assignment_type || $achievement->number) { - $achievement->assignment_type('default'); - $db->putAchievement($achievement); - } - } - ### End Transition Code. ### - my @users = $db->listUsers; $c->{allAchievementIDs} = \@allAchievementIDs; $c->{totalUsers} = scalar @users; From 98608b630242cd99e454abbee2955d84777741dc Mon Sep 17 00:00:00 2001 From: Jaimos Skriletz Date: Sat, 24 Feb 2024 20:35:01 -0700 Subject: [PATCH 6/8] Add help on where to upload .axp files in achievements manager. --- templates/HelpFiles/InstructorAchievementList.html.ep | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/HelpFiles/InstructorAchievementList.html.ep b/templates/HelpFiles/InstructorAchievementList.html.ep index 0fdc320e81..7c05936f29 100644 --- a/templates/HelpFiles/InstructorAchievementList.html.ep +++ b/templates/HelpFiles/InstructorAchievementList.html.ep @@ -105,7 +105,8 @@
<%= maketext('You can import/export from their respective action tab. Exporting saves information about the ' . 'achievement as a .axp file which is a CSV that lists each achievements information. You can import ' - . '.axp files that have been previously exported.') =%> + . '.axp files that have been previously exported. Use the "File Manager" to upload/download .axp files ' + . 'to "achievements" directory (also upload/download any achievement evaluator .at files needed).') =%>
<%= maketext('Assign achievements') %>
From f1b088e2f16e41802a16e02280fc65c08549675e Mon Sep 17 00:00:00 2001 From: Jaimos Skriletz Date: Sat, 24 Feb 2024 21:50:02 -0700 Subject: [PATCH 7/8] Save curried event listeners so they can be removed. --- htdocs/js/AchievementList/achievementlist.js | 44 +++++++++++++------- htdocs/js/ProblemSetList/problemsetlist.js | 44 +++++++++++++------- htdocs/js/UserList/userlist.js | 41 ++++++++++++------ 3 files changed, 84 insertions(+), 45 deletions(-) diff --git a/htdocs/js/AchievementList/achievementlist.js b/htdocs/js/AchievementList/achievementlist.js index ae4e8059cd..707393170e 100644 --- a/htdocs/js/AchievementList/achievementlist.js +++ b/htdocs/js/AchievementList/achievementlist.js @@ -1,5 +1,8 @@ (() => { // Action form validation. + // Store event listeners so they can be removed. + const event_listeners = {}; + const show_errors = (ids, elements) => { for (const id of ids) elements.push(document.getElementById(id)); for (const element of elements) { @@ -7,7 +10,10 @@ element?.classList.remove('d-none'); } else { element?.classList.add('is-invalid'); - element?.addEventListener('change', hide_errors([], elements)); + if (!(element.id in event_listeners)) { + event_listeners[element.id] = hide_errors([], elements); + element?.addEventListener('change', event_listeners[element.id]); + } } } }; @@ -18,11 +24,18 @@ for (const element of elements) { if (element?.id.endsWith('_err_msg')) { element?.classList.add('d-none'); - if (element.id === 'select_achievement_err_msg') - document.getElementById('achievement-table')?.removeEventListener('change', hide_errors); + if (element.id === 'select_achievement_err_msg' && 'achievement_table' in event_listeners) { + document + .getElementById('achievement-table') + ?.removeEventListener('change', event_listeners.achievement_table); + delete event_listeners.achievement_table; + } } else { element?.classList.remove('is-invalid'); - element?.removeEventListener('change', hide_errors); + if (element.id in event_listeners) { + element?.removeEventListener('change', event_listeners[element.id]); + delete event_listeners[element.id]; + } } } }; @@ -34,15 +47,13 @@ } const err_msg = document.getElementById('select_achievement_err_msg'); err_msg?.classList.remove('d-none'); - document - .getElementById('achievement-table') - ?.addEventListener( - 'change', - hide_errors( - ['filter_select', 'edit_select', 'assign_select', 'export_select', 'score_select'], - [err_msg] - ) + if (!('achievement_table' in event_listeners)) { + event_listeners.achievement_table = hide_errors( + ['filter_select', 'edit_select', 'assign_select', 'export_select', 'score_select'], + [err_msg] ); + document.getElementById('achievement-table')?.addEventListener('change', event_listeners.achievement_table); + } return false; }; @@ -108,10 +119,11 @@ // Remove all error messages when changing tabs. for (const tab of document.querySelectorAll('a[data-bs-toggle="tab"]')) { tab.addEventListener('shown.bs.tab', () => { - hide_errors( - [], - document.getElementById('achievement-list')?.querySelectorAll('div[id$=_err_msg], .is-invalid') - )(); + if (Object.keys(event_listeners).length != 0) + hide_errors( + [], + document.getElementById('achievement-list')?.querySelectorAll('div[id$=_err_msg], .is-invalid') + )(); }); } diff --git a/htdocs/js/ProblemSetList/problemsetlist.js b/htdocs/js/ProblemSetList/problemsetlist.js index 7eaec0261f..0981fc95cb 100644 --- a/htdocs/js/ProblemSetList/problemsetlist.js +++ b/htdocs/js/ProblemSetList/problemsetlist.js @@ -1,5 +1,8 @@ (() => { // Action form validation. + // Store event listeners so they can be removed. + const event_listeners = {}; + const show_errors = (ids, elements) => { for (const id of ids) elements.push(document.getElementById(id)); for (const element of elements) { @@ -7,7 +10,10 @@ element?.classList.remove('d-none'); } else { element?.classList.add('is-invalid'); - element?.addEventListener('change', hide_errors([], elements)); + if (!(element.id in event_listeners)) { + event_listeners[element.id] = hide_errors([], elements); + element?.addEventListener('change', event_listeners[element.id]); + } } } }; @@ -18,11 +24,18 @@ for (const element of elements) { if (element?.id.endsWith('_err_msg')) { element?.classList.add('d-none'); - if (element.id === 'select_set_err_msg') - document.getElementById('set_table_id')?.removeEventListener('change', hide_errors); + if (element.id === 'select_set_err_msg' && 'set_table_id' in event_listeners) { + document + .getElementById('set_table_id') + ?.removeEventListener('change', event_listeners.set_table_id); + delete event_listeners.set_table_id; + } } else { element?.classList.remove('is-invalid'); - element?.removeEventListener('change', hide_errors); + if (element.id in event_listeners) { + element?.removeEventListener('change', event_listeners[element.id]); + delete event_listeners[element.id]; + } } } }; @@ -34,15 +47,13 @@ } const err_msg = document.getElementById('select_set_err_msg'); err_msg?.classList.remove('d-none'); - document - .getElementById('set_table_id') - ?.addEventListener( - 'change', - hide_errors( - ['filter_select', 'edit_select', 'publish_filter_select', 'export_select', 'score_select'], - [err_msg] - ) + if (!('set_table_id' in event_listeners)) { + event_listeners.set_table_id = hide_errors( + ['filter_select', 'edit_select', 'publish_filter_select', 'export_select', 'score_select'], + [err_msg] ); + document.getElementById('set_table_id')?.addEventListener('change', event_listeners.set_table_id); + } return false; }; @@ -108,10 +119,11 @@ // Remove all error messages when changing tabs. for (const tab of document.querySelectorAll('a[data-bs-toggle="tab"]')) { tab.addEventListener('shown.bs.tab', () => { - hide_errors( - [], - document.getElementById('problemsetlist')?.querySelectorAll('div[id$=_err_msg], .is-invalid') - )(); + if (Object.keys(event_listeners) != 0) + hide_errors( + [], + document.getElementById('problemsetlist')?.querySelectorAll('div[id$=_err_msg], .is-invalid') + )(); }); } diff --git a/htdocs/js/UserList/userlist.js b/htdocs/js/UserList/userlist.js index 697298afff..7065a5006d 100644 --- a/htdocs/js/UserList/userlist.js +++ b/htdocs/js/UserList/userlist.js @@ -26,6 +26,9 @@ } // Action form validation. + // Store event listeners so they can be removed. + const event_listeners = {}; + const show_errors = (ids, elements) => { for (const id of ids) elements.push(document.getElementById(id)); for (const element of elements) { @@ -33,7 +36,10 @@ element?.classList.remove('d-none'); } else { element?.classList.add('is-invalid'); - element?.addEventListener('change', hide_errors([], elements)); + if (!(element.id in event_listeners)) { + event_listeners[element.id] = hide_errors([], elements); + element?.addEventListener('change', event_listeners[element.id]); + } } } }; @@ -44,11 +50,18 @@ for (const element of elements) { if (element?.id.endsWith('_err_msg')) { element?.classList.add('d-none'); - if (element.id === 'select_user_err_msg') - document.getElementById('classlist-table')?.removeEventListener('change', hide_errors); + if (element.id === 'select_user_err_msg' && 'classlist_table' in event_listeners) { + document + .getElementById('classlist-table') + ?.removeEventListener('change', event_listeners.classlist_table); + delete event_listeners.classlist_table; + } } else { element?.classList.remove('is-invalid'); - element?.removeEventListener('change', hide_errors); + if (element.id in event_listeners) { + element?.removeEventListener('change', event_listeners[element.id]); + delete event_listeners[element.id]; + } } } }; @@ -60,12 +73,13 @@ } const err_msg = document.getElementById('select_user_err_msg'); err_msg?.classList.remove('d-none'); - document - .getElementById('classlist-table') - ?.addEventListener( - 'change', - hide_errors(['filter_select', 'edit_select', 'password_select', 'export_select_scope'], [err_msg]) + if (!('classlist_table' in event_listeners)) { + event_listeners.classlist_table = hide_errors( + ['filter_select', 'edit_select', 'password_select', 'export_select_scope'], + [err_msg] ); + document.getElementById('classlist-table')?.addEventListener('change', event_listeners.classlist_table); + } return false; }; @@ -120,10 +134,11 @@ // Remove all error messages when changing tabs. for (const tab of document.querySelectorAll('a[data-bs-toggle="tab"]')) { tab.addEventListener('shown.bs.tab', () => { - hide_errors( - [], - document.getElementById('user-list-form')?.querySelectorAll('div[id$=_err_msg], .is-invalid') - )(); + if (Object.keys(event_listeners) != 0) + hide_errors( + [], + document.getElementById('user-list-form')?.querySelectorAll('div[id$=_err_msg], .is-invalid') + )(); }); } })(); From 49f9ea8a629de4475452963a5d387d25b99a0f12 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Thu, 29 Feb 2024 13:03:59 -0800 Subject: [PATCH 8/8] change achievement categrory selection to dropdown --- htdocs/js/AchievementList/achievementlist.js | 5 ----- lib/WeBWorK/DB.pm | 5 +++++ .../Instructor/AchievementList/filter_form.html.ep | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/htdocs/js/AchievementList/achievementlist.js b/htdocs/js/AchievementList/achievementlist.js index 707393170e..08406848b1 100644 --- a/htdocs/js/AchievementList/achievementlist.js +++ b/htdocs/js/AchievementList/achievementlist.js @@ -63,7 +63,6 @@ const filter_select = document.getElementById('filter_select'); const filter = filter_select?.value || ''; const filter_text = document.getElementById('filter_text'); - const filter_category = document.getElementById('filter_category'); if (filter === 'selected' && !is_achievement_selected()) { e.preventDefault(); e.stopPropagation(); @@ -72,10 +71,6 @@ e.preventDefault(); e.stopPropagation(); show_errors(['filter_text_err_msg'], [filter_select, filter_text]); - } else if (filter === 'match_category' && filter_category?.value === '') { - e.preventDefault(); - e.stopPropagation(); - show_errors(['filter_category_err_msg'], [filter_select, filter_category]); } } else if (['edit', 'assign', 'export', 'score'].includes(action)) { const action_select = document.getElementById(`${action}_select`); diff --git a/lib/WeBWorK/DB.pm b/lib/WeBWorK/DB.pm index 4d59f2504c..0b12210adb 100644 --- a/lib/WeBWorK/DB.pm +++ b/lib/WeBWorK/DB.pm @@ -1290,6 +1290,11 @@ sub getAchievements { return $self->{achievement}->gets(map { [$_] } @achievementIDs); } +sub getAchievementCategories { + my ($self) = shift->checkArgs(\@_); + return map {@$_} $self->{achievement}->get_fields_where("DISTINCT category", undef, "category"); +} + sub addAchievement { my ($self, $Achievement) = shift->checkArgs(\@_, qw/REC:achievement/); diff --git a/templates/ContentGenerator/Instructor/AchievementList/filter_form.html.ep b/templates/ContentGenerator/Instructor/AchievementList/filter_form.html.ep index c4234f0309..f4537464f8 100644 --- a/templates/ContentGenerator/Instructor/AchievementList/filter_form.html.ep +++ b/templates/ContentGenerator/Instructor/AchievementList/filter_form.html.ep @@ -27,17 +27,17 @@
<%= maketext('Please enter a list of IDs to match.') %>
+ % my @categories = $c->db->getAchievementCategories; + % for (@categories) { + % $_ = [$_ => $_]; + % }
<%= label_for 'filter_category', class => 'col-form-label col-form-label-sm col-sm-auto', begin =%> - <%= maketext('Match on which category? (enter in exact match)') =%> - * + <%= maketext('Match on which category?') =%> <% end =%>
- <%= text_field 'action.filter.category' => '', id => 'filter_category', 'aria-required' => 'true', - class => 'form-control form-control-sm', dir => 'ltr' =%> + <%= select_field 'action.filter.category' => \@categories, + id => 'filter_category', class => 'form-select form-select-sm' =%>
-
- <%= maketext('Please enter a category to match.') %> -