MDL-54751 core_course: Edit course libs to support deletion flag

Changed the course libs such that module and section deletion is now
handed off to relevant functions in the util class and exempted
modules scheduled for deletion from the course backups.
This commit is contained in:
Jake Dallimore 2016-11-02 15:07:55 +08:00
parent 37da62f360
commit 3869d77411
6 changed files with 244 additions and 16 deletions

View File

@ -152,6 +152,17 @@ function tool_recyclebin_pre_course_module_delete($cm) {
}
}
/**
* Hook called to check whether async course module deletion should be performed or not.
*
* @return true if background deletion is required (is the recyclebin is enabled), false otherwise.
*/
function tool_recyclebin_course_module_background_deletion_recommended() {
if (\tool_recyclebin\course_bin::is_enabled()) {
return true;
}
}
/**
* Hook called before we delete a course.
*

View File

@ -87,7 +87,8 @@ abstract class backup_plan_dbops extends backup_dbops {
FROM {course_modules} cm
JOIN {modules} m ON m.id = cm.module
WHERE cm.course = ?
AND cm.section = ?", array($courseid, $sectionid));
AND cm.section = ?
AND cm.deletioninprogress <> 1", array($courseid, $sectionid));
foreach (explode(',', $sequence) as $moduleid) {
if (isset($modules[$moduleid])) {
$module = array('id' => $modules[$moduleid]->id, 'modname' => $modules[$moduleid]->modname);

View File

@ -50,7 +50,7 @@ if ($deletesection) {
if (course_can_delete_section($course, $sectioninfo)) {
$confirm = optional_param('confirm', false, PARAM_BOOL) && confirm_sesskey();
if ($confirm) {
course_delete_section($course, $sectioninfo, true);
course_delete_section($course, $sectioninfo, true, true);
$courseurl = course_get_url($course, 0, array('sr' => $sectionreturn));
redirect($courseurl);
} else {

View File

@ -430,6 +430,7 @@ function get_array_of_activities($courseid) {
$mod[$seq]->completionexpected = $rawmods[$seq]->completionexpected;
$mod[$seq]->showdescription = $rawmods[$seq]->showdescription;
$mod[$seq]->availability = $rawmods[$seq]->availability;
$mod[$seq]->deletioninprogress = $rawmods[$seq]->deletioninprogress;
$modname = $mod[$seq]->mod;
$functionname = $modname."_get_coursemodule_info";
@ -504,7 +505,7 @@ function get_array_of_activities($courseid) {
foreach (array('idnumber', 'groupmode', 'groupingid',
'indent', 'completion', 'extra', 'extraclasses', 'iconurl', 'onclick', 'content',
'icon', 'iconcomponent', 'customdata', 'availability', 'completionview',
'completionexpected', 'score', 'showdescription') as $property) {
'completionexpected', 'score', 'showdescription', 'deletioninprogress') as $property) {
if (property_exists($mod[$seq], $property) &&
empty($mod[$seq]->{$property})) {
unset($mod[$seq]->{$property});
@ -1072,9 +1073,25 @@ function set_coursemodule_name($id, $name) {
* event to the DB.
*
* @param int $cmid the course module id
* @param bool $async whether or not to try to delete the module using an adhoc task. Async also depends on a plugin hook.
* @throws moodle_exception
* @since Moodle 2.5
*/
function course_delete_module($cmid) {
function course_delete_module($cmid, $async = false) {
// Check the 'course_module_background_deletion_recommended' hook first.
// Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested.
// Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it.
// It's up to plugins to handle things like whether or not they are enabled.
if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
foreach ($pluginsfunction as $plugintype => $plugins) {
foreach ($plugins as $pluginfunction) {
if ($pluginfunction()) {
return course_module_flag_for_async_deletion($cmid);
}
}
}
}
global $CFG, $DB;
require_once($CFG->libdir.'/gradelib.php');
@ -1192,6 +1209,104 @@ function course_delete_module($cmid) {
rebuild_course_cache($cm->course, true);
}
/**
* Schedule a course module for deletion in the background using an adhoc task.
*
* This method should not be called directly. Instead, please use course_delete_module($cmid, true), to denote async deletion.
* The real deletion of the module is handled by the task, which calls 'course_delete_module($cmid)'.
*
* @param int $cmid the course module id.
* @return bool whether the module was successfully scheduled for deletion.
* @throws \moodle_exception
*/
function course_module_flag_for_async_deletion($cmid) {
global $CFG, $DB;
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->libdir.'/questionlib.php');
require_once($CFG->dirroot.'/blog/lib.php');
require_once($CFG->dirroot.'/calendar/lib.php');
// Get the course module.
if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
return true;
}
// We need to be reasonably certain the deletion is going to succeed before we background the process.
// Make the necessary delete_instance checks, etc. before proceeding further. Throw exceptions if required.
// Get the course module name.
$modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST);
// Get the file location of the delete_instance function for this module.
$modlib = "$CFG->dirroot/mod/$modulename/lib.php";
// Include the file required to call the delete_instance function for this module.
if (file_exists($modlib)) {
require_once($modlib);
} else {
throw new \moodle_exception('cannotdeletemodulemissinglib', '', '', null,
"Cannot delete this module as the file mod/$modulename/lib.php is missing.");
}
$deleteinstancefunction = $modulename . '_delete_instance';
// Ensure the delete_instance function exists for this module.
if (!function_exists($deleteinstancefunction)) {
throw new \moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
"Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
}
// We are going to defer the deletion as we can't be sure how long the module's pre_delete code will run for.
$cm->deletioninprogress = '1';
$DB->update_record('course_modules', $cm);
// Create an adhoc task for the deletion of the course module. The task takes an array of course modules for removal.
$removaltask = new \core_course\task\course_delete_modules();
$removaltask->set_custom_data(array('cms' => array($cm)));
// Queue the task for the next run.
\core\task\manager::queue_adhoc_task($removaltask);
// Reset the course cache to hide the module.
rebuild_course_cache($cm->course, true);
}
/**
* Checks whether the given course has any course modules scheduled for adhoc deletion.
*
* @param int $courseid the id of the course.
* @return bool true if the course contains any modules pending deletion, false otherwise.
*/
function course_modules_pending_deletion($courseid) {
if (empty($courseid)) {
return false;
}
$modinfo = get_fast_modinfo($courseid);
foreach ($modinfo->get_cms() as $module) {
if ($module->deletioninprogress == '1') {
return true;
}
}
return false;
}
/**
* Checks whether the course module, as defined by modulename and instanceid, is scheduled for deletion within the given course.
*
* @param int $courseid the course id.
* @param string $modulename the module name. E.g. 'assign', 'book', etc.
* @param int $instanceid the module instance id.
* @return bool true if the course module is pending deletion, false otherwise.
*/
function course_module_instance_pending_deletion($courseid, $modulename, $instanceid) {
if (empty($courseid) || empty($modulename) || empty($instanceid)) {
return false;
}
$modinfo = get_fast_modinfo($courseid);
$instances = $modinfo->get_instances_of($modulename);
return isset($instances[$instanceid]) && $instances[$instanceid]->deletioninprogress;
}
function delete_mod_from_section($modid, $sectionid) {
global $DB;
@ -1285,9 +1400,10 @@ function move_section_to($course, $section, $destination, $ignorenumsections = f
* @param int|stdClass $course
* @param int|stdClass|section_info $section
* @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it.
* @param bool $async whether or not to try to delete the section using an adhoc task. Async also depends on a plugin hook.
* @return bool whether section was deleted
*/
function course_delete_section($course, $section, $forcedeleteifnotempty = true) {
function course_delete_section($course, $section, $forcedeleteifnotempty = true, $async = false) {
global $DB;
// Prepare variables.
@ -1298,6 +1414,21 @@ function course_delete_section($course, $section, $forcedeleteifnotempty = true)
// No section exists, can't proceed.
return false;
}
// Check the 'course_module_background_deletion_recommended' hook first.
// Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested.
// Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it.
// It's up to plugins to handle things like whether or not they are enabled.
if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
foreach ($pluginsfunction as $plugintype => $plugins) {
foreach ($plugins as $pluginfunction) {
if ($pluginfunction()) {
return course_delete_section_async($section, $forcedeleteifnotempty);
}
}
}
}
$format = course_get_format($course);
$sectionname = $format->get_section_name($section);
@ -1308,22 +1439,102 @@ function course_delete_section($course, $section, $forcedeleteifnotempty = true)
if ($result) {
$context = context_course::instance($courseid);
$event = \core\event\course_section_deleted::create(
array(
'objectid' => $section->id,
'courseid' => $courseid,
'context' => $context,
'other' => array(
'sectionnum' => $section->section,
'sectionname' => $sectionname,
)
array(
'objectid' => $section->id,
'courseid' => $courseid,
'context' => $context,
'other' => array(
'sectionnum' => $section->section,
'sectionname' => $sectionname,
)
);
)
);
$event->add_record_snapshot('course_sections', $section);
$event->trigger();
}
return $result;
}
/**
* Course section deletion, using an adhoc task for deletion of the modules it contains.
* 1. Schedule all modules within the section for adhoc removal.
* 2. Move all modules to course section 0.
* 3. Delete the resulting empty section.
*
* @param \stdClass $section the section to schedule for deletion.
* @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules.
* @return bool true if the section was scheduled for deletion, false otherwise.
*/
function course_delete_section_async($section, $forcedeleteifnotempty = true) {
global $DB;
// Objects only, and only valid ones.
if (!is_object($section) || empty($section->id)) {
return false;
}
// Does the object currently exist in the DB for removal (check for stale objects).
$section = $DB->get_record('course_sections', array('id' => $section->id));
if (!$section || !$section->section) {
// No section exists, or the section is 0. Can't proceed.
return false;
}
// Check whether the section can be removed.
if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
return false;
}
$format = course_get_format($section->course);
$sectionname = $format->get_section_name($section);
// Flag those modules having no existing deletion flag. Some modules may have been scheduled for deletion manually, and we don't
// want to create additional adhoc deletion tasks for these. Moving them to section 0 will suffice.
$affectedmods = $DB->get_records_select('course_modules', 'course = ? AND section = ? AND deletioninprogress <> ?',
[$section->course, $section->id, 1], '', 'id');
$DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $section->course, 'section' => $section->id]);
// Move all modules to section 0.
$modules = $DB->get_records('course_modules', ['section' => $section->id], '');
$sectionzero = $DB->get_record('course_sections', ['course' => $section->course, 'section' => '0']);
foreach ($modules as $mod) {
moveto_module($mod, $sectionzero);
}
// Create and queue an adhoc task for the deletion of the modules.
$removaltask = new \core_course\task\course_delete_modules();
$data = array(
'cms' => $affectedmods
);
$removaltask->set_custom_data($data);
\core\task\manager::queue_adhoc_task($removaltask);
// Delete the now empty section, passing in only the section number, which forces the function to fetch a new object.
// The refresh is needed because the section->sequence is now stale.
$result = $format->delete_section($section->section, $forcedeleteifnotempty);
// Trigger an event for course section deletion.
if ($result) {
$context = \context_course::instance($section->course);
$event = \core\event\course_section_deleted::create(
array(
'objectid' => $section->id,
'courseid' => $section->course,
'context' => $context,
'other' => array(
'sectionnum' => $section->section,
'sectionname' => $sectionname,
)
)
);
$event->add_record_snapshot('course_sections', $section);
$event->trigger();
}
rebuild_course_cache($section->course, true);
return $result;
}
/**
* Updates the course section
*

View File

@ -168,7 +168,7 @@ switch($requestmethod) {
switch ($class) {
case 'resource':
require_capability('moodle/course:manageactivities', $modcontext);
course_delete_module($cm->id);
course_delete_module($cm->id, true);
break;
}
break;

View File

@ -2215,7 +2215,12 @@ class file_storage {
mkdir($trashpath, $this->dirpermissions, true);
}
rename($contentfile, $trashfile);
chmod($trashfile, $this->filepermissions); // fix permissions if needed
// Fix permissions, only if needed.
$currentperms = octdec(substr(decoct(fileperms($trashfile)), -4));
if ((int)$this->filepermissions !== $currentperms) {
chmod($trashfile, $this->filepermissions);
}
}
/**