Merge branch 'MDL-54837-master' of git://github.com/marinaglancy/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 13 Jun 2016 07:51:56 +0000 (08:51 +0100)
committerDan Poltawski <dan@moodle.com>
Mon, 13 Jun 2016 07:51:56 +0000 (08:51 +0100)
43 files changed:
admin/user/user_bulk.php
admin/user/user_bulk_enrol.php [deleted file]
admin/user/user_bulk_forms.php
backup/moodle2/restore_qtype_plugin.class.php
blocks/course_overview/tests/behat/quiz_overview.feature [new file with mode: 0644]
blocks/navigation/styles.css
blocks/settings/styles.css
course/externallib.php
course/format/lib.php
install/lang/he/admin.php
install/lang/he/install.php
install/lang/he/moodle.php
install/lang/pt/install.php
lang/en/error.php
lib/classes/session/memcached.php
lib/editor/atto/autosave-ajax.php
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/build.json
lib/editor/atto/yui/src/editor/js/autosave-io.js [new file with mode: 0644]
lib/editor/atto/yui/src/editor/js/autosave.js
lib/editor/atto/yui/src/editor/meta/editor.json
lib/outputrenderers.php
lib/tests/behat/behat_hooks.php
lib/tests/weblib_format_text_test.php
lib/weblib.php
message/externallib.php
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/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/tests/base_test.php
mod/assign/tests/locallib_test.php
mod/forum/lib.php
mod/lti/locallib.php
mod/quiz/lib.php
mod/quiz/tests/lib_test.php
portfolio/download/lib.php
version.php

index 62a4d29..751a75b 100644 (file)
@@ -24,7 +24,6 @@ if ($data = $action_form->get_data()) {
         case 3: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_delete.php');
         case 4: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_display.php');
         case 5: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_download.php');
-        //case 6: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_enrol.php'); //TODO: MDL-24064
         case 7: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_forcepasswordchange.php');
         case 8: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_cohortadd.php');
     }
diff --git a/admin/user/user_bulk_enrol.php b/admin/user/user_bulk_enrol.php
deleted file mode 100644 (file)
index 21ad105..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-<?php
-/**
-* script for bulk user multi enrol operations
-*/
-
-die('this needs to be rewritten to use new enrol framework, sorry');  //TODO: MDL-24064
-
-require_once('../../config.php');
-require_once($CFG->libdir.'/adminlib.php');
-$processed = optional_param('processed', '', PARAM_BOOL);
-$sort = optional_param('sort', 'fullname', PARAM_ALPHA); //Sort by full name
-$dir  = optional_param('dir', 'asc', PARAM_ALPHA);       //Order to sort (ASC)
-
-require_login();
-admin_externalpage_setup('userbulk');
-require_capability('moodle/role:assign', context_system::instance()); //TODO: use some enrol cap
-$return = $CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk.php';
-//If no users selected then return to user_bulk.php
-if (empty($SESSION->bulk_users)) {
-    redirect($return);
-}
-$users = $SESSION->bulk_users; //Get users to display
-$usertotal = get_users(false); //Total number of users registered
-$usercount = count($users);    //number of users
-
-echo $OUTPUT->header();
-
-//take user info
-foreach ($users as $key => $id) {
-    $user = $DB->get_record('user', array('id'=>$id));
-    $user->fullname = fullname($user, true);
-    unset($user->firstname);
-    unset($user->lastname);
-    $users[$key] = $user;
-}
-
-// Need to sort by date
-function sort_compare($a, $b) {
-    global $sort, $dir;
-    if($sort == 'lastaccess') {
-        $rez = $b->lastaccess - $a->lastaccess;
-    } else {
-        $rez = strcasecmp(@$a->$sort, @$b->$sort);
-    }
-    return $dir == 'desc' ? -$rez : $rez;
-}
-usort($users, 'sort_compare');
-
-//Take courses data (id, shortname, and fullname)
-$courses = get_courses_page(1, 'c.sortorder ASC', 'c.id,c.shortname,c.fullname,c.visible', $totalcount);
-$table = new html_table();
-$table->width = "95%";
-$columns = array('fullname');
-foreach ($courses as $v)
-{
-    $columns[] = $v->shortname;
-}
-
-//Print columns headers from table
-foreach ($columns as $column) {
-    $strtitle = $column;
-    if ($sort != $column) {
-        $columnicon = '';
-        $columndir = 'asc';
-    } else {
-        $columndir = ($dir == 'asc') ? 'desc' : 'asc';
-        $columnicon = ' <img src="'.$OUTPUT->pix_url('t/'.($dir == 'asc' ? 'down' : 'up' )).'" alt="" />';
-    }
-    $table->head[] = '<a href="user_bulk_enrol.php?sort='.$column.'&amp;dir='.$columndir.'">'.$strtitle.'</a>'.$columnicon;
-    $table->align[] = 'left';
-}
-
-// process data submitting
-if(!empty($processed)) {
-    //Process data form here
-    $total = count($courses) * count($users);
-
-    for ( $i = 0; $i < $total; $i++ )
-    {
-        $param = "selected".$i;
-        $info = optional_param($param, '', PARAM_SEQUENCE);
-        /**
-         * user id:    ids[0]
-         * course id:  ids[1]
-         * enrol stat: ids[2]
-         */
-        $ids = explode(',', $info);
-        if(!empty($ids[2])) {
-            $context = context_course::instance($ids[1]);
-            role_assign(5, $ids[0], $context->id); //TODO: horrible!!
-        } else {
-            if( empty($ids[1] ) ) {
-                continue;
-            }
-            $context = context_course::instance($ids[1]);
-            role_unassign(5, $ids[0], $context->id);
-        }
-    }
-    redirect($return, get_string('changessaved')); //TODO: horrible!!
-}
-
-//Form beginning
-echo '<form id="multienrol" name="multienrol" method="post" action="user_bulk_enrol.php">';
-echo '<input type="hidden" name="processed" value="yes" />';
-$count = 0;
-foreach($users as $user) {
-    $temparray = array (
-        '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$user->id.'&amp;course='.SITEID.'">'.$user->fullname.'</a>'
-    );
-    $mycourses = enrol_get_users_courses($user->id, false);
-    foreach($courses as $acourse) {
-        $state = '';
-        if (isset($mycourses[$acourse->id])) {
-            $state = 'checked="checked"';
-        }
-        $temparray[] = '<input type="hidden" name="selected' . $count .
-                '" value="' . $user->id . ',' . $acourse->id . ',0" />' .
-                '<input type="checkbox" name="selected' . $count .
-                '" value="' . $user->id . ',' . $acourse->id . ',1" ' . $state . '/>';
-        $count++;
-    }
-    $table->data[] = $temparray;
-}
-echo $OUTPUT->heading("$usercount / $usertotal ".get_string('users'));
-echo html_writer::table($table);
-echo '<div class="continuebutton">';
-echo '<input type="submit" name="multienrolsubmit" value="save changes" />';
-echo '</div>';
-echo '</form>';
-
-echo $OUTPUT->footer();
index 0f7c25c..dd410f4 100644 (file)
@@ -24,10 +24,6 @@ class user_bulk_action_form extends moodleform {
         if (has_capability('moodle/user:update', $syscontext)) {
             $actions[5] = get_string('download', 'admin');
         }
-        if (has_capability('moodle/role:assign', $syscontext)){
-             //TODO: MDL-24064
-            //$actions[6] = get_string('enrolmultipleusers', 'admin');
-        }
         if (has_capability('moodle/user:update', $syscontext)) {
             $actions[7] = get_string('forcepasswordchange');
         }
index 60f599a..7520aa3 100644 (file)
@@ -181,7 +181,7 @@ abstract class restore_qtype_plugin extends restore_plugin {
                 $info = new stdClass();
                 $info->filequestionid = $oldquestionid;
                 $info->dbquestionid   = $newquestionid;
-                $info->answer         = $data->answertext;
+                $info->answer         = s($data->answertext);
                 throw new restore_step_exception('error_question_answers_missing_in_db', $info);
             }
             $newitemid = $this->questionanswercache[$data->answertext];
diff --git a/blocks/course_overview/tests/behat/quiz_overview.feature b/blocks/course_overview/tests/behat/quiz_overview.feature
new file mode 100644 (file)
index 0000000..238591f
--- /dev/null
@@ -0,0 +1,94 @@
+@block @block_course_overview @mod_quiz
+Feature: View the quiz being due
+  In order to know what quizzes are due
+  As a student
+  I can visit my dashboard
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+      | Course 2 | C2        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | student1 | C1     | student        |
+      | student2 | C2     | student        |
+      | teacher1 | C1     | editingteacher |
+      | teacher1 | C2     | editingteacher |
+    And the following "activities" exist:
+      | activity | course | idnumber | name                    | timeclose  |
+      | quiz     | C1     | Q1A      | Quiz 1A No deadline     | 0          |
+      | quiz     | C1     | Q1B      | Quiz 1B Past deadline   | 1337       |
+      | quiz     | C1     | Q1C      | Quiz 1C Future deadline | 9000000000 |
+      | quiz     | C1     | Q1D      | Quiz 1D Future deadline | 9000000000 |
+      | quiz     | C1     | Q1E      | Quiz 1E Future deadline | 9000000000 |
+      | quiz     | C2     | Q2A      | Quiz 2A Future deadline | 9000000000 |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | qtype     | name           | questiontext              | questioncategory |
+      | truefalse | First question | Answer the first question | Test questions   |
+    And quiz "Quiz 1A No deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1B Past deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1C Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1D Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1E Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 2A Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+
+  Scenario: View my quizzes that are due
+    Given I log in as "student1"
+    When I am on homepage
+    Then I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should not see "Quiz 2A Future deadline" in the "Course overview" "block"
+    And I log out
+    And I log in as "student2"
+    And I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should not see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should see "Quiz 2A Future deadline" in the "Course overview" "block"
+
+  Scenario: View my quizzes that are due and never finished
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Quiz 1D Future deadline"
+    And I press "Attempt quiz now"
+    And I follow "Finish attempt ..."
+    And I press "Submit all and finish"
+    And I follow "Course 1"
+    And I follow "Quiz 1E Future deadline"
+    And I press "Attempt quiz now"
+    When I am on homepage
+    Then I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 2A Future deadline" in the "Course overview" "block"
+
index 47ef1c2..140ed42 100644 (file)
@@ -3,6 +3,7 @@
 .block_navigation .block_tree ul {margin-left: 18px;}
 .block_navigation .block_tree p.hasicon {text-indent: -21px; padding-left: 21px;}
 .block_navigation .block_tree p.hasicon img {width: 16px; height: 16px; margin-top: 3px; margin-right: 5px; vertical-align: top;}
+.block_navigation .block_tree p.hasicon.visibleifjs {display: block;}
 
 .block_navigation .block_tree .tree_item {cursor:pointer; padding-left: 0;margin:3px 0px; background-position: 0 50%; background-repeat: no-repeat;}
 .block_navigation .block_tree .tree_item.branch {padding-left: 21px;}
index 31b27ec..e9c0e10 100644 (file)
@@ -1,6 +1,7 @@
 .block_settings .block_tree ul {margin-left: 18px;}
 .block_settings .block_tree p.hasicon {text-indent: -21px; padding-left: 21px;}
 .block_settings .block_tree p.hasicon img {width: 16px; height: 16px; margin-top: 3px; margin-right: 5px; vertical-align: top;}
+.block_settings .block_tree p.hasicon.visibleifjs {display: block;}
 
 .block_settings .block_tree .tree_item.branch {padding-left: 21px;}
 .block_settings .block_tree .tree_item {cursor:pointer; margin:3px 0px; background-position: 0 50%; background-repeat: no-repeat;}
index ea687c6..b348aa1 100644 (file)
@@ -2140,7 +2140,7 @@ class core_course_external extends external_api {
                 'perpage'       => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0),
                 'requiredcapabilities' => new external_multiple_structure(
                     new external_value(PARAM_CAPABILITY, 'Capability string used to filter courses by permission'),
-                    VALUE_OPTIONAL
+                    'Optional list of required capabilities (used to filter the list)', VALUE_DEFAULT, array()
                 ),
                 'limittoenrolled' => new external_value(PARAM_BOOL, 'limit to enrolled courses', VALUE_DEFAULT, 0),
             )
