Skip to content

Commit

Permalink
Add filter action to achievements manager.
Browse files Browse the repository at this point in the history
  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.
  • Loading branch information
somiaj committed Feb 17, 2024
1 parent 24f276d commit b82fd0c
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 5 deletions.
84 changes: 82 additions & 2 deletions htdocs/js/AchievementList/achievementlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
},
Expand All @@ -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();
Expand Down Expand Up @@ -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);
})();
49 changes: 46 additions & 3 deletions lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions templates/ContentGenerator/Instructor/AchievementList.html.ep
Original file line number Diff line number Diff line change
Expand Up @@ -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}) {
<p><b><%= maketext('Any changes made below will be reflected in the achievement for ALL students.') %></b></p>
% }
Expand Down Expand Up @@ -51,6 +58,12 @@
<%= submit_button maketext($formTitles->{ $formsToShow->[0] }),
id => 'take_action', class => 'btn btn-primary mb-3' =%>
</div>
<p class="mb-2">
<%= maketext('Showing [_1] out of [_2] achievements.',
scalar @{ $c->{visibleAchievementIDs} },
scalar @{ $c->{allAchievementIDs} }
) =%>
</p>
% if ($c->{exportMode}) {
<%= include 'ContentGenerator/Instructor/AchievementList/export_table' =%>
% } elsif ($c->{editMode}) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<div>
<div class="row mb-2">
<%= label_for filter_select => maketext('Show which achievements?'),
class => 'col-form-label col-form-label-sm col-sm-auto' =%>
<div class="col-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' =%>
</div>
</div>
<div id="filter_text_elements" class="row mb-2">
<%= 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)') =%>
<span class="required-field">*</span>
<% end =%>
<div class="col-auto">
<%= text_field 'action.filter.achievement_ids' => '', id => 'filter_text', 'aria-required' => 'true',
class => 'form-control form-control-sm', dir => 'ltr' =%>
</div>
</div>
<div id="filter_text_err_msg" class="alert alert-danger p-1 mb-2 d-inline-flex d-none">
<%= maketext('Please enter a list of IDs to match.') %>
</div>
<div id="filter_category_elements" class="row mb-2">
<%= 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)') =%>
<span class="required-field">*</span>
<% end =%>
<div class="col-auto">
<%= text_field 'action.filter.category' => '', id => 'filter_category', 'aria-required' => 'true',
class => 'form-control form-control-sm', dir => 'ltr' =%>
</div>
</div>
<div id="filter_category_err_msg" class="alert alert-danger p-1 mb-2 d-inline-flex d-none">
<%= maketext('Please enter a category to match.') %>
</div>
</div>
6 changes: 6 additions & 0 deletions templates/HelpFiles/InstructorAchievementList.html.ep
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@
</ul>
<h2><%= maketext('How to:') %></h2>
<dl>
<dt><%= maketext('Filter achievements') %></td>
<dd>
<%= 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.') =%>
</dd>
<dt><%= maketext('Edit achievement information') %></dt>
<dd>
<%= maketext('You can edit a single achievement by clicking on the pencil icon next to the achievement ID. '
Expand Down

0 comments on commit b82fd0c

Please sign in to comment.