Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 139 additions & 24 deletions lib/WeBWorK/ContentGenerator/CourseAdmin.pm
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,24 @@ sub initialize ($c) {
} else {
$method_to_call = 'unarchive_course_form';
}
} elsif ($subDisplay eq 'clean_course') {
if (defined $c->param('clean_course')) {
@errors = $c->clean_course_validate;
if (@errors) {
$method_to_call = 'clean_course_form';
} else {
$method_to_call = 'clean_course_confirm';
}
} elsif (defined $c->param('confirm_clean_course')) {
@errors = $c->clean_course_validate;
if (@errors) {
$method_to_call = 'clean_course_form';
} else {
$method_to_call = 'do_clean_course';
}
} else {
$method_to_call = 'clean_course_form';
}
} elsif ($subDisplay eq 'upgrade_course') {
if (defined $c->param('upgrade_course')) {
@errors = $c->upgrade_course_validate;
Expand Down Expand Up @@ -1195,7 +1213,7 @@ sub do_unarchive_course ($c) {

my $unarchive_courseID = $c->param('unarchive_courseID') || '';

eval {
my $dbRestoreType = eval {
unarchiveCourse(
newCourseID => $new_courseID,
archivePath => "$ce->{webworkDirs}{courses}/$ce->{admin_course_id}/archives/$unarchive_courseID",
Expand All @@ -1217,36 +1235,27 @@ sub do_unarchive_course ($c) {
} else {
writeLog($ce, 'hosted_courses', join("\t", "\tunarchived", '', '', "$unarchive_courseID to $new_courseID",));

if ($c->param('clean_up_course')) {
my $ce_new = WeBWorK::CourseEnvironment->new({ courseName => $new_courseID });
my $db_new = WeBWorK::DB->new($ce_new);

for my $student_id ($db_new->listPermissionLevelsWhere({ permission => $ce->{userRoles}{student} })) {
$db_new->deleteUser($student_id->[0]);
}

for my $file (values %{ $ce_new->{courseFiles}{logs} }) {
eval { Mojo::File->new($file)->remove };
$c->addbadmessage($c->maketext('Failed to remove file [_1]: [_2]', $file, $@)) if $@;
}

if (-d $ce_new->{courseDirs}{scoring}) {
eval { Mojo::File->new($ce_new->{courseDirs}{scoring})->remove_tree({ keep_root => 1 }) };
$c->addbadmessage($c->maketext('Failed to remove scoring files: [_1]', $@)) if $@;
}

if (-d $ce_new->{courseDirs}{tmpEditFileDir}) {
eval { Mojo::File->new($ce_new->{courseDirs}{tmpEditFileDir})->remove_tree({ keep_root => 1 }) };
$c->addbadmessage($c->maketext('Failed to remove temporary edited files: [_1]', $@)) if $@;
}
}
$c->clean_up_course($new_courseID) if $c->param('clean_up_course') && $dbRestoreType eq 'dbdump';

return $c->c(
$c->tag(
'div',
class => 'alert alert-success p-1 mb-2',
$c->maketext('Successfully unarchived [_1] to the course [_2]', $unarchive_courseID, $new_courseID),
),
$c->param('clean_up_course') && $dbRestoreType ne 'dbdump'
? $c->tag(
'div',
class => 'alert alert-warning p-1 mb-2',
$dbRestoreType eq 'mysqldump'
? $c->maketext(
'The course could not be cleaned because it was archived with an older version of WeBWorK. '
. 'Upgrade the course, and then use the "Clean Course" page to clean up the course.'
)
: $c->maketext(
'The course could not be cleaned because the archive does not contain the course database.')
)
: '',
$c->tag(
'div',
class => 'd-flex justify-content-between',
Expand Down Expand Up @@ -1290,6 +1299,112 @@ sub do_unarchive_course ($c) {
}
}

sub clean_course_form ($c) {
return $c->include(
'ContentGenerator/CourseAdmin/clean_course_form',
courseIDs => [
sort { lc($a) cmp lc($b) }
grep { $_ ne $c->stash('courseID') && $_ ne 'modelCourse' } listCourses($c->ce)
]
);
}

sub clean_course_validate ($c) {
my $clean_courseID = $c->param('clean_courseID') || '';

return $c->maketext('You must specify a course name.') unless $clean_courseID;
return $c->maketext('You cannot clean the course you are currently using.')
if $clean_courseID eq $c->stash('courseID');
return $c->maketext('The course [_1] does not exist.', $clean_courseID)
if $clean_courseID eq 'modelCourse' || !grep { $_ eq $clean_courseID } listCourses($c->ce);

my (undef, $dbStatus) =
WeBWorK::Utils::CourseDBIntegrityCheck->new(WeBWorK::CourseEnvironment->new({ courseName => $clean_courseID }))
->checkCourseTables($clean_courseID);
return $c->maketext(
'The database for the course [_1] must be upgraded before the course can be cleaned. '
. 'Use the "Upgrade Courses" page to upgrade the course.',
$clean_courseID
) unless $c->formatReportOnDatabaseTables($dbStatus)->{all_tables_ok};

return;
}

sub clean_course_confirm ($c) {
return $c->include('ContentGenerator/CourseAdmin/clean_course_confirm');
}

sub do_clean_course ($c) {
my $clean_courseID = $c->param('clean_courseID');

return $c->tag(
'div',
class => 'alert alert-danger p-1 my-2',
$c->maketext('An error occurred while cleaning the course [_1].', $clean_courseID)
) unless $c->clean_up_course($clean_courseID);

writeLog($c->ce, 'hosted_courses', join("\t", "\tCleaned", '', '', $clean_courseID));

return $c->c(
$c->tag(
'div',
class => 'alert alert-success p-1 my-2',
$c->maketext('Successfully cleaned the course [_1].', $clean_courseID),
),
$c->form_for(
$c->current_route,
method => 'POST',
$c->c(
$c->hidden_authen_fields,
$c->hidden_fields('subDisplay'),
$c->tag(
'div',
class => 'text-center',
$c->submit_button(
$c->maketext('OK'),
name => 'decline_clean_course',
class => 'btn btn-primary'
)
)
)->join('')
)
)->join('');
}

# Remove student users, log files, scoring files, and temporary edited files from the course. The course database must
# be up to date when this is called. Returns 1 if the student users were deleted, and 0 otherwise.
sub clean_up_course ($c, $courseID) {
my $ce = WeBWorK::CourseEnvironment->new({ courseName => $courseID });
my $db = WeBWorK::DB->new($ce);

eval {
for my $student_id ($db->listPermissionLevelsWhere({ permission => $ce->{userRoles}{student} })) {
$db->deleteUser($student_id->[0]);
}
};
if ($@) {
$c->addbadmessage($c->maketext('Failed to remove student users: [_1]', ref $@ ? $@->message : $@));
return 0;
}

for my $file (values %{ $ce->{courseFiles}{logs} }) {
eval { Mojo::File->new($file)->remove };
$c->addbadmessage($c->maketext('Failed to remove file [_1]: [_2]', $file, $@)) if $@;
}

if (-d $ce->{courseDirs}{scoring}) {
eval { Mojo::File->new($ce->{courseDirs}{scoring})->remove_tree({ keep_root => 1 }) };
$c->addbadmessage($c->maketext('Failed to remove scoring files: [_1]', $@)) if $@;
}

if (-d $ce->{courseDirs}{tmpEditFileDir}) {
eval { Mojo::File->new($ce->{courseDirs}{tmpEditFileDir})->remove_tree({ keep_root => 1 }) };
$c->addbadmessage($c->maketext('Failed to remove temporary edited files: [_1]', $@)) if $@;
}

return 1;
}

# Course upgrade methods

sub upgrade_course_form ($c) {
Expand Down
17 changes: 13 additions & 4 deletions lib/WeBWorK/Utils/CourseManagement.pm
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,14 @@ $ce is a WeBWorK::CourseEnvironment object that describes the some course's
environment. (Usually this would be the admin course.) This is used to access
the course database and get path information.

Returns a string indicating how the course database was restored. This is
'dbdump' if the database was restored from a JSON database dump, 'mysqldump' if
the database was restored from a legacy mysqldump archive, and 'none' if the
archive does not contain a database dump. If 'mysqldump' is returned, then the
database tables have not been upgraded and may not be compatible with the
current version of WeBWorK until the course is upgraded. If 'none' is returned,
then the course has no database tables.

If an error occurs, an exception is thrown.

=cut
Expand Down Expand Up @@ -1053,7 +1061,7 @@ sub unarchiveCourse {
my $ce2 = WeBWorK::CourseEnvironment->new({ get_SeedCE($ce), courseName => $currCourseID });

# Step 5: Restore the database tables and delete the database dump directory.
my $no_database;
my $dbRestoreType = 'dbdump';
my $restore_db_result = 1;
my $dbdump_dir = path("$ce2->{courseDirs}{DATA}/dbdump");
if (-e $dbdump_dir) {
Expand All @@ -1065,10 +1073,11 @@ sub unarchiveCourse {
if (-e $mysqldump_dir) {
$restore_db_result = WeBWorK::DB->new($ce2)->restore_tables_mysql($mysqldump_dir);
_archiveCourse_remove_dump_dir($ce, $mysqldump_dir);
$dbRestoreType = 'mysqldump';
} else {
warn "course '$currCourseID' has no database dump in its data directory "
. "(checked for $dbdump_dir and $mysqldump_dir). database tables will not be restored.\n";
$no_database = 1;
$dbRestoreType = 'none';
}
}

Expand Down Expand Up @@ -1109,14 +1118,14 @@ sub unarchiveCourse {
courseID => $currCourseID,
ce => $ce2,
newCourseID => $newCourseID,
skipDBRename => $no_database,
skipDBRename => $dbRestoreType eq 'none',
);
}

# Step 9: Return a conflicting course to its rightful place.
_unarchiveCourse_move_back($restoreCourseData);

return;
return $dbRestoreType;
}

sub _unarchiveCourse_move_away {
Expand Down
1 change: 1 addition & 0 deletions templates/ContentGenerator/Base/admin_links.html.ep
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
% [ 'delete_course', maketext('Delete Course') ],
% [ 'archive_course', maketext('Archive Courses') ],
% [ 'unarchive_course', maketext('Unarchive Course') ],
% [ 'clean_course', maketext('Clean Course') ],
% [ 'upgrade_course', maketext('Upgrade Courses') ],
% [ 'hide_inactive_course', maketext('Hide Courses') ],
% [ 'manage_locations', maketext('Manage Locations') ],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<h2><%= maketext('Clean Course') %> <%= $c->helpMacro('AdminCleanCourse') %></h2>
<p>
<%== maketext(
'Are you sure you want to clean the course [_1]? All student users and their associated data, the log '
. 'files, any files in the scoring folder, and any temporary edited files will be deleted. '
. 'There is no undo available.',
tag('b', param('clean_courseID') || '')
) =%>
</p>
<%= form_for current_route, method => 'POST', begin =%>
<%= $c->hidden_authen_fields =%>
<%= $c->hidden_fields('subDisplay') =%>
<%= $c->hidden_fields('clean_courseID') =%>
%
<div class="d-flex justify-content-center gap-2">
<%= submit_button maketext("Don't clean"), name => 'decline_clean_course', class => 'btn btn-primary' =%>
<%= submit_button maketext('Clean'), name => 'confirm_clean_course', class => 'btn btn-primary' =%>
</div>
<% end =%>
28 changes: 28 additions & 0 deletions templates/ContentGenerator/CourseAdmin/clean_course_form.html.ep
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<h2><%= maketext('Clean Course') %> <%= $c->helpMacro('AdminCleanCourse') %></h2>
%
% if (@$courseIDs) {
<%= form_for current_route, method => 'POST', begin =%>
<%= $c->hidden_authen_fields =%>
<%= $c->hidden_fields('subDisplay') =%>
<div class="mb-2">
<%= maketext('Select a course to clean. This will remove student users and their associated data from '
. 'the database. It will also remove the log files, any files in the scoring folder, and any '
. 'temporary edited files.') =%>
</div>
<div class="row mb-2">
<%= label_for clean_courseID => maketext('Course ID:'), class => 'col-auto col-form-label fw-bold' =%>
<div class="col-auto">
<%= select_field
clean_courseID => $courseIDs,
id => 'clean_courseID',
size => 15,
class => 'form-select' =%>
</div>
</div>
<div>
<%= submit_button maketext('Clean Course'), name => 'clean_course', class => 'btn btn-primary' =%>
</div>
<%= end =%>
% } else {
<p><%= maketext('No courses found') %></p>
% }
12 changes: 12 additions & 0 deletions templates/HelpFiles/AdminCleanCourse.html.ep
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
% layout 'help_macro';
% title maketext('Clean Course Help');
%
<p>
<%= maketext('Select a course to clean. This will remove student users and their associated data from the '
. 'database. It will also remove the log files, any files in the scoring folder, and any temporary '
. 'edited files. Warning, this cannot be undone.') =%>
</p>
<p class="mb-0">
<%= maketext('The course database must be up to date before the course can be cleaned. If the course was created '
. 'with an older version of WeBWorK, use the "Upgrade Courses" page to upgrade the course first.') =%>
</p>
4 changes: 3 additions & 1 deletion templates/HelpFiles/AdminUnarchiveCourse.html.ep
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@
<p class="mb-0">
<%= maketext('You may check the box to "clean" the unarchived course. This will remove student users '
. 'and their associated data from the database after the course is unarchived. It will also remove '
. 'the log files, any files in the scoring folder, and any temporary edited files.') =%>
. 'the log files, any files in the scoring folder, and any temporary edited files. If the archive was '
. 'created with an older version of WeBWorK, then the course cannot be cleaned when it is unarchived. '
. 'In that case, upgrade the course and then use the "Clean Course" page to clean the course.') =%>
</p>
4 changes: 4 additions & 0 deletions templates/HelpFiles/admin_links.html.ep
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
<dd><%= maketext(q{Create a .tar.gz archive which includes the course's database and all course files.}) =%></dd>
<dt><%= maketext('Unarchive Course') %></dt>
<dd><%= maketext(q{Restore a .tar.gz archive.}) =%></dd>
<dt><%= maketext('Clean Course') %></dt>
<dd>
<%= maketext('Remove student users, log files, scoring files, and temporary edited files from a course.') =%>
</dd>
<dt><%= maketext('Upgrade Courses') %></dt>
<dd><%= maketext('Upgrade courses from a previous version of WeBWorK.') =%></dd>
<dt><%= maketext('Hide Courses') %></dt>
Expand Down