index 208c188..20f39a4 100644 (file)
@@ -984,14 +984,14 @@ abstract class format_base {
         }
         if (!is_object($section)) {
             $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section),
-                'id,section,sequence');
+                'id,section,sequence,summary');
         }
         if (!$section || !$section->section) {
             // Not possible to delete 0-section.
             return false;
         }
 
-        if (!$forcedeleteifnotempty && !empty($section->sequence)) {
+        if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
             return false;
         }
 
index c9f613d..5374ba9 100644 (file)
@@ -42,4 +42,4 @@ $string['cliunknowoption'] = 'אפשרויות לא מוכרות :
 אנא השתמש באפשרות העזרה.';
 $string['cliyesnoprompt'] = 'רשום y (שפרושו כן) או n (שפרושו לא)';
 $string['environmentrequireinstall'] = 'נדרש להתקין/לאפשר זאת';
-$string['environmentrequireversion'] = '×\92×\99רסת {$a->needed} × ×\93רשת ×\95×\90ת×\94 ×\9eר×\99×¥ {$a->current}';
+$string['environmentrequireversion'] = '×\92×\99רסת {$a->needed} × ×\93רשת ×\90×\9a ×\94×\92×\99רס×\94 ×\94× ×\95×\9b×\97×\99ת ×\94×\99×\90 {$a->current}';
index 8dfd3a2..f58e8a3 100644 (file)
@@ -86,10 +86,10 @@ $string['pathsunsecuredataroot'] = 'ספריית המידע (Data Directory) ל
 $string['pathswrongadmindir'] = 'ספריית ה-admin לא קיימת';
 $string['phpextension'] = 'הרחבת PHP {$a}';
 $string['phpversion'] = 'גירסת PHP';
-$string['phpversionhelp'] = '<p>גרסת PHP חייבת להיות לפחות 4.3.0 או 5.1.0 (בגרסאות 5.0.x קיימות מספר בעיות ידועות) </p>
+$string['phpversionhelp'] = '<p>גרסת PHP חייבת להיות לפחות 4.3.0 או 5.1.0 (בגרסאות 5.0 קיימות מספר בעיות ידועות) </p>
 <p> במערכת שלך פועלת כרגע גרסת {$a} </p>
 <p> אתה חייב לשדרג את גרסת ה-PHP שלך או לעבור למחשב מארח עם עם גירסת PHP חדשה! <br/>
-(במקרים של גרסת 5.0.x תוכל גם לרדת בגרסה ל- 4.4.x)
+(במקרים של גרסת 5.0 תוכל גם לרדת בגרסה ל- 4.4)
 </p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'הינך רואה את עמוד זה מפני שהתקנת והפעלת בהלכה את <strong>{$a->packname} {$a->packversion}</strong>
index 46b031b..7ce183a 100644 (file)
@@ -33,4 +33,4 @@ defined('MOODLE_INTERNAL') || die();
 $string['language'] = 'שפת ממשק';
 $string['next'] = 'הבא';
 $string['previous'] = 'קודם';
-$string['reload'] = '×\98×¢×\9f מחדש';
+$string['reload'] = '×\98×¢×\99× ×\94 מחדש';
index 12ae97a..a4b6a21 100644 (file)
@@ -54,7 +54,7 @@ $string['paths'] = 'Caminhos';
 $string['pathserrcreatedataroot'] = 'O programa de instalação não conseguiu criar a pasta de dados <b>{$a->dataroot}</b>.';
 $string['pathshead'] = 'Confirmar caminhos';
 $string['pathsrodataroot'] = 'A pasta de dados não tem permissões de escrita.';
