Merge branch 'MDL-53262-verp-confirm-from-support' of https://github.com/brendanheywo...
authorDan Poltawski <dan@moodle.com>
Mon, 21 Mar 2016 03:37:35 +0000 (11:37 +0800)
committerDan Poltawski <dan@moodle.com>
Mon, 21 Mar 2016 03:37:35 +0000 (11:37 +0800)
52 files changed:
admin/tool/recyclebin/classes/base_bin.php [new file with mode: 0644]
admin/tool/recyclebin/classes/category_bin.php [new file with mode: 0644]
admin/tool/recyclebin/classes/course_bin.php [new file with mode: 0644]
admin/tool/recyclebin/classes/event/category_bin_item_created.php [new file with mode: 0644]
admin/tool/recyclebin/classes/event/category_bin_item_deleted.php [new file with mode: 0644]
admin/tool/recyclebin/classes/event/category_bin_item_restored.php [new file with mode: 0644]
admin/tool/recyclebin/classes/event/course_bin_item_created.php [new file with mode: 0644]
admin/tool/recyclebin/classes/event/course_bin_item_deleted.php [new file with mode: 0644]
admin/tool/recyclebin/classes/event/course_bin_item_restored.php [new file with mode: 0644]
admin/tool/recyclebin/classes/task/cleanup_category_bin.php [new file with mode: 0644]
admin/tool/recyclebin/classes/task/cleanup_course_bin.php [new file with mode: 0644]
admin/tool/recyclebin/db/access.php [new file with mode: 0644]
admin/tool/recyclebin/db/install.xml [new file with mode: 0644]
admin/tool/recyclebin/db/tasks.php [new file with mode: 0644]
admin/tool/recyclebin/index.php [new file with mode: 0644]
admin/tool/recyclebin/lang/en/tool_recyclebin.php [new file with mode: 0644]
admin/tool/recyclebin/lib.php [new file with mode: 0644]
admin/tool/recyclebin/pix/trash.png [new file with mode: 0644]
admin/tool/recyclebin/pix/trash.svg [new file with mode: 0644]
admin/tool/recyclebin/settings.php [new file with mode: 0644]
admin/tool/recyclebin/tests/behat/backup_user_data.feature [new file with mode: 0644]
admin/tool/recyclebin/tests/behat/basic_functionality.feature [new file with mode: 0644]
admin/tool/recyclebin/tests/category_bin_test.php [new file with mode: 0644]
admin/tool/recyclebin/tests/course_bin_test.php [new file with mode: 0644]
admin/tool/recyclebin/tests/events_test.php [new file with mode: 0644]
admin/tool/recyclebin/version.php [new file with mode: 0644]
course/lib.php
lang/en/moodle.php
lib/blocklib.php
lib/classes/plugin_manager.php
lib/coursecatlib.php
lib/javascript-static.js
lib/moodlelib.php
lib/tests/event_grade_deleted_test.php
message/lib.php
mod/assign/db/services.php
mod/assign/externallib.php
mod/assign/feedback/editpdf/locallib.php
mod/assign/locallib.php
mod/assign/tests/externallib_test.php
mod/assign/upgrade.txt
mod/forum/templates/forum_post_emaildigestbasic_htmlemail.mustache
mod/quiz/styles.css
search/engine/solr/classes/engine.php
search/engine/solr/classes/schema.php
search/engine/solr/lang/en/search_solr.php
search/engine/solr/settings.php
search/engine/solr/tests/engine_test.php
user/index.php
user/module.js
user/tests/behat/view_participants.feature [new file with mode: 0644]
version.php

