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 create a modal
  popup if the form is missing information, such as no items are selected,
  a text string is not provided, a valid file is not selected, and so on.

  Last, make the import tabs not create a form if no valid files are found
  to import from.
  • Loading branch information
somiaj committed Jan 8, 2024
1 parent a45ca4b commit 9719121
Show file tree
Hide file tree
Showing 30 changed files with 454 additions and 460 deletions.
47 changes: 47 additions & 0 deletions htdocs/js/AchievementList/achievementlist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
(() => {
// Action form validation.
const is_achievement_selected = () => {
const users = document.getElementsByName('selected_achievements');
for (let i = 0; i < users.length; i++) {
if (users[i].checked) { return true; }
}
return false;
};

document.getElementById('achievement-list')?.addEventListener('submit', e => {
const action = document.getElementById('current_action');
if (['edit', 'assign', 'export', 'score'].includes(action.value)) {
if (!is_achievement_selected()) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.selectText, action.dataset.okayText);
}
} else if (action.value == 'import') {
if (!document.getElementById('import_file_select').value.endsWith('.axp')) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.importText, action.dataset.okayText);
}
} else if (action.value == 'create') {
if (document.getElementById('create_text')?.value == '') {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.createText, action.dataset.okayText);
} else if (document.getElementById('create_select')?.selectedIndex == 1 && !is_achievement_selected()) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.selectText, action.dataset.okayText);
}
} else if (action.value == 'delete') {
if (!is_achievement_selected()) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.selectText, action.dataset.okayText);
} else if (document.getElementById('delete_select').value != 'yes') {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.deleteText, action.dataset.okayText);
}
}
});
})();
53 changes: 53 additions & 0 deletions htdocs/js/ActionTabs/actiontabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,57 @@
});
}
}

// Error modal popup added to window so it can be accessed from other modules.
window.takeActionErrorModal = (titleText, errorText, okayText) => {
const modal = document.createElement('div');
modal.classList.add('modal');
modal.tabIndex = -1;
modal.setAttribute('aria-labelledby', 'take-action-error-dialog');
modal.setAttribute('aria-hidden', 'true');

const modalDialog = document.createElement('div');
modalDialog.classList.add('modal-dialog', 'modal-dialog-centered');
const modalContent = document.createElement('div');
modalContent.classList.add('modal-content');

const modalHeader = document.createElement('div');
modalHeader.classList.add('modal-header');

const title = document.createElement('h5');
title.id = 'take-action-error-dialog';
title.textContent = titleText;

const closeButton = document.createElement('button');
closeButton.type = 'button';
closeButton.classList.add('btn-close');
closeButton.dataset.bsDismiss = 'modal';
closeButton.setAttribute('aria-label', 'close');

modalHeader.append(title, closeButton);

const modalBody = document.createElement('div');
modalBody.classList.add('modal-body');
const modalBodyContent = document.createElement('div');

modalBodyContent.textContent = errorText;
modalBody.append(modalBodyContent);

const modalFooter = document.createElement('div');
modalFooter.classList.add('modal-footer');

const okButton = document.createElement('button');
okButton.classList.add('btn', 'btn-primary');
okButton.textContent = okayText;
okButton.addEventListener('click', () => { bsModal.hide(); });

modalFooter.append(okButton);
modalContent.append(modalHeader, modalBody, modalFooter);
modalDialog.append(modalContent);
modal.append(modalDialog);

const bsModal = new bootstrap.Modal(modal);
bsModal.show();
}

})();
64 changes: 52 additions & 12 deletions htdocs/js/ProblemSetList/problemsetlist.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,60 @@
(() => {
// Show the filter error message if the 'Filter' button is clicked when matching set IDs without having entered
// a text to filter on.
document.getElementById('take_action')?.addEventListener('click',
(e) => {
const filter_err_msg = document.getElementById('filter_err_msg');
// Action form validation.
const is_set_selected = () => {
const users = document.getElementsByName('selected_sets');
for (let i = 0; i < users.length; i++) {
if (users[i].checked) { return true; }
}
return false;
};

if (filter_err_msg &&
document.getElementById('current_action')?.value === 'filter' &&
document.getElementById('filter_select')?.selectedIndex === 3 &&
document.getElementById('filter_text')?.value === '') {
filter_err_msg.classList.remove('d-none');
document.getElementById('problemsetlist')?.addEventListener('submit', e => {
const action = document.getElementById('current_action');
if (action.value == 'filter') {
const filter = document.getElementById('filter_select')?.selectedIndex || 0;
if (filter === 2 && !is_set_selected()) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.selectText, action.dataset.okayText);
} else if (filter === 3 && document.getElementById('filter_text')?.value === '') {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.filterText, action.dataset.okayText);
}
} else if (['edit', 'publish', 'export', 'save_export', 'score'].includes(action.value)) {
if (!is_set_selected()) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.selectText, action.dataset.okayText);
}
} else if (action.value == 'import') {
if (!document.getElementById('import_source_select').value.endsWith('.def')) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.importText, action.dataset.okayText);
}
} else if (action.value == 'create') {
if (document.getElementById('create_text')?.value == '') {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.createText, action.dataset.okayText);
} else if (document.getElementById('create_select')?.selectedIndex == 1 && !is_set_selected()) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.selectText, action.dataset.okayText);
}
} else if (action.value == 'delete') {
if (!is_set_selected()) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.selectText, action.dataset.okayText);
} else if (document.getElementById('delete_select').value != 'yes') {
e.preventDefault();
e.stopPropagation();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.deleteText, action.dataset.okayText);
}
}
);
});

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