-$string['pathsroparentdataroot'] = 'A pasta pai <b>{$a->parent}</b> não tem permissões de escrita. O programa de instalação não conseguiu criar a pasta <b>{$a->dataroot}</b>.';
+$string['pathsroparentdataroot'] = 'A pasta ascendente <b>{$a->parent}</b> não tem permissões de escrita. O programa de instalação não conseguiu criar a pasta <b>{$a->dataroot}</b>.';
 $string['pathssubadmindir'] = 'Alguns servidores Web utilizam a pasta <strong>admin</strong> em URLs especiais de acesso a funcionalidades especiais, como é o caso de painéis de controlo. Algumas situações podem criar conflitos com a localização normal das páginas de administração do Moodle. Estes problemas podem ser resolvidos renomeando a pasta <strong>admin</strong> na instalação do Moodle e indicando aqui o novo nome a utilizar. Por exemplo:<br /><br /><b>moodleadmin</b><br /><br />Esta ação resolverá os problemas de acesso das hiperligações para as funcionalidades de administração do Moodle.';
 $string['pathssubdataroot'] = '<p>Uma diretoria em que o Moodle irá armazenar todo o conteúdo de ficheiros enviados pelos utilizadores.</p>
 <p>Esta diretoria deve ser legível e gravável pelo utilizador do servidor web (geralmente \'www-data\', \'nobody\', ou \'apache\').</p>
index 6e8f415..93db2e5 100644 (file)
@@ -224,6 +224,7 @@ $string['duplicateroleshortname'] = 'There is already a role with this short nam
 $string['duplicateusername'] = 'Duplicate username - skipping record';
 $string['emailfail'] = 'Emailing failed';
 $string['error'] = 'Error occurred';
+$string['error_question_answers_missing_in_db'] = 'Failed to find an answer matching "{$a->answer}" in the question_answers database table. This occurred while restoring the question with id {$a->filequestionid} in the backup file, which has been matched to the existing question with id {$a->dbquestionid} in the database.';
 $string['errorprocessingarchive'] = 'Error processing archive file';
 $string['errorcleaningdirectory'] = 'Error cleaning directory "{$a}"';
 $string['errorcopyingfiles'] = 'Error copying files';
index bf1678b..5a1fd4d 100644 (file)
@@ -136,7 +136,12 @@ class memcached extends handler {
         ini_set('memcached.sess_locking', '1'); // Locking is required!
 
         // Try to configure lock and expire timeouts - ignored if memcached is before version 2.2.0.
-        ini_set('memcached.sess_lock_max_wait', $this->acquiretimeout);
+        if (version_compare($version, '3.0.0-dev') >= 0) {
+            ini_set('memcached.sess_lock_wait_max', $this->acquiretimeout * 1000);
+        } else {
+            ini_set('memcached.sess_lock_max_wait', $this->acquiretimeout);
+        }
+
         ini_set('memcached.sess_lock_expire', $this->lockexpire);
     }
 
index 6626075..a31117c 100644 (file)
@@ -27,156 +27,195 @@ define('AJAX_SCRIPT', true);
 require_once(dirname(__FILE__) . '/../../../config.php');
 require_once($CFG->libdir . '/filestorage/file_storage.php');
 
-$contextid = required_param('contextid', PARAM_INT);
-$elementid = required_param('elementid', PARAM_ALPHANUMEXT);
-$pagehash = required_param('pagehash', PARAM_ALPHANUMEXT);
-$pageinstance = required_param('pageinstance', PARAM_ALPHANUMEXT);
+// Clean up actions.
+$actions = array_map(function($actionparams) {
+    $action = isset($actionparams['action']) ? $actionparams['action'] : null;
+    $params = [];
+    $keys = [
+        'action' => PARAM_ALPHA,
+        'contextid' => PARAM_INT,
+        'elementid' => PARAM_ALPHANUMEXT,
+        'pagehash' => PARAM_ALPHANUMEXT,
+        'pageinstance' => PARAM_ALPHANUMEXT
+    ];
+
+    if ($action == 'save') {
+        $keys['drafttext'] = PARAM_RAW;
+    } else if ($action == 'resume') {
+        $keys['draftid'] = PARAM_INT;
+    }
+
+    foreach ($keys as $key => $type) {
+        // Replicate required_param().
+        if (!isset($actionparams[$key])) {
+            print_error('missingparam', '', '', $key);
+        }
+        $params[$key] = clean_param($actionparams[$key], $type);
+    }
+
+    return $params;
+}, isset($_REQUEST['actions']) ? $_REQUEST['actions'] : []);
+
 $now = time();
 // This is the oldest time any autosave text will be recovered from.
 // This is so that there is a good chance the draft files will still exist (there are many variables so
 // this is impossible to guarantee).
 $before = $now - 60*60*24*4;
 
-list($context, $course, $cm) = get_context_info_array($contextid);
+$context = context_system::instance();
 $PAGE->set_url('/lib/editor/atto/autosave-ajax.php');
 $PAGE->set_context($context);
 
-require_login($course, false, $cm);
-require_sesskey();
-
+require_login();
 if (isguestuser()) {
     print_error('accessdenied', 'admin');
 }
+require_sesskey();
 
 if (!in_array('atto', explode(',', get_config('core', 'texteditors')))) {
     print_error('accessdenied', 'admin');
 }
 
-$action = required_param('action', PARAM_ALPHA);
+$responses = array();
+foreach ($actions as $actionparams) {
+
+    $action = $actionparams['action'];
+    $contextid = $actionparams['contextid'];
+    $elementid = $actionparams['elementid'];
+    $pagehash = $actionparams['pagehash'];
+    $pageinstance = $actionparams['pageinstance'];
+
+    if ($action === 'save') {
+        $drafttext = $actionparams['drafttext'];
+        $params = array('elementid' => $elementid,
+                        'userid' => $USER->id,
+                        'pagehash' => $pagehash,
+                        'contextid' => $contextid);
+
+        $record = $DB->get_record('editor_atto_autosave', $params);
+        if ($record && $record->pageinstance != $pageinstance) {
+            print_error('concurrent access from the same user is not supported');
+            die();
+        }
 
-$response = array();
+        if (!$record) {
+            $record = new stdClass();
+            $record->elementid = $elementid;
+            $record->userid = $USER->id;
+            $record->pagehash = $pagehash;
+            $record->contextid = $contextid;
+            $record->drafttext = $drafttext;
+            $record->pageinstance = $pageinstance;
+            $record->timemodified = $now;
 
-if ($action === 'save') {
-    $drafttext = required_param('drafttext', PARAM_RAW);
-    $params = array('elementid' => $elementid,
-                    'userid' => $USER->id,
-                    'pagehash' => $pagehash,
-                    'contextid' => $contextid);
+            $DB->insert_record('editor_atto_autosave', $record);
 
-    $record = $DB->get_record('editor_atto_autosave', $params);
-    if ($record && $record->pageinstance != $pageinstance) {
-        print_error('concurrent access from the same user is not supported');
-        die();
-    }
+            // No response means no error.
+            $responses[] = null;
+            continue;
+        } else {
+            $record->drafttext = $drafttext;
+            $record->timemodified = time();
+            $DB->update_record('editor_atto_autosave', $record);
 
-    if (!$record) {
-        $record = new stdClass();
-        $record->elementid = $elementid;
-        $record->userid = $USER->id;
-        $record->pagehash = $pagehash;
-        $record->contextid = $contextid;
-        $record->drafttext = $drafttext;
-        $record->pageinstance = $pageinstance;
-        $record->timemodified = $now;
-
-        $DB->insert_record('editor_atto_autosave', $record);
-
-        // No response means no error.
-        die();
-    } else {
-        $record->drafttext = $drafttext;
-        $record->timemodified = time();
-        $DB->update_record('editor_atto_autosave', $record);
-
-        // No response means no error.
-        die();
-    }
-} else if ($action == 'resume') {
-    $params = array('elementid' => $elementid,
-                    'userid' => $USER->id,
-                    'pagehash' => $pagehash,
-                    'contextid' => $contextid);
-
-    $newdraftid = required_param('draftid', PARAM_INT);
-
-    $record = $DB->get_record('editor_atto_autosave', $params);
-
-    if (!$record) {
-        $record = new stdClass();
-        $record->elementid = $elementid;
-        $record->userid = $USER->id;
-        $record->pagehash = $pagehash;
-        $record->contextid = $contextid;
-        $record->pageinstance = $pageinstance;
-        $record->pagehash = $pagehash;
-        $record->draftid = $newdraftid;
-        $record->timemodified = time();
-        $record->drafttext = '';
-
-        $DB->insert_record('editor_atto_autosave', $record);
-
-        // No response means no error.
-        die();
-    } else {
-        // Copy all draft files from the old draft area.
-        $usercontext = context_user::instance($USER->id);
-        $stale = $record->timemodified < $before;
-        require_once($CFG->libdir . '/filelib.php');
-
-        $fs = get_file_storage();
-        $files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $newdraftid, '/', true, true);
-
-        $lastfilemodified = 0;
-        foreach ($files as $file) {
-            $lastfilemodified = max($lastfilemodified, $file->get_timemodified());
-        }
-        if ($record->timemodified < $lastfilemodified) {
-            $stale = true;
+            // No response means no error.
+            $responses[] = null;
+            continue;
         }
 
-        if (!$stale) {
-            // This function copies all the files in one draft area, to another area (in this case it's
-            // another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
-            $newdrafttext = file_save_draft_area_files($record->draftid,
-                                                       $usercontext->id,
-                                                       'user',
-                                                       'draft',
-                                                       $newdraftid,
-                                                       array(),
-                                                       $record->drafttext);
-
-            // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
-            $newdrafttext = file_rewrite_pluginfile_urls($newdrafttext,
-                                                         'draftfile.php',
-                                                         $usercontext->id,
-                                                         'user',
-                                                         'draft',
-                                                         $newdraftid);
-            $record->drafttext = $newdrafttext;
+    } else if ($action == 'resume') {
+        $params = array('elementid' => $elementid,
+                        'userid' => $USER->id,
+                        'pagehash' => $pagehash,
+                        'contextid' => $contextid);
+
+        $newdraftid = $actionparams['draftid'];
+
+        $record = $DB->get_record('editor_atto_autosave', $params);
 
+        if (!$record) {
+            $record = new stdClass();
+            $record->elementid = $elementid;
+            $record->userid = $USER->id;
+            $record->pagehash = $pagehash;
+            $record->contextid = $contextid;
             $record->pageinstance = $pageinstance;
+            $record->pagehash = $pagehash;
             $record->draftid = $newdraftid;
             $record->timemodified = time();
-            $DB->update_record('editor_atto_autosave', $record);
+            $record->drafttext = '';
 
-            // A response means the draft has been restored and here is the auto-saved text.
-            $response['result'] = $record->drafttext;
-            echo json_encode($response);
-        } else {
-            $DB->delete_records('editor_atto_autosave', array('id' => $record->id));
+            $DB->insert_record('editor_atto_autosave', $record);
 
             // No response means no error.
+            $responses[] = null;
+            continue;
+
+        } else {
+            // Copy all draft files from the old draft area.
+            $usercontext = context_user::instance($USER->id);
+            $stale = $record->timemodified < $before;
+            require_once($CFG->libdir . '/filelib.php');
+
+            $fs = get_file_storage();
+            $files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $newdraftid, '/', true, true);
+
+            $lastfilemodified = 0;
+            foreach ($files as $file) {
+                $lastfilemodified = max($lastfilemodified, $file->get_timemodified());
+            }
+            if ($record->timemodified < $lastfilemodified) {
+                $stale = true;
+            }
+
+            if (!$stale) {
+                // This function copies all the files in one draft area, to another area (in this case it's
+                // another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
+                $newdrafttext = file_save_draft_area_files($record->draftid,
+                                                           $usercontext->id,
+                                                           'user',
+                                                           'draft',
+                                                           $newdraftid,
+                                                           array(),
+                                                           $record->drafttext);
+
+                // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
+                $newdrafttext = file_rewrite_pluginfile_urls($newdrafttext,
+                                                             'draftfile.php',
+                                                             $usercontext->id,
+                                                             'user',
+                                                             'draft',
+                                                             $newdraftid);
+                $record->drafttext = $newdrafttext;
+
+                $record->pageinstance = $pageinstance;
+                $record->draftid = $newdraftid;
+                $record->timemodified = time();
+                $DB->update_record('editor_atto_autosave', $record);
+
+                // A response means the draft has been restored and here is the auto-saved text.
+                $response = ['result' => $record->drafttext];
+                $responses[] = $response;
+
+            } else {
+                $DB->delete_records('editor_atto_autosave', array('id' => $record->id));
+
+                // No response means no error.
+                $responses[] = null;
+            }
+            continue;
         }
-        die();
+
+    } else if ($action == 'reset') {
+        $params = array('elementid' => $elementid,
+                        'userid' => $USER->id,
+                        'pagehash' => $pagehash,
+                        'contextid' => $contextid);
+
+        $DB->delete_records('editor_atto_autosave', $params);
+        $responses[] = null;
+        continue;
     }
-} else if ($action == 'reset') {
-    $params = array('elementid' => $elementid,
-                    'userid' => $USER->id,
-                    'pagehash' => $pagehash,
-                    'contextid' => $contextid);
-
-    $DB->delete_records('editor_atto_autosave', $params);
-    die();
 }
 
-print_error('invalidarguments');
+echo json_encode($responses);
index 09ba0b7..fbcfd8b 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 912122e..c8557d0 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 27f74d3..d2b7ea5 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 3984d2f..cd310b4 100644 (file)
@@ -7,6 +7,7 @@
                 "notify.js",
                 "textarea.js",
                 "autosave.js",
+                "autosave-io.js",
                 "clean.js",
                 "commands.js",
                 "toolbar.js",
diff --git a/lib/editor/atto/yui/src/editor/js/autosave-io.js b/lib/editor/atto/yui/src/editor/js/autosave-io.js
new file mode 100644 (file)
index 0000000..a37352e
--- /dev/null
@@ -0,0 +1,244 @@
+// 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 autosave function for the Atto editor.
+ *
+ * @module     moodle-editor_atto-autosave-io
+ * @submodule  autosave-io
+ * @package    editor_atto
+ * @copyright  2016 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+var EditorAutosaveIoDispatcherInstance = null;
+
+function EditorAutosaveIoDispatcher() {
+    EditorAutosaveIoDispatcher.superclass.constructor.apply(this, arguments);
+    this._submitEvents = {};
+    this._queue = [];
+    this._throttle = null;
+}
+EditorAutosaveIoDispatcher.NAME = 'EditorAutosaveIoDispatcher';
+EditorAutosaveIoDispatcher.ATTRS = {
+
+    /**
+     * The relative path to the ajax script.
+     *
+     * @attribute autosaveAjaxScript
+     * @type String
+     * @default '/lib/editor/atto/autosave-ajax.php'
+     * @readOnly
+     */
+    autosaveAjaxScript: {
+        value: '/lib/editor/atto/autosave-ajax.php',
+        readOnly: true
+    },
+
+    /**
+     * The time buffer for the throttled requested.
+     *
+     * @attribute delay
+     * @type Number
+     * @default 50
+     * @readOnly
+     */
+    delay: {
+        value: 50,
+        readOnly: true
+    }
+
+};
+Y.extend(EditorAutosaveIoDispatcher, Y.Base, {
+
+    /**
+     * Dispatch an IO request.
+     *
+     * This method will put the requests in a queue in order to attempt to bulk them.
+     *
+     * @param  {Object} params    The parameters of the request.
+     * @param  {Object} context   The context in which the callbacks are called.
+     * @param  {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
+     *                            optional keys defining the callbacks to call. Success and Complete
+     *                            functions will receive the response as parameter. Success and Complete
+     *                            may receive an object containing the error key, use this to confirm
+     *                            that no errors occured.
+     * @return {Void}
+     */
+    dispatch: function(params, context, callbacks) {
+        if (this._throttle) {
+            this._throttle.cancel();
+        }
+
+        this._throttle = Y.later(this.get('delay'), this, this._processDispatchQueue);
+        this._queue.push([params, context, callbacks]);
+    },
+
+    /**
+     * Dispatches the requests in the queue.
+     *
+     * @return {Void}
+     */
+    _processDispatchQueue: function() {
+        var queue = this._queue,
+            data = {};
+
+        this._queue = [];
+        if (queue.length < 1) {
+            return;
+        }
+
+        Y.Array.each(queue, function(item, index) {
+            data[index] = item[0];
+        });
+
+        Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
+            method: 'POST',
+            data: Y.QueryString.stringify({
+                actions: data,
+                sesskey: M.cfg.sesskey
+            }),
+            on: {
+                start: this._makeIoEventCallback('start', queue),
+                complete: this._makeIoEventCallback('complete', queue),
+                failure: this._makeIoEventCallback('failure', queue),
+                end: this._makeIoEventCallback('end', queue),
+                success: this._makeIoEventCallback('success', queue)
+            }
+        });
+    },
+
+    /**
+     * Creates a function that dispatches an IO response to callbacks.
+     *
+     * @param  {String} event The type of event.
+     * @param  {Array} queue The queue.
+     * @return {Function}
+     */
+    _makeIoEventCallback: function(event, queue) {
+        var noop = function() {};
+        return function() {
+            var response = arguments[1],
+                parsed = {};
+
+            if ((event == 'complete' || event == 'success') && (typeof response !== 'undefined'
+                    && typeof response.responseText !== 'undefined' && response.responseText !== '')) {
+
+                // Success and complete events need to parse the response.
+                parsed = JSON.parse(response.responseText) || {};
+            }
+
+            Y.Array.each(queue, function(item, index) {
+                var context = item[1],
+                    cb = (item[2] && item[2][event]) || noop,
+                    arg;
+
+                if (parsed && parsed.error) {
+                    // The response is an error, we send it to everyone.
+                    arg = parsed;
+                } else if (parsed) {
+                    // The response was parsed, we only communicate the relevant portion of the response.
+                    arg = parsed[index];
+                }
+
+                cb.apply(context, [arg]);
+            });
+        };
+    },
+
+    /**
+     * Form submit handler.
+     *
+     * @param  {EventFacade} e The event.
+     * @return {Void}
+     */
+    _onSubmit: function(e) {
+        var data = {},
+            id = e.currentTarget.generateID(),
+            params = this._submitEvents[id];
+
+        if (!params || params.ios.length < 1) {
+            return;
+        }
+
+        Y.Array.each(params.ios, function(param, index) {
+            data[index] = param;
+        });
+
+        Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
+            method: 'POST',
+            data: Y.QueryString.stringify({
+                actions: data,
+                sesskey: M.cfg.sesskey
+            }),
+            sync: true
+        });
+    },
+
+    /**
+     * Registers a request to be made on form submission.
+     *
+     * @param  {Node} node The forum node we will listen to.
+     * @param  {Object} params Parameters for the IO request.
+     * @return {Void}
+     */
+    whenSubmit: function(node, params) {
+        if (typeof this._submitEvents[node.generateID()] === 'undefined') {
+            this._submitEvents[node.generateID()] = {
+                event: node.on('submit', this._onSubmit, this),
+                ios: []
+            };
+        }
+        this._submitEvents[node.get('id')].ios.push([params]);
+    }
+
+});
+EditorAutosaveIoDispatcherInstance = new EditorAutosaveIoDispatcher();
+
+
+function EditorAutosaveIo() {}
+EditorAutosaveIo.prototype = {
+
+    /**
+     * Dispatch an IO request.
+     *
+     * This method will put the requests in a queue in order to attempt to bulk them.
+     *
+     * @param  {Object} params    The parameters of the request.
+     * @param  {Object} context   The context in which the callbacks are called.
+     * @param  {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
+     *                            optional keys defining the callbacks to call. Success and Complete
+     *                            functions will receive the response as parameter. Success and Complete
+     *                            may receive an object containing the error key, use this to confirm
+     *                            that no errors occured.
+     * @return {Void}
+     */
+    autosaveIo: function(params, context, callbacks) {
+        EditorAutosaveIoDispatcherInstance.dispatch(params, context, callbacks);
+    },
+
+    /**
+     * Registers a request to be made on form submission.
+     *
+     * @param  {Node} form The forum node we will listen to.
+     * @param  {Object} params Parameters for the IO request.
+     * @return {Void}
+     */
+    autosaveIoOnSubmit: function(form, params) {
+        EditorAutosaveIoDispatcherInstance.whenSubmit(form, params);
+    }
+
+};
+Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosaveIo]);
index 4dd4ed4..27f227a 100644 (file)
@@ -65,19 +65,6 @@ EditorAutosave.ATTRS= {
     pageHash: {
         value: '',
         writeOnce: true
-    },
-
-    /**
-     * The relative path to the ajax script.
-     *
-     * @attribute autosaveAjaxScript
-     * @type String
-     * @default '/lib/editor/atto/autosave-ajax.php'
-     * @readOnly
-     */
-    autosaveAjaxScript: {
-        value: '/lib/editor/atto/autosave-ajax.php',
-        readOnly: true
     }
 };
 