diff --git a/admin/tool/recyclebin/classes/base_bin.php b/admin/tool/recyclebin/classes/base_bin.php
new file mode 100644 (file)
index 0000000..84c9e84
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The main interface for recycle bin methods.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_recyclebin;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Represents a recyclebin.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class base_bin {
+
+    /**
+     * Is this recyclebin enabled?
+     */
+    public static function is_enabled() {
+        return false;
+    }
+
+    /**
+     * Returns an item from the recycle bin.
+     *
+     * @param int $itemid Item ID to retrieve.
+     */
+    public abstract function get_item($itemid);
+
+    /**
+     * Returns a list of items in the recycle bin.
+     */
+    public abstract function get_items();
+
+    /**
+     * Store an item in this recycle bin.
+     *
+     * @param \stdClass $item Item to store.
+     */
+    public abstract function store_item($item);
+
+    /**
+     * Restore an item from the recycle bin.
+     *
+     * @param \stdClass $item The item database record
+     */
+    public abstract function restore_item($item);
+
+    /**
+     * Delete an item from the recycle bin.
+     *
+     * @param \stdClass $item The item database record
+     */
+    public abstract function delete_item($item);
+
+    /**
+     * Empty the recycle bin.
+     */
+    public function delete_all_items() {
+        // Cleanup all items.
+        $items = $this->get_items();
+        foreach ($items as $item) {
+            if ($this->can_delete()) {
+                $this->delete_item($item);
+            }
+        }
+    }
+
+    /**
+     * Can we view items in this recycle bin?
+     */
+    public abstract function can_view();
+
+    /**
+     * Can we restore items in this recycle bin?
+     */
+    public abstract function can_restore();
+
+    /**
+     * Can we delete this?
+     */
+    public abstract function can_delete();
+}
diff --git a/admin/tool/recyclebin/classes/category_bin.php b/admin/tool/recyclebin/classes/category_bin.php
new file mode 100644 (file)
index 0000000..06b06de
--- /dev/null
@@ -0,0 +1,337 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The main interface for recycle bin methods.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_recyclebin;
+
+defined('MOODLE_INTERNAL') || die();
+
+define('TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA', 'recyclebin_coursecat');
+
+/**
+ * Represents a category's recyclebin.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class category_bin extends base_bin {
+
+    /**
+     * @var int The category id.
+     */
+    protected $_categoryid;
+
+    /**
+     * Constructor.
+     *
+     * @param int $categoryid The category id.
+     */
+    public function __construct($categoryid) {
+        $this->_categoryid = $categoryid;
+    }
+
+    /**
+     * Is this recyclebin enabled?
+     *
+     * @return bool true if enabled, false if not.
+     */
+    public static function is_enabled() {
+        return get_config('tool_recyclebin', 'categorybinenable');
+    }
+
+    /**
+     * Returns an item from the recycle bin.
+     *
+     * @param int $itemid Item ID to retrieve.
+     * @return \stdClass the item.
+     */
+    public function get_item($itemid) {
+        global $DB;
+
+        $item = $DB->get_record('tool_recyclebin_category', array(
+            'id' => $itemid
+        ), '*', MUST_EXIST);
+
+        $item->name = get_course_display_name_for_list($item);
+
+        return $item;
+    }
+
+    /**
+     * Returns a list of items in the recycle bin for this course.
+     *
+     * @return array the list of items.
+     */
+    public function get_items() {
+        global $DB;
+
+        $items = $DB->get_records('tool_recyclebin_category', array(
+            'categoryid' => $this->_categoryid
+        ));
+
+        foreach ($items as $item) {
+            $item->name = get_course_display_name_for_list($item);
+        }
+
+        return $items;
+    }
+
+    /**
+     * Store a course in the recycle bin.
+     *
+     * @param \stdClass $course Course
+     * @throws \moodle_exception
+     */
+    public function store_item($course) {
+        global $CFG, $DB;
+
+        require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+
+        // Backup the course.
+        $user = get_admin();
+        $controller = new \backup_controller(
+            \backup::TYPE_1COURSE,
+            $course->id,
+            \backup::FORMAT_MOODLE,
+            \backup::INTERACTIVE_NO,
+            \backup::MODE_GENERAL,
+            $user->id
+        );
+        $controller->execute_plan();
+
+        // Grab the result.
+        $result = $controller->get_results();
+        if (!isset($result['backup_destination'])) {
+            throw new \moodle_exception('Failed to backup activity prior to deletion.');
+        }
+
+        // Grab the filename.
+        $file = $result['backup_destination'];
+        if (!$file->get_contenthash()) {
+            throw new \moodle_exception('Failed to backup activity prior to deletion (invalid file).');
+        }
+
+        // Record the activity, get an ID.
+        $item = new \stdClass();
+        $item->categoryid = $course->category;
+        $item->shortname = $course->shortname;
+        $item->fullname = $course->fullname;
+        $item->timecreated = time();
+        $binid = $DB->insert_record('tool_recyclebin_category', $item);
+
+        // Create the location we want to copy this file to.
+        $filerecord = array(
+            'contextid' => \context_coursecat::instance($course->category)->id,
+            'component' => 'tool_recyclebin',
+            'filearea' => TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA,
+            'itemid' => $binid,
+            'timemodified' => time()
+        );
+
+        // Move the file to our own special little place.
+        $fs = get_file_storage();
+        if (!$fs->create_file_from_storedfile($filerecord, $file)) {
+            // Failed, cleanup first.
+            $DB->delete_records('tool_recyclebin_category', array(
+                'id' => $binid
+            ));
+
+            throw new \moodle_exception("Failed to copy backup file to recyclebin.");
+        }
+
+        // Delete the old file.
+        $file->delete();
+
+        // Fire event.
+        $event = \tool_recyclebin\event\category_bin_item_created::create(array(
+            'objectid' => $binid,
+            'context' => \context_coursecat::instance($course->category)
+        ));
+        $event->trigger();
+    }
+
+    /**
+     * Restore an item from the recycle bin.
+     *
+     * @param \stdClass $item The item database record
+     * @throws \moodle_exception
+     */
+    public function restore_item($item) {
+        global $CFG, $OUTPUT, $PAGE;
+
+        require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+        require_once($CFG->dirroot . '/course/lib.php');
+
+        $user = get_admin();
+
+        // Grab the course category context.
+        $context = \context_coursecat::instance($this->_categoryid);
+
+        // Get the backup file.
+        $fs = get_file_storage();
+        $files = $fs->get_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA, $item->id,
+            'itemid, filepath, filename', false);
+
+        if (empty($files)) {
+            throw new \moodle_exception('Invalid recycle bin item!');
+        }
+
+        if (count($files) > 1) {
+            throw new \moodle_exception('Too many files found!');
+        }
+
+        // Get the backup file.
+        $file = reset($files);
+
+        // Get a temp directory name and create it.
+        $tempdir = \restore_controller::get_tempdir_name($context->id, $user->id);
+        $fulltempdir = make_temp_directory('/backup/' . $tempdir);
+
+        // Extract the backup to tmpdir.
+        $fb = get_file_packer('application/vnd.moodle.backup');
+        $fb->extract_to_pathname($file, $fulltempdir);
+
+        // Build a course.
+        $course = new \stdClass();
+        $course->category = $this->_categoryid;
+        $course->shortname = $item->shortname;
+        $course->fullname = $item->fullname;
+        $course->summary = '';
+
+        // Create a new course.
+        $course = create_course($course);
+        if (!$course) {
+            throw new \moodle_exception("Could not create course to restore into.");
+        }
+
+        // Define the import.
+        $controller = new \restore_controller(
+            $tempdir,
+            $course->id,
+            \backup::INTERACTIVE_NO,
+            \backup::MODE_GENERAL,
+            $user->id,
+            \backup::TARGET_NEW_COURSE
+        );
+
+        // Prechecks.
+        if (!$controller->execute_precheck()) {
+            $results = $controller->get_precheck_results();
+
+            // Check if errors have been found.
+            if (!empty($results['errors'])) {
+                // Delete the temporary file we created.
+                fulldelete($fulltempdir);
+
+                // Delete the course we created.
+                delete_course($course, false);
+
+                echo $OUTPUT->header();
+                $backuprenderer = $PAGE->get_renderer('core', 'backup');
+                echo $backuprenderer->precheck_notices($results);
+                echo $OUTPUT->continue_button(new \moodle_url('/course/index.php', array('categoryid' => $this->_categoryid)));
+                echo $OUTPUT->footer();
+                exit();
+            }
+        }
+
+        // Run the import.
+        $controller->execute_plan();
+
+        // Fire event.
+        $event = \tool_recyclebin\event\category_bin_item_restored::create(array(
+            'objectid' => $item->id,
+            'context' => $context
+        ));
+        $event->add_record_snapshot('tool_recyclebin_category', $item);
+        $event->trigger();
+
+        // Cleanup.
+        fulldelete($fulltempdir);
+        $this->delete_item($item);
+    }
+
+    /**
+     * Delete an item from the recycle bin.
+     *
+     * @param \stdClass $item The item database record
+     * @throws \coding_exception
+     */
+    public function delete_item($item) {
+        global $DB;
+
+        // Grab the course category context.
+        $context = \context_coursecat::instance($this->_categoryid);
+
+        // Delete the files.
+        $fs = get_file_storage();
+        $files = $fs->get_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA, $item->id);
+        foreach ($files as $file) {
+            $file->delete();
+        }
+
+        // Delete the record.
+        $DB->delete_records('tool_recyclebin_category', array(
+            'id' => $item->id
+        ));
+
+        // Fire event.
+        $event = \tool_recyclebin\event\category_bin_item_deleted::create(array(
+            'objectid' => $item->id,
+            'context' => \context_coursecat::instance($item->categoryid)
+        ));
+        $event->add_record_snapshot('tool_recyclebin_category', $item);
+        $event->trigger();
+    }
+
+    /**
+     * Can we view items in this recycle bin?
+     *
+     * @return bool returns true if they can view, false if not
+     */
+    public function can_view() {
+        $context = \context_coursecat::instance($this->_categoryid);
+        return has_capability('tool/recyclebin:viewitems', $context);
+    }
+
+    /**
+     * Can we restore items in this recycle bin?
+     *
+     * @return bool returns true if they can restore, false if not
+     */
+    public function can_restore() {
+        $context = \context_coursecat::instance($this->_categoryid);
+        return has_capability('tool/recyclebin:restoreitems', $context);
+    }
+
+    /**
+     * Can we delete items in this recycle bin?
+     *
+     * @return bool returns true if they can delete, false if not
+     */
+    public function can_delete() {
+        $context = \context_coursecat::instance($this->_categoryid);
+        return has_capability('tool/recyclebin:deleteitems', $context);
+    }
+}
diff --git a/admin/tool/recyclebin/classes/course_bin.php b/admin/tool/recyclebin/classes/course_bin.php
new file mode 100644 (file)
index 0000000..a05a7cf
--- /dev/null
@@ -0,0 +1,329 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The main interface for recycle bin methods.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_recyclebin;
+
+defined('MOODLE_INTERNAL') || die();
+
+define('TOOL_RECYCLEBIN_COURSE_BIN_FILEAREA', 'recyclebin_course');
+
+/**
+ * Represents a course's recyclebin.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_bin extends base_bin {
+
+    /**
+     * @var int The course id.
+     */
+    protected $_courseid;
+
+    /**
+     * Constructor.
+     *
+     * @param int $courseid Course ID.
+     */
+    public function __construct($courseid) {
+        $this->_courseid = $courseid;
+    }
+
+    /**
+     * Is this recyclebin enabled?
+     *
+     * @return bool true if enabled, false if not.
+     */
+    public static function is_enabled() {
+        return get_config('tool_recyclebin', 'coursebinenable');
+    }
+
+    /**
+     * Returns an item from the recycle bin.
+     *
+     * @param int $itemid Item ID to retrieve.
+     * @return \stdClass the item.
+     */
+    public function get_item($itemid) {
+        global $DB;
+
+        return $DB->get_record('tool_recyclebin_course', array(
+            'id' => $itemid
+        ), '*', MUST_EXIST);
+    }
+
+    /**
+     * Returns a list of items in the recycle bin for this course.
+     *
+     * @return array the list of items.
+     */
+    public function get_items() {
+        global $DB;
+
+        return $DB->get_records('tool_recyclebin_course', array(
+            'courseid' => $this->_courseid
+        ));
+    }
+
+    /**
+     * Store a course module in the recycle bin.
+     *
+     * @param \stdClass $cm Course module
+     * @throws \moodle_exception
+     */
+    public function store_item($cm) {
+        global $CFG, $DB;
+
+        require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+
+        // Get more information.
+        $modinfo = get_fast_modinfo($cm->course);
+
+        if (!isset($modinfo->cms[$cm->id])) {
+            return; // Can't continue without the module information.
+        }
+
+        $cminfo = $modinfo->cms[$cm->id];
+
+        // Check backup/restore support.
+        if (!plugin_supports('mod', $cminfo->modname , FEATURE_BACKUP_MOODLE2)) {
+            return;
+        }
+
+        // Backup the activity.
+        $user = get_admin();
+        $controller = new \backup_controller(
+            \backup::TYPE_1ACTIVITY,
+            $cm->id,
+            \backup::FORMAT_MOODLE,
+            \backup::INTERACTIVE_NO,
+            \backup::MODE_GENERAL,
+            $user->id
+        );
+        $controller->execute_plan();
+
+        // Grab the result.
+        $result = $controller->get_results();
+        if (!isset($result['backup_destination'])) {
+            throw new \moodle_exception('Failed to backup activity prior to deletion.');
+        }
+
+        // Grab the filename.
+        $file = $result['backup_destination'];
+        if (!$file->get_contenthash()) {
+            throw new \moodle_exception('Failed to backup activity prior to deletion (invalid file).');
+        }
+
+        // Record the activity, get an ID.
+        $activity = new \stdClass();
+        $activity->courseid = $cm->course;
+        $activity->section = $cm->section;
+        $activity->module = $cm->module;
+        $activity->name = $cminfo->name;
+        $activity->timecreated = time();
+        $binid = $DB->insert_record('tool_recyclebin_course', $activity);
+
+        // Create the location we want to copy this file to.
+        $filerecord = array(
+            'contextid' => \context_course::instance($this->_courseid)->id,
+            'component' => 'tool_recyclebin',
+            'filearea' => TOOL_RECYCLEBIN_COURSE_BIN_FILEAREA,
+            'itemid' => $binid,
+            'timemodified' => time()
+        );
+
+        // Move the file to our own special little place.
+        $fs = get_file_storage();
+        if (!$fs->create_file_from_storedfile($filerecord, $file)) {
+            // Failed, cleanup first.
+            $DB->delete_records('tool_recyclebin_course', array(
+                'id' => $binid
+            ));
+
+            throw new \moodle_exception("Failed to copy backup file to recyclebin.");
+        }
+
+        // Delete the old file.
+        $file->delete();
+
+        // Fire event.
+        $event = \tool_recyclebin\event\course_bin_item_created::create(array(
+            'objectid' => $binid,
+            'context' => \context_course::instance($cm->course)
+        ));
+        $event->trigger();
+    }
+
+    /**
+     * Restore an item from the recycle bin.
+     *
+     * @param \stdClass $item The item database record
+     * @throws \moodle_exception
+     */
+    public function restore_item($item) {
+        global $CFG, $OUTPUT, $PAGE;
+
+        require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+        $user = get_admin();
+
+        // Grab the course context.
+        $context = \context_course::instance($this->_courseid);
+
+        // Get the files..
+        $fs = get_file_storage();
+        $files = $fs->get_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSE_BIN_FILEAREA, $item->id,
+            'itemid, filepath, filename', false);
+
+        if (empty($files)) {
+            throw new \moodle_exception('Invalid recycle bin item!');
+        }
+
+        if (count($files) > 1) {
+            throw new \moodle_exception('Too many files found!');
+        }
+
+        // Get the backup file.
+        $file = reset($files);
+
+        // Get a temp directory name and create it.
+        $tempdir = \restore_controller::get_tempdir_name($context->id, $user->id);
+        $fulltempdir = make_temp_directory('/backup/' . $tempdir);
+
+        // Extract the backup to tempdir.
+        $fb = get_file_packer('application/vnd.moodle.backup');
+        $fb->extract_to_pathname($file, $fulltempdir);
+
+        // Define the import.
+        $controller = new \restore_controller(
+            $tempdir,
+            $this->_courseid,
+            \backup::INTERACTIVE_NO,
+            \backup::MODE_GENERAL,
+            $user->id,
+            \backup::TARGET_EXISTING_ADDING
+        );
+
+        // Prechecks.
+        if (!$controller->execute_precheck()) {
+            $results = $controller->get_precheck_results();
+
+            // If errors are found then delete the file we created.
+            if (!empty($results['errors'])) {
+                fulldelete($fulltempdir);
+
+                echo $OUTPUT->header();
+                $backuprenderer = $PAGE->get_renderer('core', 'backup');
+                echo $backuprenderer->precheck_notices($results);
+                echo $OUTPUT->continue_button(new \moodle_url('/course/view.php', array('id' => $this->_courseid)));
+                echo $OUTPUT->footer();
+                exit();
+            }
+        }
+
+        // Run the import.
+        $controller->execute_plan();
+
+        // Fire event.
+        $event = \tool_recyclebin\event\course_bin_item_restored::create(array(
+            'objectid' => $item->id,
+            'context' => $context
+        ));
+        $event->add_record_snapshot('tool_recyclebin_course', $item);
+        $event->trigger();
+
+        // Cleanup.
+        fulldelete($fulltempdir);
+        $this->delete_item($item);
+    }
+
+    /**
+     * Delete an item from the recycle bin.
+     *
+     * @param \stdClass $item The item database record
+     */
+    public function delete_item($item) {
+        global $DB;
+
+        // Grab the course context.
+        $context = \context_course::instance($this->_courseid);
+
+        // Delete the files.
+        $fs = get_file_storage();
+        $files = $fs->get_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSE_BIN_FILEAREA, $item->id);
+        foreach ($files as $file) {
+            $file->delete();
+        }
+
+        // Delete the record.
+        $DB->delete_records('tool_recyclebin_course', array(
+            'id' => $item->id
+        ));
+
+        // The course might have been deleted, check we have a context.
+        $context = \context_course::instance($item->courseid, \IGNORE_MISSING);
+        if (!$context) {
+            return;
+        }
+
+        // Fire event.
+        $event = \tool_recyclebin\event\course_bin_item_deleted::create(array(
+            'objectid' => $item->id,
+            'context' => $context
+        ));
+        $event->add_record_snapshot('tool_recyclebin_course', $item);
+        $event->trigger();
+    }
+
+    /**
+     * Can we view items in this recycle bin?
+     *
+     * @return bool returns true if they can view, false if not
+     */
+    public function can_view() {
+        $context = \context_course::instance($this->_courseid);
+        return has_capability('tool/recyclebin:viewitems', $context);
+    }
+
+    /**
+     * Can we restore items in this recycle bin?
+     *
+     * @return bool returns true if they can restore, false if not
+     */
+    public function can_restore() {
+        $context = \context_course::instance($this->_courseid);
+        return has_capability('tool/recyclebin:restoreitems', $context);
+    }
+
+    /**
+     * Can we delete this?
+     *
+     * @return bool returns true if they can delete, false if not
+     */
+    public function can_delete() {
+        $context = \context_course::instance($this->_courseid);
+        return has_capability('tool/recyclebin:deleteitems', $context);
+    }
+}
diff --git a/admin/tool/recyclebin/classes/event/category_bin_item_created.php b/admin/tool/recyclebin/classes/event/category_bin_item_created.php
new file mode 100644 (file)
index 0000000..f846283
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin events.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_recyclebin\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event class.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class category_bin_item_created extends \core\event\base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_recyclebin_category';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventitemcreated', 'tool_recyclebin');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return get_string('eventitemcreated_desc', 'tool_recyclebin', array(
+            'objectid' => $this->objectid
+        ));
+    }
+}
diff --git a/admin/tool/recyclebin/classes/event/category_bin_item_deleted.php b/admin/tool/recyclebin/classes/event/category_bin_item_deleted.php
new file mode 100644 (file)
index 0000000..bf0a1a2
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin events.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_recyclebin\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event class.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class category_bin_item_deleted extends \core\event\base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_recyclebin_category';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventitemdeleted', 'tool_recyclebin');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return get_string('eventitemdeleted_desc', 'tool_recyclebin', array(
+            'objectid' => $this->objectid
+        ));
+    }
+}
diff --git a/admin/tool/recyclebin/classes/event/category_bin_item_restored.php b/admin/tool/recyclebin/classes/event/category_bin_item_restored.php
new file mode 100644 (file)
index 0000000..d19aabd
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin events.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_recyclebin\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event Class
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class category_bin_item_restored extends \core\event\base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_recyclebin_category';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventitemrestored', 'tool_recyclebin');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return get_string('eventitemrestored_desc', 'tool_recyclebin', array(
+            'objectid' => $this->objectid
+        ));
+    }
+}
diff --git a/admin/tool/recyclebin/classes/event/course_bin_item_created.php b/admin/tool/recyclebin/classes/event/course_bin_item_created.php
new file mode 100644 (file)
index 0000000..ccf2a96
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin events.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_recyclebin\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event class.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_bin_item_created extends \core\event\base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_recyclebin_course';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventitemcreated', 'tool_recyclebin');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return get_string('eventitemcreated_desc', 'tool_recyclebin', array(
+            'objectid' => $this->objectid
+        ));
+    }
+}
diff --git a/admin/tool/recyclebin/classes/event/course_bin_item_deleted.php b/admin/tool/recyclebin/classes/event/course_bin_item_deleted.php
new file mode 100644 (file)
index 0000000..0f3d72d
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin events.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_recyclebin\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event class.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_bin_item_deleted extends \core\event\base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_recyclebin_course';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventitemdeleted', 'tool_recyclebin');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return get_string('eventitemdeleted_desc', 'tool_recyclebin', array(
+            'objectid' => $this->objectid
+        ));
+    }
+}
diff --git a/admin/tool/recyclebin/classes/event/course_bin_item_restored.php b/admin/tool/recyclebin/classes/event/course_bin_item_restored.php
new file mode 100644 (file)
index 0000000..e7c8045
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin events.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_recyclebin\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event class.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_bin_item_restored extends \core\event\base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'tool_recyclebin_course';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventitemrestored', 'tool_recyclebin');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return get_string('eventitemrestored_desc', 'tool_recyclebin', array(
+            'objectid' => $this->objectid
+        ));
+    }
+}
diff --git a/admin/tool/recyclebin/classes/task/cleanup_category_bin.php b/admin/tool/recyclebin/classes/task/cleanup_category_bin.php
new file mode 100644 (file)
index 0000000..16c8985
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin cron task.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_recyclebin\task;
+
+/**
+ * This task deletes expired category recyclebin items.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cleanup_category_bin extends \core\task\scheduled_task {
+
+    /**
+     * Task name.
+     */
+    public function get_name() {
+        return get_string('taskcleanupcategorybin', 'tool_recyclebin');
+    }
+
+    /**
+     * Delete all expired items.
+     */
+    public function execute() {
+        global $DB;
+
+        // Check if the category bin is disabled or there is no expiry time.
+        $lifetime = get_config('tool_recyclebin', 'categorybinexpiry');
+        if (!\tool_recyclebin\category_bin::is_enabled() || $lifetime <= 0) {
+            return true;
+        }
+
+        // Get the items we can delete.
+        $items = $DB->get_recordset_select('tool_recyclebin_category', 'timecreated <= :timecreated',
+            array('timecreated' => time() - $lifetime));
+        foreach ($items as $item) {
+            mtrace("[tool_recyclebin] Deleting item '{$item->id}' from the category recycle bin ...");
+            $bin = new \tool_recyclebin\category_bin($item->categoryid);
+            $bin->delete_item($item);
+        }
+        $items->close();
+
+        return true;
+    }
+}
diff --git a/admin/tool/recyclebin/classes/task/cleanup_course_bin.php b/admin/tool/recyclebin/classes/task/cleanup_course_bin.php
new file mode 100644 (file)
index 0000000..f5a4aac
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin cron task.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_recyclebin\task;
+
+/**
+ * This task deletes expired course recyclebin items.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cleanup_course_bin extends \core\task\scheduled_task {
+
+    /**
+     * Task name.
+     */
+    public function get_name() {
+        return get_string('taskcleanupcoursebin', 'tool_recyclebin');
+    }
+
+    /**
+     * Delete all expired items.
+     */
+    public function execute() {
+        global $DB;
+
+        // Check if the course bin is disabled or there is no expiry time.
+        $lifetime = get_config('tool_recyclebin', 'coursebinexpiry');
+        if (!\tool_recyclebin\course_bin::is_enabled() || $lifetime <= 0) {
+            return true;
+        }
+
+        // Get the items we can delete.
+        $items = $DB->get_recordset_select('tool_recyclebin_course', 'timecreated <= :timecreated',
+            array('timecreated' => time() - $lifetime));
+        foreach ($items as $item) {
+            mtrace("[tool_recyclebin] Deleting item '{$item->id}' from the course recycle bin ...");
+            $bin = new \tool_recyclebin\course_bin($item->courseid);
+            $bin->delete_item($item);
+        }
+        $items->close();
+
+        return true;
+    }
+}
diff --git a/admin/tool/recyclebin/db/access.php b/admin/tool/recyclebin/db/access.php
new file mode 100644 (file)
index 0000000..b300bfb
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Plugin capabilities.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+
+    'tool/recyclebin:deleteitems' => array(
+        'captype' => 'write',
+        'riskbitmask' => RISK_DATALOSS,
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'editingteacher' => CAP_ALLOW,
+            'manager' => CAP_ALLOW
+        )
+    ),
+
+    'tool/recyclebin:restoreitems' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'editingteacher' => CAP_ALLOW,
+            'manager' => CAP_ALLOW
+        )
+    ),
+
+    'tool/recyclebin:viewitems' => array(
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'teacher' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+            'manager' => CAP_ALLOW
+        )
+    )
+);
diff --git a/admin/tool/recyclebin/db/install.xml b/admin/tool/recyclebin/db/install.xml
new file mode 100644 (file)
index 0000000..27c32d8
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<XMLDB PATH="tool/recyclebin/db" VERSION="20160315" COMMENT="XMLDB file for Moodle tool/recyclebin"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
+>
+  <TABLES>
+    <TABLE NAME="tool_recyclebin_course" COMMENT="A list of items in the course recycle bin">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="section" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="module" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="courseid" TYPE="foreign" FIELDS="courseid" REFTABLE="course" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="courseid" UNIQUE="false" FIELDS="courseid"/>
+        <INDEX NAME="timecreated" UNIQUE="false" FIELDS="timecreated"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="tool_recyclebin_category" COMMENT="A list of items in the category recycle bin">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="categoryid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="shortname" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="fullname" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="categoryid" TYPE="foreign" FIELDS="categoryid" REFTABLE="course_categories" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="categoryid" UNIQUE="false" FIELDS="categoryid"/>
+        <INDEX NAME="timecreated" UNIQUE="false" FIELDS="timecreated"/>
+      </INDEXES>
+    </TABLE>
+  </TABLES>
+</XMLDB>
\ No newline at end of file
diff --git a/admin/tool/recyclebin/db/tasks.php b/admin/tool/recyclebin/db/tasks.php
new file mode 100644 (file)
index 0000000..4036e27
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin tasks.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$tasks = array(
+    array(
+        'classname' => 'tool_recyclebin\task\cleanup_course_bin',
+        'blocking' => 0,
+        'minute' => '*/30',
+        'hour' => '*',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    ),
+    array(
+        'classname' => 'tool_recyclebin\task\cleanup_category_bin',
+        'blocking' => 0,
+        'minute' => '*/30',
+        'hour' => '*',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    )
+);
diff --git a/admin/tool/recyclebin/index.php b/admin/tool/recyclebin/index.php
new file mode 100644 (file)
index 0000000..c509b27
--- /dev/null
@@ -0,0 +1,254 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This page shows the contents of a recyclebin for a given course.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(__FILE__) . '/../../../config.php');
+require_once($CFG->libdir . '/tablelib.php');
+
+$contextid = required_param('contextid', PARAM_INT);
+$action = optional_param('action', null, PARAM_ALPHA);
+
+$context = context::instance_by_id($contextid, MUST_EXIST);
+$PAGE->set_context($context);
+
+// We could be a course or a category.
+switch ($context->contextlevel) {
+    case CONTEXT_COURSE:
+        require_login($context->instanceid);
+
+        $recyclebin = new \tool_recyclebin\course_bin($context->instanceid);
+        if (!$recyclebin->can_view()) {
+            throw new required_capability_exception($context, 'tool/recyclebin:viewitems', 'nopermissions', '');
+        }
+
+        $PAGE->set_pagelayout('incourse');
+        // Set the $PAGE heading - this is also the same as the h2 heading.
+        $heading = format_string($COURSE->fullname, true, array('context' => $context)) . ': ' .
+            get_string('pluginname', 'tool_recyclebin');
+        $PAGE->set_heading($heading);
+
+        // Get the expiry to use later.
+        $expiry = get_config('tool_recyclebin', 'coursebinexpiry');
+    break;
+
+    case CONTEXT_COURSECAT:
+        require_login();
+
+        $recyclebin = new \tool_recyclebin\category_bin($context->instanceid);
+        if (!$recyclebin->can_view()) {
+            throw new required_capability_exception($context, 'tool/recyclebin:viewitems', 'nopermissions', '');
+        }
+
+        $PAGE->set_pagelayout('admin');
+        // Set the $PAGE heading.
+        $PAGE->set_heading($COURSE->fullname);
+        // The h2 heading on the page is going to be different than the $PAGE heading.
+        $heading = $context->get_context_name() . ': ' . get_string('pluginname', 'tool_recyclebin');
+
+        // Get the expiry to use later.
+        $expiry = get_config('tool_recyclebin', 'categorybinexpiry');
+    break;
+
+    default:
+        print_error('invalidcontext', 'tool_recyclebin');
+    break;
+}
+
+if (!$recyclebin::is_enabled()) {
+    print_error('notenabled', 'tool_recyclebin');
+}
+
+$PAGE->set_url('/admin/tool/recyclebin/index.php', array(
+    'contextid' => $contextid
+));
+$PAGE->set_title(get_string('pluginname', 'tool_recyclebin'));
+
+// If we are doing anything, we need a sesskey!
+if (!empty($action)) {
+    raise_memory_limit(MEMORY_EXTRA);
+    require_sesskey();
+
+    $item = null;
+    if ($action == 'restore' || $action == 'delete') {
+        $itemid = required_param('itemid', PARAM_INT);
+        $item = $recyclebin->get_item($itemid);
+    }
+
+    switch ($action) {
+        // Restore it.
+        case 'restore':
+            if ($recyclebin->can_restore()) {
+                $recyclebin->restore_item($item);
+                redirect($PAGE->url, get_string('alertrestored', 'tool_recyclebin', $item), 2);
+            } else {
+                print_error('nopermissions', 'error');
+            }
+        break;
+
+        // Delete it.
+        case 'delete':
+            if ($recyclebin->can_delete()) {
+                $recyclebin->delete_item($item);
+                redirect($PAGE->url, get_string('alertdeleted', 'tool_recyclebin', $item), 2);
+            } else {
+                print_error('nopermissions', 'error');
+            }
+        break;
+
+        // Empty it.
+        case 'empty':
+            $recyclebin->delete_all_items();
+            redirect($PAGE->url, get_string('alertemptied', 'tool_recyclebin'), 2);
+        break;
+    }
+}
+
+// Add a "Go Back" button.
+$goback = html_writer::start_tag('div', array('class' => 'backlink'));
+$goback .= html_writer::link($context->get_url(), get_string('backto', '', $context->get_context_name()));
+$goback .= html_writer::end_tag('div');
+
+// Output header.
+echo $OUTPUT->header();
+echo $OUTPUT->heading($heading);
+
+// Grab our items, check there is actually something to display.
+$items = $recyclebin->get_items();
+
+// Nothing to show? Bail out early.
+if (empty($items)) {
+    echo $OUTPUT->box(get_string('noitemsinbin', 'tool_recyclebin'));
+    echo $goback;
+    echo $OUTPUT->footer();
+    die;
+}
+
+// Start with a description.
+if ($expiry > 0) {
+    $expirydisplay = format_time($expiry);
+    echo '<div class=\'alert\'>' . get_string('deleteexpirywarning', 'tool_recyclebin', $expirydisplay) . '</div>';
+}
+
+// Define columns and headers.
+$firstcolstr = $context->contextlevel == CONTEXT_COURSE ? 'activity' : 'course';
+$columns = array($firstcolstr, 'date', 'restore', 'delete');
+$headers = array(
+    get_string($firstcolstr),
+    get_string('datedeleted', 'tool_recyclebin'),
+    get_string('restore'),
+    get_string('delete')
+);
+
+// Define a table.
+$table = new flexible_table('recyclebin');
+$table->define_columns($columns);
+$table->column_style('restore', 'text-align', 'center');
+$table->column_style('delete', 'text-align', 'center');
+$table->define_headers($headers);
+$table->define_baseurl($PAGE->url);
+$table->set_attribute('id', 'recycle-bin-table');
+$table->setup();
+
+// Cache a list of modules.
+$modules = null;
+if ($context->contextlevel == CONTEXT_COURSE) {
+    $modules = $DB->get_records('modules');
+}
+
+// Add all the items to the table.
+$showempty = false;
+$canrestore = $recyclebin->can_restore();
+foreach ($items as $item) {
+    $row = array();
+
+    // Build item name.
+    $name = $item->name;
+    if ($context->contextlevel == CONTEXT_COURSE) {
+        if (isset($modules[$item->module])) {
+            $mod = $modules[$item->module];
+            $modname = get_string('modulename', $mod->name);
+            $name = '<img src="' . $OUTPUT->pix_url('icon', $mod->name) . '" class="icon" alt="' . $modname . '" /> ' . $name;
+        }
+    }
+
+    $row[] = $name;
+    $row[] = userdate($item->timecreated);
+
+    // Build restore link.
+    if ($canrestore && ($context->contextlevel == CONTEXT_COURSECAT || isset($modules[$item->module]))) {
+        $restoreurl = new moodle_url($PAGE->url, array(
+            'contextid' => $contextid,
+            'itemid' => $item->id,
+            'action' => 'restore',
+            'sesskey' => sesskey()
+        ));
+        $row[] = $OUTPUT->action_icon($restoreurl, new pix_icon('t/restore', get_string('restore'), '', array(
+            'class' => 'iconsmall'
+        )));
+    } else {
+        // Show padlock.
+        $row[] = $OUTPUT->pix_icon('t/locked', get_string('locked', 'admin'), '', array('class' => 'iconsmall'));
+    }
+
+    // Build delete link.
+    if ($recyclebin->can_delete()) {
+        $showempty = true;
+        $delete = new moodle_url($PAGE->url, array(
+            'contextid' => $contextid,
+            'itemid' => $item->id,
+            'action' => 'delete',
+            'sesskey' => sesskey()
+        ));
+        $deleteaction = new confirm_action(get_string('deleteconfirm', 'tool_recyclebin'));
+        $delete = $OUTPUT->action_icon($delete, new pix_icon('t/delete', get_string('delete')), $deleteaction);
+
+        $row[] = $delete;
+    } else {
+        // Show padlock.
+        $row[] = $OUTPUT->pix_icon('t/locked', get_string('locked', 'admin'), '', array('class' => 'iconsmall'));
+    }
+
+    $table->add_data($row);
+}
+
+// Display the table now.
+$table->finish_output();
+
+// Empty recyclebin link.
+if ($showempty) {
+    $emptylink = new moodle_url($PAGE->url, array(
+        'contextid' => $contextid,
+        'action' => 'empty',
+        'sesskey' => sesskey()
+    ));
+    $emptyaction = new confirm_action(get_string('deleteallconfirm', 'tool_recyclebin'));
+    echo $OUTPUT->action_link($emptylink, get_string('deleteall', 'tool_recyclebin'), $emptyaction);
+}
+
+echo $goback;
+
+// Confirmation JS.
+$PAGE->requires->strings_for_js(array('deleteallconfirm', 'deleteconfirm'), 'tool_recyclebin');
+
+// Output footer.
+echo $OUTPUT->footer();
diff --git a/admin/tool/recyclebin/lang/en/tool_recyclebin.php b/admin/tool/recyclebin/lang/en/tool_recyclebin.php
new file mode 100644 (file)
index 0000000..87c0185
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * English strings for tool_recyclebin.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 Skylar Kelty <S.Kelty@kent.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['alertdeleted'] = '\'{$a->name}\' has been deleted.';
+$string['alertemptied'] = 'Recycle bin has been emptied.';
+$string['alertrestored'] = '\'{$a->name}\' has been restored.';
+$string['autohide'] = 'Auto hide';
+$string['autohide_desc'] = 'Automatically hides the recycle bin link when the bin is empty.';
+$string['categorybinenable'] = 'Enable category recycle bin';
+$string['categorybinexpiry'] = 'Course lifetime';
+$string['categorybinexpiry_desc'] = 'How long should a deleted course remain in the recycle bin?';
+$string['coursebinenable'] = 'Enable course recycle bin';
+$string['coursebinexpiry'] = 'Item lifetime';
+$string['coursebinexpiry_desc'] = 'How long should a deleted item remain in the recycle bin?';
+$string['datedeleted'] = 'Date deleted';
+$string['deleteall'] = 'Delete all';
+$string['deleteallconfirm'] = 'Are you sure you want to delete all items from the recycle bin?';
+$string['deleteconfirm'] = 'Are you sure you want to delete the selected item from the recycle bin?';
+$string['deleteexpirywarning'] = 'Contents will be permanently deleted after {$a}.';
+$string['eventitemcreated'] = 'Item created';
+$string['eventitemcreated_desc'] = 'Item created with ID {$a->objectid}.';
+$string['eventitemdeleted'] = 'Item deleted';
+$string['eventitemdeleted_desc'] = 'Item with ID {$a->objectid} deleted.';
+$string['eventitemrestored'] = 'Item restored';
+$string['eventitemrestored_desc'] = 'Item with ID {$a->objectid} restored.';
+$string['invalidcontext'] = 'Invalid context supplied.';
+$string['neverdelete'] = 'Never delete recycled items';
+$string['noitemsinbin'] = 'There are no items in the recycle bin.';
+$string['notenabled'] = 'Sorry, but the recycle bin has been disabled by the administrator.';
+$string['pluginname'] = 'Recycle bin';
+$string['taskcleanupcategorybin'] = 'Cleanup category recycle bin';
+$string['taskcleanupcoursebin'] = 'Cleanup course recycle bin';
+$string['recyclebin:deleteitems'] = 'Delete recycle bin items';
+$string['recyclebin:restoreitems'] = 'Restore recycle bin items';
+$string['recyclebin:viewitems'] = 'View recycle bin items';
diff --git a/admin/tool/recyclebin/lib.php b/admin/tool/recyclebin/lib.php
new file mode 100644 (file)
index 0000000..39d451b
--- /dev/null
@@ -0,0 +1,182 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Local lib code
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 Skylar Kelty <S.Kelty@kent.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Adds a recycle bin link to the course admin menu.
+ *
+ * @param navigation_node $navigation The navigation node to extend
+ * @param stdClass $course The course to object for the tool
+ * @param context $context The context of the course
+ * @return void|null return null if we don't want to display the node.
+ */
+function tool_recyclebin_extend_navigation_course($navigation, $course, $context) {
+    global $PAGE;
+
+    // Only add this settings item on non-site course pages.
+    if (!$PAGE->course || $PAGE->course->id == SITEID || !\tool_recyclebin\course_bin::is_enabled()) {
+        return null;
+    }
+
+    $coursebin = new \tool_recyclebin\course_bin($context->instanceid);
+
+    // Check we can view the recycle bin.
+    if (!$coursebin->can_view()) {
+        return null;
+    }
+
+    $url = null;
+    $settingnode = null;
+
+    $url = new moodle_url('/admin/tool/recyclebin/index.php', array(
+        'contextid' => $context->id
+    ));
+
+    // If we are set to auto-hide, check the number of items.
+    $autohide = get_config('tool_recyclebin', 'autohide');
+    if ($autohide) {
+        $items = $coursebin->get_items();
+        if (empty($items)) {
+            return null;
+        }
+    }
+
+    // Add the recyclebin link.
+    $pluginname = get_string('pluginname', 'tool_recyclebin');
+
+    $node = navigation_node::create(
+        $pluginname,
+        $url,
+        navigation_node::NODETYPE_LEAF,
+        'tool_recyclebin',
+        'tool_recyclebin',
+        new pix_icon('trash', $pluginname, 'tool_recyclebin')
+    );
+
+    if ($PAGE->url->compare($url, URL_MATCH_BASE)) {
+        $node->make_active();
+    }
+
+    $navigation->add_node($node);
+}
+
+/**
+ * Adds a recycle bin link to the course admin menu.
+ *
+ * @param navigation_node $navigation The navigation node to extend
+ * @param context $context The context of the course
+ * @return void|null return null if we don't want to display the node.
+ */
+function tool_recyclebin_extend_navigation_category_settings($navigation, $context) {
+    global $PAGE;
+
+    // Check if it is enabled.
+    if (!\tool_recyclebin\category_bin::is_enabled()) {
+        return null;
+    }
+
+    $categorybin = new \tool_recyclebin\category_bin($context->instanceid);
+
+    // Check we can view the recycle bin.
+    if (!$categorybin->can_view()) {
+        return null;
+    }
+
+    $url = null;
+    $settingnode = null;
+
+    // Add a link to the category recyclebin.
+    $url = new moodle_url('/admin/tool/recyclebin/index.php', array(
+        'contextid' => $context->id
+    ));
+
+    // If we are set to auto-hide, check the number of items.
+    $autohide = get_config('tool_recyclebin', 'autohide');
+    if ($autohide) {
+        $items = $categorybin->get_items();
+        if (empty($items)) {
+            return null;
+        }
+    }
+
+    // Add the recyclebin link.
+    $pluginname = get_string('pluginname', 'tool_recyclebin');
+
+    $node = navigation_node::create(
+        $pluginname,
+        $url,
+        navigation_node::NODETYPE_LEAF,
+        'tool_recyclebin',
+        'tool_recyclebin',
+        new pix_icon('trash', $pluginname, 'tool_recyclebin')
+    );
+
+    if ($PAGE->url->compare($url, URL_MATCH_BASE)) {
+        $node->make_active();
+    }
+
+    $navigation->add_node($node);
+}
+
+/**
+ * Hook called before we delete a course module.
+ *
+ * @param \stdClass $cm The course module record.
+ */
+function tool_recyclebin_pre_course_module_delete($cm) {
+    if (\tool_recyclebin\course_bin::is_enabled()) {
+        $coursebin = new \tool_recyclebin\course_bin($cm->course);
+        $coursebin->store_item($cm);
+    }
+}
+
+/**
+ * Hook called before we delete a course.
+ *
+ * @param \stdClass $course The course record.
+ */
+function tool_recyclebin_pre_course_delete($course) {
+    // Delete all the items in the course recycle bin, regardless if it enabled or not.
+    // It may have been enabled, then disabled later on, so may still have content.
+    $coursebin = new \tool_recyclebin\course_bin($course->id);
+    $coursebin->delete_all_items();
+
+    if (\tool_recyclebin\category_bin::is_enabled()) {
+        $categorybin = new \tool_recyclebin\category_bin($course->category);
+        $categorybin->store_item($course);
+    }
+}
+
+/**
+ * Hook called before we delete a category.
+ *
+ * @param \stdClass $category The category record.
+ */
+function tool_recyclebin_pre_course_category_delete($category) {
+    // Delete all the items in the category recycle bin, regardless if it enabled or not.
+    // It may have been enabled, then disabled later on, so may still have content.
+    $categorybin = new \tool_recyclebin\category_bin($category->id);
+    $categorybin->delete_all_items();
+}
diff --git a/admin/tool/recyclebin/pix/trash.png b/admin/tool/recyclebin/pix/trash.png
new file mode 100644 (file)
index 0000000..fc6aa0c
Binary files /dev/null and b/admin/tool/recyclebin/pix/trash.png differ
diff --git a/admin/tool/recyclebin/pix/trash.svg b/admin/tool/recyclebin/pix/trash.svg
new file mode 100644 (file)
index 0000000..a772952
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->\r
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]>\r
+<svg version="1.1"\r
+        xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"\r
+        x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"\r
+        xml:space="preserve" preserveAspectRatio="xMinYMid meet">\r
+<defs>\r
+</defs>\r
+<path style="fill:#999999;" d="M2,5v9c0,1.1,0.9,2,2,2h8c1.1,0,2-0.9,2-2V5H2z M5,13.5C5,13.8,4.8,14,4.5,14S4,13.8,4,13.5v-6\r
+       C4,7.2,4.2,7,4.5,7S5,7.2,5,7.5V13.5z M8.5,13.5C8.5,13.8,8.3,14,8,14s-0.5-0.2-0.5-0.5v-6C7.5,7.2,7.7,7,8,7s0.5,0.2,0.5,0.5V13.5z\r
+        M12,13.5c0,0.3-0.2,0.5-0.5,0.5S11,13.8,11,13.5v-6C11,7.2,11.2,7,11.5,7S12,7.2,12,7.5V13.5z M0,4c0-1.1,0.9-2,2-2h4v0\r
+       c0-1.1,0.9-2,2-2s2,0.9,2,2v0h4c1.1,0,2,0.9,2,2H0z"/>\r
+</svg>\r
diff --git a/admin/tool/recyclebin/settings.php b/admin/tool/recyclebin/settings.php
new file mode 100644 (file)
index 0000000..da19ea7
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin settings.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $PAGE;
+
+if ($hassiteconfig) {
+    $settings = new admin_settingpage('tool_recyclebin', get_string('pluginname', 'tool_recyclebin'));
+    $ADMIN->add('tools', $settings);
+
+    $settings->add(new admin_setting_configcheckbox(
+        'tool_recyclebin/coursebinenable',
+        new lang_string('coursebinenable', 'tool_recyclebin'),
+        '',
+        1
+    ));
+
+    $settings->add(new admin_setting_configduration(
+        'tool_recyclebin/coursebinexpiry',
+        new lang_string('coursebinexpiry', 'tool_recyclebin'),
+        new lang_string('coursebinexpiry_desc', 'tool_recyclebin'),
+        WEEKSECS
+    ));
+
+    $settings->add(new admin_setting_configcheckbox(
+        'tool_recyclebin/categorybinenable',
+        new lang_string('categorybinenable', 'tool_recyclebin'),
+        '',
+        1
+    ));
+
+    $settings->add(new admin_setting_configduration(
+        'tool_recyclebin/categorybinexpiry',
+        new lang_string('categorybinexpiry', 'tool_recyclebin'),
+        new lang_string('categorybinexpiry_desc', 'tool_recyclebin'),
+        WEEKSECS
+    ));
+
+    $settings->add(new admin_setting_configcheckbox(
+        'tool_recyclebin/autohide',
+        new lang_string('autohide', 'tool_recyclebin'),
+        new lang_string('autohide_desc', 'tool_recyclebin'),
+        1
+    ));
+}
diff --git a/admin/tool/recyclebin/tests/behat/backup_user_data.feature b/admin/tool/recyclebin/tests/behat/backup_user_data.feature
new file mode 100644 (file)
index 0000000..7c20c3e
--- /dev/null
@@ -0,0 +1,69 @@
+@tool @tool_recyclebin
+Feature: Backup user data
+  As a teacher
+  I want user data to be backed up when I delete a course module
+  So that I can recover student content
+
+  Background: Course with teacher and student exist.
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher@asd.com |
+      | student1 | Student | 1 | student@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following config values are set as admin:
+      | coursebinenable | 1 | tool_recyclebin |
+      | autohide | 0 | tool_recyclebin |
+
+  @javascript
+  Scenario: Delete and restore a quiz with user data
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Quiz" to section "1" and I fill the form with:
+      | Name        | Quiz 1                |
+      | Description | Test quiz description |
+    And I add a "True/False" question to the "Quiz 1" quiz with:
+      | Question name                      | TF1                          |
+      | Question text                      | First question               |
+      | General feedback                   | Thank you, this is the general feedback |
+      | Correct answer                     | False                                   |
+      | Feedback for the response 'True'.  | So you think it is true                 |
+      | Feedback for the response 'False'. | So you think it is false                |
+    And I add a "True/False" question to the "Quiz 1" quiz with:
+      | Question name                      | TF2                                     |
+      | Question text                      | Second question                         |
+      | General feedback                   | Thank you, this is the general feedback |
+      | Correct answer                     | False                                   |
+      | Feedback for the response 'True'.  | So you think it is true                 |
+      | Feedback for the response 'False'. | So you think it is false                |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    And I click on "True" "radio" in the "First question" "question"
+    And I click on "False" "radio" in the "Second question" "question"
+    And I press "Finish attempt"
+    And I press "Submit all and finish"
+    And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
+    And I should see "5.00 out of 10.00"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I delete "Quiz 1" activity
+    And I follow "Recycle bin"
+    And I should see "Quiz 1"
+    And I follow "Restore"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    When I navigate to "Grades" node in "Course administration"
+    Then "Quiz 1" row "Grade" column of "user-grade" table should contain "5"
+    And "Quiz 1" row "Percentage" column of "user-grade" table should contain "50"
diff --git a/admin/tool/recyclebin/tests/behat/basic_functionality.feature b/admin/tool/recyclebin/tests/behat/basic_functionality.feature
new file mode 100644 (file)
index 0000000..85e5d57
--- /dev/null
@@ -0,0 +1,106 @@
+@tool @tool_recyclebin
+Feature: Basic recycle bin functionality
+  As a teacher
+  I want be able to recover deleted content and manage the recycle bin content
+  So that I can fix an accidental deletion and clean the recycle bin
+
+  Background: Course with teacher exists.
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher@asd.com |
+      | student1 | Student | 1 | student@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1 |
+      | Course 2 | C2 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And the following config values are set as admin:
+      | coursebinenable | 1 | tool_recyclebin |
+      | categorybinenable | 1 | tool_recyclebin |
+      | coursebinexpiry | 604800 | tool_recyclebin |
+      | categorybinexpiry | 1209600 | tool_recyclebin |
+      | autohide | 0 | tool_recyclebin |
+
+  Scenario: Restore a deleted assignment
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assign |
+      | Description | Test |
+    And I delete "Test assign" activity
+    When I follow "Recycle bin"
+    Then I should see "Test assign"
+    And I should see "Contents will be permanently deleted after 7 days"
+    And I follow "Restore"
+    And I should see "'Test assign' has been restored"
+    And I wait to be redirected
+    And I am on homepage
+    And I follow "Course 1"
+    And I should see "Test assign" in the "Topic 1" "section"
+
+  Scenario: Restore a deleted course
+    Given I log in as "admin"
+    And I go to the courses management page
+    And I click on "delete" action for "Course 2" in management course listing
+    And I press "Delete"
+    And I should see "Deleting C2"
+    And I should see "C2 has been completely deleted"
+    And I press "Continue"
+    And I go to the courses management page
+    And I should not see "Course 2" in the "#course-listing" "css_element"
+    When I follow "Recycle bin"
+    Then I should see "Course 2"
+    And I should see "Contents will be permanently deleted after 14 days"
+    And I follow "Restore"
+    And I should see "'Course 2' has been restored"
+    And I wait to be redirected
+    And I go to the courses management page
+    And I should see "Course 2" in the "#course-listing" "css_element"
+
+  @javascript
+  Scenario: Deleting a single item from the recycle bin
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assign |
+      | Description | Test |
+    And I delete "Test assign" activity
+    And I follow "Recycle bin"
+    When I click on "Delete" "link"
+    Then I should see "Are you sure you want to delete the selected item from the recycle bin?"
+    And I press "Cancel"
+    And I should see "Test assign"
+    And I click on "Delete" "link"
+    And I press "Yes"
+    And I should see "'Test assign' has been deleted"
+    And I should see "There are no items in the recycle bin."
+
+  @javascript
+  Scenario: Deleting all the items from the recycle bin
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assign 1 |
+      | Description | Test 1 |
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assign 2 |
+      | Description | Test 2 |
+    And I delete "Test assign 1" activity
+    And I delete "Test assign 2" activity
+    And I follow "Recycle bin"
+    And I should see "Test assign 1"
+    And I should see "Test assign 2"
+    When I click on "Delete all" "link"
+    Then I should see "Are you sure you want to delete all items from the recycle bin?"
+    And I press "Cancel"
+    And I should see "Test assign 1"
+    And I should see "Test assign 2"
+    And I click on "Delete all" "link"
+    And I press "Yes"
+    And I should see "Recycle bin has been emptied"
+    And I should see "There are no items in the recycle bin."
diff --git a/admin/tool/recyclebin/tests/category_bin_test.php b/admin/tool/recyclebin/tests/category_bin_test.php
new file mode 100644 (file)
index 0000000..5e3f85d
--- /dev/null
@@ -0,0 +1,168 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin tests.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Recycle bin category tests.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_recyclebin_category_bin_tests extends advanced_testcase {
+
+    /**
+     * @var stdClass $course
+     */
+    protected $course;
+
+    /**
+     * Setup for each test.
+     */
+    protected function setUp() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // We want the category bin to be enabled.
+        set_config('categorybinenable', 1, 'tool_recyclebin');
+
+        $this->course = $this->getDataGenerator()->create_course();
+    }
+
+    /**
+     * Check that our hook is called when a course is deleted.
+     */
+    public function test_pre_course_delete_hook() {
+        global $DB;
+
+        // Should have nothing in the recycle bin.
+        $this->assertEquals(0, $DB->count_records('tool_recyclebin_category'));
+
+        delete_course($this->course, false);
+
+        // Check the course is now in the recycle bin.
+        $this->assertEquals(1, $DB->count_records('tool_recyclebin_category'));
+
+        // Try with the API.
+        $recyclebin = new \tool_recyclebin\category_bin($this->course->category);
+        $this->assertEquals(1, count($recyclebin->get_items()));
+    }
+
+    /**
+     * Check that our hook is called when a course is deleted.
+     */
+    public function test_pre_course_category_delete_hook() {
+        global $DB;
+
+        // Should have nothing in the recycle bin.
+        $this->assertEquals(0, $DB->count_records('tool_recyclebin_category'));
+
+        delete_course($this->course, false);
+
+        // Check the course is now in the recycle bin.
+        $this->assertEquals(1, $DB->count_records('tool_recyclebin_category'));
+
+        // Now let's delete the course category.
+        $category = coursecat::get($this->course->category);
+        $category->delete_full(false);
+
+        // Check that the course was deleted from the category recycle bin.
+        $this->assertEquals(0, $DB->count_records('tool_recyclebin_category'));
+    }
+
+    /**
+     * Test that we can restore recycle bin items.
+     */
+    public function test_restore() {
+        global $DB;
+
+        delete_course($this->course, false);
+
+        $recyclebin = new \tool_recyclebin\category_bin($this->course->category);
+        foreach ($recyclebin->get_items() as $item) {
+            $recyclebin->restore_item($item);
+        }
+
+        // Check that it was restored and removed from the recycle bin.
+        $this->assertEquals(2, $DB->count_records('course')); // Site course and the course we restored.
+        $this->assertEquals(0, count($recyclebin->get_items()));
+    }
+
+    /**
+     * Test that we can delete recycle bin items.
+     */
+    public function test_delete() {
+        global $DB;
+
+        delete_course($this->course, false);
+
+        $recyclebin = new \tool_recyclebin\category_bin($this->course->category);
+        foreach ($recyclebin->get_items() as $item) {
+            $recyclebin->delete_item($item);
+        }
+
+        // Item was deleted, so no course was restored.
+        $this->assertEquals(1, $DB->count_records('course')); // Just the site course.
+        $this->assertEquals(0, count($recyclebin->get_items()));
+    }
+
+    /**
+     * Test the cleanup task.
+     */
+    public function test_cleanup_task() {
+        global $DB;
+
+        // Set the expiry to 1 week.
+        set_config('categorybinexpiry', WEEKSECS, 'tool_recyclebin');
+
+        delete_course($this->course, false);
+
+        $recyclebin = new \tool_recyclebin\category_bin($this->course->category);
+
+        // Set deleted date to the distant past.
+        foreach ($recyclebin->get_items() as $item) {
+            $item->timecreated = time() - WEEKSECS;
+            $DB->update_record('tool_recyclebin_category', $item);
+        }
+
+        // Create another course to delete.
+        $course = $this->getDataGenerator()->create_course();
+        delete_course($course, false);
+
+        // Should now be two courses in the recycle bin.
+        $this->assertEquals(2, count($recyclebin->get_items()));
+
+        // Execute cleanup task.
+        $this->expectOutputRegex("/\[tool_recyclebin\] Deleting item '\d+' from the category recycle bin/");
+        $task = new \tool_recyclebin\task\cleanup_category_bin();
+        $task->execute();
+
+        // Task should only have deleted the course where we updated the time.
+        $courses = $recyclebin->get_items();
+        $this->assertEquals(1, count($courses));
+        $course = reset($courses);
+        $this->assertEquals('Test course 2', $course->fullname);
+    }
+}
diff --git a/admin/tool/recyclebin/tests/course_bin_test.php b/admin/tool/recyclebin/tests/course_bin_test.php
new file mode 100644 (file)
index 0000000..edfa1a2
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Recycle bin tests.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Recycle bin course tests.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2015 University of Kent
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_recyclebin_course_bin_tests extends advanced_testcase {
+
+    /**
+     * @var stdClass $course
+     */
+    protected $course;
+
+    /**
+     * @var stdClass the quiz record
+     */
+    protected $quiz;
+
+    /**
+     * Setup for each test.
+     */
+    protected function setUp() {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        // We want the course bin to be enabled.
+        set_config('coursebinenable', 1, 'tool_recyclebin');
+
+        $this->course = $this->getDataGenerator()->create_course();
+        $this->quiz = $this->getDataGenerator()->get_plugin_generator('mod_quiz')->create_instance(array(
+            'course' => $this->course->id
+        ));
+    }
+
+    /**
+     * Check that our hook is called when an activity is deleted.
+     */
+    public function test_pre_course_module_delete_hook() {
+        global $DB;
+
+        // Should have nothing in the recycle bin.
+        $this->assertEquals(0, $DB->count_records('tool_recyclebin_course'));
+
+        // Delete the course module.
+        course_delete_module($this->quiz->cmid);
+
+        // Check the course module is now in the recycle bin.
+        $this->assertEquals(1, $DB->count_records('tool_recyclebin_course'));
+
+        // Try with the API.
+        $recyclebin = new \tool_recyclebin\course_bin($this->course->id);
+        $this->assertEquals(1, count($recyclebin->get_items()));
+    }
+
+    /**
+     * Test that we can restore recycle bin items.
+     */
+    public function test_restore() {
+        global $DB;
+
+        // Delete the course module.
+        course_delete_module($this->quiz->cmid);
+
+        // Try restoring.
+        $recyclebin = new \tool_recyclebin\course_bin($this->course->id);
+        foreach ($recyclebin->get_items() as $item) {
+            $recyclebin->restore_item($item);
+        }
+
+        // Check that it was restored and removed from the recycle bin.
+        $this->assertEquals(1, $DB->count_records('course_modules'));
+        $this->assertEquals(0, count($recyclebin->get_items()));
+    }
+
+    /**
+     * Test that we can delete recycle bin items.
+     */
+    public function test_delete() {
+        global $DB;
+
+        // Delete the course module.
+        course_delete_module($this->quiz->cmid);
+
+        // Try purging.
+        $recyclebin = new \tool_recyclebin\course_bin($this->course->id);
+        foreach ($recyclebin->get_items() as $item) {
+            $recyclebin->delete_item($item);
+        }
+
+        // Item was deleted, so no course module was restored.
+        $this->assertEquals(0, $DB->count_records('course_modules'));
+        $this->assertEquals(0, count($recyclebin->get_items()));
+    }
+
+    /**
+     * Test the cleanup task.
+     */
+    public function test_cleanup_task() {
+        global $DB;
+
+        set_config('coursebinexpiry', WEEKSECS, 'tool_recyclebin');
+
+        // Delete the quiz.
+        course_delete_module($this->quiz->cmid);
+
+        // Set deleted date to the distant past.
+        $recyclebin = new \tool_recyclebin\course_bin($this->course->id);
+        foreach ($recyclebin->get_items() as $item) {
+            $item->timecreated = time() - WEEKSECS;
+            $DB->update_record('tool_recyclebin_course', $item);
+        }
+
+        // Create another module we are going to delete, but not alter the time it was placed in the recycle bin.
+        $book = $this->getDataGenerator()->get_plugin_generator('mod_book')->create_instance(array(
+            'course' => $this->course->id));
+
+        course_delete_module($book->cmid);
+
+        // Should have 2 items now.
+        $this->assertEquals(2, count($recyclebin->get_items()));
+
+        // Execute cleanup task.
+        $this->expectOutputRegex("/\[tool_recyclebin\] Deleting item '\d+' from the course recycle bin/");
+        $task = new \tool_recyclebin\task\cleanup_course_bin();
+        $task->execute();
+
+        // Should only have the book as it was not due to be deleted.
+        $items = $recyclebin->get_items();
+        $this->assertEquals(1, count($items));
+        $deletedbook = reset($items);
+        $this->assertEquals($book->name, $deletedbook->name);
+    }
+}
diff --git a/admin/tool/recyclebin/tests/events_test.php b/admin/tool/recyclebin/tests/events_test.php
new file mode 100644 (file)
index 0000000..05d519b
--- /dev/null
@@ -0,0 +1,225 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Events tests.
+ *
+ * @package tool_recyclebin
+ * @category test
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Events tests class.
+ *
+ * @package tool_recyclebin
+ * @category test
+ * @copyright 2016 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_recyclebin_events_testcase extends advanced_testcase {
+
+    /**
+     * Test set up.
+     *
+     * This is executed before running any test in this file.
+     */
+    public function setUp() {
+        $this->resetAfterTest();
+
+        // We want the category and course bin to be enabled.
+        set_config('categorybinenable', 1, 'tool_recyclebin');
+        set_config('coursebinenable', 1, 'tool_recyclebin');
+    }
+
+    /**
+     * Test the category bin item created event.
+     */
+    public function test_category_bin_item_created() {
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        delete_course($course, false);
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Get the item from the recycle bin.
+        $rb = new \tool_recyclebin\category_bin($course->category);
+        $items = $rb->get_items();
+        $item = reset($items);
+
+        // Check that the event contains the expected values.
+        $this->assertInstanceOf('\tooL_recyclebin\event\category_bin_item_created', $event);
+        $this->assertEquals(context_coursecat::instance($course->category), $event->get_context());
+        $this->assertEquals($item->id, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * Test the category bin item deleted event.
+     */
+    public function test_category_bin_item_deleted() {
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Delete the course.
+        delete_course($course, false);
+
+        // Get the item from the recycle bin.
+        $rb = new \tool_recyclebin\category_bin($course->category);
+        $items = $rb->get_items();
+        $item = reset($items);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $rb->delete_item($item);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Check that the event contains the expected values.
+        $this->assertInstanceOf('\tooL_recyclebin\event\category_bin_item_deleted', $event);
+        $this->assertEquals(context_coursecat::instance($course->category), $event->get_context());
+        $this->assertEquals($item->id, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * Test the category bin item restored event.
+     */
+    public function test_category_bin_item_restored() {
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Delete the course.
+        delete_course($course, false);
+
+        // Get the item from the recycle bin.
+        $rb = new \tool_recyclebin\category_bin($course->category);
+        $items = $rb->get_items();
+        $item = reset($items);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $rb->restore_item($item);
+        $events = $sink->get_events();
+        $event = $events[6];
+
+        // Check that the event contains the expected values.
+        $this->assertInstanceOf('\tooL_recyclebin\event\category_bin_item_restored', $event);
+        $this->assertEquals(context_coursecat::instance($course->category), $event->get_context());
+        $this->assertEquals($item->id, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * Test the course bin item created event.
+     */
+    public function test_course_bin_item_created() {
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create the assignment.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $instance = $generator->create_instance(array('course' => $course->id));
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        course_delete_module($instance->cmid);
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Get the item from the recycle bin.
+        $rb = new \tool_recyclebin\course_bin($course->id);
+        $items = $rb->get_items();
+        $item = reset($items);
+
+        // Check that the event contains the expected values.
+        $this->assertInstanceOf('\tooL_recyclebin\event\course_bin_item_created', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals($item->id, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * Test the course bin item deleted event.
+     */
+    public function test_course_bin_item_deleted() {
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create the assignment.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $instance = $generator->create_instance(array('course' => $course->id));
+
+        // Delete the module.
+        course_delete_module($instance->cmid);
+
+        // Get the item from the recycle bin.
+        $rb = new \tool_recyclebin\course_bin($course->id);
+        $items = $rb->get_items();
+        $item = reset($items);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $rb->delete_item($item);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+
+        // Check that the event contains the expected values.
+        $this->assertInstanceOf('\tooL_recyclebin\event\course_bin_item_deleted', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals($item->id, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * Test the course bin item restored event.
+     */
+    public function test_course_bin_item_restored() {
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create the assignment.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $instance = $generator->create_instance(array('course' => $course->id));
+
+        course_delete_module($instance->cmid);
+
+        // Get the item from the recycle bin.
+        $rb = new \tool_recyclebin\course_bin($course->id);
+        $items = $rb->get_items();
+        $item = reset($items);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $rb->restore_item($item);
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event contains the expected values.
+        $this->assertInstanceOf('\tooL_recyclebin\event\course_bin_item_restored', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals($item->id, $event->objectid);
+        $this->assertEventContextNotUsed($event);
+    }
+}
diff --git a/admin/tool/recyclebin/version.php b/admin/tool/recyclebin/version.php
new file mode 100644 (file)
index 0000000..b711511
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Defines the version.
+ *
+ * @package    tool_recyclebin
+ * @copyright  2016 Skylar Kelty <S.Kelty@kent.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2016030200; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2015111000; // Requires this Moodle version.
+$plugin->component = 'tool_recyclebin'; // Full name of the plugin (used for diagnostics).
index 59b69e7..b3e96ab 100644 (file)
@@ -1696,6 +1696,15 @@ function course_delete_module($cmid) {
             "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
     }
 
+    // Allow plugins to use this course module before we completely delete it.
+    if ($pluginsfunction = get_plugins_with_function('pre_course_module_delete')) {
+        foreach ($pluginsfunction as $plugintype => $plugins) {
+            foreach ($plugins as $pluginfunction) {
+                $pluginfunction($cm);
+            }
+        }
+    }
+
     // Delete activity context questions and question categories.
     question_delete_activity($cm);
 
index 53c98f4..8a06424 100644 (file)
@@ -1645,6 +1645,8 @@ $string['selectagroup'] = 'Select a group';
 $string['selectaregion'] = 'Select a region';
 $string['selctauser'] = 'Select a user';
 $string['selectall'] = 'Select all';
+$string['selectallusersonpage'] = 'Select all users on this page';
+$string['selectalluserswithcount'] = 'Select all {$a} users';
 $string['selectamodule'] = 'Please select an activity module';
 $string['selectanoptions'] = 'Select an option';
 $string['selectdefault'] = 'Select default';
index 595cf3b..a111b82 100644 (file)
@@ -2041,6 +2041,15 @@ function blocks_name_allowed_in_format($name, $pageformat) {
 function blocks_delete_instance($instance, $nolongerused = false, $skipblockstables = false) {
     global $DB;
 
+    // Allow plugins to use this block before we completely delete it.
+    if ($pluginsfunction = get_plugins_with_function('pre_block_delete')) {
+        foreach ($pluginsfunction as $plugintype => $plugins) {
+            foreach ($plugins as $pluginfunction) {
+                $pluginfunction($instance);
+            }
+        }
+    }
+
     if ($block = block_instance($instance->blockname, $instance)) {
         $block->instance_delete();
     }
index 48e08f3..d454849 100644 (file)
@@ -1901,7 +1901,7 @@ class core_plugin_manager {
                 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang',
                 'dbtransfer', 'filetypes', 'generator', 'health', 'innodb', 'installaddon',
                 'langimport', 'log', 'messageinbound', 'multilangupgrade', 'monitor', 'phpunit', 'profiling',
-                'replace', 'spamcleaner', 'task', 'templatelibrary',
+                'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary',
                 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
             ),
 
index fb6a4ed..0c3afde 100644 (file)
@@ -1670,6 +1670,16 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         // Make sure we won't timeout when deleting a lot of courses.
         $settimeout = core_php_time_limit::raise();
 
+        // Allow plugins to use this category before we completely delete it.
+        if ($pluginsfunction = get_plugins_with_function('pre_course_category_delete')) {
+            $category = $this->get_db_record();
+            foreach ($pluginsfunction as $plugintype => $plugins) {
+                foreach ($plugins as $pluginfunction) {
+                    $pluginfunction($category);
+                }
+            }
+        }
+
         $deletedcourses = array();
 
         // Get children. Note, we don't want to use cache here because it would be rebuilt too often.
index e559769..4cbbefc 100644 (file)
@@ -610,7 +610,7 @@ M.util.init_block_hider = function(Y, config) {
                             .setAttrs({
                                 alt:        config.tooltipVisible,
                                 src:        this.get('iconVisible'),
-                                tabindex:   0,
+                                tabIndex:   0,
                                 'title':    config.tooltipVisible
                             });
                         hide.on('keypress', this.updateStateKey, this, true);
@@ -621,7 +621,7 @@ M.util.init_block_hider = function(Y, config) {
                             .setAttrs({
                                 alt:        config.tooltipHidden,
                                 src:        this.get('iconHidden'),
-                                tabindex:   0,
+                                tabIndex:   0,
                                 'title':    config.tooltipHidden
                             });
                         show.on('keypress', this.updateStateKey, this, false);
@@ -634,8 +634,10 @@ M.util.init_block_hider = function(Y, config) {
                     M.util.set_user_preference(this.get('preference'), hide);
                     if (hide) {
                         this.get('block').addClass('hidden');
+                        this.get('block').one('.block-hider-show').focus();
                     } else {
                         this.get('block').removeClass('hidden');
+                        this.get('block').one('.block-hider-hide').focus();
                     }
                 },
                 updateStateKey : function(e, hide) {
index 3df1368..820d4f2 100644 (file)
@@ -3928,6 +3928,15 @@ function delete_user(stdClass $user) {
         return false;
     }
 
+    // Allow plugins to use this user object before we completely delete it.
+    if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
+        foreach ($pluginsfunction as $plugintype => $plugins) {
+            foreach ($plugins as $pluginfunction) {
+                $pluginfunction($user);
+            }
+        }
+    }
+
     // Keep user record before updating it, as we have to pass this to user_deleted event.
     $olduser = clone $user;
 
@@ -4677,6 +4686,15 @@ function delete_course($courseorid, $showfeedback = true) {
         return false;
     }
 
+    // Allow plugins to use this course before we completely delete it.
+    if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
+        foreach ($pluginsfunction as $plugintype => $plugins) {
+            foreach ($plugins as $pluginfunction) {
+                $pluginfunction($course);
+            }
+        }
+    }
+
     // Make the course completely empty.
     remove_course_contents($courseid, $showfeedback);
 
index 77f3a21..3f5328b 100644 (file)
@@ -69,7 +69,7 @@ class core_event_grade_deleted_testcase extends advanced_testcase {
         $sink = $this->redirectEvents();
         course_delete_module($quiz->cmid);
         $events = $sink->get_events();
-        $event = reset($events);
+        $event = $events[1];
         $sink->close();
 
         // Check the event details are correct.
index 604aca1..282d7e9 100644 (file)
@@ -1429,6 +1429,9 @@ function message_print_search_results($frm, $showicontext=false, $currentuser=nu
                 // Load user-to record.
                 if ($message->useridto !== $USER->id) {
                     $userto = core_user::get_user($message->useridto);
+                    if ($userto === false) {
+                        $userto = core_user::get_noreply_user();
+                    }
                     $tocontact = (array_key_exists($message->useridto, $contacts) and
                                     ($contacts[$message->useridto]->blocked == 0) );
                     $toblocked = (array_key_exists($message->useridto, $contacts) and
@@ -1442,6 +1445,9 @@ function message_print_search_results($frm, $showicontext=false, $currentuser=nu
                 // Load user-from record.
                 if ($message->useridfrom !== $USER->id) {
                     $userfrom = core_user::get_user($message->useridfrom);
+                    if ($userfrom === false) {
+                        $userfrom = core_user::get_noreply_user();
+                    }
                     $fromcontact = (array_key_exists($message->useridfrom, $contacts) and
                                     ($contacts[$message->useridfrom]->blocked == 0) );
                     $fromblocked = (array_key_exists($message->useridfrom, $contacts) and
@@ -1548,10 +1554,11 @@ function message_print_user ($user=false, $iscontact=false, $isblocked=false, $i
         } else {
             message_contact_link($user->id, 'block', $return, $script, $includeicontext);
         }
-    } else { // If not real user, then don't show any links.
+    } else {
+        // If not real user, then don't show any links.
         $userpictureparams['link'] = false;
-        echo $OUTPUT->user_picture($USER, $userpictureparams);
-        echo fullname($user);
+        // Stock profile picture should be displayed.
+        echo $OUTPUT->user_picture($user, $userpictureparams);
     }
 }
 
index cadf5f8..d7dc5fd 100644 (file)
@@ -180,4 +180,14 @@ $functions = array(
             'capabilities'  => 'mod/assign:view',
             'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
         ),
+
+        'mod_assign_get_submission_status' => array(
+            'classname'     => 'mod_assign_external',
+            'methodname'    => 'get_submission_status',
+            'classpath'     => 'mod/assign/externallib.php',
+            'description'   => 'Returns information about an assignment submission status for a given user.',
+            'type'          => 'read',
+            'capabilities'  => 'mod/assign:view',
+            'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+        ),
 );
index 5c7d862..2f769e1 100644 (file)
@@ -26,6 +26,7 @@
 defined('MOODLE_INTERNAL') || die;
 
 require_once("$CFG->libdir/externallib.php");
+require_once("$CFG->dirroot/mod/assign/locallib.php");
 
 /**
  * Assign functions
@@ -194,6 +195,29 @@ class mod_assign_external extends external_api {
         return $result;
     }
 
+    /**
+     * Creates a grade single structure.
+     *
+     * @return external_single_structure a grade single structure.
+     * @since  Moodle 3.1
+     */
+    private static function get_grade_structure($required = VALUE_REQUIRED) {
+        return new external_single_structure(
+            array(
+                'id'                => new external_value(PARAM_INT, 'grade id'),
+                'assignment'        => new external_value(PARAM_INT, 'assignment id', VALUE_OPTIONAL),
+                'userid'            => new external_value(PARAM_INT, 'student id'),
+                'attemptnumber'     => new external_value(PARAM_INT, 'attempt number'),
+                'timecreated'       => new external_value(PARAM_INT, 'grade creation time'),
+                'timemodified'      => new external_value(PARAM_INT, 'grade last modified time'),
+                'grader'            => new external_value(PARAM_INT, 'grader'),
+                'grade'             => new external_value(PARAM_TEXT, 'grade'),
+                'gradefordisplay'   => new external_value(PARAM_RAW, 'grade rendered into a format suitable for display',
+                                                            VALUE_OPTIONAL),
+            ), 'grade information', $required
+        );
+    }
+
     /**
      * Creates an assign_grades external_single_structure
      * @return external_single_structure
@@ -202,19 +226,8 @@ class mod_assign_external extends external_api {
     private static function assign_grades() {
         return new external_single_structure(
             array (
-                'assignmentid'    => new external_value(PARAM_INT, 'assignment id'),
-                'grades'   => new external_multiple_structure(new external_single_structure(
-                        array(
-                            'id'            => new external_value(PARAM_INT, 'grade id'),
-                            'userid'        => new external_value(PARAM_INT, 'student id'),
-                            'attemptnumber' => new external_value(PARAM_INT, 'attempt number'),
-                            'timecreated'   => new external_value(PARAM_INT, 'grade creation time'),
-                            'timemodified'  => new external_value(PARAM_INT, 'grade last modified time'),
-                            'grader'        => new external_value(PARAM_INT, 'grader'),
-                            'grade'         => new external_value(PARAM_TEXT, 'grade')
-                        )
-                    )
-                )
+                'assignmentid'  => new external_value(PARAM_INT, 'assignment id'),
+                'grades'        => new external_multiple_structure(self::get_grade_structure())
             )
         );
     }
@@ -245,7 +258,7 @@ class mod_assign_external extends external_api {
         return new external_function_parameters(
             array(
                 'courseids' => new external_multiple_structure(
-                    new external_value(PARAM_INT, 'course id'),
+                    new external_value(PARAM_INT, 'course id, empty for retrieving all the courses where the user is enroled in'),
                     '0 or more course ids',
                     VALUE_DEFAULT, array()
                 ),
@@ -253,37 +266,56 @@ class mod_assign_external extends external_api {
                     new external_value(PARAM_CAPABILITY, 'capability'),
                     'list of capabilities used to filter courses',
                     VALUE_DEFAULT, array()
-                )
+                ),
+                'includenotenrolledcourses' => new external_value(PARAM_BOOL, 'whether to return courses that the user can see
+                                                                    even if is not enroled in. This requires the parameter courseids
+                                                                    to not be empty.', VALUE_DEFAULT, false)
             )
         );
     }
 
     /**
-     * Returns an array of courses the user is enrolled in, and for each course all of the assignments that the user can
+     * Returns an array of courses the user is enrolled, and for each course all of the assignments that the user can
      * view within that course.
      *
      * @param array $courseids An optional array of course ids. If provided only assignments within the given course
-     * will be returned. If the user is not enrolled in a given course a warning will be generated and returned.
+     * will be returned. If the user is not enrolled in or can't view a given course a warning will be generated and returned.
      * @param array $capabilities An array of additional capability checks you wish to be made on the course context.
+     * @param bool $includenotenrolledcourses Wheter to return courses that the user can see even if is not enroled in.
+     * This requires the parameter $courseids to not be empty.
      * @return An array of courses and warnings.
      * @since  Moodle 2.4
      */
-    public static function get_assignments($courseids = array(), $capabilities = array()) {
+    public static function get_assignments($courseids = array(), $capabilities = array(), $includenotenrolledcourses = false) {
         global $USER, $DB, $CFG;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $params = self::validate_parameters(
             self::get_assignments_parameters(),
-            array('courseids' => $courseids, 'capabilities' => $capabilities)
+            array(
+                'courseids' => $courseids,
+                'capabilities' => $capabilities,
+                'includenotenrolledcourses' => $includenotenrolledcourses
+            )
         );
 
         $warnings = array();
+        $courses = array();
         $fields = 'sortorder,shortname,fullname,timemodified';
-        $courses = enrol_get_users_courses($USER->id, true, $fields);
-        // Used to test for ids that have been requested but can't be returned.
-        if (count($params['courseids']) > 0) {
+
+        // If the courseids list is empty, we return only the courses where the user is enrolled in.
+        if (empty($params['courseids'])) {
+            $courses = enrol_get_users_courses($USER->id, true, $fields);
+            $courseids = array_keys($courses);
+        } else if ($includenotenrolledcourses) {
+            // In this case, we don't have to check here for enrolmnents. Maybe the user can see the course even if is not enrolled.
+            $courseids = $params['courseids'];
+        } else {
+            // We need to check for enrolments.
+            $mycourses = enrol_get_users_courses($USER->id, true, $fields);
+            $mycourseids = array_keys($mycourses);
+
             foreach ($params['courseids'] as $courseid) {
-                if (!in_array($courseid, array_keys($courses))) {
+                if (!in_array($courseid, $mycourseids)) {
                     unset($courses[$courseid]);
                     $warnings[] = array(
                         'item' => 'course',
@@ -291,28 +323,35 @@ class mod_assign_external extends external_api {
                         'warningcode' => '2',
                         'message' => 'User is not enrolled or does not have requested capability'
                     );
+                } else {
+                    $courses[$courseid] = $mycourses[$courseid];
                 }
             }
+            $courseids = array_keys($courses);
         }
-        foreach ($courses as $id => $course) {
-            if (count($params['courseids']) > 0 && !in_array($id, $params['courseids'])) {
-                unset($courses[$id]);
-            }
-            $context = context_course::instance($id);
+
+        foreach ($courseids as $cid) {
+
             try {
+                $context = context_course::instance($cid);
                 self::validate_context($context);
+
+                // Check if this course was already loaded (by enrol_get_users_courses).
+                if (!isset($courses[$cid])) {
+                    $courses[$cid] = get_course($cid);
+                }
             } catch (Exception $e) {
-                unset($courses[$id]);
+                unset($courses[$cid]);
                 $warnings[] = array(
                     'item' => 'course',
-                    'itemid' => $id,
+                    'itemid' => $cid,
                     'warningcode' => '1',
-                    'message' => 'No access rights in course context '.$e->getMessage().$e->getTraceAsString()
+                    'message' => 'No access rights in course context '.$e->getMessage()
                 );
                 continue;
             }
             if (count($params['capabilities']) > 0 && !has_all_capabilities($params['capabilities'], $context)) {
-                unset($courses[$id]);
+                unset($courses[$cid]);
             }
         }
         $extrafields='m.id as assignmentid, ' .
@@ -551,6 +590,72 @@ class mod_assign_external extends external_api {
         );
     }
 
+    /**
+     * Return information (files and text fields) for the given plugins in the assignment.
+     *
+     * @param  assign $assign the assignment object
+     * @param  array $assignplugins array of assignment plugins (submission or feedback)
+     * @param  stdClass $item the item object (submission or grade)
+     * @return array an array containing the plugins returned information
+     */
+    private static function get_plugins_data($assign, $assignplugins, $item) {
+        global $CFG;
+
+        $plugins = array();
+        $fs = get_file_storage();
+
+        foreach ($assignplugins as $assignplugin) {
+
+            if (!$assignplugin->is_enabled() or !$assignplugin->is_visible()) {
+                continue;
+            }
+
+            $plugin = array(
+                'name' => $assignplugin->get_name(),
+                'type' => $assignplugin->get_type()
+            );
+            // Subtype is 'assignsubmission', type is currently 'file' or 'onlinetext'.
+            $component = $assignplugin->get_subtype().'_'.$assignplugin->get_type();
+
+            $fileareas = $assignplugin->get_file_areas();
+            foreach ($fileareas as $filearea => $name) {
+                $fileareainfo = array('area' => $filearea);
+                $files = $fs->get_area_files(
+                    $assign->get_context()->id,
+                    $component,
+                    $filearea,
+                    $item->id,
+                    "timemodified",
+                    false
+                );
+                foreach ($files as $file) {
+                    $filepath = $file->get_filepath().$file->get_filename();
+                    $fileurl = file_encode_url($CFG->wwwroot . '/webservice/pluginfile.php', '/' . $assign->get_context()->id .
+                        '/' . $component. '/'. $filearea . '/' . $item->id . $filepath);
+                    $fileinfo = array(
+                        'filepath' => $filepath,
+                        'fileurl' => $fileurl
+                        );
+                    $fileareainfo['files'][] = $fileinfo;
+                }
+                $plugin['fileareas'][] = $fileareainfo;
+            }
+
+            $editorfields = $assignplugin->get_editor_fields();
+            foreach ($editorfields as $name => $description) {
+                $editorfieldinfo = array(
+                    'name' => $name,
+                    'description' => $description,
+                    'text' => $assignplugin->get_editor_text($name, $item->id),
+                    'format' => $assignplugin->get_editor_format($name, $item->id)
+                );
+                $plugin['editorfields'][] = $editorfieldinfo;
+            }
+            $plugins[] = $plugin;
+        }
+        return $plugins;
+    }
+
     /**
      * Describes the parameters for get_submissions
      *
@@ -583,7 +688,7 @@ class mod_assign_external extends external_api {
      */
     public static function get_submissions($assignmentids, $status = '', $since = 0, $before = 0) {
         global $DB, $CFG;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
+
         $params = self::validate_parameters(self::get_submissions_parameters(),
                         array('assignmentids' => $assignmentids,
                               'status' => $status,
@@ -620,7 +725,6 @@ class mod_assign_external extends external_api {
 
         foreach ($assigns as $assign) {
             $submissions = array();
-            $submissionplugins = $assign->get_submission_plugins();
             $placeholders = array('assignid1' => $assign->get_instance()->id,
                                   'assignid2' => $assign->get_instance()->id);
 
@@ -650,7 +754,7 @@ class mod_assign_external extends external_api {
             $submissionrecords = $DB->get_records_sql($sql, $placeholders);
 
             if (!empty($submissionrecords)) {
-                $fs = get_file_storage();
+                $submissionplugins = $assign->get_submission_plugins();
                 foreach ($submissionrecords as $submissionrecord) {
                     $submission = array(
                         'id' => $submissionrecord->id,
@@ -659,53 +763,9 @@ class mod_assign_external extends external_api {
                         'timemodified' => $submissionrecord->timemodified,
                         'status' => $submissionrecord->status,
                         'attemptnumber' => $submissionrecord->attemptnumber,
-                        'groupid' => $submissionrecord->groupid
+                        'groupid' => $submissionrecord->groupid,
+                        'plugins' => self::get_plugins_data($assign, $submissionplugins, $submissionrecord)
                     );
-                    foreach ($submissionplugins as $submissionplugin) {
-                        $plugin = array(
-                            'name' => $submissionplugin->get_name(),
-                            'type' => $submissionplugin->get_type()
-                        );
-                        // Subtype is 'assignsubmission', type is currently 'file' or 'onlinetext'.
-                        $component = $submissionplugin->get_subtype().'_'.$submissionplugin->get_type();
-
-                        $fileareas = $submissionplugin->get_file_areas();
-                        foreach ($fileareas as $filearea => $name) {
-                            $fileareainfo = array('area' => $filearea);
-                            $files = $fs->get_area_files(
-                                $assign->get_context()->id,
-                                $component,
-                                $filearea,
-                                $submissionrecord->id,
-                                "timemodified",
-                                false
-                            );
-                            foreach ($files as $file) {
-                                $filepath = $file->get_filepath().$file->get_filename();
-                                $fileurl = file_encode_url($CFG->wwwroot . '/webservice/pluginfile.php', '/' . $assign->get_context()->id .
-                                    '/' . $component. '/'. $filearea . '/' . $submissionrecord->id . $filepath);
-                                $fileinfo = array(
-                                    'filepath' => $filepath,
-                                    'fileurl' => $fileurl
-                                    );
-                                $fileareainfo['files'][] = $fileinfo;
-                            }
-                            $plugin['fileareas'][] = $fileareainfo;
-                        }
-
-                        $editorfields = $submissionplugin->get_editor_fields();
-                        foreach ($editorfields as $name => $description) {
-                            $editorfieldinfo = array(
-                                'name' => $name,
-                                'description' => $description,
-                                'text' => $submissionplugin->get_editor_text($name, $submissionrecord->id),
-                                'format' => $submissionplugin->get_editor_format($name, $submissionrecord->id)
-                            );
-                            $plugin['editorfields'][] = $editorfieldinfo;
-                        }
-
-                        $submission['plugins'][] = $plugin;
-                    }
                     $submissions[] = $submission;
                 }
             } else {
@@ -732,68 +792,83 @@ class mod_assign_external extends external_api {
     }
 
     /**
-     * Creates an assign_submissions external_single_structure
+     * Creates an assignment plugin structure.
      *
-     * @return external_single_structure
-     * @since Moodle 2.5
+     * @return external_single_structure the plugin structure
      */
-    private static function get_submissions_structure() {
+    private static function get_plugin_structure() {
         return new external_single_structure(
-            array (
-                'assignmentid' => new external_value(PARAM_INT, 'assignment id'),
-                'submissions' => new external_multiple_structure(
+            array(
+                'type' => new external_value(PARAM_TEXT, 'submission plugin type'),
+                'name' => new external_value(PARAM_TEXT, 'submission plugin name'),
+                'fileareas' => new external_multiple_structure(
                     new external_single_structure(
-                        array(
-                            'id' => new external_value(PARAM_INT, 'submission id'),
-                            'userid' => new external_value(PARAM_INT, 'student id'),
-                            'attemptnumber' => new external_value(PARAM_INT, 'attempt number'),
-                            'timecreated' => new external_value(PARAM_INT, 'submission creation time'),
-                            'timemodified' => new external_value(PARAM_INT, 'submission last modified time'),
-                            'status' => new external_value(PARAM_TEXT, 'submission status'),
-                            'groupid' => new external_value(PARAM_INT, 'group id'),
-                            'plugins' => new external_multiple_structure(
+                        array (
+                            'area' => new external_value (PARAM_TEXT, 'file area'),
+                            'files' => new external_multiple_structure(
                                 new external_single_structure(
-                                    array(
-                                        'type' => new external_value(PARAM_TEXT, 'submission plugin type'),
-                                        'name' => new external_value(PARAM_TEXT, 'submission plugin name'),
-                                        'fileareas' => new external_multiple_structure(
-                                            new external_single_structure(
-                                                array (
-                                                    'area' => new external_value (PARAM_TEXT, 'file area'),
-                                                    'files' => new external_multiple_structure(
-                                                        new external_single_structure(
-                                                            array (
-                                                                'filepath' => new external_value (PARAM_TEXT, 'file path'),
-                                                                'fileurl' => new external_value (PARAM_URL, 'file download url',
-                                                                    VALUE_OPTIONAL)
-                                                            )
-                                                        ), 'files', VALUE_OPTIONAL
-                                                    )
-                                                )
-                                            ), 'fileareas', VALUE_OPTIONAL
-                                        ),
-                                        'editorfields' => new external_multiple_structure(
-                                            new external_single_structure(
-                                                array(
-                                                    'name' => new external_value(PARAM_TEXT, 'field name'),
-                                                    'description' => new external_value(PARAM_TEXT, 'field description'),
-                                                    'text' => new external_value (PARAM_RAW, 'field value'),
-                                                    'format' => new external_format_value ('text')
-                                                )
-                                            )
-                                            , 'editorfields', VALUE_OPTIONAL
-                                        )
+                                    array (
+                                        'filepath' => new external_value (PARAM_TEXT, 'file path'),
+                                        'fileurl' => new external_value (PARAM_URL, 'file download url',
+                                            VALUE_OPTIONAL)
                                     )
-                                )
-                                , 'plugins', VALUE_OPTIONAL
+                                ), 'files', VALUE_OPTIONAL
                             )
                         )
+                    ), 'fileareas', VALUE_OPTIONAL
+                ),
+                'editorfields' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_TEXT, 'field name'),
+                            'description' => new external_value(PARAM_TEXT, 'field description'),
+                            'text' => new external_value (PARAM_RAW, 'field value'),
+                            'format' => new external_format_value ('text')
+                        )
                     )
+                    , 'editorfields', VALUE_OPTIONAL
                 )
             )
         );
     }
 
+    /**
+     * Creates a submission structure.
+     *
+     * @return external_single_structure the submission structure
+     */
+    private static function get_submission_structure($required = VALUE_REQUIRED) {
+        return new external_single_structure(
+            array(
+                'id' => new external_value(PARAM_INT, 'submission id'),
+                'userid' => new external_value(PARAM_INT, 'student id'),
+                'attemptnumber' => new external_value(PARAM_INT, 'attempt number'),
+                'timecreated' => new external_value(PARAM_INT, 'submission creation time'),
+                'timemodified' => new external_value(PARAM_INT, 'submission last modified time'),
+                'status' => new external_value(PARAM_TEXT, 'submission status'),
+                'groupid' => new external_value(PARAM_INT, 'group id'),
+                'assignment' => new external_value(PARAM_INT, 'assignment id', VALUE_OPTIONAL),
+                'latest' => new external_value(PARAM_INT, 'latest attempt', VALUE_OPTIONAL),
+                'plugins' => new external_multiple_structure(self::get_plugin_structure(), 'plugins', VALUE_OPTIONAL)
+            ), 'submission info', $required
+        );
+    }
+
+    /**
+     * Creates an assign_submissions external_single_structure
+     *
+     * @return external_single_structure
+     * @since Moodle 2.5
+     */
+    private static function get_submissions_structure() {
+        return new external_single_structure(
+            array (
+                'assignmentid' => new external_value(PARAM_INT, 'assignment id'),
+                'submissions' => new external_multiple_structure(self::get_submission_structure())
+            )
+        );
+    }
+
     /**
      * Describes the get_submissions return value
      *
@@ -844,7 +919,6 @@ class mod_assign_external extends external_api {
      */
     public static function set_user_flags($assignmentid, $userflags = array()) {
         global $CFG, $DB;
-        require_once($CFG->dirroot . "/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::set_user_flags_parameters(),
                                             array('assignmentid' => $assignmentid,
@@ -1276,7 +1350,6 @@ class mod_assign_external extends external_api {
      */
     public static function lock_submissions($assignmentid, $userids) {
         global $CFG;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::lock_submissions_parameters(),
                         array('assignmentid' => $assignmentid,
@@ -1338,7 +1411,6 @@ class mod_assign_external extends external_api {
      */
     public static function revert_submissions_to_draft($assignmentid, $userids) {
         global $CFG;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::revert_submissions_to_draft_parameters(),
                         array('assignmentid' => $assignmentid,
@@ -1400,7 +1472,6 @@ class mod_assign_external extends external_api {
      */
     public static function unlock_submissions($assignmentid, $userids) {
         global $CFG;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::unlock_submissions_parameters(),
                         array('assignmentid' => $assignmentid,
@@ -1458,7 +1529,6 @@ class mod_assign_external extends external_api {
      */
     public static function submit_for_grading($assignmentid, $acceptsubmissionstatement) {
         global $CFG, $USER;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::submit_for_grading_parameters(),
                                             array('assignmentid' => $assignmentid,
@@ -1527,7 +1597,6 @@ class mod_assign_external extends external_api {
      */
     public static function save_user_extensions($assignmentid, $userids, $dates) {
         global $CFG;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::save_user_extensions_parameters(),
                         array('assignmentid' => $assignmentid,
@@ -1595,7 +1664,6 @@ class mod_assign_external extends external_api {
      */
     public static function reveal_identities($assignmentid) {
         global $CFG, $USER;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::reveal_identities_parameters(),
                                             array('assignmentid' => $assignmentid));
@@ -1634,7 +1702,6 @@ class mod_assign_external extends external_api {
      */
     public static function save_submission_parameters() {
         global $CFG;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
         $instance = new assign(null, null, null);
         $pluginsubmissionparams = array();
 
@@ -1667,7 +1734,6 @@ class mod_assign_external extends external_api {
      */
     public static function save_submission($assignmentid, $plugindata) {
         global $CFG, $USER;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::save_submission_parameters(),
                                             array('assignmentid' => $assignmentid,
@@ -1715,7 +1781,6 @@ class mod_assign_external extends external_api {
      */
     public static function save_grade_parameters() {
         global $CFG;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
         require_once("$CFG->dirroot/grade/grading/lib.php");
         $instance = new assign(null, null, null);
         $pluginfeedbackparams = array();
@@ -1793,7 +1858,6 @@ class mod_assign_external extends external_api {
                                       $plugindata = array(),
                                       $advancedgradingdata = array()) {
         global $CFG, $USER;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::save_grade_parameters(),
                                             array('assignmentid' => $assignmentid,
@@ -1857,7 +1921,6 @@ class mod_assign_external extends external_api {
      */
     public static function save_grades_parameters() {
         global $CFG;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
         require_once("$CFG->dirroot/grade/grading/lib.php");
         $instance = new assign(null, null, null);
         $pluginfeedbackparams = array();
@@ -1939,7 +2002,6 @@ class mod_assign_external extends external_api {
      */
     public static function save_grades($assignmentid, $applytoall = false, $grades) {
         global $CFG, $USER;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::save_grades_parameters(),
                                             array('assignmentid' => $assignmentid,
@@ -2025,7 +2087,6 @@ class mod_assign_external extends external_api {
      */
     public static function copy_previous_attempt($assignmentid) {
         global $CFG, $USER;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::copy_previous_attempt_parameters(),
                                             array('assignmentid' => $assignmentid));
@@ -2084,7 +2145,6 @@ class mod_assign_external extends external_api {
      */
     public static function view_grading_table($assignid) {
         global $DB, $CFG;
-        require_once($CFG->dirroot . "/mod/assign/locallib.php");
 
         $params = self::validate_parameters(self::view_grading_table_parameters(),
                                             array(
@@ -2148,7 +2208,6 @@ class mod_assign_external extends external_api {
      */
     public static function view_submission_status($assignid) {
         global $DB, $CFG;
-        require_once("$CFG->dirroot/mod/assign/locallib.php");
 
         $warnings = array();
         $params = array(
@@ -2188,4 +2247,243 @@ class mod_assign_external extends external_api {
         );
     }
 
+    /**
+     * Describes the parameters for get_submission_status.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.1
+     */
+    public static function get_submission_status_parameters() {
+        return new external_function_parameters (
+            array(
+                'assignid' => new external_value(PARAM_INT, 'assignment instance id'),
+                'userid' => new external_value(PARAM_INT, 'user id (empty for current user)', VALUE_DEFAULT, 0),
+            )
+        );
+    }
+
+    /**
+     * Returns information about an assignment submission status for a given user.
+     *
+     * @param int $assignid assignment instance id
+     * @param int $userid user id (empty for current user)
+     * @return array of warnings and grading, status, feedback and previous attempts information
+     * @since Moodle 3.1
+     * @throws required_capability_exception
+     */
+    public static function get_submission_status($assignid, $userid = 0) {
+        global $USER, $DB;
+
+        $warnings = array();
+
+        $params = array(
+            'assignid' => $assignid,
+            'userid' => $userid,
+        );
+        $params = self::validate_parameters(self::get_submission_status_parameters(), $params);
+
+        // Request and permission validation.
+        $assign = $DB->get_record('assign', array('id' => $params['assignid']), 'id', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($assign, 'assign');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        $assign = new assign($context, $cm, $course);
+
+        // Default value for userid.
+        if (empty($params['userid'])) {
+            $params['userid'] = $USER->id;
+        }
+        $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
+        core_user::require_active_user($user);
+
+        if (!$assign->can_view_submission($user->id)) {
+            throw new required_capability_exception($context, 'mod/assign:viewgrades', 'nopermission', '');
+        }
+
+        $gradingsummary = $lastattempt = $feedback = $previousattempts = null;
+
+        // Get the renderable since it contais all the info we need.
+        if ($assign->can_view_grades()) {
+            $gradingsummary = $assign->get_assign_grading_summary_renderable();
+        }
+
+        // Retrieve the rest of the renderable objects.
+        if (has_capability('mod/assign:submit', $assign->get_context(), $user)) {
+            $lastattempt = $assign->get_assign_submission_status_renderable($user, true);
+        }
+
+        $feedback = $assign->get_assign_feedback_status_renderable($user);
+
+        $previousattempts = $assign->get_assign_attempt_history_renderable($user);
+
+        // Now, build the result.
+        $result = array();
+
+        // First of all, grading summary, this is suitable for teachers/managers.
+        if ($gradingsummary) {
+            $result['gradingsummary'] = $gradingsummary;
+        }
+
+        // Did we submit anything?
+        if ($lastattempt) {
+            $submissionplugins = $assign->get_submission_plugins();
+
+            if (empty($lastattempt->submission)) {
+                unset($lastattempt->submission);
+            } else {
+                $lastattempt->submission->plugins = self::get_plugins_data($assign, $submissionplugins, $lastattempt->submission);
+            }
+
+            if (empty($lastattempt->teamsubmission)) {
+                unset($lastattempt->teamsubmission);
+            } else {
+                $lastattempt->teamsubmission->plugins = self::get_plugins_data($assign, $submissionplugins,
+                                                                                $lastattempt->teamsubmission);
+            }
+
+            // We need to change the type of some of the structures retrieved from the renderable.
+            if (!empty($lastattempt->submissiongroup)) {
+                $lastattempt->submissiongroup = $lastattempt->submissiongroup->id;
+            }
+            if (!empty($lastattempt->usergroups)) {
+                $lastattempt->usergroups = array_keys($lastattempt->usergroups);
+            }
+            // We cannot use array_keys here.
+            if (!empty($lastattempt->submissiongroupmemberswhoneedtosubmit)) {
+                $lastattempt->submissiongroupmemberswhoneedtosubmit = array_map(
+                                                                            function($e){
+                                                                                return $e->id;
+                                                                            },
+                                                                            $lastattempt->submissiongroupmemberswhoneedtosubmit);
+            }
+
+            $result['lastattempt'] = $lastattempt;
+        }
+
+        // The feedback for our latest submission.
+        if ($feedback) {
+            if ($feedback->grade) {
+                $feedbackplugins = $assign->get_feedback_plugins();
+                $feedback->plugins = self::get_plugins_data($assign, $feedbackplugins, $feedback->grade);
+            } else {
+                unset($feedback->plugins);
+                unset($feedback->grade);
+            }
+
+            $result['feedback'] = $feedback;
+        }
+
+        // Retrieve only previous attempts.
+        if ($previousattempts and count($previousattempts->submissions) > 1) {
+            // Don't show the last one because it is the current submission.
+            array_pop($previousattempts->submissions);
+
+            // Show newest to oldest.
+            $previousattempts->submissions = array_reverse($previousattempts->submissions);
+
+            foreach ($previousattempts->submissions as $i => $submission) {
+                $attempt = array();
+
+                $grade = null;
+                foreach ($previousattempts->grades as $onegrade) {
+                    if ($onegrade->attemptnumber == $submission->attemptnumber) {
+                        $grade = $onegrade;
+                        break;
+                    }
+                }
+
+                $attempt['attemptnumber'] = $submission->attemptnumber;
+
+                if ($submission) {
+                    $submission->plugins = self::get_plugins_data($assign, $previousattempts->submissionplugins, $submission);
+                    $attempt['submission'] = $submission;
+                }
+
+                if ($grade) {
+                    // From object to id.
+                    $grade->grader = $grade->grader->id;
+                    $feedbackplugins = self::get_plugins_data($assign, $previousattempts->feedbackplugins, $grade);
+
+                    $attempt['grade'] = $grade;
+                    $attempt['feedbackplugins'] = $feedbackplugins;
+                }
+                $result['previousattempts'][] = $attempt;
+            }
+        }
+
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Describes the get_submission_status return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.1
+     */
+    public static function get_submission_status_returns() {
+        return new external_single_structure(
+            array(
+                'gradingsummary' => new external_single_structure(
+                    array(
+                        'participantcount' => new external_value(PARAM_INT, 'Number of users who can submit.'),
+                        'submissiondraftscount' => new external_value(PARAM_INT, 'Number of submissions in draft status.'),
+                        'submissiondraftscount' => new external_value(PARAM_INT, 'Number of submissions in draft status.'),
+                        'submissionsenabled' => new external_value(PARAM_BOOL, 'Whether submissions are enabled or not.'),
+                        'submissionssubmittedcount' => new external_value(PARAM_INT, 'Number of submissions in submitted status.'),
+                        'submissionsneedgradingcount' => new external_value(PARAM_INT, 'Number of submissions that need grading.'),
+                        'warnofungroupedusers' => new external_value(PARAM_BOOL, 'Whether we need to warn people that there
+                                                                        are users without groups.'),
+                    ), 'Grading information.', VALUE_OPTIONAL
+                ),
+                'lastattempt' => new external_single_structure(
+                    array(
+                        'submission' => self::get_submission_structure(VALUE_OPTIONAL),
+                        'teamsubmission' => self::get_submission_structure(VALUE_OPTIONAL),
+                        'submissiongroup' => new external_value(PARAM_INT, 'The submission group id (for group submissions only).',
+                                                                VALUE_OPTIONAL),
+                        'submissiongroupmemberswhoneedtosubmit' => new external_multiple_structure(
+                            new external_value(PARAM_INT, 'USER id.'),
+                            'List of users who still need to submit (for group submissions only).',
+                            VALUE_OPTIONAL
+                        ),
+                        'submissionsenabled' => new external_value(PARAM_BOOL, 'Whether submissions are enabled or not.'),
+                        'locked' => new external_value(PARAM_BOOL, 'Whether new submissions are locked.'),
+                        'graded' => new external_value(PARAM_BOOL, 'Whether the submission is graded.'),
+                        'canedit' => new external_value(PARAM_BOOL, 'Whether the user can edit the current submission.'),
+                        'cansubmit' => new external_value(PARAM_BOOL, 'Whether the user can submit.'),
+                        'extensionduedate' => new external_value(PARAM_INT, 'Extension due date.'),
+                        'blindmarking' => new external_value(PARAM_BOOL, 'Whether blind marking is enabled.'),
+                        'gradingstatus' => new external_value(PARAM_ALPHANUMEXT, 'Grading status.'),
+                        'usergroups' => new external_multiple_structure(
+                            new external_value(PARAM_INT, 'Group id.'), 'User groups in the course.'
+                        ),
+                    ), 'Last attempt information.', VALUE_OPTIONAL
+                ),
+                'feedback' => new external_single_structure(
+                    array(
+                        'grade' => self::get_grade_structure(VALUE_OPTIONAL),
+                        'gradefordisplay' => new external_value(PARAM_RAW, 'Grade rendered into a format suitable for display.'),
+                        'gradeddate' => new external_value(PARAM_INT, 'The date the user was graded.'),
+                        'plugins' => new external_multiple_structure(self::get_plugin_structure(), 'Plugins info.', VALUE_OPTIONAL),
+                    ), 'Feedback for the last attempt.', VALUE_OPTIONAL
+                ),
+                'previousattempts' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'attemptnumber' => new external_value(PARAM_INT, 'Attempt number.'),
+                            'submission' => self::get_submission_structure(VALUE_OPTIONAL),
+                            'grade' => self::get_grade_structure(VALUE_OPTIONAL),
+                            'feedbackplugins' => new external_multiple_structure(self::get_plugin_structure(), 'Feedback info.',
+                                                                                    VALUE_OPTIONAL),
+                        )
+                    ), 'List all the previous attempts did by the user.', VALUE_OPTIONAL
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
 }
index f2c5b8d..7eddad1 100644 (file)
@@ -355,4 +355,13 @@ class assign_feedback_editpdf extends assign_feedback_plugin {
     public function is_configurable() {
         return false;
     }
+
+    /**
+     * Get file areas returns a list of areas this plugin stores files.
+     *
+     * @return array - An array of fileareas (keys) and descriptions (values)
+     */
+    public function get_file_areas() {
+        return array(document_services::FINAL_PDF_FILEAREA => $this->get_name());
+    }
 }
index ad93008..9c462e2 100644 (file)
@@ -217,7 +217,10 @@ class assign {
     public function get_return_action() {
         global $PAGE;
 
-        $params = $PAGE->url->params();
+        // Web services don't set a URL, we should avoid debugging when ussing the url object.
+        if (!WS_SERVER) {
+            $params = $PAGE->url->params();
+        }
 
         if (!empty($params['action'])) {
             return $params['action'];
@@ -3999,21 +4002,18 @@ class assign {
     }
 
     /**
-     * Print 2 tables of information with no action links -
-     * the submission summary and the grading summary.
+     * Creates an assign_submission_status renderable.
      *
-     * @param stdClass $user the user to print the report for
-     * @param bool $showlinks - Return plain text or links to the profile
-     * @return string - the html summary
+     * @param stdClass $user the user to get the report for
+     * @param bool $showlinks return plain text or links to the profile
+     * @return assign_submission_status renderable object
      */
-    public function view_student_summary($user, $showlinks) {
-        global $CFG, $DB, $PAGE;
+    public function get_assign_submission_status_renderable($user, $showlinks) {
+        global $PAGE;
 
         $instance = $this->get_instance();
-        $grade = $this->get_user_grade($user->id, false);
         $flags = $this->get_user_flags($user->id, false);
         $submission = $this->get_user_submission($user->id, false);
-        $o = '';
 
         $teamsubmission = null;
         $submissiongroup = null;
@@ -4028,159 +4028,208 @@ class assign {
             $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
         }
 
-        if ($this->can_view_submission($user->id)) {
-            $showedit = $showlinks &&
-                        ($this->is_any_submission_plugin_enabled()) &&
-                        $this->can_edit_submission($user->id);
-
-            $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($user->id, false);
-
-            // Grading criteria preview.
-            $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
-            $gradingcontrollerpreview = '';
-            if ($gradingmethod = $gradingmanager->get_active_method()) {
-                $controller = $gradingmanager->get_controller($gradingmethod);
-                if ($controller->is_form_defined()) {
-                    $gradingcontrollerpreview = $controller->render_preview($PAGE);
-                }
-            }
+        $showedit = $showlinks &&
+                    ($this->is_any_submission_plugin_enabled()) &&
+                    $this->can_edit_submission($user->id);
 
-            $showsubmit = ($showlinks && $this->submissions_open($user->id));
-            $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
+        $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($user->id, false);
 
-            $extensionduedate = null;
-            if ($flags) {
-                $extensionduedate = $flags->extensionduedate;
+        // Grading criteria preview.
+        $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
+        $gradingcontrollerpreview = '';
+        if ($gradingmethod = $gradingmanager->get_active_method()) {
+            $controller = $gradingmanager->get_controller($gradingmethod);
+            if ($controller->is_form_defined()) {
+                $gradingcontrollerpreview = $controller->render_preview($PAGE);
             }
-            $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
+        }
 
-            $gradingstatus = $this->get_grading_status($user->id);
-            $usergroups = $this->get_all_groups($user->id);
-            $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
-                                                              $instance->alwaysshowdescription,
-                                                              $submission,
-                                                              $instance->teamsubmission,
-                                                              $teamsubmission,
-                                                              $submissiongroup,
-                                                              $notsubmitted,
-                                                              $this->is_any_submission_plugin_enabled(),
-                                                              $gradelocked,
-                                                              $this->is_graded($user->id),
-                                                              $instance->duedate,
-                                                              $instance->cutoffdate,
-                                                              $this->get_submission_plugins(),
-                                                              $this->get_return_action(),
-                                                              $this->get_return_params(),
-                                                              $this->get_course_module()->id,
-                                                              $this->get_course()->id,
-                                                              assign_submission_status::STUDENT_VIEW,
-                                                              $showedit,
-                                                              $showsubmit,
-                                                              $viewfullnames,
-                                                              $extensionduedate,
-                                                              $this->get_context(),
-                                                              $this->is_blind_marking(),
-                                                              $gradingcontrollerpreview,
-                                                              $instance->attemptreopenmethod,
-                                                              $instance->maxattempts,
-                                                              $gradingstatus,
-                                                              $instance->preventsubmissionnotingroup,
-                                                              $usergroups);
-            if (has_capability('mod/assign:submit', $this->get_context(), $user)) {
-                $o .= $this->get_renderer()->render($submissionstatus);
-            }
+        $showsubmit = ($showlinks && $this->submissions_open($user->id));
+        $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
 
-            require_once($CFG->libdir.'/gradelib.php');
-            require_once($CFG->dirroot.'/grade/grading/lib.php');
+        $extensionduedate = null;
+        if ($flags) {
+            $extensionduedate = $flags->extensionduedate;
+        }
+        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
+
+        $gradingstatus = $this->get_grading_status($user->id);
+        $usergroups = $this->get_all_groups($user->id);
+        $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
+                                                          $instance->alwaysshowdescription,
+                                                          $submission,
+                                                          $instance->teamsubmission,
+                                                          $teamsubmission,
+                                                          $submissiongroup,
+                                                          $notsubmitted,
+                                                          $this->is_any_submission_plugin_enabled(),
+                                                          $gradelocked,
+                                                          $this->is_graded($user->id),
+                                                          $instance->duedate,
+                                                          $instance->cutoffdate,
+                                                          $this->get_submission_plugins(),
+                                                          $this->get_return_action(),
+                                                          $this->get_return_params(),
+                                                          $this->get_course_module()->id,
+                                                          $this->get_course()->id,
+                                                          assign_submission_status::STUDENT_VIEW,
+                                                          $showedit,
+                                                          $showsubmit,
+                                                          $viewfullnames,
+                                                          $extensionduedate,
+                                                          $this->get_context(),
+                                                          $this->is_blind_marking(),
+                                                          $gradingcontrollerpreview,
+                                                          $instance->attemptreopenmethod,
+                                                          $instance->maxattempts,
+                                                          $gradingstatus,
+                                                          $instance->preventsubmissionnotingroup,
+                                                          $usergroups);
+        return $submissionstatus;
+    }
+
+
+    /**
+     * Creates an assign_feedback_status renderable.
+     *
+     * @param stdClass $user the user to get the report for
+     * @return assign_feedback_status renderable object
+     */
+    public function get_assign_feedback_status_renderable($user) {
+        global $CFG, $DB, $PAGE;
 
-            $gradinginfo = grade_get_grades($this->get_course()->id,
-                                        'mod',
-                                        'assign',
-                                        $instance->id,
-                                        $user->id);
-
-            $gradingitem = null;
-            $gradebookgrade = null;
-            if (isset($gradinginfo->items[0])) {
-                $gradingitem = $gradinginfo->items[0];
-                $gradebookgrade = $gradingitem->grades[$user->id];
-            }
-
-            // Check to see if all feedback plugins are empty.
-            $emptyplugins = true;
-            if ($grade) {
-                foreach ($this->get_feedback_plugins() as $plugin) {
-                    if ($plugin->is_visible() && $plugin->is_enabled()) {
-                        if (!$plugin->is_empty($grade)) {
-                            $emptyplugins = false;
-                        }
+        require_once($CFG->libdir.'/gradelib.php');
+        require_once($CFG->dirroot.'/grade/grading/lib.php');
+
+        $instance = $this->get_instance();
+        $grade = $this->get_user_grade($user->id, false);
+        $gradingstatus = $this->get_grading_status($user->id);
+
+        $gradinginfo = grade_get_grades($this->get_course()->id,
+                                    'mod',
+                                    'assign',
+                                    $instance->id,
+                                    $user->id);
+
+        $gradingitem = null;
+        $gradebookgrade = null;
+        if (isset($gradinginfo->items[0])) {
+            $gradingitem = $gradinginfo->items[0];
+            $gradebookgrade = $gradingitem->grades[$user->id];
+        }
+
+        // Check to see if all feedback plugins are empty.
+        $emptyplugins = true;
+        if ($grade) {
+            foreach ($this->get_feedback_plugins() as $plugin) {
+                if ($plugin->is_visible() && $plugin->is_enabled()) {
+                    if (!$plugin->is_empty($grade)) {
+                        $emptyplugins = false;
                     }
                 }
             }
+        }
+
+        if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
+            $emptyplugins = true; // Don't show feedback plugins until released either.
+        }
 
-            if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
-                $emptyplugins = true; // Don't show feedback plugins until released either.
+        $cangrade = has_capability('mod/assign:grade', $this->get_context());
+        // If there is a visible grade, show the summary.
+        if ((!is_null($gradebookgrade->grade) || !$emptyplugins)
+                && ($cangrade || !$gradebookgrade->hidden)) {
+
+            $gradefordisplay = null;
+            $gradeddate = null;
+            $grader = null;
+            $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
+
+            // Only show the grade if it is not hidden in gradebook.
+            if (!is_null($gradebookgrade->grade) && ($cangrade || !$gradebookgrade->hidden)) {
+                if ($controller = $gradingmanager->get_active_controller()) {
+                    $menu = make_grades_menu($this->get_instance()->grade);
+                    $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
+                    $gradefordisplay = $controller->render_grade($PAGE,
+                                                                 $grade->id,
+                                                                 $gradingitem,
+                                                                 $gradebookgrade->str_long_grade,
+                                                                 $cangrade);
+                } else {
+                    $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
+                }
+                $gradeddate = $gradebookgrade->dategraded;
+                if (isset($grade->grader)) {
+                    $grader = $DB->get_record('user', array('id' => $grade->grader));
+                }
             }
 
-            $cangrade = has_capability('mod/assign:grade', $this->get_context());
-            // If there is a visible grade, show the summary.
-            if ((!is_null($gradebookgrade->grade) || !$emptyplugins)
-                    && ($cangrade || !$gradebookgrade->hidden)) {
+            $feedbackstatus = new assign_feedback_status($gradefordisplay,
+                                                  $gradeddate,
+                                                  $grader,
+                                                  $this->get_feedback_plugins(),
+                                                  $grade,
+                                                  $this->get_course_module()->id,
+                                                  $this->get_return_action(),
+                                                  $this->get_return_params());
+            return $feedbackstatus;
+        }
+        return;
+    }
+
+    /**
+     * Creates an assign_attempt_history renderable.
+     *
+     * @param stdClass $user the user to get the report for
+     * @return assign_attempt_history renderable object
+     */
+    public function get_assign_attempt_history_renderable($user) {
 
-                $gradefordisplay = null;
-                $gradeddate = null;
-                $grader = null;
-                $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
+        $allsubmissions = $this->get_all_submissions($user->id);
+        $allgrades = $this->get_all_grades($user->id);
 
-                // Only show the grade if it is not hidden in gradebook.
-                if (!is_null($gradebookgrade->grade) && ($cangrade || !$gradebookgrade->hidden)) {
-                    if ($controller = $gradingmanager->get_active_controller()) {
-                        $menu = make_grades_menu($this->get_instance()->grade);
-                        $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
-                        $gradefordisplay = $controller->render_grade($PAGE,
-                                                                     $grade->id,
-                                                                     $gradingitem,
-                                                                     $gradebookgrade->str_long_grade,
-                                                                     $cangrade);
-                    } else {
-                        $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
-                    }
-                    $gradeddate = $gradebookgrade->dategraded;
-                    if (isset($grade->grader)) {
-                        $grader = $DB->get_record('user', array('id'=>$grade->grader));
-                    }
-                }
+        $history = new assign_attempt_history($allsubmissions,
+                                              $allgrades,
+                                              $this->get_submission_plugins(),
+                                              $this->get_feedback_plugins(),
+                                              $this->get_course_module()->id,
+                                              $this->get_return_action(),
+                                              $this->get_return_params(),
+                                              false,
+                                              0,
+                                              0);
+        return $history;
+    }
 
-                $feedbackstatus = new assign_feedback_status($gradefordisplay,
-                                                      $gradeddate,
-                                                      $grader,
-                                                      $this->get_feedback_plugins(),
-                                                      $grade,
-                                                      $this->get_course_module()->id,
-                                                      $this->get_return_action(),
-                                                      $this->get_return_params());
+    /**
+     * Print 2 tables of information with no action links -
+     * the submission summary and the grading summary.
+     *
+     * @param stdClass $user the user to print the report for
+     * @param bool $showlinks - Return plain text or links to the profile
+     * @return string - the html summary
+     */
+    public function view_student_summary($user, $showlinks) {
 
-                $o .= $this->get_renderer()->render($feedbackstatus);
-            }
+        $o = '';
 
-            $allsubmissions = $this->get_all_submissions($user->id);
+        if ($this->can_view_submission($user->id)) {
 
-            if (count($allsubmissions) > 1) {
-                $allgrades = $this->get_all_grades($user->id);
-                $history = new assign_attempt_history($allsubmissions,
-                                                      $allgrades,
-                                                      $this->get_submission_plugins(),
-                                                      $this->get_feedback_plugins(),
-                                                      $this->get_course_module()->id,
-                                                      $this->get_return_action(),
-                                                      $this->get_return_params(),
-                                                      false,
-                                                      0,
-                                                      0);
+            if (has_capability('mod/assign:submit', $this->get_context(), $user)) {
+                $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks);
+                $o .= $this->get_renderer()->render($submissionstatus);
+            }
 
-                $o .= $this->get_renderer()->render($history);
+            // If there is a visible grade, show the feedback.
+            $feedbackstatus = $this->get_assign_feedback_status_renderable($user);
+            if ($feedbackstatus) {
+                $o .= $this->get_renderer()->render($feedbackstatus);
             }
 
+            // If there is more than one submission, show the history.
+            $history = $this->get_assign_attempt_history_renderable($user);
+            if (count($history->submissions) > 1) {
+                $o .= $this->get_renderer()->render($history);
+            }
         }
         return $o;
     }
@@ -4327,6 +4376,55 @@ class assign {
         return $submissions;
     }
 
+    /**
+     * Creates an assign_grading_summary renderable.
+     *
+     * @return assign_grading_summary renderable object
+     */
+    public function get_assign_grading_summary_renderable() {
+
+        $instance = $this->get_instance();
+
+        $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
+        $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+
+        $activitygroup = groups_get_activity_group($this->get_course_module());
+
+        if ($instance->teamsubmission) {
+            $defaultteammembers = $this->get_submission_group_members(0, true);
+            $warnofungroupedusers = (count($defaultteammembers) > 0 && $instance->preventsubmissionnotingroup);
+
+            $summary = new assign_grading_summary($this->count_teams($activitygroup),
+                                                  $instance->submissiondrafts,
+                                                  $this->count_submissions_with_status($draft),
+                                                  $this->is_any_submission_plugin_enabled(),
+                                                  $this->count_submissions_with_status($submitted),
+                                                  $instance->cutoffdate,
+                                                  $instance->duedate,
+                                                  $this->get_course_module()->id,
+                                                  $this->count_submissions_need_grading(),
+                                                  $instance->teamsubmission,
+                                                  $warnofungroupedusers);
+        } else {
+            // The active group has already been updated in groups_print_activity_menu().
+            $countparticipants = $this->count_participants($activitygroup);
+            $summary = new assign_grading_summary($countparticipants,
+                                                  $instance->submissiondrafts,
+                                                  $this->count_submissions_with_status($draft),
+                                                  $this->is_any_submission_plugin_enabled(),
+                                                  $this->count_submissions_with_status($submitted),
+                                                  $instance->cutoffdate,
+                                                  $instance->duedate,
+                                                  $this->get_course_module()->id,
+                                                  $this->count_submissions_need_grading(),
+                                                  $instance->teamsubmission,
+                                                  false);
+
+        }
+
+        return $summary;
+    }
+
     /**
      * View submissions page (contains details of current submission).
      *
@@ -4358,47 +4456,12 @@ class assign {
         }
 
         if ($this->can_view_grades()) {
-            $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
-            $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
-
             // Group selector will only be displayed if necessary.
             $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
             $o .= groups_print_activity_menu($this->get_course_module(), $currenturl->out(), true);
 
-            $activitygroup = groups_get_activity_group($this->get_course_module());
-
-            if ($instance->teamsubmission) {
-                $defaultteammembers = $this->get_submission_group_members(0, true);
-                $warnofungroupedusers = (count($defaultteammembers) > 0 && $instance->preventsubmissionnotingroup);
-
-                $summary = new assign_grading_summary($this->count_teams($activitygroup),
-                                                      $instance->submissiondrafts,
-                                                      $this->count_submissions_with_status($draft),
-                                                      $this->is_any_submission_plugin_enabled(),
-                                                      $this->count_submissions_with_status($submitted),
-                                                      $instance->cutoffdate,
-                                                      $instance->duedate,
-                                                      $this->get_course_module()->id,
-                                                      $this->count_submissions_need_grading(),
-                                                      $instance->teamsubmission,
-                                                      $warnofungroupedusers);
-                $o .= $this->get_renderer()->render($summary);
-            } else {
-                // The active group has already been updated in groups_print_activity_menu().
-                $countparticipants = $this->count_participants($activitygroup);
-                $summary = new assign_grading_summary($countparticipants,
-                                                      $instance->submissiondrafts,
-                                                      $this->count_submissions_with_status($draft),
-                                                      $this->is_any_submission_plugin_enabled(),
-                                                      $this->count_submissions_with_status($submitted),
-                                                      $instance->cutoffdate,
-                                                      $instance->duedate,
-                                                      $this->get_course_module()->id,
-                                                      $this->count_submissions_need_grading(),
-                                                      $instance->teamsubmission,
-                                                      false);
-                $o .= $this->get_renderer()->render($summary);
-            }
+            $summary = $this->get_assign_grading_summary_renderable();
+            $o .= $this->get_renderer()->render($summary);
         }
         $grade = $this->get_user_grade($USER->id, false);
         $submission = $this->get_user_submission($USER->id, false);
index 63e9c36..1b92d58 100644 (file)
@@ -254,6 +254,35 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
 
         $this->assertEquals(0, count($result['courses']));
         $this->assertEquals(1, count($result['warnings']));
+
+        // Test with non-enrolled user, but with view capabilities.
+        $this->setAdminUser();
+        $result = mod_assign_external::get_assignments();
+        $result = external_api::clean_returnvalue(mod_assign_external::get_assignments_returns(), $result);
+        $this->assertEquals(0, count($result['courses']));
+        $this->assertEquals(0, count($result['warnings']));
+
+        // Expect no courses, because we are not using the special flag.
+        $result = mod_assign_external::get_assignments(array($course1->id));
+        $result = external_api::clean_returnvalue(mod_assign_external::get_assignments_returns(), $result);
+        $this->assertCount(0, $result['courses']);
+
+        // Now use the special flag to return courses where you are not enroled in.
+        $result = mod_assign_external::get_assignments(array($course1->id), array(), true);
+        $result = external_api::clean_returnvalue(mod_assign_external::get_assignments_returns(), $result);
+        $this->assertCount(1, $result['courses']);
+
+        $course = $result['courses'][0];
+        $this->assertEquals('Lightwork Course 1', $course['fullname']);
+        $this->assertEquals(1, count($course['assignments']));
+        $assignment = $course['assignments'][0];
+        $this->assertEquals($assign1->id, $assignment['id']);
+        $this->assertEquals($course1->id, $assignment['course']);
+        $this->assertEquals('lightwork assignment', $assignment['name']);
+        $this->assertArrayNotHasKey('intro', $assignment);
+        $this->assertArrayNotHasKey('introattachments', $assignment);
+        $this->assertEquals(1, $assignment['markingworkflow']);
+        $this->assertEquals(1, $assignment['markingallocation']);
     }
 
     /**
@@ -336,16 +365,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals(1, count($assignment['submissions']));
         $submission = $assignment['submissions'][0];
         $this->assertEquals($sid, $submission['id']);
-        $this->assertGreaterThanOrEqual(3, count($submission['plugins']));
-        $plugins = $submission['plugins'];
-        foreach ($plugins as $plugin) {
-            $foundonlinetext = false;
-            if ($plugin['type'] == 'onlinetext') {
-                $foundonlinetext = true;
-                break;
-            }
-        }
-        $this->assertTrue($foundonlinetext);
+        $this->assertCount(1, $submission['plugins']);
     }
 
     /**
@@ -1719,4 +1739,274 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         }
     }
 
+    /**
+     * Create a submission for testing the get_submission_status function.
+     * @param  boolean $submitforgrading whether to submit for grading the submission
+     * @return array an array containing all the required data for testing
+     */
+    private function create_submission_for_testing_status($submitforgrading = false) {
+        global $DB, $CFG;
+        require_once($CFG->dirroot . '/mod/assign/tests/base_test.php');
+
+        // Create a course and assignment and users.
+        $course = self::getDataGenerator()->create_course();
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $params = array(
+            'course' => $course->id,
+            'assignsubmission_file_maxfiles' => 1,
+            'assignsubmission_file_maxsizebytes' => 1024 * 1024,
+            'assignsubmission_onlinetext_enabled' => 1,
+            'assignsubmission_file_enabled' => 1,
+            'submissiondrafts' => 1,
+            'assignfeedback_file_enabled' => 1,
+            'assignfeedback_comments_enabled' => 1,
+            'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
+            'sendnotifications' => 0
+        );
+
+        set_config('submissionreceipts', 0, 'assign');
+
+        $instance = $generator->create_instance($params);
+        $cm = get_coursemodule_from_instance('assign', $instance->id);
+        $context = context_module::instance($cm->id);
+
+        $assign = new testable_assign($context, $cm, $course);
+
+        $student1 = self::getDataGenerator()->create_user();
+        $student2 = self::getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id);
+        $teacher = self::getDataGenerator()->create_user();
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
+
+        $this->setUser($student1);
+
+        // Create a student1 with an online text submission.
+        // Simulate a submission.
+        $submission = $assign->get_user_submission($student1->id, true);
+
+        $data = new stdClass();
+        $data->onlinetext_editor = array('itemid' => file_get_unused_draft_itemid(),
+                                         'text' => 'Submission text',
+                                         'format' => FORMAT_MOODLE);
+
+        $draftidfile = file_get_unused_draft_itemid();
+        $usercontext = context_user::instance($student1->id);
+        $filerecord = array(
+            'contextid' => $usercontext->id,
+            'component' => 'user',
+            'filearea'  => 'draft',
+            'itemid'    => $draftidfile,
+            'filepath'  => '/',
+            'filename'  => 't.txt',
+        );
+        $fs = get_file_storage();
+        $fs->create_file_from_string($filerecord, 'text contents');
+
+        $data->files_filemanager = $draftidfile;
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        if ($submitforgrading) {
+            // Now, submit the draft for grading.
+            $notices = array();
+
+            $data = new stdClass;
+            $data->userid = $student1->id;
+            $assign->submit_for_grading($data, $notices);
+        }
+
+        return array($assign, $instance, $student1, $student2, $teacher);
+    }
+
+    /**
+     * Test get_submission_status for a draft submission.
+     */
+    public function test_get_submission_status_in_draft_status() {
+        $this->resetAfterTest(true);
+
+        list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status();
+
+        $result = mod_assign_external::get_submission_status($assign->get_instance()->id);
+        // We expect debugging because of the $PAGE object, this won't happen in a normal WS request.
+        $this->assertDebuggingCalled();
+
+        $result = external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result);
+
+        // The submission is now in draft mode.
+        $this->assertCount(0, $result['warnings']);
+        $this->assertFalse(isset($result['gradingsummary']));
+        $this->assertFalse(isset($result['feedback']));
+        $this->assertFalse(isset($result['previousattempts']));
+
+        $this->assertTrue($result['lastattempt']['submissionsenabled']);
+        $this->assertTrue($result['lastattempt']['canedit']);
+        $this->assertTrue($result['lastattempt']['cansubmit']);
+        $this->assertFalse($result['lastattempt']['locked']);
+        $this->assertFalse($result['lastattempt']['graded']);
+        $this->assertEmpty($result['lastattempt']['extensionduedate']);
+        $this->assertFalse($result['lastattempt']['blindmarking']);
+        $this->assertCount(0, $result['lastattempt']['submissiongroupmemberswhoneedtosubmit']);
+        $this->assertEquals('notgraded', $result['lastattempt']['gradingstatus']);
+
+        $this->assertEquals($student1->id, $result['lastattempt']['submission']['userid']);
+        $this->assertEquals(0, $result['lastattempt']['submission']['attemptnumber']);
+        $this->assertEquals('draft', $result['lastattempt']['submission']['status']);
+        $this->assertEquals(0, $result['lastattempt']['submission']['groupid']);
+        $this->assertEquals($assign->get_instance()->id, $result['lastattempt']['submission']['assignment']);
+        $this->assertEquals(1, $result['lastattempt']['submission']['latest']);
+        $this->assertEquals('Submission text', $result['lastattempt']['submission']['plugins'][0]['editorfields'][0]['text']);
+        $this->assertEquals('/t.txt', $result['lastattempt']['submission']['plugins'][1]['fileareas'][0]['files'][0]['filepath']);
+    }
+
+    /**
+     * Test get_submission_status for a submitted submission.
+     */
+    public function test_get_submission_status_in_submission_status() {
+        $this->resetAfterTest(true);
+
+        list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status(true);
+
+        $result = mod_assign_external::get_submission_status($assign->get_instance()->id);
+        // We expect debugging because of the $PAGE object, this won't happen in a normal WS request.
+        $this->assertDebuggingCalled();
+        $result = external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result);
+
+        $this->assertCount(0, $result['warnings']);
+        $this->assertFalse(isset($result['gradingsummary']));
+        $this->assertFalse(isset($result['feedback']));
+        $this->assertFalse(isset($result['previousattempts']));
+
+        $this->assertTrue($result['lastattempt']['submissionsenabled']);
+        $this->assertFalse($result['lastattempt']['canedit']);
+        $this->assertFalse($result['lastattempt']['cansubmit']);
+        $this->assertFalse($result['lastattempt']['locked']);
+        $this->assertFalse($result['lastattempt']['graded']);
+        $this->assertEmpty($result['lastattempt']['extensionduedate']);
+        $this->assertFalse($result['lastattempt']['blindmarking']);
+        $this->assertCount(0, $result['lastattempt']['submissiongroupmemberswhoneedtosubmit']);
+        $this->assertEquals('notgraded', $result['lastattempt']['gradingstatus']);
+
+    }
+
+    /**
+     * Test get_submission_status using the teacher role.
+     */
+    public function test_get_submission_status_in_submission_status_for_teacher() {
+        $this->resetAfterTest(true);
+
+        list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status(true);
+
+        // Now, as teacher, see the grading summary.
+        $this->setUser($teacher);
+        $result = mod_assign_external::get_submission_status($assign->get_instance()->id);
+        // We expect debugging because of the $PAGE object, this won't happen in a normal WS request.
+        $this->assertDebuggingCalled();
+        $result = external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result);
+
+        $this->assertCount(0, $result['warnings']);
+        $this->assertFalse(isset($result['lastattempt']));
+        $this->assertFalse(isset($result['feedback']));
+        $this->assertFalse(isset($result['previousattempts']));
+
+        $this->assertEquals(2, $result['gradingsummary']['participantcount']);
+        $this->assertEquals(0, $result['gradingsummary']['submissiondraftscount']);
+        $this->assertEquals(1, $result['gradingsummary']['submissionsenabled']);
+        $this->assertEquals(1, $result['gradingsummary']['submissionssubmittedcount']);
+        $this->assertEquals(1, $result['gradingsummary']['submissionsneedgradingcount']);
+        $this->assertFalse($result['gradingsummary']['warnofungroupedusers']);
+    }
+
+    /**
+     * Test get_submission_status for a reopened submission.
+     */
+    public function test_get_submission_status_in_reopened_status() {
+        global $USER;
+
+        $this->resetAfterTest(true);
+
+        list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status(true);
+
+        $this->setUser($teacher);
+        // Grade and reopen.
+        $feedbackpluginparams = array();
+        $feedbackpluginparams['files_filemanager'] = file_get_unused_draft_itemid();
+        $feedbackeditorparams = array('text' => 'Yeeha!',
+                                        'format' => 1);
+        $feedbackpluginparams['assignfeedbackcomments_editor'] = $feedbackeditorparams;
+        $result = mod_assign_external::save_grade($instance->id,
+                                                  $student1->id,
+                                                  50.0,
+                                                  -1,
+                                                  false,
+                                                  'released',
+                                                  false,
+                                                  $feedbackpluginparams);
+        $USER->ignoresesskey = true;
+        $assign->testable_process_add_attempt($student1->id);
+
+        $this->setUser($student1);
+
+        $result = mod_assign_external::get_submission_status($assign->get_instance()->id);
+        // We expect debugging because of the $PAGE object, this won't happen in a normal WS request.
+        $this->assertDebuggingCalled();
+        $result = external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result);
+
+        $this->assertCount(0, $result['warnings']);
+        $this->assertFalse(isset($result['gradingsummary']));
+
+        $this->assertTrue($result['lastattempt']['submissionsenabled']);
+        $this->assertTrue($result['lastattempt']['canedit']);
+        $this->assertFalse($result['lastattempt']['cansubmit']);
+        $this->assertFalse($result['lastattempt']['locked']);
+        $this->assertFalse($result['lastattempt']['graded']);
+        $this->assertEmpty($result['lastattempt']['extensionduedate']);
+        $this->assertFalse($result['lastattempt']['blindmarking']);
+        $this->assertCount(0, $result['lastattempt']['submissiongroupmemberswhoneedtosubmit']);
+        $this->assertEquals('notgraded', $result['lastattempt']['gradingstatus']);
+
+        // Check new attempt reopened.
+        $this->assertEquals($student1->id, $result['lastattempt']['submission']['userid']);
+        $this->assertEquals(1, $result['lastattempt']['submission']['attemptnumber']);
+        $this->assertEquals('reopened', $result['lastattempt']['submission']['status']);
+        $this->assertEquals(0, $result['lastattempt']['submission']['groupid']);
+        $this->assertEquals($assign->get_instance()->id, $result['lastattempt']['submission']['assignment']);
+        $this->assertEquals(1, $result['lastattempt']['submission']['latest']);
+        $this->assertCount(3, $result['lastattempt']['submission']['plugins']);
+
+        // Now see feedback and the attempts history (remember, is a submission reopened).
+        // Only 2 fields (no grade, no plugins data).
+        $this->assertCount(2, $result['feedback']);
+
+        // One previous attempt.
+        $this->assertCount(1, $result['previousattempts']);
+        $this->assertEquals(0, $result['previousattempts'][0]['attemptnumber']);
+        $this->assertEquals(50, $result['previousattempts'][0]['grade']['grade']);
+        $this->assertEquals($teacher->id, $result['previousattempts'][0]['grade']['grader']);
+        $this->assertEquals($student1->id, $result['previousattempts'][0]['grade']['userid']);
+        $this->assertEquals('Yeeha!', $result['previousattempts'][0]['feedbackplugins'][0]['editorfields'][0]['text']);
+        $submissionplugins = $result['previousattempts'][0]['submission']['plugins'];
+        $this->assertEquals('Submission text', $submissionplugins[0]['editorfields'][0]['text']);
+        $this->assertEquals('/t.txt', $submissionplugins[1]['fileareas'][0]['files'][0]['filepath']);
+    }
+
+    /**
+     * Test access control for get_submission_status.
+     */
+    public function test_get_submission_status_access_control() {
+        $this->resetAfterTest(true);
+
+        list($assign, $instance, $student1, $student2, $teacher) = $this->create_submission_for_testing_status();
+
+        $this->setUser($student2);
+
+        // Access control test.
+        $this->setExpectedException('required_capability_exception');
+        mod_assign_external::get_submission_status($assign->get_instance()->id, $student1->id);
+
+    }
 }
index e6c4652..c445ad4 100644 (file)
@@ -8,6 +8,8 @@ This files describes API changes in the assign code.
 
 === 3.0 ===
 * assign_submission_status renderable now requires $usergroups in its constructor
+* External function mod_assign_external::get_assignments now have a new optional parameter: includenotenrolledcourses.
+  Its default value is false, setting it to true will return assignments for courses a user can see even if it's not enroled.
 
 === 2.9 ===
 * External parameter now returns PARAM_RAW instead of PARAM_TEXT for webservices using feedback comments.
index 2697d23..1bfd139 100644 (file)
@@ -44,7 +44,7 @@
 <div>
   <a target="_blank" href="{{{ permalink }}}">{{{ subject }}}</a>
   {{# str }} bynameondate, forum, {
-          "name": {{# quote }}<a target=\"_blank\" href=\"{{{ authorlink }}}\">{{ authorfullname }}</a>{{/ quote }},
+          "name": {{# quote }}<a target="_blank" href="{{{ authorlink }}}">{{ authorfullname }}</a>{{/ quote }},
           "date": {{# quote }}{{ postdate }}{{/ quote }}
       } {{/ str }}
 </div>
index a017909..baf2a70 100644 (file)
@@ -648,6 +648,14 @@ table.quizreviewsummary td.cell {
 #page-mod-quiz-edit .section-heading .sectioninstance {
     position: relative;
 }
+#page-mod-quiz-edit .section-heading .instancesection {
+    white-space: nowrap;
+    max-width: 72%;
+    display: inline-block;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    vertical-align: bottom;
+}
 #page-mod-quiz-edit .section-heading form {
     display: inline;
     position: relative;
index f67c86a..cd2ccd8 100644 (file)
@@ -55,6 +55,11 @@ class engine extends \core_search\engine {
      */
     protected $client = null;
 
+    /**
+     * @var \curl Direct curl object.
+     */
+    protected $curl = null;
+
     /**
      * @var array Fields that can be highlighted.
      */
@@ -453,11 +458,10 @@ class engine extends \core_search\engine {
             'login'    => !empty($this->config->server_username) ? $this->config->server_username : '',
             'password' => !empty($this->config->server_password) ? $this->config->server_password : '',
             'port'     => !empty($this->config->server_port) ? $this->config->server_port : '',
-            'issecure' => !empty($this->config->secure) ? $this->config->secure : '',
+            'secure' => !empty($this->config->secure) ? true : false,
             'ssl_cert' => !empty($this->config->ssl_cert) ? $this->config->ssl_cert : '',
-            'ssl_cert_only' => !empty($this->config->ssl_cert_only) ? $this->config->ssl_cert_only : '',
             'ssl_key' => !empty($this->config->ssl_key) ? $this->config->ssl_key : '',
-            'ssl_password' => !empty($this->config->ssl_keypassword) ? $this->config->ssl_keypassword : '',
+            'ssl_keypassword' => !empty($this->config->ssl_keypassword) ? $this->config->ssl_keypassword : '',
             'ssl_cainfo' => !empty($this->config->ssl_cainfo) ? $this->config->ssl_cainfo : '',
             'ssl_capath' => !empty($this->config->ssl_capath) ? $this->config->ssl_capath : '',
             'timeout' => !empty($this->config->server_timeout) ? $this->config->server_timeout : '30'
@@ -471,4 +475,70 @@ class engine extends \core_search\engine {
 
         return $this->client;
     }
+
+    /**
+     * Returns a curl object for conntecting to solr.
+     *
+     * @return \curl
+     */
+    public function get_curl_object() {
+        if (!is_null($this->curl)) {
+            return $this->curl;
+        }
+
+        $this->curl = new \curl();
+
+        $options = array();
+        // Build the SSL options. Based on pecl-solr and general testing.
+        if (!empty($this->config->secure)) {
+            if (!empty($this->config->ssl_cert)) {
+                $options['CURLOPT_SSLCERT'] = $this->config->ssl_cert;
+                $options['CURLOPT_SSLCERTTYPE'] = 'PEM';
+            }
+
+            if (!empty($this->config->ssl_key)) {
+                $options['CURLOPT_SSLKEY'] = $this->config->ssl_key;
+                $options['CURLOPT_SSLKEYTYPE'] = 'PEM';
+            }
+
+            if (!empty($this->config->ssl_keypassword)) {
+                $options['CURLOPT_KEYPASSWD'] = $this->config->ssl_keypassword;
+            }
+
+            if (!empty($this->config->ssl_cainfo)) {
+                $options['CURLOPT_CAINFO'] = $this->config->ssl_cainfo;
+            }
+
+            if (!empty($this->config->ssl_capath)) {
+                $options['CURLOPT_CAPATH'] = $this->config->ssl_capath;
+            }
+        }
+
+        $this->curl->setopt($options);
+
+        if (!empty($this->config->server_username) && !empty($this->config->server_password)) {
+            $authorization = $this->config->server_username . ':' . $this->config->server_password;
+            $this->curl->setHeader('Authorization', 'Basic ' . base64_encode($authorization));
+        }
+
+        return $this->curl;
+    }
+
+    /**
+     * Return a Moodle url object for the server connection.
+     *
+     * @param string $path The solr path to append.
+     * @return \moodle_url
+     */
+    public function get_connection_url($path) {
+        // Must use the proper protocol, or SSL will fail.
+        $protocol = !empty($this->config->secure) ? 'https' : 'http';
+        $url = $protocol . '://' . rtrim($this->config->server_hostname, '/');
+        if (!empty($this->config->server_port)) {
+            $url .= ':' . $this->config->server_port;
+        }
+        $url .= '/solr/' . $this->config->indexname . '/' . ltrim($path, '/');
+
+        return new \moodle_url($url);
+    }
 }
index 5adff6d..c68268f 100644 (file)
@@ -52,16 +52,10 @@ class schema {
     protected $curl = null;
 
     /**
-     * The URL.
-     * @var string
+     * An engine instance.
+     * @var engine
      */
-    protected $url = null;
-
-    /**
-     * The schema URL.
-     * @var string
-     */
-    protected $schemaurl = null;
+    protected $engine = null;
 
     /**
      * Constructor.
@@ -78,23 +72,11 @@ class schema {
             throw new \moodle_exception('missingconfig', 'search_solr');
         }
 
-        $this->curl = new \curl();
+        $this->engine = new engine();
+        $this->curl = $this->engine->get_curl_object();
 
         // HTTP headers.
         $this->curl->setHeader('Content-type: application/json');
-        if (!empty($this->config->server_username) && !empty($this->config->server_password)) {
-            $authorization = $this->config->server_username . ':' . $this->config->server_password;
-            $this->curl->setHeader('Authorization', 'Basic ' . base64_encode($authorization));
-        }
-
-        $this->url = rtrim($this->config->server_hostname, '/');
-        if (!empty($this->config->server_port)) {
-            $this->url .= ':' . $this->config->server_port;
-        }
-        $this->url .= '/solr/' . $this->config->indexname;
-        $this->schemaurl = $this->url . '/schema';
-
-
     }
 
     /**
@@ -139,7 +121,8 @@ class schema {
     protected function check_index() {
 
         // Check that the server is available and the index exists.
-        $result = $this->curl->get($this->url . '/select?wt=json');
+        $url = $this->engine->get_connection_url('/select?wt=json');
+        $result = $this->curl->get($url);
         if ($this->curl->error) {
             throw new \moodle_exception('connectionerror', 'search_solr');
         }
@@ -167,6 +150,8 @@ class schema {
             $this->validate_fields($fields, false);
         }
 
+        $url = $this->engine->get_connection_url('/schema');
+
         // Add all fields.
         foreach ($fields as $fieldname => $data) {
 
@@ -183,7 +168,7 @@ class schema {
                     'indexed' => $data['indexed']
                 )
             );
-            $results = $this->curl->post($this->schemaurl, json_encode($params));
+            $results = $this->curl->post($url, json_encode($params));
 
             // We only validate if we are interested on it.
             if ($checkexisting) {
@@ -209,7 +194,8 @@ class schema {
         global $CFG;
 
         foreach ($fields as $fieldname => $data) {
-            $results = $this->curl->get($this->schemaurl . '/fields/' . $fieldname);
+            $url = $this->engine->get_connection_url('/schema/fields/' . $fieldname);
+            $results = $this->curl->get($url);
 
             if ($this->curl->error) {
                 throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
index 9869aa5..5633931 100644 (file)
@@ -63,11 +63,9 @@ $string['solrsslcainfo'] = 'SSL CA certificates name';
 $string['solrsslcainfo_desc'] = 'File name holding one or more CA certificates to verify peer with';
 $string['solrsslcapath'] = 'SSL CA certificates path';
 $string['solrsslcapath_desc'] = 'Directory path holding multiple CA certificates to verify peer with';
-$string['solrsslcert'] = 'SSL key & certificate';
-$string['solrsslcert_desc'] = 'File name to a PEM-formatted private key + private certificate (concatenated in that order)';
-$string['solrsslcertonly'] = 'SSL certificate';
-$string['solrsslcertonly_desc'] = 'File name to a PEM-formatted private certificate only';
+$string['solrsslcert'] = 'SSL certificate';
+$string['solrsslcert_desc'] = 'File name to a PEM-formatted private certificate';
 $string['solrsslkey'] = 'SSL key';
 $string['solrsslkey_desc'] = 'File name to a PEM-formatted private key';
-$string['solrsslkeypassword'] = 'SSL Key password';
+$string['solrsslkeypassword'] = 'SSL key password';
 $string['solrsslkeypassword_desc'] = 'Password for PEM-formatted private key file';
index 995005f..32b409e 100644 (file)
@@ -42,7 +42,6 @@ if ($ADMIN->fulltree) {
             $settings->add(new admin_setting_configtext('search_solr/server_password', new lang_string('solrauthpassword', 'search_solr'), '', '', PARAM_RAW));
             $settings->add(new admin_setting_configtext('search_solr/server_timeout', new lang_string('solrhttpconnectiontimeout', 'search_solr'), new lang_string('solrhttpconnectiontimeout_desc', 'search_solr'), 30, PARAM_INT));
             $settings->add(new admin_setting_configtext('search_solr/ssl_cert', new lang_string('solrsslcert', 'search_solr'), new lang_string('solrsslcert_desc', 'search_solr'), '', PARAM_RAW));
-            $settings->add(new admin_setting_configtext('search_solr/ssl_cert_only', new lang_string('solrsslcertonly', 'search_solr'), new lang_string('solrsslcertonly_desc', 'search_solr'), '', PARAM_RAW));
             $settings->add(new admin_setting_configtext('search_solr/ssl_key', new lang_string('solrsslkey', 'search_solr'), new lang_string('solrsslkey_desc', 'search_solr'), '', PARAM_RAW));
             $settings->add(new admin_setting_configtext('search_solr/ssl_keypassword', new lang_string('solrsslkeypassword', 'search_solr'), new lang_string('solrsslkeypassword_desc', 'search_solr'), '', PARAM_RAW));
             $settings->add(new admin_setting_configtext('search_solr/ssl_cainfo', new lang_string('solrsslcainfo', 'search_solr'), new lang_string('solrsslcainfo_desc', 'search_solr'), '', PARAM_RAW));
index e99b829..bc364eb 100644 (file)
  * Optional params:
  * - define('TEST_SEARCH_SOLR_USERNAME', '');
  * - define('TEST_SEARCH_SOLR_PASSWORD', '');
+ * - define('TEST_SEARCH_SOLR_SSLCERT', '');
+ * - define('TEST_SEARCH_SOLR_SSLKEY', '');
+ * - define('TEST_SEARCH_SOLR_KEYPASSWORD', '');
+ * - define('TEST_SEARCH_SOLR_CAINFOCERT', '');
  *
  * @package     core_search
  * @category    phpunit
@@ -71,13 +75,31 @@ class search_solr_engine_testcase extends advanced_testcase {
         set_config('indexname', TEST_SEARCH_SOLR_INDEXNAME, 'search_solr');
 
         if (defined('TEST_SEARCH_SOLR_USERNAME')) {
-            set_config('server_username', TEST_SEARCH_SOLR_USERNAME);
+            set_config('server_username', TEST_SEARCH_SOLR_USERNAME, 'search_solr');
         }
 
         if (defined('TEST_SEARCH_SOLR_PASSWORD')) {
-            set_config('server_password', TEST_SEARCH_SOLR_PASSWORD);
+            set_config('server_password', TEST_SEARCH_SOLR_PASSWORD, 'search_solr');
         }
 
+        if (defined('TEST_SEARCH_SOLR_SSLCERT')) {
+            set_config('secure', true, 'search_solr');
+            set_config('ssl_cert', TEST_SEARCH_SOLR_SSLCERT, 'search_solr');
+        }
+
+        if (defined('TEST_SEARCH_SOLR_SSLKEY')) {
+            set_config('ssl_key', TEST_SEARCH_SOLR_SSLKEY, 'search_solr');
+        }
+
+        if (defined('TEST_SEARCH_SOLR_KEYPASSWORD')) {
+            set_config('ssl_keypassword', TEST_SEARCH_SOLR_KEYPASSWORD, 'search_solr');
+        }
+
+        if (defined('TEST_SEARCH_SOLR_CAINFOCERT')) {
+            set_config('ssl_cainfo', TEST_SEARCH_SOLR_CAINFOCERT, 'search_solr');
+        }
+
+
         // Inject search solr engine into the testable core search as we need to add the mock
         // search component to it.
         $searchengine = new \search_solr\engine();
index c1d104b..d51f72f 100644 (file)
@@ -42,6 +42,7 @@ $search       = optional_param('search', '', PARAM_RAW); // Make sure it is proc
 $roleid       = optional_param('roleid', 0, PARAM_INT); // Optional roleid, 0 means all enrolled users (or all on the frontpage).
 $contextid    = optional_param('contextid', 0, PARAM_INT); // One of this or.
 $courseid     = optional_param('id', 0, PARAM_INT); // This are required.
+$selectall    = optional_param('selectall', false, PARAM_BOOL); // When rendering checkboxes against users mark them all checked.
 
 $PAGE->set_url('/user/index.php', array(
         'page' => $page,
@@ -713,7 +714,12 @@ if ($mode === MODE_USERDETAILS) {    // Print simple listing.
                 $row->cells[2]->text .= implode('', $links);
 
                 if ($bulkoperations) {
-                    $row->cells[2]->text .= '<br /><input type="checkbox" class="usercheckbox" name="user'.$user->id.'" /> ';
+                    if ($selectall) {
+                        $checked = 'checked="true"';
+                    } else {
+                        $checked = '';
+                    }
+                    $row->cells[2]->text .= '<br /><input type="checkbox" class="usercheckbox" name="user'.$user->id.'" ' .$checked .'/> ';
                 }
                 $table->data = array($row);
                 echo html_writer::table($table);
@@ -767,7 +773,12 @@ if ($mode === MODE_USERDETAILS) {    // Print simple listing.
 
             $data = array();
             if ($bulkoperations) {
-                $data[] = '<input type="checkbox" class="usercheckbox" name="user'.$user->id.'" />';
+                if ($selectall) {
+                    $checked = 'checked="true"';
+                } else {
+                    $checked = '';
+                }
+                $data[] = '<input type="checkbox" class="usercheckbox" name="user'.$user->id.'" ' . $checked .'/>';
             }
             $data[] = $OUTPUT->user_picture($user, array('size' => 35, 'courseid' => $course->id));
             $data[] = $profilelink;
@@ -797,7 +808,27 @@ if ($mode === MODE_USERDETAILS) {    // Print simple listing.
 
 if ($bulkoperations) {
     echo '<br /><div class="buttons">';
-    echo '<input type="button" id="checkall" value="'.get_string('selectall').'" /> ';
+
+    if ($matchcount > 0 && $perpage < $matchcount) {
+        $perpageurl = clone($baseurl);
+        $perpageurl->remove_params('perpage');
+        $perpageurl->param('perpage', SHOW_ALL_PAGE_SIZE);
+        $perpageurl->param('selectall', true);
+        $showalllink = $perpageurl;
+    } else {
+        $showalllink = false;
+    }
+
+    if ($perpage < $matchcount) {
+        // Select all users, refresh page showing all users and mark them all selected.
+        $label = get_string('selectalluserswithcount', 'moodle', $matchcount);
+        echo '<input type="button" id="checkall" value="' . $label . '" data-showallink="' . $showalllink . '" /> ';
+        // Select all users, mark all users on page as selected.
+        echo '<input type="button" id="checkallonpage" value="' . get_string('selectallusersonpage') . '" /> ';
+    } else {
+        echo '<input type="button" id="checkallonpage" value="' . get_string('selectall') . '" /> ';
+    }
+
     echo '<input type="button" id="checknone" value="'.get_string('deselectall').'" /> ';
     $displaylist = array();
     $displaylist['messageselect.php'] = get_string('messageselectadd');
index 324a2af..98c9040 100644 (file)
@@ -21,10 +21,17 @@ M.core_user.init_participation = function(Y) {
        }, '#formactionid');
 
     Y.on('click', function(e) {
+        // Presence of a show all link indicates we should redirect to
+        // a page with all users listed and checked, otherwise just check
+        // those already shown.
+        var showallink = this.getAttribute('data-showallink');
+        if (showallink) {
+            window.location = showallink;
+        }
         Y.all('input.usercheckbox').each(function() {
             this.set('checked', 'checked');
         });
-    }, '#checkall');
+    }, '#checkall, #checkallonpage');
 
     Y.on('click', function(e) {
         Y.all('input.usercheckbox').each(function() {
diff --git a/user/tests/behat/view_participants.feature b/user/tests/behat/view_participants.feature
new file mode 100644 (file)
index 0000000..c21ca02
--- /dev/null
@@ -0,0 +1,204 @@
+@core @core_user
+Feature: View course participants
+  In order to know who is on a course
+  As a teacher
+  I need to be able to view the participants on a course
+
+  Background:
+    Given the following "users" exist:
+      | username  | firstname | lastname | email                 |
+      | teacher1  | Teacher   | 1        | teacher1@example.com  |
+      | student1  | Student   | 1        | student1@example.com  |
+      | student2  | Student   | 2        | student2@example.com  |
+      | student3  | Student   | 3        | student3@example.com  |
+      | student4  | Student   | 4        | student4@example.com  |
+      | student5  | Student   | 5        | student5@example.com  |
+      | student6  | Student   | 6        | student6@example.com  |
+      | student7  | Student   | 7        | student7@example.com  |
+      | student8  | Student   | 8        | student8@example.com  |
+      | student9  | Student   | 9        | student9@example.com  |
+      | student10 | Student   | 10       | student10@example.com |
+      | student11 | Student   | 11       | student11@example.com |
+      | student12 | Student   | 12       | student12@example.com |
+      | student13 | Student   | 13       | student13@example.com |
+      | student14 | Student   | 14       | student14@example.com |
+      | student15 | Student   | 15       | student15@example.com |
+      | student16 | Student   | 16       | student16@example.com |
+      | student17 | Student   | 17       | student17@example.com |
+      | student18 | Student   | 18       | student18@example.com |
+      | student19 | Student   | 19       | student19@example.com |
+      | student20 | Student   | 20       | student20@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1        | topics |
+    And the following "course enrolments" exist:
+      | user      | course | role           |
+      | teacher1  | C1     | editingteacher |
+      | student1  | C1     | student        |
+      | student2  | C1     | student        |
+      | student3  | C1     | student        |
+      | student4  | C1     | student        |
+      | student5  | C1     | student        |
+      | student6  | C1     | student        |
+      | student7  | C1     | student        |
+      | student8  | C1     | student        |
+      | student9  | C1     | student        |
+      | student10 | C1     | student        |
+      | student11 | C1     | student        |
+      | student12 | C1     | student        |
+      | student13 | C1     | student        |
+      | student14 | C1     | student        |
+      | student15 | C1     | student        |
+      | student16 | C1     | student        |
+      | student17 | C1     | student        |
+      | student18 | C1     | student        |
+      | student19 | C1     | student        |
+
+  @javascript
+  Scenario: Use select and deselect all buttons
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Participants"
+    And I press "Select all"
+    Then the field with xpath "//tbody//tr[1]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[2]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[3]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[4]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[5]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[6]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[7]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[8]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[9]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[10]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[11]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[12]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[13]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[14]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[15]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[16]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[17]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[18]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[19]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[20]//input[@class='usercheckbox']" matches value "1"
+
+    And I press "Deselect all"
+    Then the field with xpath "//tbody//tr[1]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[2]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[3]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[4]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[5]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[6]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[7]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[8]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[9]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[10]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[11]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[12]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[13]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[14]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[15]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[16]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[17]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[18]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[19]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[20]//input[@class='usercheckbox']" matches value "0"
+
+  @javascript
+  Scenario: Use select all users on this page, select all n users and deselect all
+    And the following "course enrolments" exist:
+      | user      | course | role    |
+      | student20 | C1     | student |
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Participants"
+    And I press "Select all users on this page"
+    Then I should not see "Student 11"
+    Then the field with xpath "//tbody//tr[1]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[2]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[3]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[4]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[5]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[6]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[7]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[8]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[9]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[10]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[11]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[12]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[13]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[14]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[15]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[16]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[17]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[18]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[19]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[20]//input[@class='usercheckbox']" matches value "1"
+
+    And I press "Deselect all"
+    Then the field with xpath "//tbody//tr[1]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[2]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[3]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[4]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[5]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[6]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[7]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[8]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[9]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[10]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[11]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[12]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[13]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[14]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[15]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[16]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[17]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[18]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[19]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[20]//input[@class='usercheckbox']" matches value "0"
+
+    And I press "Select all 21 users"
+    Then I should see "Student 11"
+    Then the field with xpath "//tbody//tr[1]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[2]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[3]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[4]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[5]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[6]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[7]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[8]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[9]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[10]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[11]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[12]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[13]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[14]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[15]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[16]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[17]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[18]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[19]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[20]//input[@class='usercheckbox']" matches value "1"
+    Then the field with xpath "//tbody//tr[21]//input[@class='usercheckbox']" matches value "1"
+
+    And I press "Deselect all"
+    Then the field with xpath "//tbody//tr[1]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[2]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[3]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[4]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[5]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[6]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[7]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[8]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[9]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[10]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[11]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[12]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[13]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[14]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[15]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[16]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[17]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[18]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[19]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[20]//input[@class='usercheckbox']" matches value "0"
+    Then the field with xpath "//tbody//tr[21]//input[@class='usercheckbox']" matches value "0"
\ No newline at end of file
index 89dd620..2b2ee2b 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016031700.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016031700.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.