// Action form validation.
const is_user_selected = () => {
const users = document.getElementsByName('selected_users');
for (let i = 0; i < users.length; i++) {
if (users[i].checked) { return true; }
}
return false;
};

document.getElementById('user-list-form')?.addEventListener('submit', e => {
const action = document.getElementById('current_action');
if (action.value == 'filter') {
const filter = document.getElementById('filter_select')?.selectedIndex || 0;
if (filter === 2 && !is_user_selected()) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.selectText, action.dataset.okayText);
} else if (filter === 3 && document.getElementById('filter_text')?.value === '') {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.filterText, action.dataset.okayText);
}
} else if (action.value == 'edit' || action.value == 'password') {
if (!is_user_selected()) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.selectText, action.dataset.okayText);
}
} else if (action.value == 'import') {
if (!document.getElementById('action.import.source').value.endsWith('.lst')) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.importText, action.dataset.okayText);
}
} else if (action.value == 'export') {
if (!is_user_selected()) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.selectText, action.dataset.okayText);
} else if (document.getElementById('export_select_target').value == 'new' &&
document.getElementById('export_filename').value == '') {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.exportText, action.dataset.okayText);
}
} else if (action.value == 'delete') {
if (!is_user_selected()) {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.selectText, action.dataset.okayText);
} else if (document.getElementById('delete_select').value != 'yes') {
e.preventDefault();
window.takeActionErrorModal(
action.dataset.errorText, action.dataset.deleteText, action.dataset.okayText);
}
}
});
})();
77 changes: 17 additions & 60 deletions lib/WeBWorK/ContentGenerator/Instructor/AchievementList.pm
Original file line number Diff line number Diff line change
Expand Up @@ -146,36 +146,17 @@ sub initialize ($c) {

# Handler for editing achievements. Just changes the view mode.
sub edit_handler ($c) {
my $result;

my $scope = $c->param('action.edit.scope');
if ($scope eq "all") {
$c->{selectedAchievementIDs} = $c->{allAchievementIDs};
$result = $c->maketext('Editing all achievements.');
} elsif ($scope eq "selected") {
$result = $c->maketext('Editing selected achievements.');
}
$c->{editMode} = 1;

return (1, $result);
return (1, $c->maketext('Editing selected achievements.'));
}

# Handler for assigning achievements to users
sub assign_handler ($c) {
my $db = $c->db;
my $ce = $c->ce;

my $scope = $c->param('action.assign.scope');
my $overwrite = $c->param('action.assign.overwrite') eq 'everything';

my @achievementIDs;
my @users = $db->listUsers;

if ($scope eq "all") {
@achievementIDs = @{ $c->{allAchievementIDs} };
} else {
@achievementIDs = @{ $c->{selectedAchievementIDs} };
}
my $db = $c->db;
my $overwrite = $c->param('action.assign.overwrite') eq 'everything';
my @achievementIDs = @{ $c->{selectedAchievementIDs} };
my @users = $db->listUsers;

# Enable all achievements
my @achievements = $db->getAchievements(@achievementIDs);
Expand Down Expand Up @@ -222,20 +203,10 @@ sub assign_handler ($c) {

# Handler for scoring
sub score_handler ($c) {
my $ce = $c->ce;
my $db = $c->db;
my $courseName = $c->stash('courseID');

my $scope = $c->param('action.score.scope');
my @achievementsToScore;

if ($scope eq "none") {
@achievementsToScore = ();
} elsif ($scope eq "all") {
@achievementsToScore = @{ $c->{allAchievementIDs} };
} elsif ($scope eq "selected") {
@achievementsToScore = $c->param('selected_achievements');
}
my $ce = $c->ce;
my $db = $c->db;
my $courseName = $c->stash('courseID');
my @achievementsToScore = $c->param('selected_achievements');

# Define file name
my $scoreFileName = $courseName . "_achievement_scores.csv";
Expand Down Expand Up @@ -323,16 +294,12 @@ sub score_handler ($c) {

# Handler for delete action
sub delete_handler ($c) {
my $db = $c->db;
my $db = $c->db;
my $confirm = $c->param('action.delete.confirm');

my $scope = $c->param('action.delete.scope');

my @achievementIDsToDelete = ();

if ($scope eq "selected") {
@achievementIDsToDelete = @{ $c->{selectedAchievementIDs} };
}
return (1, $c->maketext('Deleted [quant,_1,achievement].', 0)) unless ($confirm eq 'yes');

my @achievementIDsToDelete = @{ $c->{selectedAchievementIDs} };
my %allAchievementIDs = map { $_ => 1 } @{ $c->{allAchievementIDs} };
my %selectedAchievementIDs = map { $_ => 1 } @{ $c->{selectedAchievementIDs} };

Expand All @@ -348,8 +315,7 @@ sub delete_handler ($c) {
$c->{allAchievementIDs} = [ keys %allAchievementIDs ];
$c->{selectedAchievementIDs} = [ keys %selectedAchievementIDs ];

my $num = @achievementIDsToDelete;
return (1, $c->maketext('Deleted [quant,_1,achievement].', $num));
return (1, $c->maketext('Deleted [quant,_1,achievement].', scalar @achievementIDsToDelete));
}

# Handler for creating an ahcievement
Expand Down Expand Up @@ -472,19 +438,10 @@ sub import_handler ($c) {
# Export handler
# This does not actually export any files, rather it sends us to a new page in order to export the files.
sub export_handler ($c) {
my $result;

my $scope = $c->param('action.export.scope');
if ($scope eq "all") {
$result = $c->maketext('Exporting all achievements.');
$c->{selectedAchievementIDs} = $c->{allAchievementIDs};
} elsif ($scope eq "selected") {
$result = $c->maketext('Exporting selected achievements.');
$c->{selectedAchievementIDs} = [ $c->param('selected_achievements') ];
}
$c->{exportMode} = 1;
$c->{selectedAchievementIDs} = [ $c->param('selected_achievements') ];
$c->{exportMode} = 1;

return (1, $result);
return (1, $c->maketext('Exporting selected achievements.'));
}

# Handler for leaving the export page.
Expand Down
Loading

0 comments on commit 9719121

Please sign in to comment.