@@ -118,8 +105,7 @@ EditorAutosave.prototype = {
             form,
             optiontype = null,
             options = this.get('filepickeroptions'),
-            params,
-            url;
+            params;
 
         if (!this.get('autosaveEnabled')) {
             // Autosave disabled for this instance.
@@ -135,99 +121,73 @@ EditorAutosave.prototype = {
 
         // First see if there are any saved drafts.
         // Make an ajax request.
-        url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
         params = {
-            sesskey: M.cfg.sesskey,
             contextid: this.get('contextid'),
             action: 'resume',
-            drafttext: '',
             draftid: draftid,
             elementid: this.get('elementid'),
             pageinstance: this.autosaveInstance,
             pagehash: this.get('pageHash')
         };
 
-        Y.io(url, {
-            method: 'POST',
-            data: params,
-            context: this,
-            on: {
-                success: function(id,o) {
-                    var response_json;
-                    if (typeof o.responseText !== "undefined" && o.responseText !== "") {
-                        response_json = JSON.parse(o.responseText);
+        this.autosaveIo(params, this, {
+            success: function(response) {
+                if (response === null) {
+                    // This can happen when there is nothing to resume from.
+                    return;
+                } else if (!response) {
+                    Y.log('Invalid response received.', 'debug', LOGNAME_AUTOSAVE);
+                    return;
+                }
 
-                        // Revert untouched editor contents to an empty string.
-                        // Check for FF and Chrome.
-                        if (response_json.result === '<p></p>' || response_json.result === '<p><br></p>' ||
-                            response_json.result === '<br>') {
-                            response_json.result = '';
-                        }
+                // Revert untouched editor contents to an empty string.
+                // Check for FF and Chrome.
+                if (response.result === '<p></p>' || response.result === '<p><br></p>' ||
+                    response.result === '<br>') {
+                    response.result = '';
+                }
 
-                        // Check for IE 9 and 10.
-                        if (response_json.result === '<p>&nbsp;</p>' || response_json.result === '<p><br>&nbsp;</p>') {
-                            response_json.result = '';
-                        }
+                // Check for IE 9 and 10.
+                if (response.result === '<p>&nbsp;</p>' || response.result === '<p><br>&nbsp;</p>') {
+                    response.result = '';
+                }
 
-                        if (response_json.error || typeof response_json.result === 'undefined') {
-                            Y.log('Error occurred recovering draft text: ' + response_json.error, 'debug', LOGNAME_AUTOSAVE);
-                            this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
-                                    NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
-                        } else if (response_json.result !== this.textarea.get('value') &&
-                                response_json.result !== '') {
-                            Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
-                            this.recoverText(response_json.result);
-                        }
-                        this._fireSelectionChanged();
-                    }
-                },
-                failure: function() {
+                if (response.error || typeof response.result === 'undefined') {
+                    Y.log('Error occurred recovering draft text: ' + response.error, 'debug', LOGNAME_AUTOSAVE);
                     this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
                             NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
+                } else if (response.result !== this.textarea.get('value') &&
+                        response.result !== '') {
+                    Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
+                    this.recoverText(response.result);
                 }
+                this._fireSelectionChanged();
+
+            },
+            failure: function() {
+                this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
+                        NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
             }
         });
 
         // Now setup the timer for periodic saves.
-
         var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000;
         this.autosaveTimer = Y.later(delay, this, this.saveDraft, false, true);
 
         // Now setup the listener for form submission.
         form = this.textarea.ancestor('form');
         if (form) {
-            form.on('submit', this.resetAutosave, this);
+            this.autosaveIoOnSubmit(form, {
+                action: 'reset',
+                contextid: this.get('contextid'),
+                elementid: this.get('elementid'),
+                pageinstance: this.autosaveInstance,
+                pagehash: this.get('pageHash')
+            });
         }
         return this;
     },
 
-    /**
-     * Clear the autosave text because the form was submitted normally.
-     *
-     * @method resetAutosave
-     * @chainable
-     */
-    resetAutosave: function() {
-        // Make an ajax request to reset the autosaved text.
-        var url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
-        var params = {
-            sesskey: M.cfg.sesskey,
-            contextid: this.get('contextid'),
-            action: 'reset',
-            elementid: this.get('elementid'),
-            pageinstance: this.autosaveInstance,
-            pagehash: this.get('pageHash')
-        };
-
-        Y.io(url, {
-            method: 'POST',
-            data: params,
-            sync: true
-        });
-        return this;
-    },
-
-
     /**
      * Recover a previous version of this text and show a message.
      *
@@ -283,29 +243,23 @@ EditorAutosave.prototype = {
             };
 
             // Reusable error handler - must be passed the correct context.
-            var ajaxErrorFunction = function(code, response) {
+            var ajaxErrorFunction = function(response) {
                 var errorDuration = parseInt(this.get('autosaveFrequency'), 10) * 1000;
-                Y.log('Error while autosaving text:' + code, 'warn', LOGNAME_AUTOSAVE);
+                Y.log('Error while autosaving text', 'warn', LOGNAME_AUTOSAVE);
                 Y.log(response, 'warn', LOGNAME_AUTOSAVE);
                 this.showMessage(M.util.get_string('autosavefailed', 'editor_atto'), NOTIFY_WARNING, errorDuration);
             };
 
-            Y.io(url, {
-                method: 'POST',
-                data: params,
-                context: this,
-                on: {
-                    error: ajaxErrorFunction,
-                    failure: ajaxErrorFunction,
-                    success: function(code, response) {
-                        if (response.responseText !== "") {
-                            Y.soon(Y.bind(ajaxErrorFunction, this, [code, response]));
-                        } else {
-                            // All working.
-                            this.lastText = newText;
-                            this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
-                                    NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
-                        }
+            this.autosaveIo(params, this, {
+                failure: ajaxErrorFunction,
+                success: function(response) {
+                    if (response && response.error) {
+                        Y.soon(Y.bind(ajaxErrorFunction, this, [response]));
+                    } else {
+                        // All working.
+                        this.lastText = newText;
+                        this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
+                                NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
                     }
                 }
             });
index a8f9ff3..65750e3 100644 (file)
@@ -15,7 +15,8 @@
             "moodle-core-notification-confirm",
             "moodle-editor_atto-rangy",
             "handlebars",
-            "timers"
+            "timers",
+            "querystring-stringify"
         ]
     },
     "moodle-editor_atto-plugin": {
index af0c977..d0d5b23 100644 (file)
@@ -566,7 +566,7 @@ class core_renderer extends renderer_base {
     public function standard_top_of_body_html() {
         global $CFG;
         $output = $this->page->requires->get_top_of_body_code();
-        if (!empty($CFG->additionalhtmltopofbody)) {
+        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmltopofbody)) {
             $output .= "\n".$CFG->additionalhtmltopofbody;
         }
         $output .= $this->maintenance_warning();
@@ -689,7 +689,7 @@ class core_renderer extends renderer_base {
         // but some of the content won't be known until later, so we return a placeholder
         // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
         $output = '';
-        if (!empty($CFG->additionalhtmlfooter)) {
+        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlfooter)) {
             $output .= "\n".$CFG->additionalhtmlfooter;
         }
         $output .= $this->unique_end_html_token;
index c2a9862..0b95413 100644 (file)
@@ -429,8 +429,19 @@ class behat_hooks extends behat_base {
             return false;
         }
 
-        list ($dir, $filename) = $this->get_faildump_filename($scope, 'png');
-        $this->saveScreenshot($filename, $dir);
+        // Some drivers (e.g. chromedriver) may throw an exception while trying to take a screenshot.  If this isn't handled,
+        // the behat run dies.  We don't want to lose the information about the failure that triggered the screenshot,
+        // so let's log the exception message to a file (to explain why there's no screenshot) and allow the run to continue,
+        // handling the failure as normal.
+        try {
+            list ($dir, $filename) = $this->get_faildump_filename($scope, 'png');
+            $this->saveScreenshot($filename, $dir);
+        } catch (Exception $e) {
+            // Catching all exceptions as we don't know what the driver might throw.
+            list ($dir, $filename) = $this->get_faildump_filename($scope, 'txt');
+            $message = "Could not save screenshot due to an error\n" . $e->getMessage();
+            file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $message);
+        }
     }
 
     /**
@@ -442,9 +453,14 @@ class behat_hooks extends behat_base {
     protected function take_contentdump(AfterStepScope $scope) {
         list ($dir, $filename) = $this->get_faildump_filename($scope, 'html');
 
-        $fh = fopen($dir . DIRECTORY_SEPARATOR . $filename, 'w');
-        fwrite($fh, $this->getSession()->getPage()->getContent());
-        fclose($fh);
+        try {
+            // Driver may throw an exception during getContent(), so do it first to avoid getting an empty file.
+            $content = $this->getSession()->getPage()->getContent();
+        } catch (Exception $e) {
+            // Catching all exceptions as we don't know what the driver might throw.
+            $content = "Could not save contentdump due to an error\n" . $e->getMessage();
+        }
+        file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $content);
     }
 
     /**
index a7aa3bf..d0be09f 100644 (file)
@@ -115,36 +115,47 @@ class core_weblib_format_text_testcase extends advanced_testcase {
      */
     public function format_text_blanktarget_testcases() {
         return [
-            'Simple link' =>
-                [
-                    '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4">Hey, that\'s pretty good!</a>',
-                    '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
-                        ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
-                ],
-            'Link with rel' =>
-                [
-                    '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow">Hey, that\'s pretty good!</a>',
-                    '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow noreferrer"' .
-                        ' target="_blank">Hey, that\'s pretty good!</a></div>'
-                ],
-            'Link with rel noreferrer' =>
-                [
-                    '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer">Hey, that\'s pretty good!</a>',
-                    '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer"' .
-                     ' target="_blank">Hey, that\'s pretty good!</a></div>'
-                ],
-            'Link with target' =>
-                [
-                    '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">Hey, that\'s pretty good!</a>',
-                    '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">' .
-                        'Hey, that\'s pretty good!</a></div>'
-                ],
-            'Link with target blank' =>
-                [
-                    '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank">Hey, that\'s pretty good!</a>',
-                    '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
-                        ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
-                ]
+            'Simple link' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
+                    ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with rel' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow noreferrer"' .
+                    ' target="_blank">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with rel noreferrer' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer"' .
+                 ' target="_blank">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with target' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">' .
+                    'Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with target blank' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
+                    ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with Frank\'s casket inscription' => [
+                '<a href="https://en.wikipedia.org/wiki/Franks_Casket">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻ' .
+                    'ᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a>',
+                '<div class="text_to_html"><a href="https://en.wikipedia.org/wiki/Franks_Casket" target="_blank" ' .
+                    'rel="noreferrer">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾ' .
+                    'ᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a></div>'
+             ],
+            'No link' => [
+                'Some very boring text written with the Latin script',
+                '<div class="text_to_html">Some very boring text written with the Latin script</div>'
+            ],
+            'No link with Thror\'s map runes' => [
+                'ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ',
+                '<div class="text_to_html">ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹ' .
+                'ᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ</div>'
+            ]
         ];
     }
 }
index e094828..c86dc0e 100644 (file)
@@ -1321,7 +1321,7 @@ function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseidd
 
     if ($options['blanktarget']) {
         $domdoc = new DOMDocument();
-        $domdoc->loadHTML($text);
+        $domdoc->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $text);
         foreach ($domdoc->getElementsByTagName('a') as $link) {
             if ($link->hasAttribute('target') && strpos($link->getAttribute('target'), '_blank') === false) {
                 continue;
@@ -1336,7 +1336,7 @@ function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseidd
         // $domdoc->loadHTML($text, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); however it seems like the libxml
         // version that travis uses doesn't work properly and ends up leaving <html><body>, so I'm forced to use
         // this regex to remove those tags.
-        $text = trim(preg_replace('~<(?:!DOCTYPE|/?(?:html|body))[^>]*>\s*~i', '', $domdoc->saveHTML()));
+        $text = trim(preg_replace('~<(?:!DOCTYPE|/?(?:html|body))[^>]*>\s*~i', '', $domdoc->saveHTML($domdoc->documentElement)));
     }
 
     return $text;
index 88d0a5f..6702c3b 100644 (file)
@@ -137,7 +137,8 @@ class core_message_external extends external_api {
             if ($success && empty($contactlist[$message['touserid']]) && !empty($blocknoncontacts)) {
                 // The user isn't a contact and they have selected to block non contacts so this message won't be sent.
                 $success = false;
-                $errormessage = get_string('userisblockingyounoncontact', 'message');
+                $errormessage = get_string('userisblockingyounoncontact', 'message',
+                        fullname(core_user::get_user($message['touserid'])));
             }
 
             //now we can send the message (at least try)
index 347dbdf..8525ac1 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 084e92b..a5c1922 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 347dbdf..8525ac1 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 6912735..8d842f8 100644 (file)
@@ -1106,7 +1106,7 @@ EDITOR.prototype = {
         drawingcanvas.setStyle('height', page.height + 'px');
 
         // Update page select.
-        this.get_dialogue_element(SELECTOR.PAGESELECT).set('value', this.currentpage);
+        this.get_dialogue_element(SELECTOR.PAGESELECT).set('selectedIndex', this.currentpage);
 
         this.resize(); // Internally will call 'redraw', after checking the dialogue size.
     },
index c4ce567..cd23c8e 100644 (file)
@@ -1411,7 +1411,11 @@ function assign_get_completion_state($course, $cm, $userid, $type) {
 
     // If completion option is enabled, evaluate it and return true/false.
     if ($assign->get_instance()->completionsubmit) {
-        $submission = $assign->get_user_submission($userid, false);
+        if ($assign->get_instance()->teamsubmission) {
+            $submission = $assign->get_group_submission($userid, 0, false);
+        } else {
+            $submission = $assign->get_user_submission($userid, false);
+        }
         return $submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED;
     } else {
         // Completion option is not enabled so just return $type.
index c5a2b1e..5160225 100644 (file)
@@ -5466,11 +5466,10 @@ class assign {
      * @param int $updatetime
      * @return void
      */
-    public function send_notification($userfrom,
-                                      $userto,
-                                      $messagetype,
-                                      $eventtype,
-                                      $updatetime) {
+    public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) {
+        global $USER;
+        $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
+        $uniqueid = $this->get_uniqueid_for_user($userid);
         self::send_assignment_notification($userfrom,
                                            $userto,
                                            $messagetype,
@@ -5482,7 +5481,7 @@ class assign {
                                            $this->get_module_name(),
                                            $this->get_instance()->name,
                                            $this->is_blind_marking(),
-                                           $this->get_uniqueid_for_user($userfrom->id));
+                                           $uniqueid);
     }
 
     /**
@@ -5633,7 +5632,12 @@ class assign {
             $this->update_submission($submission, $userid, true, $instance->teamsubmission);
             $completion = new completion_info($this->get_course());
             if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
-                $completion->update_state($this->get_course_module(), COMPLETION_COMPLETE, $userid);
+                $this->update_activity_completion_records($instance->teamsubmission,
+                                                          $instance->requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          COMPLETION_COMPLETE,
+                                                          $completion);
             }
 
             if (!empty($data->submissionstatement) && $USER->id == $userid) {
@@ -6325,7 +6329,12 @@ class assign {
         }
         $completion = new completion_info($this->get_course());
         if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
-            $completion->update_state($this->get_course_module(), $complete, $USER->id);
+            $this->update_activity_completion_records($instance->teamsubmission,
+                                                      $instance->requireallteammemberssubmit,
+                                                      $submission,
+                                                      $USER->id,
+                                                      $complete,
+                                                      $completion);
         }
 
         if (!$instance->submissiondrafts) {
@@ -7991,6 +8000,42 @@ class assign {
         }
         return $this->get_course_module()->id . '_' . $id;
     }
+
+    /**
+     * Updates and creates the completion records in mdl_course_modules_completion.
+     *
+     * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
+     * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
+     * @param obj $submission the submission
+     * @param int $userid the user id
+     * @param int $complete
+     * @param obj $completion
+     *
+     * @return null
+     */
+    protected function update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion) {
+
+        if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
+            ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
+             $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
+
+            $members = groups_get_members($submission->groupid);
+
+            foreach ($members as $member) {
+                $completion->update_state($this->get_course_module(), $complete, $member->id);
+            }
+        } else {
+            $completion->update_state($this->get_course_module(), $complete, $userid);
+        }
+
+        return;
+    }
+
 }
 
 /**
index 412f72f..be6166d 100644 (file)
@@ -209,32 +209,6 @@ class mod_assign_mod_form extends moodleform_mod {
         $this->apply_admin_defaults();
 
         $this->add_action_buttons();
-
-        // Add warning popup/noscript tag, if grades are changed by user.
-        $hasgrade = false;
-        if (!empty($this->_instance)) {
-            $hasgrade = $DB->record_exists_select('assign_grades',
-                                                  'assignment = ? AND grade <> -1',
-                                                  array($this->_instance));
-        }
-
-        if ($mform->elementExists('grade') && $hasgrade) {
-            $module = array(
-                'name' => 'mod_assign',
-                'fullpath' => '/mod/assign/module.js',
-                'requires' => array('node', 'event'),
-                'strings' => array(array('changegradewarning', 'mod_assign'))
-                );
-            $PAGE->requires->js_init_call('M.mod_assign.init_grade_change', null, false, $module);
-
-            // Add noscript tag in case.
-            $noscriptwarning = $mform->createElement('static',
-                                                     'warning',
-                                                     null,
-                                                     html_writer::tag('noscript',
-                                                     get_string('changegradewarning', 'mod_assign')));
-            $mform->insertElementBefore($noscriptwarning, 'grade');
-        }
     }
 
     /**
index 57c234b..5c92b96 100644 (file)
@@ -90,7 +90,7 @@ class mod_assign_base_testcase extends advanced_testcase {
 
         $this->resetAfterTest(true);
 
-        $this->course = $this->getDataGenerator()->create_course();
+        $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
         $this->teachers = array();
         for ($i = 0; $i < self::DEFAULT_TEACHER_COUNT; $i++) {
             array_push($this->teachers, $this->getDataGenerator()->create_user());
@@ -350,4 +350,18 @@ class testable_assign extends assign {
 
         return $mform;
     }
+
+    public function testable_update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion) {
+        return parent::update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion);
+    }
 }
index 2062696..65670a5 100644 (file)
@@ -2631,4 +2631,94 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $grade = $assign->get_user_grade($this->students[0]->id, false);
         $this->assertEquals('30.0', $grade->grade);
     }
+
+    /**
+     * Test updating activity completion when submitting an assessment.
+     */
+    public function test_update_activity_completion_records_solitary_submission() {
+        $assign = $this->create_instance(array('grade' => 100,
+                'completion' => COMPLETION_TRACKING_AUTOMATIC,
+                'requireallteammemberssubmit' => 0));
+
+        $cm = $assign->get_course_module();
+
+        $student = $this->students[0];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        $this->setUser($student);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+        $completion = new completion_info($this->course);
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $submission = $assign->get_user_submission($student->id, true);
+
+        // Check that completion is not met yet.
+        $completiondata = $completion->get_data($cm, false, $student->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $assign->testable_update_activity_completion_records(0, 0, $submission,
+                $student->id, COMPLETION_COMPLETE, $completion);
+        // Completion should now be met.
+        $completiondata = $completion->get_data($cm, false, $student->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+    }
+
+    /**
+     * Test updating activity completion when submitting an assessment.
+     */
+    public function test_update_activity_completion_records_team_submission() {
+        $assign = $this->create_instance(array('grade' => 100,
+                'completion' => COMPLETION_TRACKING_AUTOMATIC,
+                 'teamsubmission' => 1));
+
+        $cm = $assign->get_course_module();
+
+        $student1 = $this->students[0];
+        $student2 = $this->students[1];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        // Put both users into a group.
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $this->course->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student2->id));
+
+        $this->setUser($student1);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+        $completion = new completion_info($this->course);
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $submission = $assign->get_user_submission($student1->id, true);
+        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+        $submission->groupid = $group1->id;
+
+        // Check that completion is not met yet.
+        $completiondata = $completion->get_data($cm, false, $student1->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $completiondata = $completion->get_data($cm, false, $student2->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $assign->testable_update_activity_completion_records(1, 0, $submission, $student1->id,
+                COMPLETION_COMPLETE, $completion);
+        // Completion should now be met.
+        $completiondata = $completion->get_data($cm, false, $student1->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+        $completiondata = $completion->get_data($cm, false, $student2->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+    }
 }
index fe08813..4816808 100644 (file)
@@ -3078,6 +3078,12 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
 
     // String cache
     static $str;
+    // This is an extremely hacky way to ensure we only print the 'unread' anchor
+    // the first time we encounter an unread post on a page. Ideally this would
+    // be moved into the caller somehow, and be better testable. But at the time
+    // of dealing with this bug, this static workaround was the most surgical and
+    // it fits together with only printing th unread anchor id once on a given page.
+    static $firstunreadanchorprinted = false;
 
     $modcontext = context_module::instance($cm->id);
 
@@ -3293,7 +3299,11 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
             $forumpostclass = ' read';
         } else {
             $forumpostclass = ' unread';
-            $output .= html_writer::tag('a', '', array('name'=>'unread'));
+            // If this is the first unread post printed then give it an anchor and id of unread.
+            if (!$firstunreadanchorprinted) {
+                $output .= html_writer::tag('a', '', array('id' => 'unread'));
+                $firstunreadanchorprinted = true;
+            }
         }
     } else {
         // ignore trackign status if not tracked or tracked param missing
index 75f7d8e..e8709c6 100644 (file)
@@ -2596,17 +2596,31 @@ function lti_load_type_from_cartridge($url, $type) {
         array(
             "title" => "lti_typename",
             "launch_url" => "lti_toolurl",
-            "description" => "lti_description"
+            "description" => "lti_description",
+            "icon" => "lti_icon",
+            "secure_icon" => "lti_secureicon"
         ),
         array(
-            "icon_url" => "lti_icon",
-            "secure_icon_url" => "lti_secureicon"
+            "icon_url" => "lti_extension_icon",
+            "secure_icon_url" => "lti_extension_secureicon"
         )
     );
     // If an activity name exists, unset the cartridge name so we don't override it.
     if (isset($type->lti_typename)) {
         unset($toolinfo['lti_typename']);
     }
+
+    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
+    if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) {
+        $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon'];
+    }
+    unset($toolinfo['lti_extension_icon']);
+
+    if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) {
+        $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon'];
+    }
+    unset($toolinfo['lti_extension_secureicon']);
+
     foreach ($toolinfo as $property => $value) {
         $type->$property = $value;
     }
@@ -2626,17 +2640,31 @@ function lti_load_tool_from_cartridge($url, $lti) {
             "title" => "name",
             "launch_url" => "toolurl",
             "secure_launch_url" => "securetoolurl",
-            "description" => "intro"
+            "description" => "intro",
+            "icon" => "icon",
+            "secure_icon" => "secureicon"
         ),
         array(
-            "icon_url" => "icon",
-            "secure_icon_url" => "secureicon"
+            "icon_url" => "extension_icon",
+            "secure_icon_url" => "extension_secureicon"
         )
     );
     // If an activity name exists, unset the cartridge name so we don't override it.
     if (isset($lti->name)) {
         unset($toolinfo['name']);
     }
+
+    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
+    if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) {
+        $toolinfo['icon'] = $toolinfo['extension_icon'];
+    }
+    unset($toolinfo['extension_icon']);
+
+    if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) {
+        $toolinfo['secureicon'] = $toolinfo['extension_secureicon'];
+    }
+    unset($toolinfo['extension_secureicon']);
+
     foreach ($toolinfo as $property => $value) {
         $lti->$property = $value;
     }
index cd021e3..f4b9175 100644 (file)
@@ -544,14 +544,14 @@ function quiz_cron() {
 }
 
 /**
- * @param int $quizid the quiz id.
+ * @param int|array $quizids A quiz ID, or an array of quiz IDs.
  * @param int $userid the userid.
  * @param string $status 'all', 'finished' or 'unfinished' to control
  * @param bool $includepreviews
  * @return an array of all the user's attempts at this quiz. Returns an empty
  *      array if there are none.
  */
-function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
+function quiz_get_user_attempts($quizids, $userid, $status = 'finished', $includepreviews = false) {
     global $DB, $CFG;
     // TODO MDL-33071 it is very annoying to have to included all of locallib.php
     // just to get the quiz_attempt::FINISHED constants, but I will try to sort
@@ -578,15 +578,18 @@ function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $include
             break;
     }
 
+    $quizids = (array) $quizids;
+    list($insql, $inparams) = $DB->get_in_or_equal($quizids, SQL_PARAMS_NAMED);
+    $params += $inparams;
+    $params['userid'] = $userid;
+
     $previewclause = '';
     if (!$includepreviews) {
         $previewclause = ' AND preview = 0';
     }
 
-    $params['quizid'] = $quizid;
-    $params['userid'] = $userid;
     return $DB->get_records_select('quiz_attempts',
-            'quiz = :quizid AND userid = :userid' . $previewclause . $statuscondition,
+            "quiz $insql AND userid = :userid" . $previewclause . $statuscondition,
             $params, 'attempt ASC');
 }
 
@@ -1465,6 +1468,20 @@ function quiz_print_overview($courses, &$htmlarray) {
         return;
     }
 
+    // Get the quizzes attempts.
+    $attemptsinfo = [];
+    $quizids = [];
+    foreach ($quizzes as $quiz) {
+        $quizids[] = $quiz->id;
+        $attemptsinfo[$quiz->id] = ['count' => 0, 'hasfinished' => false];
+    }
+    $attempts = quiz_get_user_attempts($quizids, $USER->id);
+    foreach ($attempts as $attempt) {
+        $attemptsinfo[$attempt->quiz]['count']++;
+        $attemptsinfo[$attempt->quiz]['hasfinished'] = true;
+    }
+    unset($attempts);
+
     // Fetch some language strings outside the main loop.
     $strquiz = get_string('modulename', 'quiz');
     $strnoattempts = get_string('noattempts', 'quiz');
@@ -1474,15 +1491,7 @@ function quiz_print_overview($courses, &$htmlarray) {
     $now = time();
     foreach ($quizzes as $quiz) {
         if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
-            // Give a link to the quiz, and the deadline.
-            $str = '<div class="quiz overview">' .
-                    '<div class="name">' . $strquiz . ': <a ' .
-                    ($quiz->visible ? '' : ' class="dimmed"') .
-                    ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
-                    $quiz->coursemodule . '">' .
-                    $quiz->name . '</a></div>';
-            $str .= '<div class="info">' . get_string('quizcloseson', 'quiz',
-                    userdate($quiz->timeclose)) . '</div>';
+            $str = '';
 
             // Now provide more information depending on the uers's role.
             $context = context_module::instance($quiz->coursemodule);
@@ -1490,30 +1499,48 @@ function quiz_print_overview($courses, &$htmlarray) {
                 // For teacher-like people, show a summary of the number of student attempts.
                 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
                 // fields set to make the following call work.
-                $str .= '<div class="info">' .
-                        quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
-            } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
-                    $context)) { // Student
+                $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
+
+            } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
                 // For student-like people, tell them how many attempts they have made.
-                if (isset($USER->id) &&
-                        ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
-                    $numattempts = count($attempts);
-                    $str .= '<div class="info">' .
-                            get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
+
+                if (isset($USER->id)) {
+                    if ($attemptsinfo[$quiz->id]['hasfinished']) {
+                        // The student's last attempt is finished.
+                        continue;
+                    }
+
+                    if ($attemptsinfo[$quiz->id]['count'] > 0) {
+                        $str .= '<div class="info">' .
+                            get_string('numattemptsmade', 'quiz', $attemptsinfo[$quiz->id]['count']) . '</div>';
+                    } else {
+                        $str .= '<div class="info">' . $strnoattempts . '</div>';
+                    }
+
                 } else {
                     $str .= '<div class="info">' . $strnoattempts . '</div>';
                 }
+
             } else {
                 // For ayone else, there is no point listing this quiz, so stop processing.
                 continue;
             }
 
-            // Add the output for this quiz to the rest.
-            $str .= '</div>';
+            // Give a link to the quiz, and the deadline.
+            $html = '<div class="quiz overview">' .
+                    '<div class="name">' . $strquiz . ': <a ' .
+                    ($quiz->visible ? '' : ' class="dimmed"') .
+                    ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
+                    $quiz->coursemodule . '">' .
+                    $quiz->name . '</a></div>';
+            $html .= '<div class="info">' . get_string('quizcloseson', 'quiz',
+                    userdate($quiz->timeclose)) . '</div>';
+            $html .= $str;
+            $html .= '</div>';
             if (empty($htmlarray[$quiz->course]['quiz'])) {
-                $htmlarray[$quiz->course]['quiz'] = $str;
+                $htmlarray[$quiz->course]['quiz'] = $html;
             } else {
-                $htmlarray[$quiz->course]['quiz'] .= $str;
+                $htmlarray[$quiz->course]['quiz'] .= $html;
             }
         }
     }
index 75edda7..22353ff 100644 (file)
@@ -227,4 +227,226 @@ class mod_quiz_lib_testcase extends advanced_testcase {
         $this->assertTrue(quiz_get_completion_state($course, $cm, $passstudent->id, 'return'));
         $this->assertFalse(quiz_get_completion_state($course, $cm, $failstudent->id, 'return'));
     }
+
+    public function test_quiz_get_user_attempts() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $dg = $this->getDataGenerator();
+        $quizgen = $dg->get_plugin_generator('mod_quiz');
+        $course = $dg->create_course();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
+        $u4 = $dg->create_user();
+        $role = $DB->get_record('role', ['shortname' => 'student']);
+
+        $dg->enrol_user($u1->id, $course->id, $role->id);
+        $dg->enrol_user($u2->id, $course->id, $role->id);
+        $dg->enrol_user($u3->id, $course->id, $role->id);
+        $dg->enrol_user($u4->id, $course->id, $role->id);
+
+        $quiz1 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
+        $quiz2 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
+
+        // Questions.
+        $questgen = $dg->get_plugin_generator('core_question');
+        $quizcat = $questgen->create_question_category();
+        $question = $questgen->create_question('numerical', null, ['category' => $quizcat->id]);
+        quiz_add_quiz_question($question->id, $quiz1);
+        quiz_add_quiz_question($question->id, $quiz2);
+
+        $quizobj1a = quiz::create($quiz1->id, $u1->id);
+        $quizobj1b = quiz::create($quiz1->id, $u2->id);
+        $quizobj1c = quiz::create($quiz1->id, $u3->id);
+        $quizobj1d = quiz::create($quiz1->id, $u4->id);
+        $quizobj2a = quiz::create($quiz2->id, $u1->id);
+
+        // Set attempts.
+        $quba1a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1a->get_context());
+        $quba1a->set_preferred_behaviour($quizobj1a->get_quiz()->preferredbehaviour);
+        $quba1b = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1b->get_context());
+        $quba1b->set_preferred_behaviour($quizobj1b->get_quiz()->preferredbehaviour);
+        $quba1c = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1c->get_context());
+        $quba1c->set_preferred_behaviour($quizobj1c->get_quiz()->preferredbehaviour);
+        $quba1d = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1d->get_context());
+        $quba1d->set_preferred_behaviour($quizobj1d->get_quiz()->preferredbehaviour);
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $timenow = time();
+
+        // User 1 passes quiz 1.
+        $attempt = quiz_create_attempt($quizobj1a, 1, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj1a, $quba1a, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1a, $quba1a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]);
+        $attemptobj->process_finish($timenow, false);
+
+        // User 2 goes overdue in quiz 1.
+        $attempt = quiz_create_attempt($quizobj1b, 1, false, $timenow, false, $u2->id);
+        quiz_start_new_attempt($quizobj1b, $quba1b, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1b, $quba1b, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_going_overdue($timenow, true);
+
+        // User 3 does not finish quiz 1.
+        $attempt = quiz_create_attempt($quizobj1c, 1, false, $timenow, false, $u3->id);
+        quiz_start_new_attempt($quizobj1c, $quba1c, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1c, $quba1c, $attempt);
+
+        // User 4 abandons the quiz 1.
+        $attempt = quiz_create_attempt($quizobj1d, 1, false, $timenow, false, $u4->id);
+        quiz_start_new_attempt($quizobj1d, $quba1d, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1d, $quba1d, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_abandon($timenow, true);
+
+        // User 1 attempts the quiz three times (abandon, finish, in progress).
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 1, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_abandon($timenow, true);
+
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 2, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 2, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_finish($timenow, false);
+
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 3, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 3, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+
+        // Check for user 1.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'finished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'unfinished');
+        $this->assertCount(0, $attempts);
+
+        // Check for user 2.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
+        $this->assertEquals($u2->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'finished');
+        $this->assertCount(0, $attempts);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
+        $this->assertEquals($u2->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        // Check for user 3.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u3->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'finished');
+        $this->assertCount(0, $attempts);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u3->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        // Check for user 4.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u4->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'finished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u4->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'unfinished');
+        $this->assertCount(0, $attempts);
+
+        // Multiple attempts for user 1 in quiz 2.
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'all');
+        $this->assertCount(3, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'finished');
+        $this->assertCount(2, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+
+        // Multiple quiz attempts fetched at once.
+        $attempts = quiz_get_user_attempts([$quiz1->id, $quiz2->id], $u1->id, 'all');
+        $this->assertCount(4, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+    }
+
 }
index 3959382..081db48 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 require_once($CFG->libdir . '/portfoliolib.php');
+require_once($CFG->libdir . '/portfolio/plugin.php');
 
 class portfolio_plugin_download extends portfolio_plugin_pull_base {
 
index 09a8aaf..5734d6b 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016052300.02;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016052300.03;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.2dev (Build: 20160603)'; // Human-friendly version name
+$release  = '3.2dev (Build: 20160609)'; // Human-friendly version name
 
 $branch   = '32';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.