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 @@ +