Merge branch 'MDL-59629-master' of git://github.com/peterRd/moodle
authorJun Pataleta <jun@moodle.com>
Mon, 17 Dec 2018 07:57:52 +0000 (15:57 +0800)
committerJun Pataleta <jun@moodle.com>
Mon, 17 Dec 2018 07:57:52 +0000 (15:57 +0800)
31 files changed:
backup/moodle2/restore_stepslib.php
blocks/navigation/block_navigation.php
competency/tests/api_test.php
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/classes/filetypes.php
lib/classes/user.php
lib/tests/user_test.php
lib/upgrade.txt
mod/assign/feedback/editpdf/ajax.php
mod/assign/feedback/editpdf/classes/combined_document.php
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/classes/renderer.php
mod/assign/feedback/editpdf/classes/task/convert_submissions.php
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/styles.css
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/globals.js
mod/assign/feedback/editpdf/yui/src/editor/meta/editor.json
mod/assign/styles.css
question/type/random/classes/task/remove_unused_questions.php [new file with mode: 0644]
question/type/random/db/tasks.php [new file with mode: 0644]
question/type/random/lang/en/qtype_random.php
question/type/random/tests/cleanup_task_test.php [new file with mode: 0644]
question/type/random/version.php
theme/boost/scss/moodle/modules.scss
theme/boost/style/moodle.css
theme/upgrade.txt

index 1e78ee3..859198e 100644 (file)
@@ -4518,6 +4518,12 @@ class restore_create_categories_and_questions extends restore_structure_step {
                 unset($data->idnumber);
             }
 
+            if ($data->qtype === 'random') {
+                // Ensure that this newly created question is considered by
+                // \qtype_random\task\remove_unused_questions.
+                $data->hidden = 0;
+            }
+
             $newitemid = $DB->insert_record('question', $data);
             $this->set_mapping('question', $oldid, $newitemid);
             // Also annotate them as question_created, we need
