MDL-69559 course: Add course content download UI and execution page
authorMichael Hawkins <michaelh@moodle.com>
Thu, 3 Sep 2020 05:55:30 +0000 (13:55 +0800)
committerMichael Hawkins <michaelh@moodle.com>
Mon, 26 Oct 2020 02:19:39 +0000 (10:19 +0800)
course/amd/build/downloadcontent.min.js [new file with mode: 0644]
course/amd/build/downloadcontent.min.js.map [new file with mode: 0644]
course/amd/src/downloadcontent.js [new file with mode: 0644]
course/classes/output/content_export_link.php [new file with mode: 0644]
course/downloadcontent.php [new file with mode: 0644]
course/view.php
lang/en/course.php
lib/navigationlib.php

diff --git a/course/amd/build/downloadcontent.min.js b/course/amd/build/downloadcontent.min.js
new file mode 100644 (file)
index 0000000..8e99320
Binary files /dev/null and b/course/amd/build/downloadcontent.min.js differ
diff --git a/course/amd/build/downloadcontent.min.js.map b/course/amd/build/downloadcontent.min.js.map
new file mode 100644 (file)
index 0000000..29cc63d
Binary files /dev/null and b/course/amd/build/downloadcontent.min.js.map differ
diff --git a/course/amd/src/downloadcontent.js b/course/amd/src/downloadcontent.js
new file mode 100644 (file)
index 0000000..67a6d83
--- /dev/null
@@ -0,0 +1,125 @@
+// 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/>.
+
+/**
+ * Functions related to downloading course content.
+ *
+ * @module     core_course/downloadcontent
+ * @package    core_course
+ * @copyright  2020 Michael Hawkins <michaelh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import Config from 'core/config';
+import CustomEvents from 'core/custom_interaction_events';
+import * as ModalFactory from 'core/modal_factory';
+import jQuery from 'jquery';
+import Pending from 'core/pending';
+
+/**
+ * Set up listener to trigger the download course content modal.
+ *
+ * @return {void}
+ */
+export const init = () => {
+    const pendingPromise = new Pending();
+
+    document.addEventListener('click', (e) => {
+        const downloadModalTrigger = e.target.closest('[data-downloadcourse]');
+
+        if (downloadModalTrigger) {
+            e.preventDefault();
+            displayDownloadConfirmation(downloadModalTrigger);
+        }
+    });
+
+    pendingPromise.resolve();
+};
+
+/**
+ * Display the download course content modal.
+ *
+ * @method displayDownloadConfirmation
+ * @param {Object} downloadModalTrigger The DOM element that triggered the download modal.
+ * @return {void}
+ */
+const displayDownloadConfirmation = (downloadModalTrigger) => {
+    ModalFactory.create({
+        title: downloadModalTrigger.dataset.downloadTitle,
+        type: ModalFactory.types.SAVE_CANCEL,
+        body: `<p>${downloadModalTrigger.dataset.downloadBody}</p>`,
+        buttons: {
+            save: downloadModalTrigger.dataset.downloadButtonText
+        },
+        templateContext: {
+            classes: 'downloadcoursecontentmodal'
+        }
+    })
+    .then(modal => {
+        // Display the modal.
+        modal.show();
+
+        const saveButton = document.querySelector('.modal .downloadcoursecontentmodal [data-action="save"]');
+        const cancelButton = document.querySelector('.modal .downloadcoursecontentmodal [data-action="cancel"]');
+        const modalContainer = document.querySelector('.modal[data-region="modal-container"]');
+
+        // Create listener to trigger the download when the "Download" button is pressed.
+        jQuery(saveButton).on(CustomEvents.events.activate, (e) => downloadContent(e, downloadModalTrigger, modal));
+
+        // Create listener to destroy the modal when closing modal by cancelling.
+        jQuery(cancelButton).on(CustomEvents.events.activate, () => {
+            modal.destroy();
+        });
+
+        // Create listener to destroy the modal when closing modal by clicking outside of it.
+        if (modalContainer.querySelector('.downloadcoursecontentmodal')) {
+            jQuery(modalContainer).on(CustomEvents.events.activate, () => {
+                modal.destroy();
+            });
+        }
+    });
+};
+
+/**
+ * Trigger downloading of course content.
+ *
+ * @method downloadContent
+ * @param {Event} e The event triggering the download.
+ * @param {Object} downloadModalTrigger The DOM element that triggered the download modal.
+ * @param {Object} modal The modal object.
+ * @return {void}
+ */
+const downloadContent = (e, downloadModalTrigger, modal) => {
+    e.preventDefault();
+
+    // Create a form to submit the file download request, so we can avoid sending sesskey over GET.
+    const downloadForm = document.createElement('form');
+    downloadForm.action = downloadModalTrigger.dataset.downloadLink;
+    downloadForm.method = 'POST';
+    // Open download in a new tab, so current course view is not disrupted.
+    downloadForm.target = '_blank';
+    const downloadSesskey = document.createElement('input');
+    downloadSesskey.name = 'sesskey';
+    downloadSesskey.value = Config.sesskey;
+    downloadForm.appendChild(downloadSesskey);
+    downloadForm.style.display = 'none';
+
+    document.body.appendChild(downloadForm);
+    downloadForm.submit();
+    document.body.removeChild(downloadForm);
+
+    // Destroy the modal to prevent duplicates if reopened later.
+    modal.destroy();
+};
diff --git a/course/classes/output/content_export_link.php b/course/classes/output/content_export_link.php
new file mode 100644 (file)
index 0000000..6b3deea
--- /dev/null
@@ -0,0 +1,60 @@
+<?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/>.
+
+/**
+ * Prepares content for buttons/links to course content export/download.
+ *
+ * @package   core_course
+ * @copyright 2020 Michael Hawkins <michaelh@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_course\output;
+
+/**
+ * Prepares content for buttons/links to course content export/download.
+ *
+ * @package   core_course
+ * @copyright 2020 Michael Hawkins <michaelh@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class content_export_link {
+
+    /**
+     * Prepare and return the various attributes required for a link/button to populate/trigger the download course content modal.
+     *
+     * @param \context $context The context of the content being exported.
+     * @return stdClass
+     */
+    public static function get_attributes(\context $context): \stdClass {
+        global $CFG;
+        $downloadattr = new \stdClass();
+        $downloadattr->url = new \moodle_url('/course/downloadcontent.php', ['contextid' => $context->id]);
+        $downloadattr->displaystring = get_string('downloadcoursecontent', 'course');
+        $maxfilesize = display_size($CFG->maxsizeperdownloadcoursefile);
+        $downloadlink = new \moodle_url('/course/downloadcontent.php', ['contextid' => $context->id, 'download' => 1]);
+
+        $downloadattr->elementattributes = [
+            'data-downloadcourse' => 1,
+            'data-download-body' => get_string('downloadcourseconfirmation', 'course', $maxfilesize),
+            'data-download-button-text' => get_string('download'),
+            'data-download-link' => $downloadlink->out(false),
+            'data-download-title' => get_string('downloadcoursecontent', 'course'),
+        ];
+
+        return $downloadattr;
+    }
+}
diff --git a/course/downloadcontent.php b/course/downloadcontent.php
new file mode 100644 (file)
index 0000000..7813a0f
--- /dev/null
@@ -0,0 +1,94 @@
+<?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/>.
+
+/**
+ * Download course content confirmation and execution.
+ *
+ * @package    core
+ * @subpackage course
+ * @copyright  2020 Michael Hawkins <michaelh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../config.php');
+
+use core\content;
+use core\content\export\zipwriter;
+
+$contextid = required_param('contextid', PARAM_INT);
+$isdownload = optional_param('download', 0, PARAM_BOOL);
+$coursecontext = context::instance_by_id($contextid);
+$courseid = $coursecontext->instanceid;
+$courselink = new moodle_url('/course/view.php', ['id' => $courseid]);
+
+if (!\core\content::can_export_context($coursecontext, $USER)) {
+    redirect($courselink);
+}
+
+$PAGE->set_url('/course/downloadcontent.php', ['contextid' => $contextid]);
+require_login($courseid);
+
+$courseinfo = get_fast_modinfo($courseid)->get_course();
+$filename = str_replace('/', '', str_replace(' ', '_', $courseinfo->shortname)) . '_' . time() . '.zip';
+
+// If download confirmed, prepare and start the zipstream of the course download content.
+if ($isdownload) {
+    confirm_sesskey();
+
+    $exportoptions = null;
+
+    if (!empty($CFG->maxsizeperdownloadcoursefile)) {
+        $exportoptions = new stdClass();
+        $exportoptions->maxfilesize = $CFG->maxsizeperdownloadcoursefile;
+    }
+
+    // Use file writer in debug developer mode, so any errors can be displayed instead of being streamed into the output file.
+    if (debugging('', DEBUG_DEVELOPER)) {
+        $writer = zipwriter::get_file_writer($filename, $exportoptions);
+
+        ob_start();
+        content::export_context($coursecontext, $USER, $writer);
+        $content = ob_get_clean();
+
+        // If no errors found, output the file.
+        if (empty($content)) {
+            send_file($writer->get_file_path(), $filename);
+            redirect($courselink);
+        } else {
+            // If any errors occurred, display them instead of outputting the file.
+            debugging("Errors found while producing the download course content output:\n {$content}", DEBUG_DEVELOPER);
+        }
+    } else {
+        // If not developer debugging, stream the output file directly.
+        $writer = zipwriter::get_stream_writer($filename, $exportoptions);
+        content::export_context($coursecontext, $USER, $writer);
+
+        redirect($courselink);
+    }
+
+} else {
+    $PAGE->set_title(get_string('downloadcoursecontent', 'course'));
+    $PAGE->set_heading(format_string($courseinfo->fullname));
+
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading(get_string('downloadcoursecontent', 'course'));
+
+    // Prepare download confirmation information and display it.
+    $maxfilesize = display_size($CFG->maxsizeperdownloadcoursefile);
+    $downloadlink = new moodle_url('/course/downloadcontent.php', ['contextid' => $contextid, 'download' => 1]);
+
+    echo $OUTPUT->confirm(get_string('downloadcourseconfirmation', 'course', $maxfilesize), $downloadlink, $courselink);
+}
index 611ed6c..80b7026 100644 (file)
         $PAGE->requires->js_init_call('M.core_completion.init');
     }
 
         $PAGE->requires->js_init_call('M.core_completion.init');
     }
 
+    // Determine whether the user has permission to download course content.
+    $candownloadcourse = \core\content::can_export_context($context, $USER);
+
     // We are currently keeping the button here from 1.x to help new teachers figure out
     // what to do, even though the link also appears in the course admin block.  It also
     // means you can back out of a situation where you removed the admin block. :)
     if ($PAGE->user_allowed_editing()) {
         $buttons = $OUTPUT->edit_button($PAGE->url);
         $PAGE->set_button($buttons);
     // We are currently keeping the button here from 1.x to help new teachers figure out
     // what to do, even though the link also appears in the course admin block.  It also
     // means you can back out of a situation where you removed the admin block. :)
     if ($PAGE->user_allowed_editing()) {
         $buttons = $OUTPUT->edit_button($PAGE->url);
         $PAGE->set_button($buttons);
+    } else if ($candownloadcourse) {
+        // Show the download course content button if user has permission to access it.
+        // Only showing this if user doesn't have edit rights, since those who do will access it via the actions menu.
+        $buttonattr = \core_course\output\content_export_link::get_attributes($context);
+        $button = new single_button($buttonattr->url, $buttonattr->displaystring, 'post', false, $buttonattr->elementattributes);
+        $PAGE->set_button($OUTPUT->render($button));
     }
 
     // If viewing a section, make the title more specific
     }
 
     // If viewing a section, make the title more specific
     // Include course AJAX
     include_course_ajax($course, $modnamesused);
 
     // Include course AJAX
     include_course_ajax($course, $modnamesused);
 
+    // If available, include the JS to prepare the download course content modal.
+    if ($candownloadcourse) {
+        $PAGE->requires->js_call_amd('core_course/downloadcontent', 'init');
+    }
+
     echo $OUTPUT->footer();
     echo $OUTPUT->footer();
index 506ade4..a4e069d 100644 (file)
@@ -54,6 +54,7 @@ $string['customfield_visibility_help'] = 'This setting determines who can view t
 $string['customfield_visibletoall'] = 'Everyone';
 $string['customfield_visibletoteachers'] = 'Teachers';
 $string['customfieldsettings'] = 'Common course custom fields settings';
 $string['customfield_visibletoall'] = 'Everyone';
 $string['customfield_visibletoteachers'] = 'Teachers';
 $string['customfieldsettings'] = 'Common course custom fields settings';
+$string['downloadcourseconfirmation'] = 'You are about to download a zip file of course content (excluding items which cannot be downloaded and any files larger than {$a}).';
 $string['downloadcoursecontent'] = 'Download course content';
 $string['downloadcoursecontent_help'] = 'This setting determines whether course content may be downloaded by users with the download course content capability (by default users with the role of student or teacher).';
 $string['enabledownloadcoursecontent'] = 'Enable download course content';
 $string['downloadcoursecontent'] = 'Download course content';
 $string['downloadcoursecontent_help'] = 'This setting determines whether course content may be downloaded by users with the download course content capability (by default users with the role of student or teacher).';
 $string['enabledownloadcoursecontent'] = 'Enable download course content';
index f656e8b..d579e8e 100644 (file)
@@ -4448,7 +4448,7 @@ class settings_navigation extends navigation_node {
      * @return navigation_node|false
      */
     protected function load_course_settings($forceopen = false) {
      * @return navigation_node|false
      */
     protected function load_course_settings($forceopen = false) {
-        global $CFG;
+        global $CFG, $USER;
         require_once($CFG->dirroot . '/course/lib.php');
 
         $course = $this->page->course;
         require_once($CFG->dirroot . '/course/lib.php');
 
         $course = $this->page->course;
@@ -4605,6 +4605,16 @@ class settings_navigation extends navigation_node {
             }
         }
 
             }
         }
 
+        // Prepare data for course content download functionality if it is enabled.
+        // Will only be included here if the action menu is already in use, otherwise a button will be added to the UI elsewhere.
+        if (\core\content::can_export_context($coursecontext, $USER) && !empty($coursenode->get_children_key_list())) {
+            $linkattr = \core_course\output\content_export_link::get_attributes($coursecontext);
+            $actionlink = new action_link($linkattr->url, $linkattr->displaystring, null, $linkattr->elementattributes);
+
+            $coursenode->add($linkattr->displaystring, $actionlink, self::TYPE_SETTING, null, 'download',
+                    new pix_icon('t/download', ''));
+        }
+
         // Return we are done
         return $coursenode;
     }
         // Return we are done
         return $coursenode;
     }