Skip to content

Commit

Permalink
Remove action scope and add action form validation.
Browse files Browse the repository at this point in the history
  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 openwebwork#1991.

  In addition add javascript form validation that will inform the user
  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, disable the import tab if no valid files are found to import from.
  • Loading branch information
somiaj committed Jan 10, 2024
1 parent a45ca4b commit 03799c8
Show file tree
Hide file tree
Showing 36 changed files with 359 additions and 361 deletions.
61 changes: 61 additions & 0 deletions htdocs/js/AchievementList/achievementlist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
(() => {
// Action form validation.
const is_achievement_selected = () => {
const users = document.getElementsByName('selected_achievements');
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', e => {
document.getElementById('select_achievement_err_msg')?.classList.add('d-none');
}, { once : true });
return false;
};

document.getElementById('achievement-list')?.addEventListener('submit', e => {
const action = document.getElementById('current_action')?.value || '';
if (['edit', 'assign', 'export', 'score'].includes(action)) {
if (!is_achievement_selected()) {
e.preventDefault();
e.stopPropagation();
}
} else if (action === 'create') {
const create_text = document.getElementById('create_text');
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', e => {
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')?.selectedIndex == 1 && !is_achievement_selected()) {
e.preventDefault();
e.stopPropagation();
}
} else if (action === 'delete') {
const delete_confirm = document.getElementById('delete_select');
if (!is_achievement_selected()) {
e.preventDefault();
e.stopPropagation();
} 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', e => {
document.getElementById('delete_select')?.classList.remove('is-invalid');
document.getElementById('delete_confirm_err_msg')?.classList.add('d-none');
}, { once : true });
}
}
});

// Remove select error message when changing tabs.
for (const tab of document.querySelectorAll('a[data-bs-toggle="tab"]')) {
tab.addEventListener('shown.bs.tab', e => {
document.getElementById('select_achievement_err_msg')?.classList.add('d-none');
}, { once : true });
}
})();
1 change: 1 addition & 0 deletions htdocs/js/ActionTabs/actiontabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@
});
}
}

})();
94 changes: 82 additions & 12 deletions htdocs/js/ProblemSetList/problemsetlist.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,90 @@
(() => {
// 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');

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');
// Action form validation.
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', e => {
document.getElementById('select_set_err_msg')?.classList.add('d-none');
}, { once : true });
return false;
};

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_text = document.getElementById('filter_text');
if (filter === 2 && !is_set_selected()) {
e.preventDefault();
e.stopPropagation();
} else if (filter === 3 && 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', e => {
document.getElementById('filter_err_msg')?.classList.add('d-none');
document.getElementById('filter_text')?.classList.remove('is-invalid');
}, { once : true });
}
} else if (['edit', 'publish', 'export', 'save_export', 'score'].includes(action)) {
if (!is_set_selected()) {
e.preventDefault();
e.stopPropagation();
}
} 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', e => {
document.getElementById('import_source_select')?.classList.remove('is-invalid');
document.getElementById('import_file_err_msg')?.classList.add('d-none');
}, { once : true });
}
} else if (action === 'create') {
const create_text = document.getElementById('create_text');
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', e => {
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')?.selectedIndex == 1 && !is_set_selected()) {
e.preventDefault();
e.stopPropagation();
}
} else if (action === 'delete') {
const delete_confirm = document.getElementById('delete_select');
if (!is_set_selected()) {
e.preventDefault();
e.stopPropagation();
} 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', e => {
document.getElementById('delete_select')?.classList.remove('is-invalid');
document.getElementById('delete_confirm_err_msg')?.classList.add('d-none');
}, { once : true });
}
}
);
});

// Remove select error message when changing tabs.
for (const tab of document.querySelectorAll('a[data-bs-toggle="tab"]')) {
tab.addEventListener('shown.bs.tab', e => {
document.getElementById('select_set_err_msg')?.classList.add('d-none');
});
}

// Toggle the display of the filter elements as the filter select changes.
const filter_select = document.getElementById('filter_select');
Expand Down
76 changes: 76 additions & 0 deletions htdocs/js/UserList/userlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,80 @@
export_select_target.addEventListener('change', classlist_add_export_elements);
classlist_add_export_elements();
}

// Action form validation.
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', e => {
document.getElementById('select_user_err_msg')?.classList.add('d-none');
}, { once : true });
return false;
};

document.getElementById('user-list-form')?.addEventListener('submit', e => {
const action = document.getElementById('current_action')?.value || '';
if (action === 'filter') {
const filter = document.getElementById('filter_select')?.selectedIndex || 0;
const filter_text = document.getElementById('filter_text');
if (filter === 2 && !is_user_selected()) {
e.preventDefault();
e.stopPropagation();
} else if (filter === 3 && 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', e => {
document.getElementById('filter_text')?.classList.remove('is-invalid');
document.getElementById('filter_err_msg')?.classList.add('d-none');
}, { once : true });
}
} else if (action === 'edit' || action === 'password') {
if (!is_user_selected()) {
e.preventDefault();
e.stopPropagation();
}
} else if (action == 'export') {
const export_filename = document.getElementById('export_filename');
if (!is_user_selected()) {
e.preventDefault();
e.stopPropagation();
} else if (document.getElementById('export_select_target')?.value === 'new' &&
export_filename.value === '') {
e.preventDefault();
e.stopPropagation();
document.getElementById('export_file_err_msg')?.classList.remove('d-none');
export_filename.classList.add('is-invalid');
export_filename.addEventListener('change', e => {
document.getElementById('export_filename')?.classList.remove('is-invalid');
document.getElementById('export_file_err_msg')?.classList.add('d-none');
}, { once : true });
}
} else if (action === 'delete') {
const delete_confirm = document.getElementById('delete_select');
if (!is_user_selected()) {
e.preventDefault();
e.stopPropagation();
} 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', e => {
document.getElementById('delete_select')?.classList.remove('is-invalid');
document.getElementById('delete_confirm_err_msg')?.classList.add('d-none');
}, { once : true });
}
}
});

// Remove select error message when changing tabs.
for (const tab of document.querySelectorAll('a[data-bs-toggle="tab"]')) {
tab.addEventListener('shown.bs.tab', e => {
document.getElementById('select_user_err_msg')?.classList.add('d-none');
});
}
})();
78 changes: 18 additions & 60 deletions lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ sub initialize ($c) {
$c->stash->{formsToShow} = VIEW_FORMS();
$c->stash->{formTitles} = FORM_TITLES();
$c->stash->{achievements} = [];
$c->stash->{axpList} = [ $c->getAxpList ];

# Check permissions
return unless $authz->hasPermissions($user, 'edit_achievements');
Expand Down Expand Up @@ -146,36 +147,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);
Expand Down Expand Up @@ -222,20 +204,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";
Expand Down Expand Up @@ -323,16 +295,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} };

Expand All @@ -348,8 +316,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
Expand Down Expand Up @@ -472,19 +439,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.
Expand Down
Loading

0 comments on commit 03799c8

Please sign in to comment.