index e7a5c11..76a4c3f 100644 (file)
@@ -192,13 +192,6 @@ class block_navigation extends block_base {
         if (!empty($this->config->expansionlimit)) {
             $expansionlimit = $this->config->expansionlimit;
         }
-        $arguments = array(
-            'id'             => $this->instance->id,
-            'instance'       => $this->instance->id,
-            'candock'        => $this->instance_can_be_docked(),
-            'courselimit'    => $limit,
-            'expansionlimit' => $expansionlimit
-        );
 
         $options = array();
         $options['linkcategories'] = (!empty($this->config->linkcategories) && $this->config->linkcategories == 'yes');
index 0037152..705a32a 100644 (file)
@@ -2501,6 +2501,10 @@ class core_competency_api_testcase extends advanced_testcase {
         $this->resetAfterTest(true);
         $dg = $this->getDataGenerator();
 
+        // Create and assign a current user.
+        $currentuser = $dg->create_user();
+        $this->setUser($currentuser);
+
         // Create a course.
         $course = $dg->create_course();
         $record = array('courseid' => $course->id, 'pushratingstouserplans' => false);
index 0369c31..e66e5ff 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 58a7fc2..c63e561 100644 (file)
@@ -683,6 +683,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         }
 
         var suggestionsElement = $(document.getElementById(state.suggestionsId));
+        // Remove any click handler first.
+        suggestionsElement.parent().prop("onclick", null).off("click");
         suggestionsElement.parent().on('click', '[role=option]', function(e) {
             var pendingKey = 'form-autocomplete-parent';
             M.util.js_pending(pendingKey);
index 4a84e55..2d05dbe 100644 (file)
@@ -337,16 +337,13 @@ abstract class core_filetypes {
         // Get defaults.
         $mimetypes = self::get_default_types();
 
-        // If there are no custom types, just return.
+        // Get custom file types.
         $custom = self::get_custom_types();
-        if (empty($custom)) {
-            return $mimetypes;
-        }
 
         // Check value is an array.
         if (!is_array($custom)) {
             debugging('Invalid $CFG->customfiletypes (not array)', DEBUG_DEVELOPER);
-            return $mimetypes;
+            $custom = array();
         }
 
         foreach ($custom as $customentry) {
index 09a8053..f95b1a2 100644 (file)
@@ -524,18 +524,17 @@ class core_user {
     }
 
     /**
-     * Return true is user id is greater than self::NOREPLY_USER and
-     * alternatively check db.
+     * Return true if user id is greater than 0 and alternatively check db.
      *
      * @param int $userid user id.
      * @param bool $checkdb if true userid will be checked in db. By default it's false, and
-     *                      userid is compared with NOREPLY_USER for performance.
+     *                      userid is compared with 0 for performance.
      * @return bool true is real user else false.
      */
     public static function is_real_user($userid, $checkdb = false) {
         global $DB;
 
-        if ($userid < 0) {
+        if ($userid <= 0) {
             return false;
         }
         if ($checkdb) {
index a36d2e6..155719d 100644 (file)
@@ -732,4 +732,44 @@ class core_user_testcase extends advanced_testcase {
         $this->assertNotEquals($enuser, $xxuser);
     }
 
+    /**
+     * Test is_real_user method.
+     */
+    public function test_is_real_user() {
+        global $CFG, $USER;
+
+        // Real users are real users.
+        $auser = $this->getDataGenerator()->create_user();
+        $guest = guest_user();
+        $this->assertTrue(\core_user::is_real_user($auser->id));
+        $this->assertTrue(\core_user::is_real_user($auser->id, true));
+        $this->assertTrue(\core_user::is_real_user($guest->id));
+        $this->assertTrue(\core_user::is_real_user($guest->id, true));
+
+        // Non-logged in users are not real users.
+        $this->assertSame(0, $USER->id, 'The non-logged in user should have an ID of 0.');
+        $this->assertFalse(\core_user::is_real_user($USER->id));
+        $this->assertFalse(\core_user::is_real_user($USER->id, true));
+
+        // Other types of logged in users are real users.
+        $this->setAdminUser();
+        $this->assertTrue(\core_user::is_real_user($USER->id));
+        $this->assertTrue(\core_user::is_real_user($USER->id, true));
+        $this->setGuestUser();
+        $this->assertTrue(\core_user::is_real_user($USER->id));
+        $this->assertTrue(\core_user::is_real_user($USER->id, true));
+        $this->setUser($auser);
+        $this->assertTrue(\core_user::is_real_user($USER->id));
+        $this->assertTrue(\core_user::is_real_user($USER->id, true));
+
+        // Fake accounts are not real users.
+        $CFG->noreplyuserid = null;
+        $this->assertFalse(\core_user::is_real_user(core_user::get_noreply_user()->id));
+        $this->assertFalse(\core_user::is_real_user(core_user::get_noreply_user()->id, true));
+        $CFG->supportuserid = null;
+        $CFG->supportemail = 'test@example.com';
+        $this->assertFalse(\core_user::is_real_user(core_user::get_support_user()->id));
+        $this->assertFalse(\core_user::is_real_user(core_user::get_support_user()->id, true));
+    }
+
 }
index 023fab1..1c2f473 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
+=== 3.7 ===
+
+* The method core_user::is_real_user() now returns false for userid = 0 parameter
+
 === 3.6 ===
 
 * A new token-based version of pluginfile.php has been added which can be used for out-of-session file serving by
index 538d920..fa1e21d 100644 (file)
@@ -70,6 +70,7 @@ if ($action === 'pollconversions') {
             'filecount' => 0,
             'pagecount' => 0,
             'pageready' => 0,
+            'partial' => false,
             'pages' => [],
         ];
 
@@ -77,10 +78,13 @@ if ($action === 'pollconversions') {
     $response->status = $combineddocument->get_status();
     $response->filecount = $combineddocument->get_document_count();
 
-    if ($response->status === combined_document::STATUS_READY) {
+    $readystatuslist = [combined_document::STATUS_READY, combined_document::STATUS_READY_PARTIAL];
+    $completestatuslist = [combined_document::STATUS_COMPLETE, combined_document::STATUS_FAILED];
+
+    if (in_array($response->status, $readystatuslist)) {
         $combineddocument = document_services::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
         $response->pagecount = $combineddocument->get_page_count();
-    } else if ($response->status === combined_document::STATUS_COMPLETE || $response->status === combined_document::STATUS_FAILED) {
+    } else if (in_array($response->status, $completestatuslist)) {
         $pages = document_services::get_page_images_for_attempt($assignment,
                                                                 $userid,
                                                                 $attemptnumber,
@@ -95,6 +99,7 @@ if ($action === 'pollconversions') {
         if ($readonly) {
             $filearea = document_services::PAGE_IMAGE_READONLY_FILEAREA;
         }
+        $response->partial = $combineddocument->is_partial_conversion();
 
         foreach ($pages as $id => $pagefile) {
             $index = count($response->pages);
index 08f6163..d757599 100644 (file)
@@ -44,6 +44,11 @@ class combined_document {
      */
     const STATUS_READY = 1;
 
+    /**
+     * Status value representing all documents are ready to be combined as are supported.
+     */
+    const STATUS_READY_PARTIAL = 3;
+
     /**
      * Status value representing a successful conversion.
      */
@@ -76,6 +81,9 @@ class combined_document {
 
     /**
      * Check the current status of the document combination.
+     * Note that the combined document may not contain all the source files if some of the
+     * source files were not able to be converted. An example is an audio file with a pdf cover sheet. Only
+     * the cover sheet will be included in the combined document.
      *
      * @return  int
      */
@@ -97,6 +105,7 @@ class combined_document {
         }
 
         $pending = false;
+        $partial = false;
         foreach ($this->sourcefiles as $file) {
             // The combined file has not yet been generated.
             // Check the status of each source file.
@@ -108,14 +117,19 @@ class combined_document {
                         $pending = true;
                         break;
 
+                    // There are 4 status flags, so the only remaining one is complete which is fine.
                     case \core_files\conversion::STATUS_FAILED:
-                        return self::STATUS_FAILED;
+                        $partial = true;
+                        break;
                 }
             }
         }
         if ($pending) {
             return self::STATUS_PENDING_INPUT;
         } else {
+            if ($partial) {
+                return self::STATUS_READY_PARTIAL;
+            }
             return self::STATUS_READY;
         }
     }
@@ -131,6 +145,20 @@ class combined_document {
         return $this;
     }
 
+    /**
+     * Return true of the combined file contained only some of the submission files.
+     *
+     * @return  boolean
+     */
+    public function is_partial_conversion() {
+        $combinedfile = $this->get_combined_file();
+        if (empty($combinedfile)) {
+            return false;
+        }
+        $filearea = $combinedfile->get_filearea();
+        return $filearea == document_services::PARTIAL_PDF_FILEAREA;
+    }
+
     /**
      * Retrieve the completed combined file.
      *
@@ -210,11 +238,12 @@ class combined_document {
         global $CFG;
 
         $currentstatus = $this->get_status();
+        $readystatuslist = [self::STATUS_READY, self::STATUS_READY_PARTIAL];
         if ($currentstatus === self::STATUS_FAILED) {
             $this->store_empty_document($contextid, $itemid);
 
             return $this;
-        } else if ($currentstatus !== self::STATUS_READY) {
+        } else if (!in_array($currentstatus, $readystatuslist)) {
             // The document is either:
             // * already combined; or
             // * pending input being fully converted; or
@@ -235,7 +264,10 @@ class combined_document {
             // Note: We drop non-compatible files.
             $compatiblepdf = false;
             if (is_a($file, \core_files\conversion::class)) {
-                $compatiblepdf = pdf::ensure_pdf_compatible($file->get_destfile());
+                $status = $file->get('status');
+                if ($status == \core_files\conversion::STATUS_COMPLETE) {
+                    $compatiblepdf = pdf::ensure_pdf_compatible($file->get_destfile());
+                }
             } else {
                 $compatiblepdf = pdf::ensure_pdf_compatible($file);
             }
@@ -270,7 +302,7 @@ class combined_document {
         }
 
         // Store the newly created file as a stored_file.
-        $this->store_combined_file($tmpfile, $contextid, $itemid);
+        $this->store_combined_file($tmpfile, $contextid, $itemid, ($currentstatus == self::STATUS_READY_PARTIAL));
 
         // Note the verified page count.
         $this->pagecount = $verifypagecount;
@@ -295,11 +327,12 @@ class combined_document {
      * @param   string $tmpfile The path to the file on disk to be stored.
      * @param   int $contextid The contextid for the file to be stored under
      * @param   int $itemid The itemid for the file to be stored under
+     * @param   boolean $partial The combined pdf contains only some of the source files.
      * @return  $this
      */
-    protected function store_combined_file($tmpfile, $contextid, $itemid) {
+    protected function store_combined_file($tmpfile, $contextid, $itemid, $partial = false) {
         // Store the file.
-        $record = $this->get_stored_file_record($contextid, $itemid);
+        $record = $this->get_stored_file_record($contextid, $itemid, $partial);
         $fs = get_file_storage();
 
         // Delete existing files first.
@@ -349,12 +382,14 @@ class combined_document {
             return $this->pagecount;
         }
 
-        if ($this->get_status() === self::STATUS_FAILED) {
+        $status = $this->get_status();
+
+        if ($status === self::STATUS_FAILED) {
             // The empty document will be returned.
             return 1;
         }
 
-        if ($this->get_status() !== self::STATUS_COMPLETE) {
+        if ($status !== self::STATUS_COMPLETE) {
             // No pages yet.
             return 0;
         }
@@ -391,13 +426,18 @@ class combined_document {
      *
      * @param   int $contextid The contextid for the file to be stored under
      * @param   int $itemid The itemid for the file to be stored under
+     * @param   boolean $partial The combined file contains only some of the source files.
      * @return  stdClass
      */
-    protected function get_stored_file_record($contextid, $itemid) {
+    protected function get_stored_file_record($contextid, $itemid, $partial = false) {
+        $filearea = document_services::COMBINED_PDF_FILEAREA;
+        if ($partial) {
+            $filearea = document_services::PARTIAL_PDF_FILEAREA;
+        }
         return (object) [
             'contextid' => $contextid,
             'component' => 'assignfeedback_editpdf',
-            'filearea' => document_services::COMBINED_PDF_FILEAREA,
+            'filearea' => $filearea,
             'itemid' => $itemid,
             'filepath' => '/',
             'filename' => document_services::COMBINED_PDF_FILENAME,
index 384ccdd..45b4083 100644 (file)
@@ -42,6 +42,8 @@ class document_services {
     const FINAL_PDF_FILEAREA = 'download';
     /** File area for combined pdf */
     const COMBINED_PDF_FILEAREA = 'combined';
+    /** File area for partial combined pdf */
+    const PARTIAL_PDF_FILEAREA = 'partial';
     /** File area for importing html */
     const IMPORT_HTML_FILEAREA = 'importhtml';
     /** File area for page images */
@@ -261,15 +263,23 @@ EOD;
             $submission = $assignment->get_user_submission($userid, false, $attemptnumber);
         }
 
+
         $contextid = $assignment->get_context()->id;
         $component = 'assignfeedback_editpdf';
         $filearea = self::COMBINED_PDF_FILEAREA;
+        $partialfilearea = self::PARTIAL_PDF_FILEAREA;
         $itemid = $grade->id;
         $filepath = '/';
         $filename = self::COMBINED_PDF_FILENAME;
         $fs = get_file_storage();
 
-        $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename);
+        $partialpdf = $fs->get_file($contextid, $component, $partialfilearea, $itemid, $filepath, $filename);
+        if (!empty($partialpdf)) {
+            $combinedpdf = $partialpdf;
+        } else {
+            $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename);
+        }
+
         if ($combinedpdf && $submission) {
             if ($combinedpdf->get_timemodified() < $submission->timemodified) {
                 // The submission has been updated since the PDF was generated.
@@ -381,6 +391,7 @@ EOD;
 
         $tmpdir = \make_temp_directory('assignfeedback_editpdf/pageimages/' . self::hash($assignment, $userid, $attemptnumber));
         $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
+
         $document->get_combined_file()->copy_content_to($combined); // Copy the file.
 
         $pdf = new pdf();
index 882fdc4..7e1ddb5 100644 (file)
@@ -217,15 +217,14 @@ class assignfeedback_editpdf_renderer extends plugin_renderer_base {
 
         $canvas = html_writer::div($loading, 'drawingcanvas');
         $canvas = html_writer::div($canvas, 'drawingregion');
-        $changesmessage = html_writer::tag('div',
-                                           get_string('draftchangessaved', 'assignfeedback_editpdf'),
-                                           array(
-                                               'class' => 'assignfeedback_editpdf_unsavedchanges warning label label-info'
-                                           ));
-
-        $changesmessage = html_writer::div($changesmessage, 'unsaved-changes');
+        // Place for messages, but no warnings displayed yet.
+        $changesmessage = html_writer::div('', 'warningmessages');
         $canvas .= $changesmessage;
 
+        $infoicon = $this->image_icon('i/info', '');
+        $infomessage = html_writer::div($infoicon, 'infoicon');
+        $canvas .= $infomessage;
+
         $body .= $canvas;
 
         $footer = '';
@@ -268,7 +267,9 @@ class assignfeedback_editpdf_renderer extends plugin_renderer_base {
             'stamp',
             'stamppicker',
             'cannotopenpdf',
-            'pagenumber'
+            'pagenumber',
+            'partialwarning',
+            'draftchangessaved'
         ), 'assignfeedback_editpdf');
 
         return $html;
index 68295fd..be6b0a1 100644 (file)
@@ -102,10 +102,9 @@ class convert_submissions extends scheduled_task {
             foreach ($users as $userid) {
                 try {
                     $combineddocument = document_services::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
-                    $status = $combineddocument->get_status();
-
                     switch ($combineddocument->get_status()) {
                         case combined_document::STATUS_READY:
+                        case combined_document::STATUS_READY_PARTIAL:
                         case combined_document::STATUS_PENDING_INPUT:
                             // The document has not been converted yet or is somehow still ready.
                             continue 2;
index 0dbffba..c6f0186 100644 (file)
@@ -72,6 +72,7 @@ $string['output'] = 'Output:';
 $string['pagenumber'] = 'Page {$a}';
 $string['pagexofy'] = 'Page {$a->page} of {$a->total}';
 $string['pen'] = 'Pen';
+$string['partialwarning'] = 'Some of the files in this submission can only be accessed by direct download.';
 $string['pluginname'] = 'Annotate PDF';
 $string['privacy:metadata:colourpurpose'] = 'Colour of the comment or annotation';
 $string['privacy:metadata:conversionpurpose'] = 'Files are converted to PDFs to allow for annotations.';
index 844c943..6e22ae7 100644 (file)
     cursor: move;
 }
 
+.assignfeedback_editpdf_widget .infoicon {
+    display: none;
+}
+
+.assignfeedback_editpdf_widget .warningmessages {
+    position: absolute;
+    margin-left: 20px;
+    margin-right: 20px;
+    bottom: 20px;
+}
+
 .assignfeedback_editpdf_widget .drawingregion {
     border: 1px solid #ccc;
     left: 1em;
     padding: 0;
 }
 
-.assignfeedback_editpdf_widget .assignfeedback_editpdf_unsavedchanges.haschanges {
-    display: inline-block;
-}
-
-.assignfeedback_editpdf_widget .assignfeedback_editpdf_unsavedchanges {
-    display: none;
-    position: absolute;
-    left: 20px;
-    top: 60px;
-}
-
 .yui3-colourpicker-hidden,
 .yui3-commentsearch-hidden,
 .yui3-commentmenu-hidden {
     margin-right: 0;
     margin-top: 0;
     border-radius: 4px;
+    width: 30px;
 }
 
 .moodle-dialogue-base .moodle-dialogue.assignfeedback_editpdf_dropdown .moodle-dialogue-bd {
index bea07f5..0b406a0 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index bb0f77d..d6986c9 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index bea07f5..0b406a0 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index 878eb49..21e2068 100644 (file)
@@ -483,7 +483,7 @@ EDITOR.prototype = {
                             // The combined document is still waiting for input to be ready.
                             poll = true;
 
-                        } else if (data.status === 1) {
+                        } else if (data.status === 1 || data.status === 3) {
                             // The combine document is ready for conversion into a single PDF.
                             poll = true;
 
@@ -556,6 +556,47 @@ EDITOR.prototype = {
         });
     },
 
+    /**
+     * Display an error in a small part of the page (don't block everything).
+     *
+     * @param string The error text.
+     * @param boolean dismissable Not critical messages can be removed after a short display.
+     * @protected
+     * @method warning
+     */
+    warning: function(message, dismissable) {
+        var icontemplate = this.get_dialogue_element(SELECTOR.ICONMESSAGECONTAINER);
+        var warningregion = this.get_dialogue_element(SELECTOR.WARNINGMESSAGECONTAINER);
+        var delay = 15, duration = 1;
+        var messageclasses = 'assignfeedback_editpdf_warningmessages alert alert-warning';
+        if (dismissable) {
+            delay = 4;
+            messageclasses = 'assignfeedback_editpdf_warningmessages alert alert-info';
+        }
+        var warningelement = Y.Node.create('<div class="' + messageclasses + '" role="alert"></div>');
+
+        // Copy info icon template.
+        warningelement.append(icontemplate.one('*').cloneNode());
+
+        // Append the message.
+        warningelement.append(message);
+
+        // Add the entire warning to the container.
+        warningregion.prepend(warningelement);
+
+        // Remove the message after a short delay.
+        warningelement.transition(
+            {
+                duration: duration,
+                delay: delay,
+                opacity: 0
+            },
+            function() {
+                warningelement.remove();
+            }
+        );
+    },
+
     /**
      * The info about all pages in the pdf has been returned.
      *
@@ -564,7 +605,8 @@ EDITOR.prototype = {
      * @method prepare_pages_for_display
      */
     prepare_pages_for_display: function(data) {
-        var i, j, comment, error;
+        var i, j, comment, error, annotation, readonly;
+
         if (!data.pagecount) {
             if (this.dialogue) {
                 this.dialogue.hide();
@@ -590,11 +632,17 @@ EDITOR.prototype = {
                                                                                  comment.rawtext);
             }
             for (j = 0; j < this.pages[i].annotations.length; j++) {
-                data = this.pages[i].annotations[j];
-                this.pages[i].annotations[j] = this.create_annotation(data.type, data);
+                annotation = this.pages[i].annotations[j];
+                this.pages[i].annotations[j] = this.create_annotation(annotation.type, annotation);
             }
         }
 
+        readonly = this.get('readonly');
+        if (!readonly && data.partial) {
+            // Warn about non converted files, but only for teachers.
+            this.warning(M.util.get_string('partialwarning', 'assignfeedback_editpdf'), false);
+        }
+
         // Update the ui.
         if (this.quicklist) {
             this.quicklist.load();
@@ -1181,6 +1229,7 @@ EDITOR.prototype = {
      * @method save_current_page
      */
     save_current_page: function() {
+        this.clear_warnings(false);
         if (this.get('destroyed')) {
             return;
         }
@@ -1208,16 +1257,9 @@ EDITOR.prototype = {
                         if (jsondata.error) {
                             return new M.core.ajaxException(jsondata);
                         }
+                        // Show warning that we have not saved the feedback.
                         Y.one(SELECTOR.UNSAVEDCHANGESINPUT).set('value', 'true');
-                        Y.one(SELECTOR.UNSAVEDCHANGESDIV).setStyle('opacity', 1);
-                        Y.one(SELECTOR.UNSAVEDCHANGESDIV).setStyle('display', 'inline-block');
-                        Y.one(SELECTOR.UNSAVEDCHANGESDIV).transition({
-                            duration: 1,
-                            delay: 2,
-                            opacity: 0
-                        }, function() {
-                            Y.one(SELECTOR.UNSAVEDCHANGESDIV).setStyle('display', 'none');
-                        });
+                        this.warning(M.util.get_string('draftchangessaved', 'assignfeedback_editpdf'), true);
                     } catch (e) {
                         return new M.core.exception(e);
                     }
@@ -1292,6 +1334,22 @@ EDITOR.prototype = {
         }
     },
 
+    /**
+     * Clear all current warning messages from display.
+     * @protected
+     * @method clear_warnings
+     * @param {Boolean} allwarnings If true, all previous warnings are removed.
+     */
+    clear_warnings: function(allwarnings) {
+        // Remove all warning messages, they may not relate to the current document or page anymore.
+        var warningregion = this.get_dialogue_element(SELECTOR.WARNINGMESSAGECONTAINER);
+        if (allwarnings) {
+            warningregion.empty();
+        } else {
+            warningregion.all('.alert-info').remove(true);
+        }
+    },
+
     /**
      * Load the image for this pdf page and remove the loading icon (if there).
      * @protected
@@ -1322,6 +1380,7 @@ EDITOR.prototype = {
         drawingcanvas.setStyle('backgroundImage', 'url("' + page.url + '")');
         drawingcanvas.setStyle('width', page.width + 'px');
         drawingcanvas.setStyle('height', page.height + 'px');
+        drawingcanvas.scrollIntoView();
 
         // Update page select.
         this.get_dialogue_element(SELECTOR.PAGESELECT).set('selectedIndex', this.currentpage);
@@ -1358,6 +1417,7 @@ EDITOR.prototype = {
         pageselect.removeAttribute('disabled');
         pageselect.on('change', function() {
             this.currentpage = pageselect.get('value');
+            this.clear_warnings(false);
             this.change_page();
         }, this);
 
@@ -1381,6 +1441,7 @@ EDITOR.prototype = {
         if (this.currentpage < 0) {
             this.currentpage = 0;
         }
+        this.clear_warnings(false);
         this.change_page();
     },
 
@@ -1395,6 +1456,7 @@ EDITOR.prototype = {
         if (this.currentpage >= this.pages.length) {
             this.currentpage = this.pages.length - 1;
         }
+        this.clear_warnings(false);
         this.change_page();
     },
 
index f073632..205a456 100644 (file)
@@ -41,7 +41,9 @@ var AJAXBASE = M.cfg.wwwroot + '/mod/assign/feedback/editpdf/ajax.php',
         COMMENTMENU: '.commentdrawable a',
         ANNOTATIONCOLOURBUTTON:  '.annotationcolourbutton',
         DELETEANNOTATIONBUTTON: '.deleteannotationbutton',
-        UNSAVEDCHANGESDIV: '.assignfeedback_editpdf_unsavedchanges',
+        WARNINGMESSAGECONTAINER: '.warningmessages',
+        ICONMESSAGECONTAINER: '.infoicon',
+        UNSAVEDCHANGESDIV: '.assignfeedback_editpdf_warningmessages',
         UNSAVEDCHANGESINPUT: 'input[name="assignfeedback_editpdf_haschanges"]',
         STAMPSBUTTON: '.currentstampbutton',
         DIALOGUE: '.' + CSS.DIALOGUE
index 2f2887d..dd8acf9 100644 (file)
@@ -13,6 +13,7 @@
         "querystring-stringify-simple",
         "moodle-core-notification-dialog",
         "moodle-core-notification-alert",
+        "moodle-core-notification-warning",
         "moodle-core-notification-exception",
         "moodle-core-notification-ajaxexception"
     ]
index b0a3786..3593c73 100644 (file)
     border: 1px solid #ccc;
     border-top: none;
     background-color: #fff;
-    z-index: 99999;
 }
 
 .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon,
     left: auto;
     right: 15px;
     margin: 0;
-    height: 100%;
     line-height: 60px;
 }
 
         border: 1px solid #ccc;
         border-radius: 0 0 4px 0;
         background-color: #fff;
-        z-index: 99999;
     }
 
     .path-mod-assign [data-region="review-panel"] [data-region="review-panel-toggle"] .expand-icon,
diff --git a/question/type/random/classes/task/remove_unused_questions.php b/question/type/random/classes/task/remove_unused_questions.php
new file mode 100644 (file)
index 0000000..08844f8
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * A scheduled task to remove unneeded random questions.
+ *
+ * @package   qtype_random
+ * @category  task
+ * @copyright 2018 Bo Pierce <email.bO.pierce@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace qtype_random\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * A scheduled task to remove unneeded random questions.
+ *
+ * @copyright 2018 Bo Pierce <email.bO.pierce@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class remove_unused_questions extends \core\task\scheduled_task {
+
+    public function get_name() {
+        return get_string('taskunusedrandomscleanup', 'qtype_random');
+    }
+
+    public function execute() {
+        global $DB, $CFG;
+        require_once($CFG->libdir . '/questionlib.php');
+
+        // Find potentially unused random questions (up to 10000).
+        // Note, because we call question_delete_question below,
+        // the question will not actually be deleted if something else
+        // is using them, but nothing else in Moodle core uses qtype_random,
+        // and not many third-party plugins do.
+        $unusedrandomids = $DB->get_records_sql("
+                SELECT q.id, 1
+                  FROM {question} q
+             LEFT JOIN {quiz_slots} qslots ON q.id = qslots.questionid
+                 WHERE qslots.questionid IS NULL
+                   AND q.qtype = ? AND hidden = ?", ['random', 0], 0, 10000);
+
+        $count = 0;
+        foreach ($unusedrandomids as $unusedrandomid => $notused) {
+            question_delete_question($unusedrandomid);
+            // In case the question was not actually deleted (because it was in use somehow
+            // mark it as hidden so the query above will not return it again.
+            $DB->set_field('question', 'hidden', 1, ['id' => $unusedrandomid]);
+            $count += 1;
+        }
+        mtrace('Cleaned up ' . $count . ' unused random questions.');
+    }
+}
diff --git a/question/type/random/db/tasks.php b/question/type/random/db/tasks.php
new file mode 100644 (file)
index 0000000..e6298dc
--- /dev/null
@@ -0,0 +1,38 @@
+<?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/>.
+
+/**
+ * Definition of question/type/random scheduled tasks.
+ *
+ * @package   qtype_random
+ * @category  task
+ * @copyright 2018 Bo Pierce <email.bO.pierce@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = array(
+    array(
+        'classname' => 'qtype_random\task\remove_unused_questions',
+        'blocking' => 0,
+        'minute' => 'R',
+        'hour' => '*',
+        'day' => '*',
+        'month' => '*',
+        'dayofweek' => '*'
+    )
+);
index feade72..67af780 100644 (file)
@@ -45,3 +45,4 @@ $string['randomqplusnamesystemtags'] = 'Random (Any system-level category, tags:
 $string['randomqplusnametags'] = 'Random ({$a->category} and subcategories, tags: {$a->tags})';
 $string['selectedby'] = '{$a->questionname} selected by {$a->randomname}';
 $string['selectmanualquestions'] = 'Random questions can use manually graded questions';
+$string['taskunusedrandomscleanup'] = 'Remove unused random questions';
diff --git a/question/type/random/tests/cleanup_task_test.php b/question/type/random/tests/cleanup_task_test.php
new file mode 100644 (file)
index 0000000..a8f43b0
--- /dev/null
@@ -0,0 +1,70 @@
+<?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/>.
+
+/**
+ * Tests of the scheduled task for cleaning up random questions.
+ *
+ * @package    qtype_random
+ * @copyright  2018 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+
+
+/**
+ * Tests of the scheduled task for cleaning up random questions.
+ *
+ * @copyright  2018 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_random_cleanup_task_testcase extends advanced_testcase {
+
+    public function test_cleanup_task_removes_unused_question() {
+        global $DB;
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $questiongenerator = $generator->get_plugin_generator('core_question');
+        $quizgenerator = $generator->get_plugin_generator('mod_quiz');
+        $cat = $questiongenerator->create_question_category();
+        $quiz = $quizgenerator->create_instance(['course' => SITEID]);
+
+        // Add two random questions.
+        quiz_add_random_questions($quiz, 0, $cat->id, 2, false);
+        $quizslots = $DB->get_records('quiz_slots', ['quizid' => $quiz->id],
+                'slot', 'slot, id, questionid');
+
+        // Now remove the second from the quiz. (Do it manually,
+        // because the API cleans up the random question, but we are trying to
+        // create an orphaned random question.)
+        $DB->delete_records('quiz_slots', array('id' => $quizslots[2]->id));
+
+        // Run the scheduled task.
+        $task = new \qtype_random\task\remove_unused_questions();
+        $this->expectOutputString("Cleaned up 1 unused random questions.\n");
+        $task->execute();
+
+        // Verify.
+        $this->assertTrue($DB->record_exists('question', ['id' => $quizslots[1]->questionid]));
+        $this->assertFalse($DB->record_exists('question', ['id' => $quizslots[2]->questionid]));
+    }
+}
index 409383b..36c920a 100644 (file)
@@ -26,7 +26,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'qtype_random';
-$plugin->version   = 2018120300;
+$plugin->version   = 2018120301;
 
 $plugin->requires  = 2018112800;
 
index d3f11d6..799df13 100644 (file)
@@ -642,8 +642,8 @@ $popout-header-height: 4rem;
 }
 
 .assignfeedback_editpdf_widget .label {
-    @extend .tag;
-    @extend .tag-info;
+    @extend .alert;
+    @extend .alert-info;
 }
 
 .assignfeedback_editpdf_menu {
index d63018c..ea95acf 100644 (file)
@@ -5792,7 +5792,7 @@ tbody.collapse.show {
 #page-admin-index .adminwarning,
 #page-admin-index .adminerror, .que .formulation,
 .que .outcome,
-.que .comment {
+.que .comment, .assignfeedback_editpdf_widget .label {
   position: relative;
   padding: 0.75rem 1.25rem;
   margin-bottom: 1rem;
@@ -5841,13 +5841,13 @@ tbody.collapse.show {
   .alert-success .alert-link, .que .comment .alert-link {
     color: #1f3e1f; }
 
-.alert-info, #page-admin-index .releasenoteslink, #adminsettings .form-overridden, .que .formulation {
+.alert-info, #page-admin-index .releasenoteslink, #adminsettings .form-overridden, .que .formulation, .assignfeedback_editpdf_widget .label {
   color: #2f6473;
   background-color: #def2f8;
   border-color: #d1edf6; }
-  .alert-info hr, #page-admin-index .releasenoteslink hr, #adminsettings .form-overridden hr, .que .formulation hr {
+  .alert-info hr, #page-admin-index .releasenoteslink hr, #adminsettings .form-overridden hr, .que .formulation hr, .assignfeedback_editpdf_widget .label hr {
     border-top-color: #bce5f2; }
-  .alert-info .alert-link, #page-admin-index .releasenoteslink .alert-link, #adminsettings .form-overridden .alert-link, .que .formulation .alert-link {
+  .alert-info .alert-link, #page-admin-index .releasenoteslink .alert-link, #adminsettings .form-overridden .alert-link, .que .formulation .alert-link, .assignfeedback_editpdf_widget .label .alert-link {
     color: #20454f; }
 
 .alert-warning,
@@ -8524,7 +8524,7 @@ a.dimmed_text:visited,
 /* stylelint-disable function-url-scheme-blacklist */
 /* stylelint-enable function-url-scheme-blacklist */
 .tag, .statusok, .statuswarning, .statusserious,
-.statuscritical, .que .correctness, .assignfeedback_editpdf_widget .label, .label, .badge {
+.statuscritical, .que .correctness, .label, .badge {
   display: inline-block;
   padding: 0.25rem 0.4em;
   font-size: 75%;
@@ -8570,9 +8570,9 @@ a.dimmed_text:visited,
   #page-admin-report-backups-index .backup-notyetrun[href]:focus {
     background-color: #449d44; }
 
-.tag-info, #page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity200 .info.release, .assignfeedback_editpdf_widget .label {
+.tag-info, #page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity200 .info.release {
   background-color: #5bc0de; }
-  .tag-info[href]:hover, #page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity200 .info.release[href]:hover, .assignfeedback_editpdf_widget .label[href]:hover, .tag-info[href]:focus, #page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity200 .info.release[href]:focus, .assignfeedback_editpdf_widget .label[href]:focus {
+  .tag-info[href]:hover, #page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity200 .info.release[href]:hover, .tag-info[href]:focus, #page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity200 .info.release[href]:focus {
     background-color: #31b0d5; }
 
 .tag-warning, .statuswarning, #page-admin-report-backups-index .backup-warning, #page-admin-index .updateplugin .updatepluginconfirmexternal, #page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity100 .info.release,
@@ -12424,7 +12424,9 @@ span.editinstructions {
   #course-category-listings .select-a-category .que .outcome,
   .que #course-category-listings .select-a-category .outcome,
   #course-category-listings .select-a-category .que .comment,
-  .que #course-category-listings .select-a-category .comment {
+  .que #course-category-listings .select-a-category .comment,
+  #course-category-listings .select-a-category .assignfeedback_editpdf_widget .label,
+  .assignfeedback_editpdf_widget #course-category-listings .select-a-category .label {
     margin: 1em; }
 
 #course-category-listings #course-listing .listitem .drag-handle {
index 2062b8b..a300d62 100644 (file)
@@ -3,6 +3,13 @@ information provided here is intended especially for theme designer.
 
 === 3.6 ===
 
+* A new callback has been added to the theme layout files allowing plugins to inject their content
+  into the page. For Boost it was added as a mustache variable "{{{ output.standard_after_main_region_html }}}" in
+  templates/columns1.mustache and templates/columns2.mustache. In bootstrapbase it was added as a
+  function call "$OUTPUT->standard_after_main_region_html()" in layout/columns1.php, layout/columns2.php.
+  and layout/columns3.php. Child themes not inheriting these templates should add the function call
+  after the div containing the main content region. Please refer to the messaging drawer UI for an example of
+  making use of this new feature.
 * The body tag has various browser-specific classes for those cases where some
   browser-sniffing is unavoidable. This used not to recognised Microsoft Edge,
   but now it does (class 'edge'). Also, both Safari and Chrome used to be