Merge branch 'MDL-57918-master' of git://github.com/jleyva/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 28 Mar 2017 23:27:19 +0000 (01:27 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 28 Mar 2017 23:27:19 +0000 (01:27 +0200)
135 files changed:
admin/editors.php
admin/renderer.php
admin/tool/task/schedule_task.php
admin/tool/uploadcourse/classes/step2_form.php
admin/user.php
badges/backpack_form.php
badges/backpackemailverify.php
blocks/comments/tests/behat/behat_block_comments.php
course/amd/build/actions.min.js
course/amd/src/actions.js
course/edit_form.php
course/lib.php
enrol/tests/enrollib_test.php
grade/grading/form/guide/renderer.php
grade/grading/form/guide/tests/behat/edit_guide.feature
install/lang/et/admin.php
install/lang/et/error.php
install/lang/et/install.php
install/lang/et/langconfig.php
install/lang/et/moodle.php
install/lang/ja/error.php
install/lang/pt_br/install.php
install/lang/sr_cr/install.php
install/lang/sr_lt/install.php
lang/en/badges.php
lang/en/moodle.php
lib/classes/filetypes.php
lib/coursecatlib.php
lib/datalib.php
lib/enrollib.php
lib/form/defaultcustom.php
lib/phpunit/classes/util.php
mod/feedback/classes/completion.php
mod/feedback/classes/external.php
mod/feedback/classes/external/feedback_value_exporter.php [new file with mode: 0644]
mod/feedback/db/services.php
mod/feedback/db/upgrade.php
mod/feedback/tests/external_test.php
mod/feedback/version.php
mod/lesson/backup/moodle2/backup_lesson_stepslib.php
mod/lesson/classes/external.php
mod/lesson/classes/external/lesson_summary_exporter.php [new file with mode: 0644]
mod/lesson/continue.php
mod/lesson/db/install.xml
mod/lesson/db/services.php
mod/lesson/db/upgrade.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/mod_form.php
mod/lesson/pagetypes/branchtable.php
mod/lesson/pagetypes/essay.php
mod/lesson/pagetypes/matching.php
mod/lesson/pagetypes/multichoice.php
mod/lesson/renderer.php
mod/lesson/report.php
mod/lesson/tests/external_test.php
mod/lesson/tests/lib_test.php
mod/lesson/version.php
mod/lesson/view.php
mod/quiz/locallib.php
pix/f/archive.svg
pix/f/audio.svg
pix/f/avi.svg
pix/f/base.svg
pix/f/bmp.svg
pix/f/calc.svg
pix/f/chart.svg
pix/f/database.svg
pix/f/dmg.svg
pix/f/document.svg
pix/f/draw.svg
pix/f/eps.svg
pix/f/epub.svg
pix/f/flash.svg
pix/f/folder-open.svg
pix/f/folder.svg
pix/f/gif.svg
pix/f/html.svg
pix/f/image.svg
pix/f/impress.svg
pix/f/isf.svg
pix/f/jpeg.svg
pix/f/markup.svg
pix/f/math.svg
pix/f/moodle.svg
pix/f/mov.svg
pix/f/mp3.svg
pix/f/mpeg.svg
pix/f/oth.svg
pix/f/pdf.svg
pix/f/png.svg
pix/f/powerpoint.svg
pix/f/psd.svg
pix/f/publisher.svg
pix/f/quicktime.svg
pix/f/sourcecode.svg
pix/f/spreadsheet.svg
pix/f/text.svg
pix/f/tiff.svg
pix/f/unknown.svg
pix/f/video.svg
pix/f/wav.svg
pix/f/wmv.svg
pix/f/writer.svg
question/category.php
question/edit.php
question/export.php
question/import.php
question/renderer.php
theme/boost/classes/output/core_question/bank_renderer.php [new file with mode: 0644]
theme/boost/pix/fp/add_file.svg
theme/boost/pix/fp/alias.svg
theme/boost/pix/fp/check.svg
theme/boost/pix/fp/create_folder.svg
theme/boost/pix/fp/cross.svg
theme/boost/pix/fp/download_all.svg
theme/boost/pix/fp/folder.svg
theme/boost/pix/fp/help.svg
theme/boost/pix/fp/link.svg
theme/boost/pix/fp/link_sm.svg
theme/boost/pix/fp/list.svg
theme/boost/pix/fp/logout.svg
theme/boost/pix/fp/path_folder.svg
theme/boost/pix/fp/path_folder_rtl.svg
theme/boost/pix/fp/refresh.svg
theme/boost/pix/fp/search.svg
theme/boost/pix/fp/setting.svg
theme/boost/pix/fp/th.svg
theme/boost/pix/fp/view_list_active.svg
theme/boost/pix/fp/view_tree_active.svg
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
theme/image.php
version.php

index e1be31f..5d8a50c 100644 (file)
@@ -47,6 +47,7 @@ switch ($action) {
         // remove from enabled list
         $key = array_search($editor, $active_editors);
         unset($active_editors[$key]);
+        add_to_config_log('editor_visibility', '1', '0', $editor);
         break;
 
     case 'enable':
@@ -54,6 +55,7 @@ switch ($action) {
         if (!in_array($editor, $active_editors)) {
             $active_editors[] = $editor;
             $active_editors = array_unique($active_editors);
+            add_to_config_log('editor_visibility', '0', '1', $editor);
         }
         break;
 
@@ -66,6 +68,7 @@ switch ($action) {
                 $fsave = $active_editors[$key];
                 $active_editors[$key] = $active_editors[$key + 1];
                 $active_editors[$key + 1] = $fsave;
+                add_to_config_log('editor_position', $key, $key + 1, $editor);
             }
         }
         break;
@@ -79,6 +82,7 @@ switch ($action) {
                 $fsave = $active_editors[$key];
                 $active_editors[$key] = $active_editors[$key - 1];
                 $active_editors[$key - 1] = $fsave;
+                add_to_config_log('editor_position', $key, $key - 1, $editor);
             }
         }
         break;
index 613c8e1..451a504 100644 (file)
@@ -1615,7 +1615,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 if ($this->page->theme->resolve_image_location('icon', $plugin->type . '_' . $plugin->name)) {
                     $icon = $this->output->pix_icon('icon', '', $plugin->type . '_' . $plugin->name, array('class' => 'icon pluginicon'));
                 } else {
-                    $icon = $this->output->spacer('spacer');
+                    $icon = $this->output->spacer();
                 }
                 $status = $plugin->get_status();
                 $row->attributes['class'] .= ' status-'.$status;
index 53136f2..afb20b6 100644 (file)
@@ -24,6 +24,8 @@
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+define('NO_OUTPUT_BUFFERING', true);
+
 require('../../../config.php');
 
 require_once($CFG->libdir.'/cronlib.php');
@@ -36,12 +38,6 @@ require_once($CFG->libdir.'/cronlib.php');
  */
 function tool_task_mtrace_wrapper($message, $eol) {
     echo s($message . $eol);
-    // Both types of flush may be necessary in order to actually output progressively to browser.
-    // It depends on the theme.
-    if (ob_get_status()) {
-        ob_flush();
-    }
-    flush();
 }
 
 // Allow execution of single task. This requires login and has different rules.
index cb69f1e..4601500 100644 (file)
@@ -89,8 +89,8 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
         $choices = array();
         $choices['0'] = get_string('hide');
         $choices['1'] = get_string('show');
-        $mform->addElement('select', 'defaults[visible]', get_string('visible'), $choices);
-        $mform->addHelpButton('defaults[visible]', 'visible');
+        $mform->addElement('select', 'defaults[visible]', get_string('coursevisibility'), $choices);
+        $mform->addHelpButton('defaults[visible]', 'coursevisibility');
         $mform->setDefault('defaults[visible]', $courseconfig->visible);
 
         $mform->addElement('date_selector', 'defaults[startdate]', get_string('startdate'));
index b131727..65f9f8c 100644 (file)
             } else {
                 $columnicon = ($dir == "ASC") ? "sort_asc" : "sort_desc";
             }
-            $columnicon = $OUTPUT->pix_icon('t/' . $columnicon, get_string($columndir), 'core', ['class' => 'iconsort']);
+            $columnicon = $OUTPUT->pix_icon('t/' . $columnicon, get_string(strtolower($columndir)), 'core',
+                                            ['class' => 'iconsort']);
 
         }
         $$column = "<a href=\"user.php?sort=$column&amp;dir=$columndir\">".$string[$column]."</a>$columnicon";
index dda5ed4..417c960 100644 (file)
@@ -101,6 +101,8 @@ class edit_backpack_form extends moodleform {
             $request = $bp->curl_request('user');
             if (isset($request->status) && $request->status == 'missing') {
                 $errors['email'] = get_string('error:nosuchuser', 'badges');
+            } else if (!isset($request->status) || $request->status !== 'okay') {
+                $errors['email'] = get_string('backpackconnectionunexpectedresult', 'badges');
             }
         }
         return $errors;
index 2ffb35f..d9dfda2 100644 (file)
@@ -43,11 +43,20 @@ if (!is_null($storedsecret)) {
         $data->email = $storedemail;
         $bp = new OpenBadgesBackpackHandler($data);
 
+        // Make sure we have all the required information before trying to save the connection.
+        $backpackuser = $bp->curl_request('user');
+        if (isset($backpackuser->status) && $backpackuser->status === 'okay' && isset($backpackuser->userId)) {
+            $backpackuid = $backpackuser->userId;
+        } else {
+            redirect(new moodle_url($redirect), get_string('backpackconnectionunexpectedresult', 'badges'),
+                null, \core\output\notification::NOTIFY_ERROR);
+        }
+
         $obj = new stdClass();
         $obj->userid = $USER->id;
         $obj->email = $data->email;
         $obj->backpackurl = $data->backpackurl;
-        $obj->backpackuid = $bp->curl_request('user')->userId;
+        $obj->backpackuid = $backpackuid;
         $obj->autosync = 0;
         $obj->password = '';
         $DB->insert_record('badge_backpack', $obj);
index 07285e9..587695b 100644 (file)
@@ -100,7 +100,7 @@ class behat_block_comments extends behat_base {
 
         // Click on delete icon.
         $this->execute('behat_general::i_click_on_in_the',
-            array(get_string('deletecomment'), "icon", $this->escape($commentxpath), "xpath_element")
+            array("Delete comment posted by", "icon", $this->escape($commentxpath), "xpath_element")
         );
 
         // Wait for the animation to finish, in theory is just 1 sec, adding 4 just in case.
index 797a71e..d8e4d3b 100644 (file)
Binary files a/course/amd/build/actions.min.js and b/course/amd/build/actions.min.js differ
index 5c4a72c..da81240 100644 (file)
@@ -355,15 +355,22 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
          */
         var replaceActionItem = function(actionitem, image, stringname,
                                            stringcomponent, titlestr, titlecomponent, newaction) {
-            actionitem.find('img').attr('src', url.imageUrl(image, 'core'));
+
             str.get_string(stringname, stringcomponent).done(function(newstring) {
                 actionitem.find('span.menu-action-text').html(newstring);
                 actionitem.attr('title', newstring);
             });
             if (titlestr) {
-                str.get_string(titlestr, titlecomponent).done(function(newtitle) {
+                str.get_string(titlestr, titlecomponent).then(function(newtitle) {
+                    templates.renderPix(image, 'core', newtitle).then(function(html) {
+                        actionitem.find('.icon').replaceWith(html);
+                    });
                     actionitem.attr('title', newtitle);
                 });
+            } else {
+                templates.renderPix(image, 'core', '').then(function(html) {
+                    actionitem.find('.icon').replaceWith(html);
+                });
             }
             actionitem.attr('data-action', newaction);
         };
@@ -591,4 +598,4 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                 replaceActionItem(actionitem, image, stringname, stringcomponent, titlestr, titlecomponent, newaction);
             }
         };
-    });
\ No newline at end of file
+    });
index ea22fe7..4c03e2d 100644 (file)
@@ -106,8 +106,8 @@ class course_edit_form extends moodleform {
         $choices = array();
         $choices['0'] = get_string('hide');
         $choices['1'] = get_string('show');
-        $mform->addElement('select', 'visible', get_string('visible'), $choices);
-        $mform->addHelpButton('visible', 'visible');
+        $mform->addElement('select', 'visible', get_string('coursevisibility'), $choices);
+        $mform->addHelpButton('visible', 'coursevisibility');
         $mform->setDefault('visible', $courseconfig->visible);
         if (!empty($course->id)) {
             if (!has_capability('moodle/course:visibility', $coursecontext)) {
index 49223f3..980173f 100644 (file)
@@ -2491,7 +2491,7 @@ function update_course($data, $editoroptions = NULL) {
     // Set newsitems to 0 if format does not support announcements.
     if (isset($data->format)) {
         $newcourseformat = course_get_format((object)['format' => $data->format]);
-        if (!empty($data->newsitems) && !$newcourseformat->supports_news()) {
+        if (!$newcourseformat->supports_news()) {
             $data->newsitems = 0;
         }
     }
index 0355f13..b3dcfc1 100644 (file)
@@ -422,4 +422,58 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertEquals('self', $event->other['enrol']);
         $this->assertEventContextNotUsed($event);
     }
+
+    /**
+     * Confirms that timemodified field was updated after modification of user enrollment
+     */
+    public function test_enrollment_update_timemodified() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+        $datagen = $this->getDataGenerator();
+
+        /** @var enrol_manual_plugin $manualplugin */
+        $manualplugin = enrol_get_plugin('manual');
+        $this->assertNotNull($manualplugin);
+
+        $studentroleid = $DB->get_field('role', 'id', ['shortname' => 'student'], MUST_EXIST);
+        $course = $datagen->create_course();
+        $user = $datagen->create_user();
+
+        $instanceid = null;
+        $instances = enrol_get_instances($course->id, true);
+        foreach ($instances as $inst) {
+            if ($inst->enrol == 'manual') {
+                $instanceid = (int)$inst->id;
+                break;
+            }
+        }
+        if (empty($instanceid)) {
+            $instanceid = $manualplugin->add_default_instance($course);
+            if (empty($instanceid)) {
+                $instanceid = $manualplugin->add_instance($course);
+            }
+        }
+        $this->assertNotNull($instanceid);
+
+        $instance = $DB->get_record('enrol', ['id' => $instanceid], '*', MUST_EXIST);
+        $manualplugin->enrol_user($instance, $user->id, $studentroleid, 0, 0, ENROL_USER_ACTIVE);
+        $userenrolorig = (int)$DB->get_field(
+            'user_enrolments',
+            'timemodified',
+            ['enrolid' => $instance->id, 'userid' => $user->id],
+            MUST_EXIST
+        );
+        $this->waitForSecond();
+        $this->waitForSecond();
+        $manualplugin->update_user_enrol($instance, $user->id, ENROL_USER_SUSPENDED);
+        $userenrolpost = (int)$DB->get_field(
+            'user_enrolments',
+            'timemodified',
+            ['enrolid' => $instance->id, 'userid' => $user->id],
+            MUST_EXIST
+        );
+
+        $this->assertGreaterThan($userenrolorig, $userenrolpost);
+    }
 }
index a0466f5..1292a9e 100644 (file)
@@ -249,7 +249,7 @@ class gradingform_guide_renderer extends plugin_renderer_base {
                 foreach ($comments as $id => $comment) {
                     $commentoption = new stdClass();
                     $commentoption->id = $id;
-                    $commentoption->description = s($comment['description']);
+                    $commentoption->description = $comment['description'];
                     $commentoptions[] = $commentoption;
                 }
 
index 8f1ff15..37a2563 100644 (file)
@@ -37,7 +37,7 @@ Feature: Marking guides can be created and edited
       | Comment 1 |
       | Comment 2 |
       | Comment 3 |
-      | Comment 4 |
+      | Comment "4" |
     And I press "Save marking guide and make it ready"
     Then I should see "Ready for use"
     And I should see "Guide criterion A"
@@ -46,7 +46,7 @@ Feature: Marking guides can be created and edited
     And I should see "Comment 1"
     And I should see "Comment 2"
     And I should see "Comment 3"
-    And I should see "Comment 4"
+    And I should see "Comment \"4\""
 
   @javascript
   Scenario: Deleting criterion and comment
@@ -66,7 +66,7 @@ Feature: Marking guides can be created and edited
     And I press "Save"
     Then I should see "Comment 1"
     And I should see "Comment 2"
-    And I should see "Comment 4"
+    And I should see "Comment \"4\""
     But I should not see "Comment 3"
 
   @javascript
@@ -80,9 +80,9 @@ Feature: Marking guides can be created and edited
     # Inserting frequently used comment.
     And I click on "Insert frequently used comment" "button" in the "Guide criterion B" "table_row"
     And I wait "1" seconds
-    And I press "Comment 4"
+    And I press "Comment \"4\""
     And I wait "1" seconds
-    Then the field "Guide criterion B criterion remark" matches value "Comment 4"
+    Then the field "Guide criterion B criterion remark" matches value "Comment \"4\""
     When I press "Save changes"
     And I press "Ok"
     And I follow "Edit settings"
@@ -98,7 +98,7 @@ Feature: Marking guides can be created and edited
     And I should see "80" in the ".feedback" "css_element"
     And I should see "Marking guide test description" in the ".feedback" "css_element"
     And I should see "Very good"
-    And I should see "Comment 4"
+    And I should see "Comment \"4\""
     And I should see "Nice!"
 
   Scenario: I can use marking guides to grade and edit them later updating students grades with Javascript disabled
index 3dbf987..39165fa 100644 (file)
@@ -42,3 +42,4 @@ Palun kasuta --help valikut.';
 $string['cliyesnoprompt'] = 'kirjuta y (tähendab jah) või n (tähendab ei)';
 $string['environmentrequireinstall'] = 'on nõutud, et oleks installeeritud ja võimaldatud';
 $string['environmentrequireversion'] = 'versioon {$a->needed} on nõutud, Sinul on jooksmas versioon {$a->current}';
+$string['upgradekeyset'] = 'Versioonitäienduse võti (jätke tühjaks, kui te ei soovi seda määrata)';
index 138c22b..4ecc180 100644 (file)
@@ -39,6 +39,7 @@ $string['cannotsavemd5file'] = 'Ei saa salvestada md5 faili.';
 $string['cannotsavezipfile'] = 'Ei saa salvestada ZIP faili.';
 $string['cannotunzipfile'] = 'Ei saa lahti pakkida faili.';
 $string['componentisuptodate'] = 'Komponent on ajakohane';
+$string['dmlexceptiononinstall'] = '<p>Ilmnes andmebaasitõrge [{$a->errorcode}].<br />{$a->debuginfo}</p>';
 $string['downloadedfilecheckfailed'] = 'Alla laetud faili kontroll ebaõnnestus.';
 $string['invalidmd5'] = 'Vigane md5';
 $string['missingrequiredfield'] = 'Mõni nõutav väli on puudu';
index e76f766..41d379e 100644 (file)
@@ -34,6 +34,7 @@ $string['admindirname'] = 'Administreerimiskataloog';
 $string['availablelangs'] = 'Saadaval keelte nimekiri';
 $string['chooselanguagehead'] = 'Valige keel';
 $string['chooselanguagesub'] = 'Palun vali keel, mida kasutatakse AINULT installeerimise käigus. Hiljem saab valida õpikeskkonna ja kasutajate keelt.';
+$string['clialreadyconfigured'] = 'Fail „config.php“ on juba olemas. Kasutage sellele saidile Moodle‘i installimiseks faili „admin/cli/install_database.php“.';
 $string['clialreadyinstalled'] = 'Fail config.php juba eksisteerib, palun kasuta admin/cli/upgrade.php kui soovid uuendada oma saiti.';
 $string['cliinstallheader'] = 'Moodle’i {$a} käsureapõhine installiprogramm';
 $string['databasehost'] = 'Andmebaasi host';
index f0d9ab0..efc549b 100644 (file)
@@ -32,4 +32,4 @@ defined('MOODLE_INTERNAL') || die();
 
 $string['parentlanguage'] = '';
 $string['thisdirection'] = 'ltr';
-$string['thislanguage'] = 'inglise';
+$string['thislanguage'] = 'eesti';
index 49c6c21..21144e5 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'Keel';
+$string['moodlelogo'] = 'Moodle‘i logo';
 $string['next'] = 'Järgmine';
 $string['previous'] = 'Eelmine';
 $string['reload'] = 'Laadi uuesti';
index 4e85def..55ddba9 100644 (file)
@@ -46,7 +46,7 @@ $string['dmlexceptiononinstall'] = '<p>データベースエラーが発生し
 $string['downloadedfilecheckfailed'] = 'ダウンロードファイルのチェックに失敗しました。';
 $string['invalidmd5'] = 'チェック変数が正しくありません - 再度お試しください。';
 $string['missingrequiredfield'] = 'いくつかの必須入力フィールドが入力されていません。';
-$string['remotedownloaderror'] = '<p>ã\81\82ã\81ªã\81\9fã\81®ã\82µã\83¼ã\83\90ã\81¸ã\81®ã\82³ã\83³ã\83\9dã\83¼ã\83\8dã\83³ã\83\88ã\81®ã\83\80ã\82¦ã\83³ã\83­ã\83¼ã\83\89ã\81«å¤±æ\95\97ã\81\97ã\81¾ã\81\97ã\81\9fã\80\82ã\83\97ã\83­ã\82¯シ設定を確認してください。PHP cURL拡張モジュールの使用を強くお勧めします。</p>
+$string['remotedownloaderror'] = '<p>ã\81\82ã\81ªã\81\9fã\81®ã\82µã\83¼ã\83\90ã\81¸ã\81®ã\82³ã\83³ã\83\9dã\83¼ã\83\8dã\83³ã\83\88ã\81®ã\83\80ã\82¦ã\83³ã\83­ã\83¼ã\83\89ã\81«å¤±æ\95\97ã\81\97ã\81¾ã\81\97ã\81\9fã\80\82ã\83\97ã\83­ã\82­シ設定を確認してください。PHP cURL拡張モジュールの使用を強くお勧めします。</p>
 <p><a href="{$a->url}">{$a->url}</a>ファイルを手動でダウンロードした後、あなたのサーバの「{$a->dest}」にコピーおよび展開してください。</p>';
 $string['wrongdestpath'] = '宛先パスが正しくありません。';
 $string['wrongsourcebase'] = 'ソースURLベースが正しくありません。';
index d4b10fc..af2c5bd 100644 (file)
@@ -82,10 +82,9 @@ $string['pathsunsecuredataroot'] = 'A localização da pasta de dados não é se
 $string['pathswrongadmindir'] = 'Diretório Admin não existe';
 $string['phpextension'] = 'Extensão PHP {$a}';
 $string['phpversion'] = 'Versão do PHP';
-$string['phpversionhelp'] = '<p>Moodle requer a versão 4.3.0 de PHP ou posterior.</p>
-<p>A sua versão é  a {$a}</p>
-<p>Atualize a versão do PHP!</p>
-(atenção, a versão 5.0.x tem muitos problemas - use a versão 5.1.0 ou a 4.4)';
+$string['phpversionhelp'] = '<p>O Moodle requer uma versão PHP de pelo menos 5.6.5 (o 7.x tem algumas limitações do motor). </p>
+<p>Atualmente, você está executando a versão {$ a}. </p>
+<p>Você deve atualizar o PHP ou migrar para um host com uma versão mais recente do PHP. </p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Você está vendo esa página pois instalou com sucesso o pacote<strong>{$a->packname} {$a->packversion}</strong>. Parabéns!';
 $string['welcomep30'] = 'Esta versão do <strong>{$a->installername}</strong> inclui as aplicações para a criação de um ambiente em que <strong>Moodle</strong> possa operar:';
index 2305267..924e37a 100644 (file)
@@ -80,10 +80,9 @@ $string['pathsunsecuredataroot'] = 'Dataroot локација није безб
 $string['pathswrongadmindir'] = 'Админ директоријум не постоји';
 $string['phpextension'] = '{$a} PHP екстензија';
 $string['phpversion'] = 'PHP верзија';
-$string['phpversionhelp'] = '<p>Moodle захтева најмање PHP верзију 4.3.0 или 5.1.0 (5.0.x има  бројне уочене проблеме).</p>
+$string['phpversionhelp'] = '<p>Moodle захтева најмање PHP верзију 5.6.7 (7.0.x има нека ограничења).</p>
 <p>Тренутно користите верзију {$a}</p>
-<p>Морате надоградити PHP или преместити Moodle на веб сервер са новијом верзијом PHP-a!</br>
-(У случају верзије 5.0.x можете, такође, да се вратите на 4.4.x верзију)</p>';
+<p>Морате да надоградите PHP или да преместити Moodle на веб сервер са новијом верзијом PHP-a.</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Ову страницу видите зато што сте успешно инсталирали и покренули <strong>{$a->packname} {$a->packversion}</strong> пакет на свом серверу. Честитамо!';
 $string['welcomep30'] = 'Ово издање <strong>{$a->installername}</strong> укључује апликације за креирање окружења у којем ће <strong>Moodle</strong> успешно функционисати, конкретно:';
index cac942b..74e5e8b 100644 (file)
@@ -80,10 +80,9 @@ $string['pathsunsecuredataroot'] = 'Dataroot lokacija nije bezbedna';
 $string['pathswrongadmindir'] = 'Admin direktorijum ne postoji';
 $string['phpextension'] = '{$a} PHP ekstenѕija';
 $string['phpversion'] = 'PHP verzija';
-$string['phpversionhelp'] = '<p>Moodle zahteva najmanje PHP verziju 4.3.0 ili 5.1.0 (5.0.x ima  brojne uočene probleme).</p>
+$string['phpversionhelp'] = '<p>Moodle zahteva najmanje PHP verziju 5.6.7 (7.0.x ima neka ograničenja).</p>
 <p>Trenutno koristite verziju {$a}</p>
-<p>Morate nadograditi PHP ili premestiti Moodle na veb server sa novijom verzijom PHP-a!</br>
-(U slučaju verzije 5.0.x možete, takođe, da se vratite na 4.4.x verziju)</p>';
+<p>Morate da nadogradite PHP ili da premestiti Moodle na veb server sa novijom verzijom PHP-a.</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Ovu stranicu vidite zato što ste uspešno instalirali i pokrenuli <strong>{$a->packname} {$a->packversion}</strong> paket na svom serveru. Čestitamo!';
 $string['welcomep30'] = 'Ovo izdanje <strong>{$a->installername}</strong> uključuje aplikacije za kreiranje okruženja u kojem će <strong>Moodle</strong> uspešno funkcionisati, konkretno:';
index fbc4434..7ac9a73 100644 (file)
@@ -87,6 +87,7 @@ Currently, only <a href="http://backpack.openbadges.org">Mozilla OpenBadges Back
 $string['backpackconnectioncancelattempt'] = 'Connect using a different email address';
 $string['backpackconnectionconnect'] = 'Connect to Backpack';
 $string['backpackconnectionresendemail'] = 'Resend verification email';
+$string['backpackconnectionunexpectedresult'] = 'There was a problem contacting the Backpack. Please try again.<br><br>If this problem persists, please contact your system administrator.';
 $string['backpackdetails'] = 'Backpack settings';
 $string['backpackemail'] = 'Email address';
 $string['backpackemail_help'] = 'The email address associated with your backpack. While you are connected, any badges earned on this site will be associated with this email address.';
index 839580f..24fa36d 100644 (file)
@@ -381,6 +381,8 @@ $string['coursesummary'] = 'Course summary';
 $string['coursesummary_help'] = 'The course summary is displayed in the list of courses. A course search searches course summary text in addition to course names.';
 $string['coursetitle'] = 'Course: {$a->course}';
 $string['courseupdates'] = 'Course updates';
+$string['coursevisibility'] = 'Course visibility';
+$string['coursevisibility_help'] = 'This setting determines whether the course appears in the list of courses and whether students can access it. If set to Hide, then access is restricted to users with the capability to view hidden courses (such as teachers).';
 $string['create'] = 'Create';
 $string['createaccount'] = 'Create my new account';
 $string['createcategory'] = 'Create category';
index 4a160b0..b2ac062 100644 (file)
@@ -171,8 +171,10 @@ abstract class core_filetypes {
             'odm' => array('type' => 'application/vnd.oasis.opendocument.text-master', 'icon' => 'writer'),
             'odg' => array('type' => 'application/vnd.oasis.opendocument.graphics', 'icon' => 'draw'),
             'otg' => array('type' => 'application/vnd.oasis.opendocument.graphics-template', 'icon' => 'draw'),
-            'odp' => array('type' => 'application/vnd.oasis.opendocument.presentation', 'icon' => 'impress'),
-            'otp' => array('type' => 'application/vnd.oasis.opendocument.presentation-template', 'icon' => 'impress'),
+            'odp' => array('type' => 'application/vnd.oasis.opendocument.presentation', 'icon' => 'impress',
+                    'groups' => array('presentation')),
+            'otp' => array('type' => 'application/vnd.oasis.opendocument.presentation-template', 'icon' => 'impress',
+                    'groups' => array('presentation')),
             'ods' => array('type' => 'application/vnd.oasis.opendocument.spreadsheet',
                     'icon' => 'calc', 'groups' => array('spreadsheet')),
             'ots' => array('type' => 'application/vnd.oasis.opendocument.spreadsheet-template',
@@ -197,15 +199,19 @@ abstract class core_filetypes {
             'pps' => array('type' => 'application/vnd.ms-powerpoint', 'icon' => 'powerpoint', 'groups' => array('presentation')),
             'ppt' => array('type' => 'application/vnd.ms-powerpoint', 'icon' => 'powerpoint', 'groups' => array('presentation')),
             'pptx' => array('type' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
-                    'icon' => 'powerpoint'),
-            'pptm' => array('type' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', 'icon' => 'powerpoint'),
+                    'icon' => 'powerpoint', 'groups' => array('presentation')),
+            'pptm' => array('type' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', 'icon' => 'powerpoint',
+                    'groups' => array('presentation')),
             'potx' => array('type' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
-                    'icon' => 'powerpoint'),
-            'potm' => array('type' => 'application/vnd.ms-powerpoint.template.macroEnabled.12', 'icon' => 'powerpoint'),
-            'ppam' => array('type' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', 'icon' => 'powerpoint'),
+                    'icon' => 'powerpoint', 'groups' => array('presentation')),
+            'potm' => array('type' => 'application/vnd.ms-powerpoint.template.macroEnabled.12', 'icon' => 'powerpoint',
+                    'groups' => array('presentation')),
+            'ppam' => array('type' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', 'icon' => 'powerpoint',
+                    'groups' => array('presentation')),
             'ppsx' => array('type' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
-                    'icon' => 'powerpoint'),
-            'ppsm' => array('type' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', 'icon' => 'powerpoint'),
+                    'icon' => 'powerpoint', 'groups' => array('presentation')),
+            'ppsm' => array('type' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', 'icon' => 'powerpoint',
+                    'groups' => array('presentation')),
             'ps' => array('type' => 'application/postscript', 'icon' => 'pdf'),
             'pub' => array('type' => 'application/x-mspublisher', 'icon' => 'publisher', 'groups' => array('presentation')),
 
@@ -247,8 +253,9 @@ abstract class core_filetypes {
             'stc' => array('type' => 'application/vnd.sun.xml.calc.template', 'icon' => 'calc'),
             'sxd' => array('type' => 'application/vnd.sun.xml.draw', 'icon' => 'draw'),
             'std' => array('type' => 'application/vnd.sun.xml.draw.template', 'icon' => 'draw'),
-            'sxi' => array('type' => 'application/vnd.sun.xml.impress', 'icon' => 'impress'),
-            'sti' => array('type' => 'application/vnd.sun.xml.impress.template', 'icon' => 'impress'),
+            'sxi' => array('type' => 'application/vnd.sun.xml.impress', 'icon' => 'impress', 'groups' => array('presentation')),
+            'sti' => array('type' => 'application/vnd.sun.xml.impress.template', 'icon' => 'impress',
+                    'groups' => array('presentation')),
             'sxg' => array('type' => 'application/vnd.sun.xml.writer.global', 'icon' => 'writer'),
             'sxm' => array('type' => 'application/vnd.sun.xml.math', 'icon' => 'math'),
 
index d8ce7b8..e5edde1 100644 (file)
@@ -626,9 +626,7 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         }
         // We must add countall to all in case it was the requested ID.
         $all['countall'] = $count;
-        foreach ($all as $key => $children) {
-            $coursecattreecache->set($key, $children);
-        }
+        $coursecattreecache->set_many($all);
         if (array_key_exists($id, $all)) {
             return $all[$id];
         }
index 96a940c..df48d6d 100644 (file)
@@ -787,7 +787,7 @@ function get_courses_search($searchterms, $sort, $page, $recordsperpage, &$total
             $searchcond[] = "$concat $REGEXP :ss$i";
             $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)";
 
-        } else if (substr($searchterm,0,1) == "-") {
+        } else if ((substr($searchterm,0,1) == "-") && (core_text::strlen($searchterm) > 1)) {
             $searchterm = trim($searchterm, '+-');
             $searchterm = preg_quote($searchterm, '|');
             $searchcond[] = "$concat $NOTREGEXP :ss$i";
index 8bb17af..18350d9 100644 (file)
@@ -1744,6 +1744,7 @@ abstract class enrol_plugin {
         }
 
         $ue->modifierid = $USER->id;
+        $ue->timemodified = time();
         $DB->update_record('user_enrolments', $ue);
         context_course::instance($instance->courseid)->mark_dirty(); // reset enrol caches
 
index a09d566..b8512aa 100644 (file)
@@ -126,7 +126,7 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
         if (!$this->has_customize_switch()) {
             $element = $this->createFormElement('hidden', 'customize', 1);
         } else {
-            $element = $this->createFormElement('checkbox', 'customize', '', $this->_options['customlabel']);
+            $element = $this->createFormElement('advcheckbox', 'customize', '', $this->_options['customlabel']);
         }
         $this->_elements[] = $element;
 
@@ -181,12 +181,14 @@ class MoodleQuickForm_defaultcustom extends MoodleQuickForm_group {
                 break;
             case 'createElement':
                 $rv = parent::onQuickFormEvent($event, $arg, $caller);
-                if ($this->_options['type'] === 'text') {
-                    $caller->disabledIf($arg[0] . '[value]', $arg[0] . '[customize]', 'notchecked');
-                } else {
-                    $caller->disabledIf($arg[0] . '[value][day]', $arg[0] . '[customize]', 'notchecked');
-                    $caller->disabledIf($arg[0] . '[value][month]', $arg[0] . '[customize]', 'notchecked');
-                    $caller->disabledIf($arg[0] . '[value][year]', $arg[0] . '[customize]', 'notchecked');
+                if ($this->has_customize_switch()) {
+                    if ($this->_options['type'] === 'text') {
+                        $caller->disabledIf($arg[0] . '[value]', $arg[0] . '[customize]', 'notchecked');
+                    } else {
+                        $caller->disabledIf($arg[0] . '[value][day]', $arg[0] . '[customize]', 'notchecked');
+                        $caller->disabledIf($arg[0] . '[value][month]', $arg[0] . '[customize]', 'notchecked');
+                        $caller->disabledIf($arg[0] . '[value][year]', $arg[0] . '[customize]', 'notchecked');
+                    }
                 }
                 return $rv;
             case 'addElement':
index f390f14..a3997c4 100644 (file)
@@ -546,7 +546,15 @@ class phpunit_util extends testing_util {
             <testsuite name="@component@_testsuite">
                 <directory suffix="_test.php">.</directory>
             </testsuite>
-        </testsuites>';
+        </testsuites>
+        <filter>
+            <whitelist processUncoveredFilesFromWhitelist="false">
+                <directory suffix=".php">.</directory>
+                <exclude>
+                    <directory suffix="_test.php">.</directory>
+                </exclude>
+            </whitelist>
+        </filter>';
 
         // Start a sequence between 100000 and 199000 to ensure each call to init produces
         // different ids in the database.  This reduces the risk that hard coded values will
index b9e2adf..cea2107 100644 (file)
@@ -253,6 +253,22 @@ class mod_feedback_completion extends mod_feedback_structure {
         return $this->valuestmp;
     }
 
+    /**
+     * Retrieves responses from an finished attempt.
+     *
+     * @return array the responses (from the feedback_value table)
+     * @since  Moodle 3.3
+     */
+    public function get_finished_responses() {
+        global $DB;
+        $responses = array();
+
+        if ($this->completed) {
+            $responses = $DB->get_records('feedback_value', ['completed' => $this->completed->id]);
+        }
+        return $responses;
+    }
+
     /**
      * Returns all completed values for this feedback or just a value for an item
      * @param stdClass $item
@@ -261,11 +277,10 @@ class mod_feedback_completion extends mod_feedback_structure {
     protected function get_values($item = null) {
         global $DB;
         if ($this->values === null) {
-            if ($this->completed) {
-                $this->values = $DB->get_records_menu('feedback_value',
-                        ['completed' => $this->completed->id], '', 'item, value');
-            } else {
-                $this->values = array();
+            $this->values = array();
+            $responses = $this->get_finished_responses();
+            foreach ($responses as $r) {
+                $this->values[$r->item] = $r->value;
             }
         }
         if ($item) {
@@ -556,9 +571,9 @@ class mod_feedback_completion extends mod_feedback_structure {
      *
      * @return stdClass record from feedback_completed or false if not found
      */
-    protected function find_last_completed() {
+    public function find_last_completed() {
         global $USER, $DB;
-        if (isloggedin() || isguestuser()) {
+        if (!isloggedin() || isguestuser()) {
             // Not possible to retrieve completed feedback for guests.
             return false;
         }
index 17ed251..c065b88 100644 (file)
@@ -32,6 +32,7 @@ use mod_feedback\external\feedback_summary_exporter;
 use mod_feedback\external\feedback_completedtmp_exporter;
 use mod_feedback\external\feedback_item_exporter;
 use mod_feedback\external\feedback_valuetmp_exporter;
+use mod_feedback\external\feedback_value_exporter;
 
 /**
  * Feedback external functions
@@ -894,4 +895,165 @@ class mod_feedback_external extends external_api {
             )
         );
     }
+
+    /**
+     * Describes the parameters for get_finished_responses.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_finished_responses_parameters() {
+        return new external_function_parameters (
+            array(
+                'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id.'),
+            )
+        );
+    }
+
+    /**
+     * Retrieves responses from the last finished attempt.
+     *
+     * @param array $feedbackid feedback instance id
+     * @return array of warnings and the responses
+     * @since Moodle 3.3
+     */
+    public static function get_finished_responses($feedbackid) {
+        global $PAGE;
+
+        $params = array('feedbackid' => $feedbackid);
+        $params = self::validate_parameters(self::get_finished_responses_parameters(), $params);
+        $warnings = $itemsdata = array();
+
+        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
+        $feedbackcompletion = new mod_feedback_completion($feedback, $cm, $course->id);
+
+        $responses = array();
+        // Load and get the responses from the last completed feedback.
+        $feedbackcompletion->find_last_completed();
+        $unfinished = $feedbackcompletion->get_finished_responses();
+        foreach ($unfinished as $u) {
+            $exporter = new feedback_value_exporter($u);
+            $responses[] = $exporter->export($PAGE->get_renderer('core'));
+        }
+
+        $result = array(
+            'responses' => $responses,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the get_finished_responses return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_finished_responses_returns() {
+        return new external_single_structure(
+            array(
+            'responses' => new external_multiple_structure(
+                feedback_value_exporter::get_read_structure()
+            ),
+            'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+    /**
+     * Describes the parameters for get_non_respondents.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_non_respondents_parameters() {
+        return new external_function_parameters (
+            array(
+                'feedbackid' => new external_value(PARAM_INT, 'Feedback instance id'),
+                'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group.',
+                                                VALUE_DEFAULT, 0),
+                'sort' => new external_value(PARAM_ALPHA, 'Sort param, must be firstname, lastname or lastaccess (default).',
+                                                VALUE_DEFAULT, 'lastaccess'),
+                'page' => new external_value(PARAM_INT, 'The page of records to return.', VALUE_DEFAULT, 0),
+                'perpage' => new external_value(PARAM_INT, 'The number of records to return per page.', VALUE_DEFAULT, 0),
+            )
+        );
+    }
+
+    /**
+     * Retrieves a list of students who didn't submit the feedback.
+     *
+     * @param int $feedbackid feedback instance id
+     * @param int $groupid Group id, 0 means that the function will determine the user group'
+     * @param str $sort sort param, must be firstname, lastname or lastaccess (default)
+     * @param int $page the page of records to return
+     * @param int $perpage the number of records to return per page
+     * @return array of warnings and users ids
+     * @since Moodle 3.3
+     */
+    public static function get_non_respondents($feedbackid, $groupid = 0, $sort = 'lastaccess', $page = 0, $perpage = 0) {
+
+        $params = array('feedbackid' => $feedbackid, 'groupid' => $groupid, 'sort' => $sort, 'page' => $page, 'perpage' => $perpage);
+        $params = self::validate_parameters(self::get_non_respondents_parameters(), $params);
+        $warnings = $itemsdata = array();
+
+        list($feedback, $course, $cm, $context) = self::validate_feedback($params['feedbackid']);
+
+        // Check permissions.
+        require_capability('mod/feedback:viewreports', $context);
+
+        if (!empty($params['groupid'])) {
+            $groupid = $params['groupid'];
+            // Determine is the group is visible to user.
+            if (!groups_group_visible($groupid, $course, $cm)) {
+                throw new moodle_exception('notingroup');
+            }
+        } else {
+            // Check to see if groups are being used here.
+            if ($groupmode = groups_get_activity_groupmode($cm)) {
+                $groupid = groups_get_activity_group($cm);
+                // Determine is the group is visible to user (this is particullary for the group 0 -> all groups).
+                if (!groups_group_visible($groupid, $course, $cm)) {
+                    throw new moodle_exception('notingroup');
+                }
+            } else {
+                $groupid = 0;
+            }
+        }
+
+        if ($params['sort'] !== 'firstname' && $params['sort'] !== 'lastname' && $params['sort'] !== 'lastaccess') {
+            throw new invalid_parameter_exception('Invalid sort param, must be firstname, lastname or lastaccess.');
+        }
+        $params['sort'] = 'u.' . $params['sort'];
+
+        // Check if we are page filtering.
+        if ($params['page'] == 0 && $params['perpage'] == 0) {
+            $params['page'] = false;
+            $params['perpage'] = false;
+        }
+        $users = feedback_get_incomplete_users($cm, $groupid, $params['sort'], $params['page'], $params['perpage']);
+
+        $result = array(
+            'users' => $users,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the get_non_respondents return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_non_respondents_returns() {
+        return new external_single_structure(
+            array(
+            'users' => new external_multiple_structure(
+                new external_value(PARAM_INT, 'The user id')
+            ),
+            'warnings' => new external_warnings(),
+            )
+        );
+    }
 }
diff --git a/mod/feedback/classes/external/feedback_value_exporter.php b/mod/feedback/classes/external/feedback_value_exporter.php
new file mode 100644 (file)
index 0000000..a594be6
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for exporting a feedback response.
+ *
+ * @package    mod_feedback
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_feedback\external;
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+
+/**
+ * Class for exporting a feedback response.
+ *
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class feedback_value_exporter extends exporter {
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array list of properties
+     */
+    protected static function define_properties() {
+        return array(
+            'id' => array(
+                'type' => PARAM_INT,
+                'description' => 'The record id.',
+            ),
+            'course_id' => array(
+                'type' => PARAM_INT,
+                'description' => 'The course id this record belongs to.',
+            ),
+            'item' => array(
+                'type' => PARAM_INT,
+                'description' => 'The item id that was responded.',
+            ),
+            'completed' => array(
+                'type' => PARAM_INT,
+                'description' => 'Reference to the feedback_completed table.',
+            ),
+            'tmp_completed' => array(
+                'type' => PARAM_INT,
+                'description' => 'Old field - not used anymore.',
+            ),
+            'value' => array(
+                'type' => PARAM_RAW,
+                'description' => 'The response value.',
+            ),
+        );
+    }
+}
index b696dea..6fbf031 100644 (file)
@@ -109,4 +109,20 @@ $functions = array(
         'capabilities'  => 'mod/feedback:view',
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
+    'mod_feedback_get_finished_responses' => array(
+        'classname'     => 'mod_feedback_external',
+        'methodname'    => 'get_finished_responses',
+        'description'   => 'Retrieves responses from the last finished attempt.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/feedback:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_feedback_get_non_respondents' => array(
+        'classname'     => 'mod_feedback_external',
+        'methodname'    => 'get_non_respondents',
+        'description'   => 'Retrieves a list of students who didn\'t submit the feedback.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/feedback:viewreports',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
 );
index 1e248fe..d223803 100644 (file)
@@ -149,5 +149,30 @@ function xmldb_feedback_upgrade($oldversion) {
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2017032800) {
+
+        // Delete duplicated records in feedback_completed. We just keep the last record of completion.
+        // Related values in feedback_value won't be deleted (they won't be used and can be kept there as a backup).
+        $sql = "SELECT MAX(id) as maxid, userid, feedback, courseid
+                  FROM {feedback_completed}
+                 WHERE userid <> 0
+              GROUP BY userid, feedback, courseid
+                HAVING COUNT(id) > 1";
+
+        $duplicatedrows = $DB->get_recordset_sql($sql);
+        foreach ($duplicatedrows as $row) {
+            $DB->delete_records_select('feedback_completed', 'userid = ? AND feedback = ? AND courseid = ? AND id <> ?', array(
+                $row->userid,
+                $row->feedback,
+                $row->courseid,
+                $row->maxid,
+            ));
+        }
+        $duplicatedrows->close();
+
+        // Feedback savepoint reached.
+        upgrade_mod_savepoint(true, 2017032800, 'feedback');
+    }
+
     return true;
 }
index b1e9fdd..b2d9e9c 100644 (file)
@@ -628,4 +628,99 @@ class mod_feedback_external_testcase extends externallib_advanced_testcase {
             }
         }
     }
+
+    /**
+     * Test get_finished_responses.
+     */
+    public function test_get_finished_responses() {
+        // Test user with full capabilities.
+        $this->setUser($this->student);
+
+        // Create a very simple feedback.
+        $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
+        $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
+        $textfielditem = $feedbackgenerator->create_item_textfield($this->feedback);
+
+        $pagedata = [
+            ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
+            ['name' => $textfielditem->typ .'_'. $textfielditem->id, 'value' => 'abc'],
+        ];
+
+        // Process the feedback, there is only one page so the feedback will be completed.
+        $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
+        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
+        $this->assertTrue($result['completed']);
+
+        // Retrieve the responses.
+        $result = mod_feedback_external::get_finished_responses($this->feedback->id);
+        $result = external_api::clean_returnvalue(mod_feedback_external::get_finished_responses_returns(), $result);
+        // Check that ids and responses match.
+        foreach ($result['responses'] as $r) {
+            if ($r['item'] == $numericitem->id) {
+                $this->assertEquals(5, $r['value']);
+            } else {
+                $this->assertEquals($textfielditem->id, $r['item']);
+                $this->assertEquals('abc', $r['value']);
+            }
+        }
+    }
+
+    /**
+     * Test get_non_respondents (student trying to get this information).
+     */
+    public function test_get_non_respondents_no_permissions() {
+        $this->setUser($this->student);
+        $this->setExpectedException('moodle_exception');
+        mod_feedback_external::get_non_respondents($this->feedback->id);
+    }
+
+    /**
+     * Test get_non_respondents.
+     */
+    public function test_get_non_respondents() {
+        // Create another student.
+        $anotherstudent = self::getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($anotherstudent->id, $this->course->id, $this->studentrole->id, 'manual');
+        $this->setUser($anotherstudent);
+
+        // Test user with full capabilities.
+        $this->setUser($this->student);
+
+        // Create a very simple feedback.
+        $feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
+        $numericitem = $feedbackgenerator->create_item_numeric($this->feedback);
+
+        $pagedata = [
+            ['name' => $numericitem->typ .'_'. $numericitem->id, 'value' => 5],
+        ];
+
+        // Process the feedback, there is only one page so the feedback will be completed.
+        $result = mod_feedback_external::process_page($this->feedback->id, 0, $pagedata);
+        $result = external_api::clean_returnvalue(mod_feedback_external::process_page_returns(), $result);
+        $this->assertTrue($result['completed']);
+
+        // Retrieve the non-respondent users.
+        $this->setUser($this->teacher);
+        $result = mod_feedback_external::get_non_respondents($this->feedback->id);
+        $result = external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(1, $result['users']);
+        $this->assertEquals($anotherstudent->id, $result['users'][0]);
+
+        // Create another student.
+        $anotherstudent2 = self::getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($anotherstudent2->id, $this->course->id, $this->studentrole->id, 'manual');
+        $this->setUser($anotherstudent2);
+        $this->setUser($this->teacher);
+        $result = mod_feedback_external::get_non_respondents($this->feedback->id);
+        $result = external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(2, $result['users']);
+
+        // Test pagination.
+        $result = mod_feedback_external::get_non_respondents($this->feedback->id, 0, 'lastaccess', 0, 1);
+        $result = external_api::clean_returnvalue(mod_feedback_external::get_non_respondents_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(1, $result['users']);
+    }
 }
index c7dfd44..a223c62 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016120510;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2017032802;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2016112900;    // Requires this Moodle version
 $plugin->component = 'mod_feedback';   // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index c2351c8..388d55d 100644 (file)
@@ -77,7 +77,7 @@ class backup_lesson_activity_structure_step extends backup_activity_structure_st
             'mediafile', 'mediaheight', 'mediawidth', 'mediaclose', 'slideshow',
             'width', 'height', 'bgcolor', 'displayleft', 'displayleftif', 'progressbar',
             'available', 'deadline', 'timemodified',
-            'completionendreached', 'completiontimespent'
+            'completionendreached', 'completiontimespent', 'allowofflineattempts'
         ));
 
         // The lesson_pages table
@@ -131,7 +131,7 @@ class backup_lesson_activity_structure_step extends backup_activity_structure_st
         // Grouped by a `timers` element this is relational to the lesson and user.
         $timers = new backup_nested_element('timers');
         $timer = new backup_nested_element('timer', array('id'), array(
-            'userid', 'starttime', 'lessontime', 'completed'
+            'userid', 'starttime', 'lessontime', 'completed', 'timemodifiedoffline'
         ));
 
         $overrides = new backup_nested_element('overrides');
index ae96faf..1e8232c 100644 (file)
@@ -29,6 +29,8 @@ defined('MOODLE_INTERNAL') || die;
 require_once($CFG->libdir . '/externallib.php');
 require_once($CFG->dirroot . '/mod/lesson/locallib.php');
 
+use mod_lesson\external\lesson_summary_exporter;
+
 /**
  * Lesson external functions
  *
@@ -40,6 +42,48 @@ require_once($CFG->dirroot . '/mod/lesson/locallib.php');
  */
 class mod_lesson_external extends external_api {
 
+    /**
+     * Return a lesson record ready for being exported.
+     *
+     * @param  stdClass $lessonrecord lesson record
+     * @param  string $password       lesson password
+     * @return stdClass the lesson record ready for exporting.
+     */
+    protected static function get_lesson_summary_for_exporter($lessonrecord, $password = '') {
+        global $USER;
+
+        $lesson = new lesson($lessonrecord);
+        $lesson->update_effective_access($USER->id);
+        $lessonavailable = $lesson->get_time_restriction_status() === false;
+        $lessonavailable = $lessonavailable && $lesson->get_password_restriction_status($password) === false;
+        $lessonavailable = $lessonavailable && $lesson->get_dependencies_restriction_status() === false;
+        $canmanage = $lesson->can_manage();
+
+        if (!$canmanage && !$lessonavailable) {
+            $fields = array('intro', 'introfiles', 'mediafiles', 'practice', 'modattempts', 'usepassword',
+                'grade', 'custom', 'ongoing', 'usemaxgrade',
+                'maxanswers', 'maxattempts', 'review', 'nextpagedefault', 'feedback', 'minquestions',
+                'maxpages', 'timelimit', 'retake', 'mediafile', 'mediaheight', 'mediawidth',
+                'mediaclose', 'slideshow', 'width', 'height', 'bgcolor', 'displayleft', 'displayleftif',
+                'progressbar', 'allowofflineattempts');
+
+            foreach ($fields as $field) {
+                unset($lessonrecord->{$field});
+            }
+        }
+
+        // Fields only for managers.
+        if (!$canmanage) {
+            $fields = array('password', 'dependency', 'conditions', 'activitylink', 'available', 'deadline',
+                            'timemodified', 'completionendreached', 'completiontimespent');
+
+            foreach ($fields as $field) {
+                unset($lessonrecord->{$field});
+            }
+        }
+        return $lessonrecord;
+    }
+
     /**
      * Describes the parameters for get_lessons_by_courses.
      *
@@ -65,7 +109,7 @@ class mod_lesson_external extends external_api {
      * @since Moodle 3.3
      */
     public static function get_lessons_by_courses($courseids = array()) {
-        global $USER;
+        global $PAGE;
 
         $warnings = array();
         $returnedlessons = array();
@@ -89,49 +133,17 @@ class mod_lesson_external extends external_api {
             // Get the lessons in this course, this function checks users visibility permissions.
             // We can avoid then additional validate_context calls.
             $lessons = get_all_instances_in_courses("lesson", $courses);
-            foreach ($lessons as $lesson) {
-                $context = context_module::instance($lesson->coursemodule);
-
-                $lesson = new lesson($lesson);
-                $lesson->update_effective_access($USER->id);
-
-                // Entry to return.
-                $lessondetails = array();
-                // First, we return information that any user can see in the web interface.
-                $lessondetails['id'] = $lesson->id;
-                $lessondetails['coursemodule']      = $lesson->coursemodule;
-                $lessondetails['course']            = $lesson->course;
-                $lessondetails['name']              = external_format_string($lesson->name, $context->id);
-
-                $lessonavailable = $lesson->get_time_restriction_status() === false;
-                $lessonavailable = $lessonavailable && $lesson->get_password_restriction_status('') === false;
-                $lessonavailable = $lessonavailable && $lesson->get_dependencies_restriction_status() === false;
-
-                if ($lessonavailable) {
-                    // Format intro.
-                    list($lessondetails['intro'], $lessondetails['introformat']) = external_format_text($lesson->intro,
-                                                                    $lesson->introformat, $context->id, 'mod_lesson', 'intro', null);
-
-                    $lessondetails['introfiles'] = external_util::get_area_files($context->id, 'mod_lesson', 'intro', false, false);
-                    $lessondetails['mediafiles'] = external_util::get_area_files($context->id, 'mod_lesson', 'mediafile', 0);
-                    $viewablefields = array('practice', 'modattempts', 'usepassword', 'grade', 'custom', 'ongoing', 'usemaxgrade',
-                                            'maxanswers', 'maxattempts', 'review', 'nextpagedefault', 'feedback', 'minquestions',
-                                            'maxpages', 'timelimit', 'retake', 'mediafile', 'mediaheight', 'mediawidth',
-                                            'mediaclose', 'slideshow', 'width', 'height', 'bgcolor', 'displayleft', 'displayleftif',
-                                            'progressbar');
-
-                    // Fields only for managers.
-                    if ($lesson->can_manage()) {
-                        $additionalfields = array('password', 'dependency', 'conditions', 'activitylink', 'available', 'deadline',
-                                                  'timemodified', 'completionendreached', 'completiontimespent');
-                        $viewablefields = array_merge($viewablefields, $additionalfields);
-                    }
+            foreach ($lessons as $lessonrecord) {
+                $context = context_module::instance($lessonrecord->coursemodule);
 
-                    foreach ($viewablefields as $field) {
-                        $lessondetails[$field] = $lesson->{$field};
-                    }
-                }
-                $returnedlessons[] = $lessondetails;
+                // Remove fields added by get_all_instances_in_courses.
+                unset($lessonrecord->coursemodule, $lessonrecord->section, $lessonrecord->visible, $lessonrecord->groupmode,
+                    $lessonrecord->groupingid);
+
+                $lessonrecord = self::get_lesson_summary_for_exporter($lessonrecord);
+
+                $exporter = new lesson_summary_exporter($lessonrecord, array('context' => $context));
+                $returnedlessons[] = $exporter->export($PAGE->get_renderer('core'));
             }
         }
         $result = array();
@@ -150,60 +162,7 @@ class mod_lesson_external extends external_api {
         return new external_single_structure(
             array(
                 'lessons' => new external_multiple_structure(
-                    new external_single_structure(
-                        array(
-                            'id' => new external_value(PARAM_INT, 'Standard Moodle primary key.'),
-                            'course' => new external_value(PARAM_INT, 'Foreign key reference to the course this lesson is part of.'),
-                            'coursemodule' => new external_value(PARAM_INT, 'Course module id.'),
-                            'name' => new external_value(PARAM_RAW, 'Lesson name.'),
-                            'intro' => new external_value(PARAM_RAW, 'Lesson introduction text.', VALUE_OPTIONAL),
-                            'introformat' => new external_format_value('intro', VALUE_OPTIONAL),
-                            'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
-                            'practice' => new external_value(PARAM_INT, 'Practice lesson?', VALUE_OPTIONAL),
-                            'modattempts' => new external_value(PARAM_INT, 'Allow student review?', VALUE_OPTIONAL),
-                            'usepassword' => new external_value(PARAM_INT, 'Password protected lesson?', VALUE_OPTIONAL),
-                            'password' => new external_value(PARAM_RAW, 'Password', VALUE_OPTIONAL),
-                            'dependency' => new external_value(PARAM_INT, 'Dependent on (another lesson id)', VALUE_OPTIONAL),
-                            'conditions' => new external_value(PARAM_RAW, 'Conditions to enable the lesson', VALUE_OPTIONAL),
-                            'grade' => new external_value(PARAM_INT, 'The total that the grade is scaled to be out of',
-                                                            VALUE_OPTIONAL),
-                            'custom' => new external_value(PARAM_INT, 'Custom scoring?', VALUE_OPTIONAL),
-                            'ongoing' => new external_value(PARAM_INT, 'Display ongoing score?', VALUE_OPTIONAL),
-                            'usemaxgrade' => new external_value(PARAM_INT, 'How to calculate the final grade', VALUE_OPTIONAL),
-                            'maxanswers' => new external_value(PARAM_INT, 'Maximum answers per page', VALUE_OPTIONAL),
-                            'maxattempts' => new external_value(PARAM_INT, 'Maximum attempts', VALUE_OPTIONAL),
-                            'review' => new external_value(PARAM_INT, 'Provide option to try a question again', VALUE_OPTIONAL),
-                            'nextpagedefault' => new external_value(PARAM_INT, 'Action for a correct answer', VALUE_OPTIONAL),
-                            'feedback' => new external_value(PARAM_INT, 'Display default feedback', VALUE_OPTIONAL),
-                            'minquestions' => new external_value(PARAM_INT, 'Minimum number of questions', VALUE_OPTIONAL),
-                            'maxpages' => new external_value(PARAM_INT, 'Number of pages to show', VALUE_OPTIONAL),
-                            'timelimit' => new external_value(PARAM_INT, 'Time limit', VALUE_OPTIONAL),
-                            'retake' => new external_value(PARAM_INT, 'Re-takes allowed', VALUE_OPTIONAL),
-                            'activitylink' => new external_value(PARAM_INT, 'Link to next activity', VALUE_OPTIONAL),
-                            'mediafile' => new external_value(PARAM_RAW, 'Local file path or full external URL', VALUE_OPTIONAL),
-                            'mediafiles' => new external_files('Media files', VALUE_OPTIONAL),
-                            'mediaheight' => new external_value(PARAM_INT, 'Popup for media file height', VALUE_OPTIONAL),
-                            'mediawidth' => new external_value(PARAM_INT, 'Popup for media with', VALUE_OPTIONAL),
-                            'mediaclose' => new external_value(PARAM_INT, 'Display a close button in the popup?', VALUE_OPTIONAL),
-                            'slideshow' => new external_value(PARAM_INT, 'Display lesson as slideshow', VALUE_OPTIONAL),
-                            'width' => new external_value(PARAM_INT, 'Slideshow width', VALUE_OPTIONAL),
-                            'height' => new external_value(PARAM_INT, 'Slideshow height', VALUE_OPTIONAL),
-                            'bgcolor' => new external_value(PARAM_TEXT, 'Slideshow bgcolor', VALUE_OPTIONAL),
-                            'displayleft' => new external_value(PARAM_INT, 'Display left pages menu?', VALUE_OPTIONAL),
-                            'displayleftif' => new external_value(PARAM_INT, 'Minimum grade to display menu', VALUE_OPTIONAL),
-                            'progressbar' => new external_value(PARAM_INT, 'Display progress bar?', VALUE_OPTIONAL),
-                            'available' => new external_value(PARAM_INT, 'Available from', VALUE_OPTIONAL),
-                            'deadline' => new external_value(PARAM_INT, 'Available until', VALUE_OPTIONAL),
-                            'timemodified' => new external_value(PARAM_INT, 'Last time settings were updated', VALUE_OPTIONAL),
-                            'completionendreached' => new external_value(PARAM_INT, 'Require end reached for completion?',
-                                                                            VALUE_OPTIONAL),
-                            'completiontimespent' => new external_value(PARAM_INT, 'Student must do this activity at least for',
-                                                                        VALUE_OPTIONAL),
-                            'visible' => new external_value(PARAM_INT, 'Visible?', VALUE_OPTIONAL),
-                            'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
-                            'groupingid' => new external_value(PARAM_INT, 'Grouping id', VALUE_OPTIONAL),
-                        )
-                    )
+                    lesson_summary_exporter::get_read_structure()
                 ),
                 'warnings' => new external_warnings(),
             )
@@ -221,16 +180,16 @@ class mod_lesson_external extends external_api {
         global $DB, $USER;
 
         // Request and permission validation.
-        $lesson = $DB->get_record('lesson', array('id' => $lessonid), '*', MUST_EXIST);
-        list($course, $cm) = get_course_and_cm_from_instance($lesson, 'lesson');
+        $lessonrecord = $DB->get_record('lesson', array('id' => $lessonid), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($lessonrecord, 'lesson');
 
-        $lesson = new lesson($lesson, $cm, $course);
+        $lesson = new lesson($lessonrecord, $cm, $course);
         $lesson->update_effective_access($USER->id);
 
         $context = $lesson->context;
         self::validate_context($context);
 
-        return array($lesson, $course, $cm, $context);
+        return array($lesson, $course, $cm, $context, $lessonrecord);
     }
 
     /**
@@ -390,7 +349,7 @@ class mod_lesson_external extends external_api {
         );
         $params = self::validate_parameters(self::get_lesson_access_information_parameters(), $params);
 
-        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 
         $result = array();
         // Capabilities first.
@@ -485,7 +444,7 @@ class mod_lesson_external extends external_api {
         $params = self::validate_parameters(self::view_lesson_parameters(), $params);
         $warnings = array();
 
-        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
         self::validate_attempt($lesson, $params);
 
         $lesson->set_module_viewed();
@@ -574,7 +533,7 @@ class mod_lesson_external extends external_api {
         $params = self::validate_parameters(self::get_questions_attempts_parameters(), $params);
         $warnings = array();
 
-        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 
         // Default value for userid.
         if (empty($params['userid'])) {
@@ -657,7 +616,7 @@ class mod_lesson_external extends external_api {
         $params = self::validate_parameters(self::get_user_grade_parameters(), $params);
         $warnings = array();
 
-        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 
         // Default value for userid.
         if (empty($params['userid'])) {
@@ -708,6 +667,28 @@ class mod_lesson_external extends external_api {
         );
     }
 
+    /**
+     * Describes an attempt grade structure.
+     *
+     * @param  int $required if the structure is required or optional
+     * @return external_single_structure the structure
+     * @since  Moodle 3.3
+     */
+    protected static function get_user_attempt_grade_structure($required = VALUE_REQUIRED) {
+        $data = array(
+            'nquestions' => new external_value(PARAM_INT, 'Number of questions answered'),
+            'attempts' => new external_value(PARAM_INT, 'Number of question attempts'),
+            'total' => new external_value(PARAM_FLOAT, 'Max points possible'),
+            'earned' => new external_value(PARAM_FLOAT, 'Points earned by student'),
+            'grade' => new external_value(PARAM_FLOAT, 'Calculated percentage grade'),
+            'nmanual' => new external_value(PARAM_INT, 'Number of manually graded questions'),
+            'manualpoints' => new external_value(PARAM_FLOAT, 'Point value for manually graded questions'),
+        );
+        return new external_single_structure(
+            $data, 'Attempt grade', $required
+        );
+    }
+
     /**
      * Describes the parameters for get_user_attempt_grade.
      *
@@ -746,7 +727,7 @@ class mod_lesson_external extends external_api {
         $params = self::validate_parameters(self::get_user_attempt_grade_parameters(), $params);
         $warnings = array();
 
-        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 
         // Default value for userid.
         if (empty($params['userid'])) {
@@ -758,7 +739,8 @@ class mod_lesson_external extends external_api {
             self::check_can_view_user_data($params['userid'], $course, $cm, $context);
         }
 
-        $result = (array) lesson_grade($lesson, $params['lessonattempt'], $params['userid']);
+        $result = array();
+        $result['grade'] = (array) lesson_grade($lesson, $params['lessonattempt'], $params['userid']);
         $result['warnings'] = $warnings;
         return $result;
     }
@@ -772,13 +754,7 @@ class mod_lesson_external extends external_api {
     public static function get_user_attempt_grade_returns() {
         return new external_single_structure(
             array(
-                'nquestions' => new external_value(PARAM_INT, 'Number of questions answered'),
-                'attempts' => new external_value(PARAM_INT, 'Number of question attempts'),
-                'total' => new external_value(PARAM_FLOAT, 'Max points possible'),
-                'earned' => new external_value(PARAM_FLOAT, 'Points earned by student'),
-                'grade' => new external_value(PARAM_FLOAT, 'Calculated percentage grade'),
-                'nmanual' => new external_value(PARAM_INT, 'Number of manually graded questions'),
-                'manualpoints' => new external_value(PARAM_FLOAT, 'Point value for manually graded questions'),
+                'grade' => self::get_user_attempt_grade_structure(),
                 'warnings' => new external_warnings(),
             )
         );
@@ -821,7 +797,7 @@ class mod_lesson_external extends external_api {
         $params = self::validate_parameters(self::get_content_pages_viewed_parameters(), $params);
         $warnings = array();
 
-        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 
         // Default value for userid.
         if (empty($params['userid'])) {
@@ -904,7 +880,7 @@ class mod_lesson_external extends external_api {
         $params = self::validate_parameters(self::get_user_timers_parameters(), $params);
         $warnings = array();
 
-        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
 
         // Default value for userid.
         if (empty($params['userid'])) {
@@ -942,6 +918,7 @@ class mod_lesson_external extends external_api {
                             'starttime' => new external_value(PARAM_INT, 'First access time for a new timer session'),
                             'lessontime' => new external_value(PARAM_INT, 'Last access time to the lesson during the timer session'),
                             'completed' => new external_value(PARAM_INT, 'If the lesson for this timer was completed'),
+                            'timemodifiedoffline' => new external_value(PARAM_INT, 'Last modified time via webservices.'),
                         ),
                         'The timers'
                     )
@@ -1045,7 +1022,7 @@ class mod_lesson_external extends external_api {
         $params = self::validate_parameters(self::get_pages_parameters(), $params);
         $warnings = array();
 
-        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
         self::validate_attempt($lesson, $params);
 
         $lessonpages = $lesson->load_all_pages();
@@ -1129,8 +1106,8 @@ class mod_lesson_external extends external_api {
             array(
                 'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
                 'password' => new external_value(PARAM_RAW, 'optional password (the lesson may be protected)', VALUE_DEFAULT, ''),
-                'pageid' => new external_value(PARAM_RAW, 'page id to continue from (only when continuing an attempt)', VALUE_DEFAULT, 0),
-                'review' => new external_value(PARAM_RAW, 'if we want to review just after finishing', VALUE_DEFAULT, false),
+                'pageid' => new external_value(PARAM_INT, 'page id to continue from (only when continuing an attempt)', VALUE_DEFAULT, 0),
+                'review' => new external_value(PARAM_BOOL, 'if we want to review just after finishing', VALUE_DEFAULT, false),
             )
         );
     }
@@ -1189,7 +1166,7 @@ class mod_lesson_external extends external_api {
         $params = self::validate_parameters(self::launch_attempt_parameters(), $params);
         $warnings = array();
 
-        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
         self::validate_attempt($lesson, $params);
 
         $newpageid = 0;
@@ -1284,7 +1261,7 @@ class mod_lesson_external extends external_api {
         $pagecontent = $ongoingscore = '';
         $progress = null;
 
-        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
         self::validate_attempt($lesson, $params);
 
         $pageid = $params['pageid'];
@@ -1315,8 +1292,9 @@ class mod_lesson_external extends external_api {
                     'answerfiles' => external_util::get_area_files($context->id, 'mod_lesson', 'page_answers', $a->id),
                     'responsefiles' => external_util::get_area_files($context->id, 'mod_lesson', 'page_responses', $a->id),
                 );
-                // For managers, return all the information (including scoring, jumps).
-                if ($lesson->can_manage()) {
+                // For managers, return all the information (including correct answers, jumps).
+                // If the teacher enabled offline attempts, this information will be downloaded too.
+                if ($lesson->can_manage() || $lesson->allowofflineattempts) {
                     $extraproperties = array('jumpto', 'grade', 'score', 'flags', 'timecreated', 'timemodified');
                     foreach ($extraproperties as $prop) {
                         $answer[$prop] = $a->{$prop};
@@ -1395,4 +1373,623 @@ class mod_lesson_external extends external_api {
             )
         );
     }
+
+    /**
+     * Describes the parameters for process_page.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function process_page_parameters() {
+        return new external_function_parameters (
+            array(
+                'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
+                'pageid' => new external_value(PARAM_INT, 'the page id'),
+                'data' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_RAW, 'data name'),
+                            'value' => new external_value(PARAM_RAW, 'data value'),
+                        )
+                    ), 'the data to be saved'
+                ),
+                'password' => new external_value(PARAM_RAW, 'optional password (the lesson may be protected)', VALUE_DEFAULT, ''),
+                'review' => new external_value(PARAM_BOOL, 'if we want to review just after finishing (1 hour margin)',
+                    VALUE_DEFAULT, false),
+            )
+        );
+    }
+
+    /**
+     * Processes page responses
+     *
+     * @param int $lessonid lesson instance id
+     * @param int $pageid page id
+     * @param array $data the data to be saved
+     * @param str $password optional password (the lesson may be protected)
+     * @param bool $review if we want to review just after finishing (1 hour margin)
+     * @return array of warnings and status result
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function process_page($lessonid, $pageid,  $data, $password = '', $review = false) {
+        global $USER;
+
+        $params = array('lessonid' => $lessonid, 'pageid' => $pageid, 'data' => $data, 'password' => $password,
+            'review' => $review);
+        $params = self::validate_parameters(self::process_page_parameters(), $params);
+
+        $warnings = array();
+        $pagecontent = $ongoingscore = '';
+        $progress = null;
+
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
+
+        // Update timer so the validation can check the time restrictions.
+        $timer = $lesson->update_timer();
+        self::validate_attempt($lesson, $params);
+
+        // Create the $_POST object required by the lesson question engine.
+        $_POST = array();
+        foreach ($data as $element) {
+            // First check if we are handling editor fields like answer[text].
+            if (preg_match('/(.+)\[(.+)\]$/', $element['name'], $matches)) {
+                $_POST[$matches[1]][$matches[2]] = $element['value'];
+            } else {
+                $_POST[$element['name']] = $element['value'];
+            }
+        }
+
+        // Ignore sesskey (deep in some APIs), the request is already validated.
+        $USER->ignoresesskey = true;
+
+        // Process page.
+        $page = $lesson->load_page($params['pageid']);
+        $result = $lesson->process_page_responses($page);
+
+        // Prepare messages.
+        $reviewmode = $lesson->is_in_review_mode();
+        $lesson->add_messages_on_page_process($page, $result, $reviewmode);
+
+        // Additional lesson information.
+        if (!$lesson->can_manage()) {
+            if ($lesson->ongoing && !$reviewmode) {
+                $ongoingscore = $lesson->get_ongoing_score_message();
+            }
+            if ($lesson->progressbar) {
+                $progress = $lesson->calculate_progress();
+            }
+        }
+
+        // Check conditionally everything coming from result (except newpageid because is always set).
+        $result = array(
+            'newpageid'         => (int) $result->newpageid,
+            'inmediatejump'     => $result->inmediatejump,
+            'nodefaultresponse' => !empty($result->nodefaultresponse),
+            'feedback'          => (isset($result->feedback)) ? $result->feedback : '',
+            'attemptsremaining' => (isset($result->attemptsremaining)) ? $result->attemptsremaining : null,
+            'correctanswer'     => !empty($result->correctanswer),
+            'noanswer'          => !empty($result->noanswer),
+            'isessayquestion'   => !empty($result->isessayquestion),
+            'maxattemptsreached' => !empty($result->maxattemptsreached),
+            'response'          => (isset($result->response)) ? $result->response : '',
+            'studentanswer'     => (isset($result->studentanswer)) ? $result->studentanswer : '',
+            'userresponse'      => (isset($result->userresponse)) ? $result->userresponse : '',
+            'reviewmode'        => $reviewmode,
+            'ongoingscore'      => $ongoingscore,
+            'progress'          => $progress,
+            'displaymenu'       => !empty(lesson_displayleftif($lesson)),
+            'messages'          => self::format_lesson_messages($lesson),
+            'warnings'          => $warnings,
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the process_page return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function process_page_returns() {
+        return new external_single_structure(
+            array(
+                'newpageid' => new external_value(PARAM_INT, 'New page id (if a jump was made).'),
+                'inmediatejump' => new external_value(PARAM_BOOL, 'Whether the page processing redirect directly to anoter page.'),
+                'nodefaultresponse' => new external_value(PARAM_BOOL, 'Whether there is not a default response.'),
+                'feedback' => new external_value(PARAM_RAW, 'The response feedback.'),
+                'attemptsremaining' => new external_value(PARAM_INT, 'Number of attempts remaining.'),
+                'correctanswer' => new external_value(PARAM_BOOL, 'Whether the answer is correct.'),
+                'noanswer' => new external_value(PARAM_BOOL, 'Whether there aren\'t answers.'),
+                'isessayquestion' => new external_value(PARAM_BOOL, 'Whether is a essay question.'),
+                'maxattemptsreached' => new external_value(PARAM_BOOL, 'Whether we reachered the max number of attempts.'),
+                'response' => new external_value(PARAM_RAW, 'The response.'),
+                'studentanswer' => new external_value(PARAM_RAW, 'The student answer.'),
+                'userresponse' => new external_value(PARAM_RAW, 'The user response.'),
+                'reviewmode' => new external_value(PARAM_BOOL, 'Whether the user is reviewing.'),
+                'ongoingscore' => new external_value(PARAM_TEXT, 'The ongoing message.'),
+                'progress' => new external_value(PARAM_INT, 'Progress percentage in the lesson.'),
+                'displaymenu' => new external_value(PARAM_BOOL, 'Whether we should display the menu or not in this page.'),
+                'messages' => self::external_messages(),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+    /**
+     * Describes the parameters for finish_attempt.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function finish_attempt_parameters() {
+        return new external_function_parameters (
+            array(
+                'lessonid' => new external_value(PARAM_INT, 'Lesson instance id.'),
+                'password' => new external_value(PARAM_RAW, 'Optional password (the lesson may be protected).', VALUE_DEFAULT, ''),
+                'outoftime' => new external_value(PARAM_BOOL, 'If the user run out of time.', VALUE_DEFAULT, false),
+                'review' => new external_value(PARAM_BOOL, 'If we want to review just after finishing (1 hour margin).',
+                    VALUE_DEFAULT, false),
+            )
+        );
+    }
+
+    /**
+     * Finishes the current attempt.
+     *
+     * @param int $lessonid lesson instance id
+     * @param str $password optional password (the lesson may be protected)
+     * @param bool $outoftime optional if the user run out of time
+     * @param bool $review if we want to review just after finishing (1 hour margin)
+     * @return array of warnings and information about the finished attempt
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function finish_attempt($lessonid, $password = '', $outoftime = false, $review = false) {
+
+        $params = array('lessonid' => $lessonid, 'password' => $password, 'outoftime' => $outoftime, 'review' => $review);
+        $params = self::validate_parameters(self::finish_attempt_parameters(), $params);
+
+        $warnings = array();
+
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
+
+        // Update timer so the validation can check the time restrictions.
+        $timer = $lesson->update_timer();
+
+        // Return the validation to avoid exceptions in case the user is out of time.
+        $params['pageid'] = LESSON_EOL;
+        $validation = self::validate_attempt($lesson, $params, true);
+
+        if (array_key_exists('eolstudentoutoftime', $validation)) {
+            // Maybe we run out of time just now.
+            $params['outoftime'] = true;
+            unset($validation['eolstudentoutoftime']);
+        }
+        // Check if there are more errors.
+        if (!empty($validation)) {
+            reset($validation);
+            throw new moodle_exception(key($validation), 'lesson', '', current($validation));   // Throw first error.
+        }
+
+        $result = $lesson->process_eol_page($params['outoftime']);
+
+        // Return the data.
+         $validmessages = array(
+            'notenoughtimespent', 'numberofpagesviewed', 'youshouldview', 'numberofcorrectanswers',
+            'displayscorewithessays', 'displayscorewithoutessays', 'yourcurrentgradeisoutof', 'eolstudentoutoftimenoanswers',
+            'welldone', 'displayofgrade', 'reviewlesson', 'modattemptsnoteacher', 'progresscompleted');
+
+        $data = array();
+        foreach ($result as $el => $value) {
+            if ($value !== false) {
+                $message = '';
+                if (in_array($el, $validmessages)) { // Check if the data comes with an informative message.
+                    $a = (is_bool($value)) ? null : $value;
+                    $message = get_string($el, 'lesson', $a);
+                }
+                // Return the data.
+                $data[] = array(
+                    'name' => $el,
+                    'value' => (is_bool($value)) ? 1 : json_encode($value), // The data can be a php object.
+                    'message' => $message
+                );
+            }
+        }
+
+        $result = array(
+            'data'     => $data,
+            'messages' => self::format_lesson_messages($lesson),
+            'warnings' => $warnings,
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the finish_attempt return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function finish_attempt_returns() {
+        return new external_single_structure(
+            array(
+                'data' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_ALPHANUMEXT, 'Data name.'),
+                            'value' => new external_value(PARAM_RAW, 'Data value.'),
+                            'message' => new external_value(PARAM_RAW, 'Data message (translated string).'),
+                        )
+                    ), 'The EOL page information data.'
+                ),
+                'messages' => self::external_messages(),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+    /**
+     * Describes the parameters for get_attempts_overview.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_attempts_overview_parameters() {
+        return new external_function_parameters (
+            array(
+                'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
+                'groupid' => new external_value(PARAM_INT, 'group id, 0 means that the function will determine the user group',
+                                                VALUE_DEFAULT, 0),
+            )
+        );
+    }
+
+    /**
+     * Get a list of all the attempts made by users in a lesson.
+     *
+     * @param int $lessonid lesson instance id
+     * @param int $groupid group id, 0 means that the function will determine the user group
+     * @return array of warnings and status result
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function get_attempts_overview($lessonid, $groupid = 0) {
+
+        $params = array('lessonid' => $lessonid, 'groupid' => $groupid);
+        $params = self::validate_parameters(self::get_attempts_overview_parameters(), $params);
+        $studentsdata = $warnings = array();
+
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
+        require_capability('mod/lesson:viewreports', $context);
+
+        if (!empty($params['groupid'])) {
+            $groupid = $params['groupid'];
+            // Determine is the group is visible to user.
+            if (!groups_group_visible($groupid, $course, $cm)) {
+                throw new moodle_exception('notingroup');
+            }
+        } else {
+            // Check to see if groups are being used here.
+            if ($groupmode = groups_get_activity_groupmode($cm)) {
+                $groupid = groups_get_activity_group($cm);
+                // Determine is the group is visible to user (this is particullary for the group 0 -> all groups).
+                if (!groups_group_visible($groupid, $course, $cm)) {
+                    throw new moodle_exception('notingroup');
+                }
+            } else {
+                $groupid = 0;
+            }
+        }
+
+        list($table, $data) = lesson_get_overview_report_table_and_data($lesson, $groupid);
+        if ($data !== false) {
+            $studentsdata = $data;
+        }
+
+        $result = array(
+            'data' => $studentsdata,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the get_attempts_overview return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_attempts_overview_returns() {
+        return new external_single_structure(
+            array(
+                'data' => new external_single_structure(
+                    array(
+                        'lessonscored' => new external_value(PARAM_BOOL, 'True if the lesson was scored.'),
+                        'numofattempts' => new external_value(PARAM_INT, 'Number of attempts.'),
+                        'avescore' => new external_value(PARAM_FLOAT, 'Average score.'),
+                        'highscore' => new external_value(PARAM_FLOAT, 'High score.'),
+                        'lowscore' => new external_value(PARAM_FLOAT, 'Low score.'),
+                        'avetime' => new external_value(PARAM_INT, 'Average time (spent in taking the lesson).'),
+                        'hightime' => new external_value(PARAM_INT, 'High time.'),
+                        'lowtime' => new external_value(PARAM_INT, 'Low time.'),
+                        'students' => new external_multiple_structure(
+                            new external_single_structure(
+                                array(
+                                    'id' => new external_value(PARAM_INT, 'User id.'),
+                                    'fullname' => new external_value(PARAM_TEXT, 'User full name.'),
+                                    'bestgrade' => new external_value(PARAM_FLOAT, 'Best grade.'),
+                                    'attempts' => new external_multiple_structure(
+                                        new external_single_structure(
+                                            array(
+                                                'try' => new external_value(PARAM_INT, 'Attempt number.'),
+                                                'grade' => new external_value(PARAM_FLOAT, 'Attempt grade.'),
+                                                'timestart' => new external_value(PARAM_INT, 'Attempt time started.'),
+                                                'timeend' => new external_value(PARAM_INT, 'Attempt last time continued.'),
+                                                'end' => new external_value(PARAM_INT, 'Attempt time ended.'),
+                                            )
+                                        )
+                                    )
+                                )
+                            ), 'Students data, including attempts.', VALUE_OPTIONAL
+                        ),
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+    /**
+     * Describes the parameters for get_user_attempt.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_user_attempt_parameters() {
+        return new external_function_parameters (
+            array(
+                'lessonid' => new external_value(PARAM_INT, 'Lesson instance id.'),
+                'userid' => new external_value(PARAM_INT, 'The user id. 0 for current user.'),
+                'lessonattempt' => new external_value(PARAM_INT, 'The attempt number.'),
+            )
+        );
+    }
+
+    /**
+     * Return information about the given user attempt (including answers).
+     *
+     * @param int $lessonid lesson instance id
+     * @param int $userid the user id
+     * @param int $lessonattempt the attempt number
+     * @return array of warnings and page attempts
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function get_user_attempt($lessonid, $userid, $lessonattempt) {
+        global $USER;
+
+        $params = array(
+            'lessonid' => $lessonid,
+            'userid' => $userid,
+            'lessonattempt' => $lessonattempt,
+        );
+        $params = self::validate_parameters(self::get_user_attempt_parameters(), $params);
+        $warnings = array();
+
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
+
+        // Default value for userid.
+        if (empty($params['userid'])) {
+            $params['userid'] = $USER->id;
+        }
+
+        // Extra checks so only users with permissions can view other users attempts.
+        if ($USER->id != $params['userid']) {
+            self::check_can_view_user_data($params['userid'], $course, $cm, $context);
+        }
+
+        list($answerpages, $userstats) = lesson_get_user_detailed_report_data($lesson, $userid, $params['lessonattempt']);
+
+        $result = array(
+            'answerpages' => $answerpages,
+            'userstats' => $userstats,
+            'warnings' => $warnings,
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the get_user_attempt return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_user_attempt_returns() {
+        return new external_single_structure(
+            array(
+                'answerpages' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'title' => new external_value(PARAM_RAW, 'Page title.'),
+                            'contents' => new external_value(PARAM_RAW, 'Page contents.'),
+                            'qtype' => new external_value(PARAM_TEXT, 'Identifies the page type of this page.'),
+                            'grayout' => new external_value(PARAM_INT, 'If is required to apply a grayout.'),
+                            'answerdata' => new external_single_structure(
+                                array(
+                                    'score' => new external_value(PARAM_TEXT, 'The score (text version).'),
+                                    'response' => new external_value(PARAM_RAW, 'The response text.'),
+                                    'responseformat' => new external_format_value('response.'),
+                                    'answers' => new external_multiple_structure(
+                                        new external_multiple_structure(new external_value(PARAM_RAW, 'Possible answers and info.'))
+                                    )
+                                ), 'Answer data (empty in content pages created in Moodle 1.x).', VALUE_OPTIONAL
+                            )
+                        )
+                    )
+                ),
+                'userstats' => new external_single_structure(
+                    array(
+                        'grade' => new external_value(PARAM_FLOAT, 'Attempt final grade.'),
+                        'completed' => new external_value(PARAM_INT, 'Time completed.'),
+                        'timetotake' => new external_value(PARAM_INT, 'Time taken.'),
+                        'gradeinfo' => self::get_user_attempt_grade_structure(VALUE_OPTIONAL)
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+    /**
+     * Describes the parameters for get_pages_possible_jumps.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_pages_possible_jumps_parameters() {
+        return new external_function_parameters (
+            array(
+                'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
+            )
+        );
+    }
+
+    /**
+     * Return all the possible jumps for the pages in a given lesson.
+     *
+     * You may expect different results on consecutive executions due to the random nature of the lesson module.
+     *
+     * @param int $lessonid lesson instance id
+     * @return array of warnings and possible jumps
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function get_pages_possible_jumps($lessonid) {
+        global $USER;
+
+        $params = array('lessonid' => $lessonid);
+        $params = self::validate_parameters(self::get_pages_possible_jumps_parameters(), $params);
+
+        $warnings = $jumps = array();
+
+        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+
+        // Only return for managers or if offline attempts are enabled.
+        if ($lesson->can_manage() || $lesson->allowofflineattempts) {
+
+            $lessonpages = $lesson->load_all_pages();
+            foreach ($lessonpages as $page) {
+                $jump = array();
+                $jump['pageid'] = $page->id;
+
+                $answers = $page->get_answers();
+                if (count($answers) > 0) {
+                    foreach ($answers as $answer) {
+                        $jump['answerid'] = $answer->id;
+                        $jump['jumpto'] = $answer->jumpto;
+                        $jump['calculatedjump'] = $lesson->calculate_new_page_on_jump($page, $answer->jumpto);
+                        // Special case, only applies to branch/end of branch.
+                        if ($jump['calculatedjump'] == LESSON_RANDOMBRANCH) {
+                            $jump['calculatedjump'] = lesson_unseen_branch_jump($lesson, $USER->id);
+                        }
+                        $jumps[] = $jump;
+                    }
+                } else {
+                    // Imported lessons from 1.x.
+                    $jump['answerid'] = 0;
+                    $jump['jumpto'] = $page->nextpageid;
+                    $jump['calculatedjump'] = $lesson->calculate_new_page_on_jump($page, $page->nextpageid);
+                    $jumps[] = $jump;
+                }
+            }
+        }
+
+        $result = array(
+            'jumps' => $jumps,
+            'warnings' => $warnings,
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the get_pages_possible_jumps return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_pages_possible_jumps_returns() {
+        return new external_single_structure(
+            array(
+                'jumps' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'pageid' => new external_value(PARAM_INT, 'The page id'),
+                            'answerid' => new external_value(PARAM_INT, 'The answer id'),
+                            'jumpto' => new external_value(PARAM_INT, 'The jump (page id or type of jump)'),
+                            'calculatedjump' => new external_value(PARAM_INT, 'The real page id (or EOL) to jump'),
+                        ), 'Jump for a page answer'
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
+    /**
+     * Describes the parameters for get_lesson.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_lesson_parameters() {
+        return new external_function_parameters (
+            array(
+                'lessonid' => new external_value(PARAM_INT, 'lesson instance id'),
+                'password' => new external_value(PARAM_RAW, 'lesson password', VALUE_DEFAULT, ''),
+            )
+        );
+    }
+
+    /**
+     * Return information of a given lesson.
+     *
+     * @param int $lessonid lesson instance id
+     * @param str $password optional password (the lesson may be protected)
+     * @return array of warnings and status result
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function get_lesson($lessonid, $password = '') {
+        global $PAGE;
+
+        $params = array('lessonid' => $lessonid, 'password' => $password);
+        $params = self::validate_parameters(self::get_lesson_parameters(), $params);
+        $warnings = array();
+
+        list($lesson, $course, $cm, $context, $lessonrecord) = self::validate_lesson($params['lessonid']);
+
+        $lessonrecord = self::get_lesson_summary_for_exporter($lessonrecord, $params['password']);
+        $exporter = new lesson_summary_exporter($lessonrecord, array('context' => $context));
+
+        $result = array();
+        $result['lesson'] = $exporter->export($PAGE->get_renderer('core'));
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Describes the get_lesson return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_lesson_returns() {
+        return new external_single_structure(
+            array(
+                'lesson' => lesson_summary_exporter::get_read_structure(),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
 }
diff --git a/mod/lesson/classes/external/lesson_summary_exporter.php b/mod/lesson/classes/external/lesson_summary_exporter.php
new file mode 100644 (file)
index 0000000..0e9a9cb
--- /dev/null
@@ -0,0 +1,307 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for exporting partial lesson data.
+ *
+ * @package    mod_lesson
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_lesson\external;
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+use renderer_base;
+use external_files;
+use external_util;
+
+/**
+ * Class for exporting partial lesson data (some fields are only viewable by admins).
+ *
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lesson_summary_exporter extends exporter {
+
+    protected static function define_properties() {
+
+        return array(
+            'id' => array(
+                'type' => PARAM_INT,
+                'description' => 'Standard Moodle primary key.'
+            ),
+            'course' => array(
+                'type' => PARAM_INT,
+                'description' => 'Foreign key reference to the course this lesson is part of.'
+            ),
+            'coursemodule' => array(
+                'type' => PARAM_INT,
+                'description' => 'Course module id.'
+            ),
+            'name' => array(
+                'type' => PARAM_RAW,
+                'description' => 'Lesson name.'
+            ),
+            'intro' => array(
+                'type' => PARAM_RAW,
+                'description' => 'Lesson introduction text.',
+                'optional' => true,
+            ),
+            'introformat' => array(
+                'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN),
+                'type' => PARAM_INT,
+                'default' => FORMAT_MOODLE
+            ),
+            'practice' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Practice lesson?',
+                'optional' => true,
+            ),
+            'modattempts' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Allow student review?',
+                'optional' => true,
+            ),
+            'usepassword' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Password protected lesson?',
+                'optional' => true,
+            ),
+            'password' => array(
+                'type' => PARAM_RAW,
+                'description' => 'Password',
+                'optional' => true,
+            ),
+            'dependency' => array(
+                'type' => PARAM_INT,
+                'description' => 'Dependent on (another lesson id)',
+                'optional' => true,
+            ),
+            'conditions' => array(
+                'type' => PARAM_RAW,
+                'description' => 'Conditions to enable the lesson',
+                'optional' => true,
+            ),
+            'grade' => array(
+                'type' => PARAM_INT,
+                'description' => 'The total that the grade is scaled to be out of',
+                'optional' => true,
+            ),
+            'custom' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Custom scoring?',
+                'optional' => true,
+            ),
+            'ongoing' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Display ongoing score?',
+                'optional' => true,
+            ),
+            'usemaxgrade' => array(
+                'type' => PARAM_INT,
+                'description' => 'How to calculate the final grade',
+                'optional' => true,
+            ),
+            'maxanswers' => array(
+                'type' => PARAM_INT,
+                'description' => 'Maximum answers per page',
+                'optional' => true,
+            ),
+            'maxattempts' => array(
+                'type' => PARAM_INT,
+                'description' => 'Maximum attempts',
+                'optional' => true,
+            ),
+            'review' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Provide option to try a question again',
+                'optional' => true,
+            ),
+            'nextpagedefault' => array(
+                'type' => PARAM_INT,
+                'description' => 'Action for a correct answer',
+                'optional' => true,
+            ),
+            'feedback' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Display default feedback',
+                'optional' => true,
+            ),
+            'minquestions' => array(
+                'type' => PARAM_INT,
+                'description' => 'Minimum number of questions',
+                'optional' => true,
+            ),
+            'maxpages' => array(
+                'type' => PARAM_INT,
+                'description' => 'Number of pages to show',
+                'optional' => true,
+            ),
+            'timelimit' => array(
+                'type' => PARAM_INT,
+                'description' => 'Time limit',
+                'optional' => true,
+            ),
+            'retake' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Re-takes allowed',
+                'optional' => true,
+            ),
+            'activitylink' => array(
+                'type' => PARAM_INT,
+                'description' => 'Id of the next activity to be linked once the lesson is completed',
+                'optional' => true,
+            ),
+            'mediafile' => array(
+                'type' => PARAM_RAW,
+                'description' => 'Local file path or full external URL',
+                'optional' => true,
+            ),
+            'mediaheight' => array(
+                'type' => PARAM_INT,
+                'description' => 'Popup for media file height',
+                'optional' => true,
+            ),
+            'mediawidth' => array(
+                'type' => PARAM_INT,
+                'description' => 'Popup for media with',
+                'optional' => true,
+            ),
+            'mediaclose' => array(
+                'type' => PARAM_INT,
+                'description' => 'Display a close button in the popup?',
+                'optional' => true,
+            ),
+            'slideshow' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Display lesson as slideshow',
+                'optional' => true,
+            ),
+            'width' => array(
+                'type' => PARAM_INT,
+                'description' => 'Slideshow width',
+                'optional' => true,
+            ),
+            'height' => array(
+                'type' => PARAM_INT,
+                'description' => 'Slideshow height',
+                'optional' => true,
+            ),
+            'bgcolor' => array(
+                'type' => PARAM_TEXT,
+                'description' => 'Slideshow bgcolor',
+                'optional' => true,
+            ),
+            'displayleft' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Display left pages menu?',
+                'optional' => true,
+            ),
+            'displayleftif' => array(
+                'type' => PARAM_INT,
+                'description' => 'Minimum grade to display menu',
+                'optional' => true,
+            ),
+            'progressbar' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Display progress bar?',
+                'optional' => true,
+            ),
+            'available' => array(
+                'type' => PARAM_INT,
+                'description' => 'Available from',
+                'optional' => true,
+            ),
+            'deadline' => array(
+                'type' => PARAM_INT,
+                'description' => 'Available until',
+                'optional' => true,
+            ),
+            'timemodified' => array(
+                'type' => PARAM_INT,
+                'description' => 'Last time settings were updated',
+                'optional' => true,
+            ),
+            'completionendreached' => array(
+                'type' => PARAM_INT,
+                'description' => 'Require end reached for completion?',
+                'optional' => true,
+            ),
+            'completiontimespent' => array(
+                'type' => PARAM_INT,
+                'description' => 'Student must do this activity at least for',
+                'optional' => true,
+             ),
+            'allowofflineattempts' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Whether to allow the lesson to be attempted offline in the mobile app',
+                'optional' => true,
+            ),
+        );
+    }
+
+    protected static function define_related() {
+        return array(
+            'context' => 'context'
+        );
+    }
+
+    protected static function define_other_properties() {
+        return array(
+            'coursemodule' => array(
+                'type' => PARAM_INT
+            ),
+            'introfiles' => array(
+                'type' => external_files::get_properties_for_exporter(),
+                'multiple' => true,
+                'optional' => true,
+            ),
+            'mediafiles' => array(
+                'type' => external_files::get_properties_for_exporter(),
+                'multiple' => true,
+                'optional' => true,
+            ),
+        );
+    }
+
+    protected function get_other_values(renderer_base $output) {
+        $context = $this->related['context'];
+
+        $values = array(
+            'coursemodule' => $context->instanceid,
+        );
+
+        if (isset($this->data->intro)) {
+            $values['introfiles'] = external_util::get_area_files($context->id, 'mod_lesson', 'intro', false, false);
+            $values['mediafiles'] = external_util::get_area_files($context->id, 'mod_lesson', 'mediafile', 0);
+        }
+
+        return $values;
+    }
+
+    /**
+     * Get the formatting parameters for the intro.
+     *
+     * @return array
+     */
+    protected function get_format_parameters_for_intro() {
+        return [
+            'component' => 'mod_lesson',
+            'filearea' => 'intro',
+        ];
+    }
+}
index 0639604..fe7fe69 100644 (file)
@@ -31,7 +31,7 @@ $id = required_param('id', PARAM_INT);
 
 $cm = get_coursemodule_from_id('lesson', $id, 0, false, MUST_EXIST);
 $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
-$lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST));
+$lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST), $cm, $course);
 
 require_login($course, false, $cm);
 require_sesskey();
@@ -39,8 +39,8 @@ require_sesskey();
 // Apply overrides.
 $lesson->update_effective_access($USER->id);
 
-$context = context_module::instance($cm->id);
-$canmanage = has_capability('mod/lesson:manage', $context);
+$context = $lesson->context;
+$canmanage = $lesson->can_manage();
 $lessonoutput = $PAGE->get_renderer('mod_lesson');
 
 $url = new moodle_url('/mod/lesson/continue.php', array('id'=>$cm->id));
@@ -66,97 +66,16 @@ $page = $lesson->load_page(required_param('pageid', PARAM_INT));
 
 $reviewmode = $lesson->is_in_review_mode();
 
-// Check the page has answers [MDL-25632]
-if (count($page->answers) > 0) {
-    $result = $page->record_attempt($context);
-} else {
-    // The page has no answers so we will just progress to the next page in the
-    // sequence (as set by newpageid).
-    $result = new stdClass;
-    $result->newpageid       = optional_param('newpageid', $page->nextpageid, PARAM_INT);
-    $result->nodefaultresponse  = true;
-}
-
-if (isset($USER->modattempts[$lesson->id])) {
-    // make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time
-    if ($USER->modattempts[$lesson->id]->pageid == $page->id && $page->nextpageid == 0) {  // remember, this session variable holds the pageid of the last page that the user saw
-        $result->newpageid = LESSON_EOL;
-    } else {
-        $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$USER->id));
-        $nretakes--; // make sure we are looking at the right try.
-        $attempts = $DB->get_records("lesson_attempts", array("lessonid"=>$lesson->id, "userid"=>$USER->id, "retry"=>$nretakes), "timeseen", "id, pageid");
-        $found = false;
-        $temppageid = 0;
-        // Make sure that the newpageid always defaults to something valid.
-        $result->newpageid = LESSON_EOL;
-        foreach($attempts as $attempt) {
-            if ($found && $temppageid != $attempt->pageid) { // now try to find the next page, make sure next few attempts do no belong to current page
-                $result->newpageid = $attempt->pageid;
-                break;
-            }
-            if ($attempt->pageid == $page->id) {
-                $found = true; // if found current page
-                $temppageid = $attempt->pageid;
-            }
-        }
-    }
-} elseif ($result->newpageid != LESSON_CLUSTERJUMP && $page->id != 0 && $result->newpageid > 0) {
-    // going to check to see if the page that the user is going to view next, is a cluster page.
-    // If so, dont display, go into the cluster.  The $result->newpageid > 0 is used to filter out all of the negative code jumps.
-    $newpage = $lesson->load_page($result->newpageid);
-    if ($newpageid = $newpage->override_next_page($result->newpageid)) {
-        $result->newpageid = $newpageid;
-    }
-} elseif ($result->newpageid == LESSON_UNSEENBRANCHPAGE) {
-    if ($canmanage) {
-        if ($page->nextpageid == 0) {
-            $result->newpageid = LESSON_EOL;
-        } else {
-            $result->newpageid = $page->nextpageid;
-        }
-    } else {
-        $result->newpageid = lesson_unseen_question_jump($lesson, $USER->id, $page->id);
-    }
-} elseif ($result->newpageid == LESSON_PREVIOUSPAGE) {
-    $result->newpageid = $page->prevpageid;
-} elseif ($result->newpageid == LESSON_RANDOMPAGE) {
-    $result->newpageid = lesson_random_question_jump($lesson, $page->id);
-} elseif ($result->newpageid == LESSON_CLUSTERJUMP) {
-    if ($canmanage) {
-        if ($page->nextpageid == 0) {  // if teacher, go to next page
-            $result->newpageid = LESSON_EOL;
-        } else {
-            $result->newpageid = $page->nextpageid;
-        }
-    } else {
-        $result->newpageid = $lesson->cluster_jump($page->id);
-    }
-}
+// Process the page responses.
+$result = $lesson->process_page_responses($page);
 
-if ($result->nodefaultresponse) {
-    // Don't display feedback
+if ($result->nodefaultresponse || $result->inmediatejump) {
+    // Don't display feedback or force a redirecto to newpageid.
     redirect(new moodle_url('/mod/lesson/view.php', array('id'=>$cm->id,'pageid'=>$result->newpageid)));
 }
 
-/// Set Messages
-
-if ($canmanage) {
-    // This is the warning msg for teachers to inform them that cluster and unseen does not work while logged in as a teacher
-    if(lesson_display_teacher_warning($lesson)) {
-        $warningvars = new stdClass();
-        $warningvars->cluster = get_string("clusterjump", "lesson");
-        $warningvars->unseen = get_string("unseenpageinbranch", "lesson");
-        $lesson->add_message(get_string("teacherjumpwarning", "lesson", $warningvars));
-    }
-    // Inform teacher that s/he will not see the timer
-    if ($lesson->timelimit) {
-        $lesson->add_message(get_string("teachertimerwarning", "lesson"));
-    }
-}
-// Report attempts remaining
-if ($result->attemptsremaining != 0 && $lesson->review && !$reviewmode) {
-    $lesson->add_message(get_string('attemptsremaining', 'lesson', $result->attemptsremaining));
-}
+// Set Messages.
+$lesson->add_messages_on_page_process($page, $result, $reviewmode);
 
 $PAGE->set_url('/mod/lesson/view.php', array('id' => $cm->id, 'pageid' => $page->id));
 $PAGE->set_subpage($page->id);
index 1a4694a..583b408 100644 (file)
@@ -47,6 +47,7 @@
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="completionendreached" TYPE="int" LENGTH="1" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="completiontimespent" TYPE="int" LENGTH="11" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="allowofflineattempts" TYPE="int" LENGTH="1" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Whether to allow the lesson to be attempted offline in the mobile app"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <FIELD NAME="starttime" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="lessontime" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="completed" TYPE="int" LENGTH="1" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="timemodifiedoffline" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Last modified time via web services (mobile app)."/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index f6cbb10..a5d176c 100644 (file)
@@ -116,4 +116,52 @@ $functions = array(
         'capabilities'  => 'mod/lesson:view',
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
+    'mod_lesson_process_page' => array(
+        'classname'     => 'mod_lesson_external',
+        'methodname'    => 'process_page',
+        'description'   => 'Processes page responses.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/lesson:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_lesson_finish_attempt' => array(
+        'classname'     => 'mod_lesson_external',
+        'methodname'    => 'finish_attempt',
+        'description'   => 'Finishes the current attempt.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/lesson:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_lesson_get_attempts_overview' => array(
+        'classname'     => 'mod_lesson_external',
+        'methodname'    => 'get_attempts_overview',
+        'description'   => 'Get a list of all the attempts made by users in a lesson.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/lesson:viewreports',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_lesson_get_user_attempt' => array(
+        'classname'     => 'mod_lesson_external',
+        'methodname'    => 'get_user_attempt',
+        'description'   => 'Return information about the given user attempt (including answers).',
+        'type'          => 'read',
+        'capabilities'  => 'mod/lesson:viewreports',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_lesson_get_pages_possible_jumps' => array(
+        'classname'     => 'mod_lesson_external',
+        'methodname'    => 'get_pages_possible_jumps',
+        'description'   => 'Return all the possible jumps for the pages in a given lesson.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/lesson:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_lesson_get_lesson' => array(
+        'classname'     => 'mod_lesson_external',
+        'methodname'    => 'get_lesson',
+        'description'   => 'Return information of a given lesson.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/lesson:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
 );
index d12bb8e..578f6c6 100644 (file)
@@ -390,5 +390,28 @@ function xmldb_lesson_upgrade($oldversion) {
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2016120515) {
+        // Define new fields to be added to lesson.
+        $table = new xmldb_table('lesson');
+        $field = new xmldb_field('allowofflineattempts', XMLDB_TYPE_INTEGER, '1', null, null, null, 0, 'completiontimespent');
+        // Conditionally launch add field allowofflineattempts.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        // Lesson savepoint reached.
+        upgrade_mod_savepoint(true, 2016120515, 'lesson');
+    }
+    if ($oldversion < 2016120516) {
+        // New field for lesson_timer.
+        $table = new xmldb_table('lesson_timer');
+        $field = new xmldb_field('timemodifiedoffline', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0, 'completed');
+        // Conditionally launch add field timemodifiedoffline.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        // Lesson savepoint reached.
+        upgrade_mod_savepoint(true, 2016120516, 'lesson');
+    }
+
     return true;
 }
index 4b3cea4..b61512e 100644 (file)
@@ -52,6 +52,10 @@ $string['addnewgroupoverride'] = 'Add group override';
 $string['addnewuseroverride'] = 'Add user override';
 $string['additionalattemptsremaining'] = 'Completed, You can re-attempt this lesson';
 $string['addpage'] = 'Add a page';
+$string['allowofflineattempts'] = 'Allow lesson to be attempted offline using the mobile app';
+$string['allowofflineattempts_help'] = 'If enabled, a mobile app user can download the lesson and attempt it offline.
+All the possible answers and correct responses will be downloaded as well.
+Note: It is not possible for a lesson to be attempted offline if it has a time limit.';
 $string['and'] = 'AND';
 $string['anchortitle'] = 'Start of main content';
 $string['answer'] = 'Answer';
@@ -355,6 +359,7 @@ $string['numberofpagesviewed'] = 'Number of questions answered: {$a}';
 $string['numberofpagesviewedheader'] = 'Number of questions answered';
 $string['numberofpagesviewednotice'] = 'Number of questions answered: {$a->nquestions} (You should answer at least {$a->minquestions})';
 $string['numerical'] = 'Numerical';
+$string['offlinedatamessage'] = 'You have worked on this attempt using a mobile device. Data was last saved to this site {$a} ago. Please check that you do not have any unsaved work.';
 $string['ongoing'] = 'Display ongoing score';
 $string['ongoing_help'] = 'If enabled, each page will display the student\'s current points earned out of the total possible thus far.';
 $string['ongoingcustom'] = 'You have earned {$a->score} point(s) out of {$a->currenthigh} point(s) thus far.';
index 26572f4..8f7526e 100644 (file)
@@ -1489,3 +1489,72 @@ function mod_lesson_get_fontawesome_icon_map() {
         'mod_lesson:e/copy' => 'fa-clone',
     ];
 }
+
+/*
+ * Check if the module has any update that affects the current user since a given time.
+ *
+ * @param  cm_info $cm course module data
+ * @param  int $from the time to check updates from
+ * @param  array $filter  if we need to check only specific updates
+ * @return stdClass an object with the different type of areas indicating if they were updated or not
+ * @since Moodle 3.3
+ */
+function lesson_check_updates_since(cm_info $cm, $from, $filter = array()) {
+    global $DB, $USER;
+
+    $updates = course_check_module_updates_since($cm, $from, array(), $filter);
+
+    // Check if there are new pages or answers in the lesson.
+    $updates->pages = (object) array('updated' => false);
+    $updates->answers = (object) array('updated' => false);
+    $select = 'lessonid = ? AND (timecreated > ? OR timemodified > ?)';
+    $params = array($cm->instance, $USER->id, $from);
+
+    $pages = $DB->get_records_select('lesson_pages', $select, $params, '', 'id');
+    if (!empty($pages)) {
+        $updates->pages->updated = true;
+        $updates->pages->itemids = array_keys($pages);
+    }
+    $answers = $DB->get_records_select('lesson_answers', $select, $params, '', 'id');
+    if (!empty($answers)) {
+        $updates->answers->updated = true;
+        $updates->answers->itemids = array_keys($answers);
+    }
+
+    // Check for new question attempts, grades, pages viewed and timers.
+    $updates->questionattempts = (object) array('updated' => false);
+    $updates->grades = (object) array('updated' => false);
+    $updates->pagesviewed = (object) array('updated' => false);
+    $updates->timers = (object) array('updated' => false);
+
+    $select = 'lessonid = ? AND userid = ? AND timeseen > ?';
+    $params = array($cm->instance, $USER->id, $from);
+
+    $questionattempts = $DB->get_records_select('lesson_attempts', $select, $params, '', 'id');
+    if (!empty($questionattempts)) {
+        $updates->questionattempts->updated = true;
+        $updates->questionattempts->itemids = array_keys($questionattempts);
+    }
+    $pagesviewed = $DB->get_records_select('lesson_branch', $select, $params, '', 'id');
+    if (!empty($pagesviewed)) {
+        $updates->pagesviewed->updated = true;
+        $updates->pagesviewed->itemids = array_keys($pagesviewed);
+    }
+
+    $select = 'lessonid = ? AND userid = ? AND completed > ?';
+    $grades = $DB->get_records_select('lesson_grades', $select, $params, '', 'id');
+    if (!empty($grades)) {
+        $updates->grades->updated = true;
+        $updates->grades->itemids = array_keys($grades);
+    }
+
+    $select = 'lessonid = ? AND userid = ? AND (starttime > ? OR lessontime > ? OR timemodifiedoffline > ?)';
+    $params = array($cm->instance, $USER->id, $from, $from, $from);
+    $timers = $DB->get_records_select('lesson_timer', $select, $params, '', 'id');
+    if (!empty($timers)) {
+        $updates->timers->updated = true;
+        $updates->timers->itemids = array_keys($timers);
+    }
+
+    return $updates;
+}
index 43b3031..9496357 100644 (file)
@@ -674,6 +674,484 @@ function lesson_process_group_deleted_in_course($courseid, $groupid = null) {
     $DB->delete_records_list('lesson_overrides', 'id', array_keys($records));
 }
 
+/**
+ * Return the overview report table and data.
+ *
+ * @param  lesson $lesson       lesson instance
+ * @param  mixed $currentgroup  false if not group used, 0 for all groups, group id (int) to filter by that groups
+ * @return mixed false if there is no information otherwise html_table and stdClass with the table and data
+ * @since  Moodle 3.3
+ */
+function lesson_get_overview_report_table_and_data(lesson $lesson, $currentgroup) {
+    global $DB;
+
+    $context = $lesson->context;
+    $cm = $lesson->cm;
+    // Count the number of branch and question pages in this lesson.
+    $branchcount = $DB->count_records('lesson_pages', array('lessonid' => $lesson->id, 'qtype' => LESSON_PAGE_BRANCHTABLE));
+    $questioncount = ($DB->count_records('lesson_pages', array('lessonid' => $lesson->id)) - $branchcount);
+
+    // Only load students if there attempts for this lesson.
+    $attempts = $DB->record_exists('lesson_attempts', array('lessonid' => $lesson->id));
+    $branches = $DB->record_exists('lesson_branch', array('lessonid' => $lesson->id));
+    $timer = $DB->record_exists('lesson_timer', array('lessonid' => $lesson->id));
+    if ($attempts or $branches or $timer) {
+        list($esql, $params) = get_enrolled_sql($context, '', $currentgroup, true);
+        list($sort, $sortparams) = users_order_by_sql('u');
+
+        $params['a1lessonid'] = $lesson->id;
+        $params['b1lessonid'] = $lesson->id;
+        $params['c1lessonid'] = $lesson->id;
+        $ufields = user_picture::fields('u');
+        $sql = "SELECT DISTINCT $ufields
+                FROM {user} u
+                JOIN (
+                    SELECT userid, lessonid FROM {lesson_attempts} a1
+                    WHERE a1.lessonid = :a1lessonid
+                        UNION
+                    SELECT userid, lessonid FROM {lesson_branch} b1
+                    WHERE b1.lessonid = :b1lessonid
+                        UNION
+                    SELECT userid, lessonid FROM {lesson_timer} c1
+                    WHERE c1.lessonid = :c1lessonid
+                    ) a ON u.id = a.userid
+                JOIN ($esql) ue ON ue.id = a.userid
+                ORDER BY $sort";
+
+        $students = $DB->get_recordset_sql($sql, $params);
+        if (!$students->valid()) {
+            $students->close();
+            return array(false, false);
+        }
+    } else {
+        return array(false, false);
+    }
+
+    if (! $grades = $DB->get_records('lesson_grades', array('lessonid' => $lesson->id), 'completed')) {
+        $grades = array();
+    }
+
+    if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id), 'starttime')) {
+        $times = array();
+    }
+
+    // Build an array for output.
+    $studentdata = array();
+
+    $attempts = $DB->get_recordset('lesson_attempts', array('lessonid' => $lesson->id), 'timeseen');
+    foreach ($attempts as $attempt) {
+        // if the user is not in the array or if the retry number is not in the sub array, add the data for that try.
+        if (empty($studentdata[$attempt->userid]) || empty($studentdata[$attempt->userid][$attempt->retry])) {
+            // restore/setup defaults
+            $n = 0;
+            $timestart = 0;
+            $timeend = 0;
+            $usergrade = null;
+            $eol = false;
+
+            // search for the grade record for this try. if not there, the nulls defined above will be used.
+            foreach($grades as $grade) {
+                // check to see if the grade matches the correct user
+                if ($grade->userid == $attempt->userid) {
+                    // see if n is = to the retry
+                    if ($n == $attempt->retry) {
+                        // get grade info
+                        $usergrade = round($grade->grade, 2); // round it here so we only have to do it once
+                        break;
+                    }
+                    $n++; // if not equal, then increment n
+                }
+            }
+            $n = 0;
+            // search for the time record for this try. if not there, the nulls defined above will be used.
+            foreach($times as $time) {
+                // check to see if the grade matches the correct user
+                if ($time->userid == $attempt->userid) {
+                    // see if n is = to the retry
+                    if ($n == $attempt->retry) {
+                        // get grade info
+                        $timeend = $time->lessontime;
+                        $timestart = $time->starttime;
+                        $eol = $time->completed;
+                        break;
+                    }
+                    $n++; // if not equal, then increment n
+                }
+            }
+
+            // build up the array.
+            // this array represents each student and all of their tries at the lesson
+            $studentdata[$attempt->userid][$attempt->retry] = array( "timestart" => $timestart,
+                                                                    "timeend" => $timeend,
+                                                                    "grade" => $usergrade,
+                                                                    "end" => $eol,
+                                                                    "try" => $attempt->retry,
+                                                                    "userid" => $attempt->userid);
+        }
+    }
+    $attempts->close();
+
+    $branches = $DB->get_recordset('lesson_branch', array('lessonid' => $lesson->id), 'timeseen');
+    foreach ($branches as $branch) {
+        // If the user is not in the array or if the retry number is not in the sub array, add the data for that try.
+        if (empty($studentdata[$branch->userid]) || empty($studentdata[$branch->userid][$branch->retry])) {
+            // Restore/setup defaults.
+            $n = 0;
+            $timestart = 0;
+            $timeend = 0;
+            $usergrade = null;
+            $eol = false;
+            // Search for the time record for this try. if not there, the nulls defined above will be used.
+            foreach ($times as $time) {
+                // Check to see if the grade matches the correct user.
+                if ($time->userid == $branch->userid) {
+                    // See if n is = to the retry.
+                    if ($n == $branch->retry) {
+                        // Get grade info.
+                        $timeend = $time->lessontime;
+                        $timestart = $time->starttime;
+                        $eol = $time->completed;
+                        break;
+                    }
+                    $n++; // If not equal, then increment n.
+                }
+            }
+
+            // Build up the array.
+            // This array represents each student and all of their tries at the lesson.
+            $studentdata[$branch->userid][$branch->retry] = array( "timestart" => $timestart,
+                                                                    "timeend" => $timeend,
+                                                                    "grade" => $usergrade,
+                                                                    "end" => $eol,
+                                                                    "try" => $branch->retry,
+                                                                    "userid" => $branch->userid);
+        }
+    }
+    $branches->close();
+
+    // Need the same thing for timed entries that were not completed.
+    foreach ($times as $time) {
+        $endoflesson = $time->completed;
+        // If the time start is the same with another record then we shouldn't be adding another item to this array.
+        if (isset($studentdata[$time->userid])) {
+            $foundmatch = false;
+            $n = 0;
+            foreach ($studentdata[$time->userid] as $key => $value) {
+                if ($value['timestart'] == $time->starttime) {
+                    // Don't add this to the array.
+                    $foundmatch = true;
+                    break;
+                }
+            }
+            $n = count($studentdata[$time->userid]) + 1;
+            if (!$foundmatch) {
+                // Add a record.
+                $studentdata[$time->userid][] = array(
+                                "timestart" => $time->starttime,
+                                "timeend" => $time->lessontime,
+                                "grade" => null,
+                                "end" => $endoflesson,
+                                "try" => $n,
+                                "userid" => $time->userid
+                            );
+            }
+        } else {
+            $studentdata[$time->userid][] = array(
+                                "timestart" => $time->starttime,
+                                "timeend" => $time->lessontime,
+                                "grade" => null,
+                                "end" => $endoflesson,
+                                "try" => 0,
+                                "userid" => $time->userid
+                            );
+        }
+    }
+
+    // To store all the data to be returned by the function.
+    $data = new stdClass();
+
+    // Determine if lesson should have a score.
+    if ($branchcount > 0 AND $questioncount == 0) {
+        // This lesson only contains content pages and is not graded.
+        $data->lessonscored = false;
+    } else {
+        // This lesson is graded.
+        $data->lessonscored = true;
+    }
+    // set all the stats variables
+    $data->numofattempts = 0;
+    $data->avescore      = 0;
+    $data->avetime       = 0;
+    $data->highscore     = null;
+    $data->lowscore      = null;
+    $data->hightime      = null;
+    $data->lowtime       = null;
+    $data->students      = array();
+
+    $table = new html_table();
+
+    // Set up the table object.
+    if ($data->lessonscored) {
+        $table->head = array(get_string('name'), get_string('attempts', 'lesson'), get_string('highscore', 'lesson'));
+    } else {
+        $table->head = array(get_string('name'), get_string('attempts', 'lesson'));
+    }
+    $table->align = array('center', 'left', 'left');
+    $table->wrap = array('nowrap', 'nowrap', 'nowrap');
+    $table->attributes['class'] = 'standardtable generaltable';
+    $table->size = array(null, '70%', null);
+
+    // print out the $studentdata array
+    // going through each student that has attempted the lesson, so, each student should have something to be displayed
+    foreach ($students as $student) {
+        // check to see if the student has attempts to print out
+        if (array_key_exists($student->id, $studentdata)) {
+            // set/reset some variables
+            $attempts = array();
+            $dataforstudent = new stdClass;
+            $dataforstudent->attempts = array();
+            // gather the data for each user attempt
+            $bestgrade = 0;
+            $bestgradefound = false;
+            // $tries holds all the tries/retries a student has done
+            $tries = $studentdata[$student->id];
+            $studentname = fullname($student, true);
+
+            foreach ($tries as $try) {
+                $dataforstudent->attempts[] = $try;
+
+                // Start to build up the checkbox and link.
+                if (has_capability('mod/lesson:edit', $context)) {
+                    $temp = '<input type="checkbox" id="attempts" name="attempts['.$try['userid'].']['.$try['try'].']" /> ';
+                } else {
+                    $temp = '';
+                }
+
+                $temp .= "<a href=\"report.php?id=$cm->id&amp;action=reportdetail&amp;userid=".$try['userid']
+                        .'&amp;try='.$try['try'].'" class="lesson-attempt-link">';
+                if ($try["grade"] !== null) { // if null then not done yet
+                    // this is what the link does when the user has completed the try
+                    $timetotake = $try["timeend"] - $try["timestart"];
+
+                    $temp .= $try["grade"]."%";
+                    $bestgradefound = true;
+                    if ($try["grade"] > $bestgrade) {
+                        $bestgrade = $try["grade"];
+                    }
+                    $temp .= "&nbsp;".userdate($try["timestart"]);
+                    $temp .= ",&nbsp;(".format_time($timetotake).")</a>";
+                } else {
+                    if ($try["end"]) {
+                        // User finished the lesson but has no grade. (Happens when there are only content pages).
+                        $temp .= "&nbsp;".userdate($try["timestart"]);
+                        $timetotake = $try["timeend"] - $try["timestart"];
+                        $temp .= ",&nbsp;(".format_time($timetotake).")</a>";
+                    } else {
+                        // This is what the link does/looks like when the user has not completed the attempt.
+                        $temp .= get_string("notcompleted", "lesson");
+                        if ($try['timestart'] !== 0) {
+                            // Teacher previews do not track time spent.
+                            $temp .= "&nbsp;".userdate($try["timestart"]);
+                        }
+                        $temp .= "</a>";
+                        $timetotake = null;
+                    }
+                }
+                // build up the attempts array
+                $attempts[] = $temp;
+
+                // Run these lines for the stats only if the user finnished the lesson.
+                if ($try["end"]) {
+                    // User has completed the lesson.
+                    $data->numofattempts++;
+                    $data->avetime += $timetotake;
+                    if ($timetotake > $data->hightime || $data->hightime == null) {
+                        $data->hightime = $timetotake;
+                    }
+                    if ($timetotake < $data->lowtime || $data->lowtime == null) {
+                        $data->lowtime = $timetotake;
+                    }
+                    if ($try["grade"] !== null) {
+                        // The lesson was scored.
+                        $data->avescore += $try["grade"];
+                        if ($try["grade"] > $data->highscore || $data->highscore === null) {
+                            $data->highscore = $try["grade"];
+                        }
+                        if ($try["grade"] < $data->lowscore || $data->lowscore === null) {
+                            $data->lowscore = $try["grade"];
+                        }
+
+                    }
+                }
+            }
+            // get line breaks in after each attempt
+            $attempts = implode("<br />\n", $attempts);
+
+            if ($data->lessonscored) {
+                // Add the grade if the lesson is graded.
+                $table->data[] = array($studentname, $attempts, $bestgrade . "%");
+            } else {
+                // This lesson does not have a grade.
+                $table->data[] = array($studentname, $attempts);
+            }
+            // Add the student data.
+            $dataforstudent->id = $student->id;
+            $dataforstudent->fullname = $studentname;
+            $dataforstudent->bestgrade = $bestgrade;
+            $data->students[] = $dataforstudent;
+        }
+    }
+    $students->close();
+    if ($data->numofattempts > 0) {
+        $data->avescore = $data->avescore / $data->numofattempts;
+    }
+
+    return array($table, $data);
+}
+
+/**
+ * Return information about one user attempt (including answers)
+ * @param  lesson $lesson  lesson instance
+ * @param  int $userid     the user id
+ * @param  int $attempt    the attempt number
+ * @return array the user answers (array) and user data stats (object)
+ * @since  Moodle 3.3
+ */
+function lesson_get_user_detailed_report_data(lesson $lesson, $userid, $attempt) {
+    global $DB;
+
+    $context = $lesson->context;
+    if (!empty($userid)) {
+        // Apply overrides.
+        $lesson->update_effective_access($userid);
+    }
+
+    $lessonpages = $lesson->load_all_pages();
+    foreach ($lessonpages as $lessonpage) {
+        if ($lessonpage->prevpageid == 0) {
+            $pageid = $lessonpage->id;
+        }
+    }
+
+    // now gather the stats into an object
+    $firstpageid = $pageid;
+    $pagestats = array();
+    while ($pageid != 0) { // EOL
+        $page = $lessonpages[$pageid];
+        $params = array ("lessonid" => $lesson->id, "pageid" => $page->id);
+        if ($allanswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND pageid = :pageid", $params, "timeseen")) {
+            // get them ready for processing
+            $orderedanswers = array();
+            foreach ($allanswers as $singleanswer) {
+                // ordering them like this, will help to find the single attempt record that we want to keep.
+                $orderedanswers[$singleanswer->userid][$singleanswer->retry][] = $singleanswer;
+            }
+            // this is foreach user and for each try for that user, keep one attempt record
+            foreach ($orderedanswers as $orderedanswer) {
+                foreach($orderedanswer as $tries) {
+                    $page->stats($pagestats, $tries);
+                }
+            }
+        } else {
+            // no one answered yet...
+        }
+        //unset($orderedanswers);  initialized above now
+        $pageid = $page->nextpageid;
+    }
+
+    $manager = lesson_page_type_manager::get($lesson);
+    $qtypes = $manager->get_page_type_strings();
+
+    $answerpages = array();
+    $answerpage = "";
+    $pageid = $firstpageid;
+    // cycle through all the pages
+    //  foreach page, add to the $answerpages[] array all the data that is needed
+    //  from the question, the users attempt, and the statistics
+    // grayout pages that the user did not answer and Branch, end of branch, cluster
+    // and end of cluster pages
+    while ($pageid != 0) { // EOL
+        $page = $lessonpages[$pageid];
+        $answerpage = new stdClass;
+        $data ='';
+
+        $answerdata = new stdClass;
+        // Set some defaults for the answer data.
+        $answerdata->score = null;
+        $answerdata->response = null;
+        $answerdata->responseformat = FORMAT_PLAIN;
+
+        $answerpage->title = format_string($page->title);
+
+        $options = new stdClass;
+        $options->noclean = true;
+        $options->overflowdiv = true;
+        $options->context = $context;
+        $answerpage->contents = format_text($page->contents, $page->contentsformat, $options);
+
+        $answerpage->qtype = $qtypes[$page->qtype].$page->option_description_string();
+        $answerpage->grayout = $page->grayout;
+        $answerpage->context = $context;
+
+        if (empty($userid)) {
+            // there is no userid, so set these vars and display stats.
+            $answerpage->grayout = 0;
+            $useranswer = null;
+        } elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$attempt,"pageid"=>$page->id), "timeseen")) {
+            // get the user's answer for this page
+            // need to find the right one
+            $i = 0;
+            foreach ($useranswers as $userattempt) {
+                $useranswer = $userattempt;
+                $i++;
+                if ($lesson->maxattempts == $i) {
+                    break; // reached maxattempts, break out
+                }
+            }
+        } else {
+            // user did not answer this page, gray it out and set some nulls
+            $answerpage->grayout = 1;
+            $useranswer = null;
+        }
+        $i = 0;
+        $n = 0;
+        $answerpages[] = $page->report_answers(clone($answerpage), clone($answerdata), $useranswer, $pagestats, $i, $n);
+        $pageid = $page->nextpageid;
+    }
+
+    $userstats = new stdClass;
+    if (!empty($userid)) {
+        $params = array("lessonid"=>$lesson->id, "userid"=>$userid);
+
+        $alreadycompleted = true;
+
+        if (!$grades = $DB->get_records_select("lesson_grades", "lessonid = :lessonid and userid = :userid", $params, "completed", "*", $attempt, 1)) {
+            $userstats->grade = -1;
+            $userstats->completed = -1;
+            $alreadycompleted = false;
+        } else {
+            $userstats->grade = current($grades);
+            $userstats->completed = $userstats->grade->completed;
+            $userstats->grade = round($userstats->grade->grade, 2);
+        }
+
+        if (!$times = $lesson->get_user_timers($userid, 'starttime', '*', $attempt, 1)) {
+            $userstats->timetotake = -1;
+            $alreadycompleted = false;
+        } else {
+            $userstats->timetotake = current($times);
+            $userstats->timetotake = $userstats->timetotake->lessontime - $userstats->timetotake->starttime;
+        }
+
+        if ($alreadycompleted) {
+            $userstats->gradeinfo = lesson_grade($lesson, $attempt, $userid);
+        }
+    }
+
+    return array($answerpages, $userstats);
+}
+
+
 /**
  * Abstract class that page type's MUST inherit from.
  *
@@ -970,6 +1448,7 @@ abstract class lesson_add_page_form_base extends moodleform {
  * @property int $available Timestamp of when this lesson becomes available
  * @property int $deadline Timestamp of when this lesson is no longer available
  * @property int $timemodified Timestamp when lesson was last modified
+ * @property int $allowofflineattempts Whether to allow the lesson to be attempted offline in the mobile app
  *
  * These properties are calculated
  * @property int $firstpageid Id of the first page of this lesson (prevpageid=0)
@@ -1557,11 +2036,16 @@ class lesson extends lesson_base {
         $event->trigger();
 
         $USER->startlesson[$this->properties->id] = true;
+
+        $timenow = time();
         $startlesson = new stdClass;
         $startlesson->lessonid = $this->properties->id;
         $startlesson->userid = $USER->id;
-        $startlesson->starttime = time();
-        $startlesson->lessontime = time();
+        $startlesson->starttime = $timenow;
+        $startlesson->lessontime = $timenow;
+        if (WS_SERVER) {
+            $startlesson->timemodifiedoffline = $timenow;
+        }
         $DB->insert_record('lesson_timer', $startlesson);
         if ($this->properties->timelimit) {
             $this->add_message(get_string('timelimitwarning', 'lesson', format_time($this->properties->timelimit)), 'center');
@@ -1617,7 +2101,11 @@ class lesson extends lesson_base {
             }
         }
 
-        $timer->lessontime = time();
+        $timenow = time();
+        $timer->lessontime = $timenow;
+        if (WS_SERVER) {
+            $timer->timemodifiedoffline = $timenow;
+        }
         $timer->completed = $endreached;
         $DB->update_record('lesson_timer', $timer);
 
@@ -2675,6 +3163,336 @@ class lesson extends lesson_base {
 
         return array($page, $lessoncontent);
     }
+
+    /**
+     * This returns a real page id to jump to (or LESSON_EOL) after processing page responses.
+     *
+     * @param  lesson_page $page      lesson page
+     * @param  int         $newpageid the new page id
+     * @return int the real page to jump to (or end of lesson)
+     * @since  Moodle 3.3
+     */
+    public function calculate_new_page_on_jump(lesson_page $page, $newpageid) {
+        global $USER, $DB;
+
+        $canmanage = $this->can_manage();
+
+        if (isset($USER->modattempts[$this->properties->id])) {
+            // Make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time.
+            if ($USER->modattempts[$this->properties->id]->pageid == $page->id && $page->nextpageid == 0) {
+                // Remember, this session variable holds the pageid of the last page that the user saw.
+                $newpageid = LESSON_EOL;
+            } else {
+                $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
+                $nretakes--; // Make sure we are looking at the right try.
+                $attempts = $DB->get_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes), "timeseen", "id, pageid");
+                $found = false;
+                $temppageid = 0;
+                // Make sure that the newpageid always defaults to something valid.
+                $newpageid = LESSON_EOL;
+                foreach ($attempts as $attempt) {
+                    if ($found && $temppageid != $attempt->pageid) {
+                        // Now try to find the next page, make sure next few attempts do no belong to current page.
+                        $newpageid = $attempt->pageid;
+                        break;
+                    }
+                    if ($attempt->pageid == $page->id) {
+                        $found = true; // If found current page.
+                        $temppageid = $attempt->pageid;
+                    }
+                }
+            }
+        } else if ($newpageid != LESSON_CLUSTERJUMP && $page->id != 0 && $newpageid > 0) {
+            // Going to check to see if the page that the user is going to view next, is a cluster page.
+            // If so, dont display, go into the cluster.
+            // The $newpageid > 0 is used to filter out all of the negative code jumps.
+            $newpage = $this->load_page($newpageid);
+            if ($overridenewpageid = $newpage->override_next_page($newpageid)) {
+                $newpageid = $overridenewpageid;
+            }
+        } else if ($newpageid == LESSON_UNSEENBRANCHPAGE) {
+            if ($canmanage) {
+                if ($page->nextpageid == 0) {
+                    $newpageid = LESSON_EOL;
+                } else {
+                    $newpageid = $page->nextpageid;
+                }
+            } else {
+                $newpageid = lesson_unseen_question_jump($this, $USER->id, $page->id);
+            }
+        } else if ($newpageid == LESSON_PREVIOUSPAGE) {
+            $newpageid = $page->prevpageid;
+        } else if ($newpageid == LESSON_RANDOMPAGE) {
+            $newpageid = lesson_random_question_jump($this, $page->id);
+        } else if ($newpageid == LESSON_CLUSTERJUMP) {
+            if ($canmanage) {
+                if ($page->nextpageid == 0) {  // If teacher, go to next page.
+                    $newpageid = LESSON_EOL;
+                } else {
+                    $newpageid = $page->nextpageid;
+                }
+            } else {
+                $newpageid = $this->cluster_jump($page->id);
+            }
+        } else if ($newpageid == 0) {
+            $newpageid = $page->id;
+        } else if ($newpageid == LESSON_NEXTPAGE) {
+            $newpageid = $this->get_next_page($page->nextpageid);
+        }
+
+        return $newpageid;
+    }
+
+    /**
+     * Process page responses.
+     *
+     * @param lesson_page $page page object
+     * @since  Moodle 3.3
+     */
+    public function process_page_responses(lesson_page $page) {
+        $context = $this->get_context();
+
+        // Check the page has answers [MDL-25632].
+        if (count($page->answers) > 0) {
+            $result = $page->record_attempt($context);
+        } else {
+            // The page has no answers so we will just progress to the next page in the
+            // sequence (as set by newpageid).
+            $result = new stdClass;
+            $result->newpageid       = optional_param('newpageid', $page->nextpageid, PARAM_INT);
+            $result->nodefaultresponse  = true;
+        }
+
+        if ($result->inmediatejump) {
+            return $result;
+        }
+
+        $result->newpageid = $this->calculate_new_page_on_jump($page, $result->newpageid);
+
+        return $result;
+    }
+
+    /**
+     * Add different informative messages to the given page.
+     *
+     * @param lesson_page $page page object
+     * @param stdClass $result the page processing result object
+     * @param bool $reviewmode whether we are in review mode or not
+     * @since  Moodle 3.3
+     */
+    public function add_messages_on_page_process(lesson_page $page, $result, $reviewmode) {
+
+        if ($this->can_manage()) {
+            // This is the warning msg for teachers to inform them that cluster and unseen does not work while logged in as a teacher.
+            if (lesson_display_teacher_warning($this)) {
+                $warningvars = new stdClass();
+                $warningvars->cluster = get_string("clusterjump", "lesson");
+                $warningvars->unseen = get_string("unseenpageinbranch", "lesson");
+                $this->add_message(get_string("teacherjumpwarning", "lesson", $warningvars));
+            }
+            // Inform teacher that s/he will not see the timer.
+            if ($this->properties->timelimit) {
+                $lesson->add_message(get_string("teachertimerwarning", "lesson"));
+            }
+        }
+        // Report attempts remaining.
+        if ($result->attemptsremaining != 0 && $this->properties->review && !$reviewmode) {
+            $this->add_message(get_string('attemptsremaining', 'lesson', $result->attemptsremaining));
+        }
+    }
+
+    /**
+     * Process and return all the information for the end of lesson page.
+     *
+     * @param string $outoftime used to check to see if the student ran out of time
+     * @return stdclass an object with all the page data ready for rendering
+     * @since  Moodle 3.3
+     */
+    public function process_eol_page($outoftime) {
+        global $DB, $USER;
+
+        $course = $this->get_courserecord();
+        $cm = $this->get_cm();
+        $canmanage = $this->can_manage();
+
+        // Init all the possible fields and values.
+        $data = (object) array(
+            'gradelesson' => true,
+            'notenoughtimespent' => false,
+            'numberofpagesviewed' => false,
+            'youshouldview' => false,
+            'numberofcorrectanswers' => false,
+            'displayscorewithessays' => false,
+            'displayscorewithoutessays' => false,
+            'yourcurrentgradeisoutof' => false,
+            'eolstudentoutoftimenoanswers' => false,
+            'welldone' => false,
+            'progressbar' => false,
+            'displayofgrade' => false,
+            'reviewlesson' => false,
+            'modattemptsnoteacher' => false,
+            'activitylink' => false,
+            'progresscompleted' => false,
+        );
+
+        $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
+        if (isset($USER->modattempts[$this->properties->id])) {
+            $ntries--;  // Need to look at the old attempts :).
+        }
+
+        $gradeinfo = lesson_grade($this, $ntries);
+        $data->gradeinfo = $gradeinfo;
+        if ($this->properties->custom && !$canmanage) {
+            // Before we calculate the custom score make sure they answered the minimum
+            // number of questions. We only need to do this for custom scoring as we can
+            // not get the miniumum score the user should achieve. If we are not using
+            // custom scoring (so all questions are valued as 1) then we simply check if
+            // they answered more than the minimum questions, if not, we mark it out of the
+            // number specified in the minimum questions setting - which is done in lesson_grade().
+            // Get the number of answers given.
+            if ($gradeinfo->nquestions < $this->properties->minquestions) {
+                $data->gradelesson = false;
+                $a = new stdClass;
+                $a->nquestions = $gradeinfo->nquestions;
+                $a->minquestions = $this->properties->minquestions;
+                $this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
+            }
+        }
+
+        if (!$canmanage) {
+            if ($data->gradelesson) {
+                // Store this now before any modifications to pages viewed.
+                $progresscompleted = $this->calculate_progress();
+
+                // Update the clock / get time information for this user.
+                $this->stop_timer();
+
+                // Update completion state.
+                $completion = new completion_info($course);
+                if ($completion->is_enabled($cm) && $this->properties->completionendreached) {
+                    $completion->update_state($cm, COMPLETION_COMPLETE);
+                }
+
+                if ($this->properties->completiontimespent > 0) {
+                    $duration = $DB->get_field_sql(
+                        "SELECT SUM(lessontime - starttime)
+                                       FROM {lesson_timer}
+                                      WHERE lessonid = :lessonid
+                                        AND userid = :userid",
+                        array('userid' => $USER->id, 'lessonid' => $this->properties->id));
+                    if (!$duration) {
+                        $duration = 0;
+                    }
+
+                    // If student has not spend enough time in the lesson, display a message.
+                    if ($duration < $this->properties->completiontimespent) {
+                        $a = new stdClass;
+                        $a->timespentraw = $duration;
+                        $a->timespent = format_time($duration);
+                        $a->timerequiredraw = $this->properties->completiontimespent;
+                        $a->timerequired = format_time($this->properties->completiontimespent);
+                        $data->notenoughtimespent = $a;
+                    }
+                }
+
+                if ($gradeinfo->attempts) {
+                    if (!$this->properties->custom) {
+                        $data->numberofpagesviewed = $gradeinfo->nquestions;
+                        if ($this->properties->minquestions) {
+                            if ($gradeinfo->nquestions < $this->properties->minquestions) {
+                                $data->youshouldview = $this->properties->minquestions;
+                            }
+                        }
+                        $data->numberofcorrectanswers = $gradeinfo->earned;
+                    }
+                    $a = new stdClass;
+                    $a->score = $gradeinfo->earned;
+                    $a->grade = $gradeinfo->total;
+                    if ($gradeinfo->nmanual) {
+                        $a->tempmaxgrade = $gradeinfo->total - $gradeinfo->manualpoints;
+                        $a->essayquestions = $gradeinfo->nmanual;
+                        $data->displayscorewithessays = $a;
+                    } else {
+                        $data->displayscorewithoutessays = $a;
+                    }
+                    if ($this->properties->grade != GRADE_TYPE_NONE) {
+                        $a = new stdClass;
+                        $a->grade = number_format($gradeinfo->grade * $this->properties->grade / 100, 1);
+                        $a->total = $this->properties->grade;
+                        $data->yourcurrentgradeisoutof = $a;
+                    }
+
+                    $grade = new stdClass();
+                    $grade->lessonid = $this->properties->id;
+                    $grade->userid = $USER->id;
+                    $grade->grade = $gradeinfo->grade;
+                    $grade->completed = time();
+                    if (isset($USER->modattempts[$this->properties->id])) { // If reviewing, make sure update old grade record.
+                        if (!$grades = $DB->get_records("lesson_grades",
+                            array("lessonid" => $this->properties->id, "userid" => $USER->id), "completed DESC", '*', 0, 1)) {
+                            throw new moodle_exception('cannotfindgrade', 'lesson');
+                        }
+                        $oldgrade = array_shift($grades);
+                        $grade->id = $oldgrade->id;
+                        $DB->update_record("lesson_grades", $grade);
+                    } else {
+                        $newgradeid = $DB->insert_record("lesson_grades", $grade);
+                    }
+                } else {
+                    if ($this->properties->timelimit) {
+                        if ($outoftime == 'normal') {
+                            $grade = new stdClass();
+                            $grade->lessonid = $this->properties->id;
+                            $grade->userid = $USER->id;
+                            $grade->grade = 0;
+                            $grade->completed = time();
+                            $newgradeid = $DB->insert_record("lesson_grades", $grade);
+                            $data->eolstudentoutoftimenoanswers = true;
+                        }
+                    } else {
+                        $data->welldone = true;
+                    }
+                }
+
+                // Update central gradebook.
+                lesson_update_grades($this, $USER->id);
+                $data->progresscompleted = $progresscompleted;
+            }
+        } else {
+            // Display for teacher.
+            if ($this->properties->grade != GRADE_TYPE_NONE) {
+                $data->displayofgrade = true;
+            }
+        }
+
+        if ($this->properties->modattempts && !$canmanage) {
+            // Make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time
+            // look at the attempt records to find the first QUESTION page that the user answered, then use that page id
+            // to pass to view again.  This is slick cause it wont call the empty($pageid) code
+            // $ntries is decremented above.
+            if (!$attempts = $this->get_attempts($ntries)) {
+                $attempts = array();
+                $url = new moodle_url('/mod/lesson/view.php', array('id' => $cm->id));
+            } else {
+                $firstattempt = current($attempts);
+                $pageid = $firstattempt->pageid;
+                // If the student wishes to review, need to know the last question page that the student answered.
+                // This will help to make sure that the student can leave the lesson via pushing the continue button.
+                $lastattempt = end($attempts);
+                $USER->modattempts[$this->properties->id] = $lastattempt->pageid;
+
+                $url = new moodle_url('/mod/lesson/view.php', array('id' => $cm->id, 'pageid' => $pageid));
+            }
+            $data->reviewlesson = $url;
+        } else if ($this->properties->modattempts && $canmanage) {
+            $data->modattemptsnoteacher = true;
+        }
+
+        if ($this->properties->activitylink) {
+            $data->activitylink = $this->link_for_activitylink();
+        }
+        return $data;
+    }
 }
 
 
@@ -3115,6 +3933,11 @@ abstract class lesson_page extends lesson_base {
          */
         $result = $this->check_answer();
 
+        // Processes inmediate jumps.
+        if ($result->inmediatejump) {
+            return $result;
+        }
+
         $result->attemptsremaining  = 0;
         $result->maxattemptsreached = false;
 
@@ -3191,12 +4014,6 @@ abstract class lesson_page extends lesson_base {
                     }
                 }
             }
-            // TODO: merge this code with the jump code below.  Convert jumpto page into a proper page id
-            if ($result->newpageid == 0) {
-                $result->newpageid = $this->properties->id;
-            } elseif ($result->newpageid == LESSON_NEXTPAGE) {
-                $result->newpageid = $this->lesson->get_next_page($this->properties->nextpageid);
-            }
 
             // Determine default feedback if necessary
             if (empty($result->response)) {
@@ -3630,6 +4447,7 @@ abstract class lesson_page extends lesson_base {
         $result->userresponse    = null;
         $result->feedback        = '';
         $result->nodefaultresponse  = false; // Flag for redirecting when default feedback is turned off
+        $result->inmediatejump = false; // Flag to detect when we should do a jump from the page without further processing.
         return $result;
     }
 
index 9c9df17..252c2b5 100644 (file)
@@ -49,7 +49,7 @@ class mod_lesson_mod_form extends moodleform_mod {
     }
 
     function definition() {
-        global $CFG, $COURSE, $DB;
+        global $CFG, $COURSE, $DB, $OUTPUT;
 
         $mform    = $this->_form;
 
@@ -249,6 +249,22 @@ class mod_lesson_mod_form extends moodleform_mod {
                     'completed' => 0, 'gradebetterthan' => 0));
         }
 
+        // Allow to enable offline lessons only if the Mobile services are enabled.
+        if ($CFG->enablemobilewebservice) {
+            $mform->addElement('selectyesno', 'allowofflineattempts', get_string('allowofflineattempts', 'lesson'));
+            $mform->addHelpButton('allowofflineattempts', 'allowofflineattempts', 'lesson');
+            $mform->setDefault('allowofflineattempts', 0);
+            $mform->setAdvanced('allowofflineattempts');
+            $mform->disabledIf('allowofflineattempts', 'timelimit[number]', 'neq', 0);
+
+            $mform->addElement('static', 'allowofflineattemptswarning', '',
+                    $OUTPUT->notification(get_string('allowofflineattempts_help', 'lesson'), 'warning'));
+            $mform->setAdvanced('allowofflineattemptswarning');
+        } else {
+            $mform->addElement('hidden', 'allowofflineattempts', 0);
+            $mform->setType('allowofflineattempts', PARAM_INT);
+        }
+
         // Flow control.
         $mform->addElement('header', 'flowcontrol', get_string('flowcontrol', 'lesson'));
 
index ee1a3c1..5a43fce 100644 (file)
@@ -160,6 +160,8 @@ class lesson_page_type_branchtable extends lesson_page {
     public function check_answer() {
         global $USER, $DB, $PAGE, $CFG;
 
+        $result = parent::check_answer();
+
         require_sesskey();
         $newpageid = optional_param('jumpto', null, PARAM_INT);
         // going to insert into lesson_branch
@@ -210,7 +212,10 @@ class lesson_page_type_branchtable extends lesson_page {
         $branch->nextpageid = $newpageid;
         $DB->insert_record("lesson_branch", $branch);
 
-        redirect(new moodle_url('/mod/lesson/view.php', array('id' => $PAGE->cm->id, 'pageid' => $newpageid)));
+        // This will force to redirect to the newpageid.
+        $result->inmediatejump = true;
+        $result->newpageid = $newpageid;
+        return $result;
     }
 
     public function display_answers(html_table $table) {
index 9592180..992200d 100644 (file)
@@ -120,7 +120,9 @@ class lesson_page_type_essay extends lesson_page {
         require_sesskey();
 
         if (!$data) {
-            redirect(new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id, 'pageid'=>$this->properties->id)));
+            $result->inmediatejump = true;
+            $result->newpageid = $this->properties->id;
+            return $result;
         }
 
         if (is_array($data->answer)) {
index cb8e522..c43f700 100644 (file)
@@ -181,7 +181,9 @@ class lesson_page_type_matching extends lesson_page {
         require_sesskey();
 
         if (!$data) {
-            redirect(new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id, 'pageid'=>$this->properties->id)));
+            $result->inmediatejump = true;
+            $result->newpageid = $this->properties->id;
+            return $result;
         }
 
         $response = $data->response;
index b56fa30..37b6a8d 100644 (file)
@@ -135,7 +135,9 @@ class lesson_page_type_multichoice extends lesson_page {
         require_sesskey();
 
         if (!$data) {
-            redirect(new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id, 'pageid'=>$this->properties->id)));
+            $result->inmediatejump = true;
+            $result->newpageid = $this->properties->id;
+            return $result;
         }
 
         if ($this->properties->qoption) {
index 1f7e95a..c223ff1 100644 (file)
@@ -484,9 +484,10 @@ class mod_lesson_renderer extends plugin_renderer_base {
      * Returns HTML to display a progress bar of progression through a lesson
      *
      * @param lesson $lesson
+     * @param int $progress optional, if empty it will be calculated
      * @return string
      */
-    public function progress_bar(lesson $lesson) {
+    public function progress_bar(lesson $lesson, $progress = null) {
         $context = context_module::instance($this->page->cm->id);
 
         // lesson setting to turn progress bar on or off
@@ -499,8 +500,9 @@ class mod_lesson_renderer extends plugin_renderer_base {
             return $this->output->notification(get_string('progressbarteacherwarning2', 'lesson'));
         }
 
-        // Check if the user is reviewing the attempt.
-        $progress = $lesson->calculate_progress();
+        if ($progress === null) {
+            $progress = $lesson->calculate_progress();
+        }
 
         // print out the Progress Bar.  Attempted to put as much as possible in the style sheets.
         $content = '<br />' . html_writer::tag('div', $progress . '%', array('class' => 'progress_bar_completed', 'style' => 'width: '. $progress . '%;'));
@@ -541,4 +543,91 @@ class mod_lesson_renderer extends plugin_renderer_base {
         $output = html_writer::tag('p', $contents, $attributes);
         return $output;
     }
+
+    /**
+     * Returns the HTML for displaying the end of lesson page.
+     *
+     * @param  lesson $lesson lesson instance
+     * @param  stdclass $data lesson data to be rendered
+     * @return string         HTML contents
+     */
+    public function display_eol_page(lesson $lesson, $data) {
+
+        $output = '';
+        $canmanage = $lesson->can_manage();
+        $course = $lesson->courserecord;
+
+        if ($lesson->custom && !$canmanage && (($data->gradeinfo->nquestions < $lesson->minquestions))) {
+            $output .= $this->box_start('generalbox boxaligncenter');
+        }
+
+        if ($data->gradelesson) {
+            // We are using level 3 header because the page title is a sub-heading of lesson title (MDL-30911).
+            $output .= $this->heading(get_string("congratulations", "lesson"), 3);
+            $output .= $this->box_start('generalbox boxaligncenter');
+        }
+
+        if ($data->notenoughtimespent !== false) {
+            $output .= $this->paragraph(get_string("notenoughtimespent", "lesson", $data->notenoughtimespent), 'center');
+        }
+
+        if ($data->numberofpagesviewed !== false) {
+            $output .= $this->paragraph(get_string("numberofpagesviewed", "lesson", $data->numberofpagesviewed), 'center');
+        }
+        if ($data->youshouldview !== false) {
+            $output .= $this->paragraph(get_string("youshouldview", "lesson", $data->youshouldview), 'center');
+        }
+        if ($data->numberofcorrectanswers !== false) {
+            $output .= $this->paragraph(get_string("numberofcorrectanswers", "lesson", $data->numberofcorrectanswers), 'center');
+        }
+
+        if ($data->displayscorewithessays !== false) {
+            $output .= $this->box(get_string("displayscorewithessays", "lesson", $data->displayscorewithessays), 'center');
+        } else if ($data->displayscorewithoutessays !== false) {
+            $output .= $this->box(get_string("displayscorewithoutessays", "lesson", $data->displayscorewithoutessays), 'center');
+        }
+
+        if ($data->yourcurrentgradeisoutof !== false) {
+            $output .= $this->paragraph(get_string("yourcurrentgradeisoutof", "lesson", $data->yourcurrentgradeisoutof), 'center');
+        }
+        if ($data->eolstudentoutoftimenoanswers !== false) {
+            $output .= $this->paragraph(get_string("eolstudentoutoftimenoanswers", "lesson"));
+        }
+        if ($data->welldone !== false) {
+            $output .= $this->paragraph(get_string("welldone", "lesson"));
+        }
+
+        if ($data->progresscompleted !== false) {
+            $output .= $this->progress_bar($lesson, $data->progresscompleted);
+        }
+
+        if ($data->displayofgrade !== false) {
+            $output .= $this->paragraph(get_string("displayofgrade", "lesson"), 'center');
+        }
+
+        $output .= $this->box_end(); // End of Lesson button to Continue.
+
+        if ($data->reviewlesson !== false) {
+            $output .= html_writer::link($data->reviewlesson, get_string('reviewlesson', 'lesson'), array('class' => 'centerpadded lessonbutton standardbutton p-r-1'));
+        }
+        if ($data->modattemptsnoteacher !== false) {
+            $output .= $this->paragraph(get_string("modattemptsnoteacher", "lesson"), 'centerpadded');
+        }
+
+        if ($data->activitylink !== false) {
+            $output .= $data->activitylink;
+        }
+
+        $url = new moodle_url('/course/view.php', array('id' => $course->id));
+        $output .= html_writer::link($url, get_string('returnto', 'lesson', format_string($course->fullname, true)),
+                array('class' => 'centerpadded lessonbutton standardbutton p-r-1'));
+
+        if (has_capability('gradereport/user:view', context_course::instance($course->id))
+                && $course->showgrades && $lesson->grade != 0 && !$lesson->practice) {
+            $url = new moodle_url('/grade/index.php', array('id' => $course->id));
+            $output .= html_writer::link($url, get_string('viewgrades', 'lesson'),
+                array('class' => 'centerpadded lessonbutton standardbutton p-r-1'));
+        }
+        return $output;
+    }
 }
index df158a5..bb21b49 100644 (file)
@@ -114,47 +114,10 @@ if ($action === 'delete') {
     this action is for default view and overview view
     **************************************************************************/
 
-    // Count the number of branch and question pages in this lesson.
-    $branchcount = $DB->count_records('lesson_pages', array('lessonid' => $lesson->id, 'qtype' => LESSON_PAGE_BRANCHTABLE));
-    $questioncount = ($DB->count_records('lesson_pages', array('lessonid' => $lesson->id)) - $branchcount);
-
-    // Only load students if there attempts for this lesson.
-    $attempts = $DB->record_exists('lesson_attempts', array('lessonid' => $lesson->id));
-    $branches = $DB->record_exists('lesson_branch', array('lessonid' => $lesson->id));
-    $timer = $DB->record_exists('lesson_timer', array('lessonid' => $lesson->id));
-    if ($attempts or $branches or $timer) {
-        list($esql, $params) = get_enrolled_sql($context, '', $currentgroup, true);
-        list($sort, $sortparams) = users_order_by_sql('u');
-
-        $params['a1lessonid'] = $lesson->id;
-        $params['b1lessonid'] = $lesson->id;
-        $params['c1lessonid'] = $lesson->id;
-        $ufields = user_picture::fields('u');
-        $sql = "SELECT DISTINCT $ufields
-                FROM {user} u
-                JOIN (
-                    SELECT userid, lessonid FROM {lesson_attempts} a1
-                    WHERE a1.lessonid = :a1lessonid
-                        UNION
-                    SELECT userid, lessonid FROM {lesson_branch} b1
-                    WHERE b1.lessonid = :b1lessonid
-                        UNION
-                    SELECT userid, lessonid FROM {lesson_timer} c1
-                    WHERE c1.lessonid = :c1lessonid
-                    ) a ON u.id = a.userid
-                JOIN ($esql) ue ON ue.id = a.userid
-                ORDER BY $sort";
-
-        $students = $DB->get_recordset_sql($sql, $params);
-        if (!$students->valid()) {
-            $students->close();
-            $nothingtodisplay = true;
-        }
-    } else {
-        $nothingtodisplay = true;
-    }
+    // Get the table and data for build statistics.
+    list($table, $data) = lesson_get_overview_report_table_and_data($lesson, $currentgroup);
 
-    if ($nothingtodisplay) {
+    if ($table === false) {
         echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('nolessonattempts', 'lesson'));
         if (!empty($currentgroup)) {
             $groupname = groups_get_group_name($currentgroup);
@@ -167,14 +130,6 @@ if ($action === 'delete') {
         exit();
     }
 
-    if (! $grades = $DB->get_records('lesson_grades', array('lessonid' => $lesson->id), 'completed')) {
-        $grades = array();
-    }
-
-    if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id), 'starttime')) {
-        $times = array();
-    }
-
     echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('overview', 'lesson'));
     groups_print_activity_menu($cm, $url);
 
@@ -185,266 +140,15 @@ if ($action === 'delete') {
         echo $OUTPUT->box($seeallgradeslink, 'allcoursegrades');
     }
 
-    // Build an array for output.
-    $studentdata = array();
-
-    $attempts = $DB->get_recordset('lesson_attempts', array('lessonid' => $lesson->id), 'timeseen');
-    foreach ($attempts as $attempt) {
-        // if the user is not in the array or if the retry number is not in the sub array, add the data for that try.
-        if (empty($studentdata[$attempt->userid]) || empty($studentdata[$attempt->userid][$attempt->retry])) {
-            // restore/setup defaults
-            $n = 0;
-            $timestart = 0;
-            $timeend = 0;
-            $usergrade = null;
-            $eol = false;
-
-            // search for the grade record for this try. if not there, the nulls defined above will be used.
-            foreach($grades as $grade) {
-                // check to see if the grade matches the correct user
-                if ($grade->userid == $attempt->userid) {
-                    // see if n is = to the retry
-                    if ($n == $attempt->retry) {
-                        // get grade info
-                        $usergrade = round($grade->grade, 2); // round it here so we only have to do it once
-                        break;
-                    }
-                    $n++; // if not equal, then increment n
-                }
-            }
-            $n = 0;
-            // search for the time record for this try. if not there, the nulls defined above will be used.
-            foreach($times as $time) {
-                // check to see if the grade matches the correct user
-                if ($time->userid == $attempt->userid) {
-                    // see if n is = to the retry
-                    if ($n == $attempt->retry) {
-                        // get grade info
-                        $timeend = $time->lessontime;
-                        $timestart = $time->starttime;
-                        $eol = $time->completed;
-                        break;
-                    }
-                    $n++; // if not equal, then increment n
-                }
-            }
-
-            // build up the array.
-            // this array represents each student and all of their tries at the lesson
-            $studentdata[$attempt->userid][$attempt->retry] = array( "timestart" => $timestart,
-                                                                    "timeend" => $timeend,
-                                                                    "grade" => $usergrade,
-                                                                    "end" => $eol,
-                                                                    "try" => $attempt->retry,
-                                                                    "userid" => $attempt->userid);
-        }
-    }
-    $attempts->close();
-
-    $branches = $DB->get_recordset('lesson_branch', array('lessonid' => $lesson->id), 'timeseen');
-    foreach ($branches as $branch) {
-        // If the user is not in the array or if the retry number is not in the sub array, add the data for that try.
-        if (empty($studentdata[$branch->userid]) || empty($studentdata[$branch->userid][$branch->retry])) {
-            // Restore/setup defaults.
-            $n = 0;
-            $timestart = 0;
-            $timeend = 0;
-            $usergrade = null;
-            $eol = false;
-            // Search for the time record for this try. if not there, the nulls defined above will be used.
-            foreach ($times as $time) {
-                // Check to see if the grade matches the correct user.
-                if ($time->userid == $branch->userid) {
-                    // See if n is = to the retry.
-                    if ($n == $branch->retry) {
-                        // Get grade info.
-                        $timeend = $time->lessontime;
-                        $timestart = $time->starttime;
-                        $eol = $time->completed;
-                        break;
-                    }
-                    $n++; // If not equal, then increment n.
-                }
-            }
-
-            // Build up the array.
-            // This array represents each student and all of their tries at the lesson.
-            $studentdata[$branch->userid][$branch->retry] = array( "timestart" => $timestart,
-                                                                    "timeend" => $timeend,
-                                                                    "grade" => $usergrade,
-                                                                    "end" => $eol,
-                                                                    "try" => $branch->retry,
-                                                                    "userid" => $branch->userid);
-        }
-    }
-    $branches->close();
-
-    // Need the same thing for timed entries that were not completed.
-    foreach ($times as $time) {
-        $endoflesson = $time->completed;
-        // If the time start is the same with another record then we shouldn't be adding another item to this array.
-        if (isset($studentdata[$time->userid])) {
-            $foundmatch = false;
-            $n = 0;
-            foreach ($studentdata[$time->userid] as $key => $value) {
-                if ($value['timestart'] == $time->starttime) {
-                    // Don't add this to the array.
-                    $foundmatch = true;
-                    break;
-                }
-            }
-            $n = count($studentdata[$time->userid]) + 1;
-            if (!$foundmatch) {
-                // Add a record.
-                $studentdata[$time->userid][] = array(
-                                "timestart" => $time->starttime,
-                                "timeend" => $time->lessontime,
-                                "grade" => null,
-                                "end" => $endoflesson,
-                                "try" => $n,
-                                "userid" => $time->userid
-                            );
-            }
-        } else {
-            $studentdata[$time->userid][] = array(
-                                "timestart" => $time->starttime,
-                                "timeend" => $time->lessontime,
-                                "grade" => null,
-                                "end" => $endoflesson,
-                                "try" => 0,
-                                "userid" => $time->userid
-                            );
-        }
-    }
-    // Determine if lesson should have a score.
-    if ($branchcount > 0 AND $questioncount == 0) {
-        // This lesson only contains content pages and is not graded.
-        $lessonscored = false;
-    } else {
-        // This lesson is graded.
-        $lessonscored = true;
-    }
-    // set all the stats variables
-    $numofattempts = 0;
-    $avescore      = 0;
-    $avetime       = 0;
-    $highscore     = null;
-    $lowscore      = null;
-    $hightime      = null;
-    $lowtime       = null;
-
-    $table = new html_table();
-
-    // Set up the table object.
-    if ($lessonscored) {
-        $table->head = array(get_string('name'), get_string('attempts', 'lesson'), get_string('highscore', 'lesson'));
-    } else {
-        $table->head = array(get_string('name'), get_string('attempts', 'lesson'));
-    }
-    $table->align = array('center', 'left', 'left');
-    $table->wrap = array('nowrap', 'nowrap', 'nowrap');
-    $table->attributes['class'] = 'standardtable generaltable';
-    $table->size = array(null, '70%', null);
-
-    // print out the $studentdata array
-    // going through each student that has attempted the lesson, so, each student should have something to be displayed
-    foreach ($students as $student) {
-        // check to see if the student has attempts to print out
-        if (array_key_exists($student->id, $studentdata)) {
-            // set/reset some variables
-            $attempts = array();
-            // gather the data for each user attempt
-            $bestgrade = 0;
-            $bestgradefound = false;
-            // $tries holds all the tries/retries a student has done
-            $tries = $studentdata[$student->id];
-            $studentname = fullname($student, true);
-            foreach ($tries as $try) {
-            // start to build up the checkbox and link
-                if (has_capability('mod/lesson:edit', $context)) {
-                    $temp = '<input type="checkbox" id="attempts" name="attempts['.$try['userid'].']['.$try['try'].']" /> ';
-                } else {
-                    $temp = '';
-                }
-
-                $temp .= "<a href=\"report.php?id=$cm->id&amp;action=reportdetail&amp;userid=".$try['userid']
-                        .'&amp;try='.$try['try'].'" class="lesson-attempt-link">';
-                if ($try["grade"] !== null) { // if null then not done yet
-                    // this is what the link does when the user has completed the try
-                    $timetotake = $try["timeend"] - $try["timestart"];
-
-                    $temp .= $try["grade"]."%";
-                    $bestgradefound = true;
-                    if ($try["grade"] > $bestgrade) {
-                        $bestgrade = $try["grade"];
-                    }
-                    $temp .= "&nbsp;".userdate($try["timestart"]);
-                    $temp .= ",&nbsp;(".format_time($timetotake).")</a>";
-                } else {
-                    if ($try["end"]) {
-                        // User finished the lesson but has no grade. (Happens when there are only content pages).
-                        $temp .= "&nbsp;".userdate($try["timestart"]);
-                        $timetotake = $try["timeend"] - $try["timestart"];
-                        $temp .= ",&nbsp;(".format_time($timetotake).")</a>";
-                    } else {
-                        // This is what the link does/looks like when the user has not completed the attempt.
-                        $temp .= get_string("notcompleted", "lesson");
-                        if ($try['timestart'] !== 0) {
-                            // Teacher previews do not track time spent.
-                            $temp .= "&nbsp;".userdate($try["timestart"]);
-                        }
-                        $temp .= "</a>";
-                        $timetotake = null;
-                    }
-                }
-                // build up the attempts array
-                $attempts[] = $temp;
-
-                // Run these lines for the stats only if the user finnished the lesson.
-                if ($try["end"]) {
-                    // User has completed the lesson.
-                    $numofattempts++;
-                    $avetime += $timetotake;
-                    if ($timetotake > $hightime || $hightime == null) {
-                        $hightime = $timetotake;
-                    }
-                    if ($timetotake < $lowtime || $lowtime == null) {
-                        $lowtime = $timetotake;
-                    }
-                    if ($try["grade"] !== null) {
-                        // The lesson was scored.
-                        $avescore += $try["grade"];
-                        if ($try["grade"] > $highscore || $highscore === null) {
-                            $highscore = $try["grade"];
-                        }
-                        if ($try["grade"] < $lowscore || $lowscore === null) {
-                            $lowscore = $try["grade"];
-                        }
-
-                    }
-                }
-            }
-            // get line breaks in after each attempt
-            $attempts = implode("<br />\n", $attempts);
-
-            if ($lessonscored) {
-                // Add the grade if the lesson is graded.
-                $bestgrade = $bestgrade."%";
-                $table->data[] = array($studentname, $attempts, $bestgrade);
-            } else {
-                // This lesson does not have a grade.
-                $table->data[] = array($studentname, $attempts);
-            }
-        }
-    }
-    $students->close();
     // Print it all out!
     if (has_capability('mod/lesson:edit', $context)) {
         echo  "<form id=\"mod-lesson-report-form\" method=\"post\" action=\"report.php\">\n
                <input type=\"hidden\" name=\"sesskey\" value=\"".sesskey()."\" />\n
                <input type=\"hidden\" name=\"id\" value=\"$cm->id\" />\n";
     }
+
     echo html_writer::table($table);
+
     if (has_capability('mod/lesson:edit', $context)) {
         $checklinks  = '<a id="checkall" href="#">'.get_string('selectall').'</a> / ';
         $checklinks .= '<a id="checknone" href="#">'.get_string('deselectall').'</a>';
@@ -471,38 +175,38 @@ if ($action === 'delete') {
     }
 
     // Calculate the Statistics.
-    if ($avetime == null) {
-        $avetime = get_string("notcompleted", "lesson");
+    if ($data->avetime == null) {
+        $data->avetime = get_string("notcompleted", "lesson");
     } else {
-        $avetime = format_float($avetime/$numofattempts, 0);
-        $avetime = format_time($avetime);
+        $data->avetime = format_float($data->avetime / $data->numofattempts, 0);
+        $data->avetime = format_time($data->avetime);
     }
-    if ($hightime == null) {
-        $hightime = get_string("notcompleted", "lesson");
+    if ($data->hightime == null) {
+        $data->hightime = get_string("notcompleted", "lesson");
     } else {
-        $hightime = format_time($hightime);
+        $data->hightime = format_time($data->hightime);
     }
-    if ($lowtime == null) {
-        $lowtime = get_string("notcompleted", "lesson");
+    if ($data->lowtime == null) {
+        $data->lowtime = get_string("notcompleted", "lesson");
     } else {
-        $lowtime = format_time($lowtime);
+        $data->lowtime = format_time($data->lowtime);
     }
 
-    if ($lessonscored) {
-        if ($numofattempts == 0) {
-            $avescore = get_string("notcompleted", "lesson");
+    if ($data->lessonscored) {
+        if ($data->numofattempts == 0) {
+            $data->avescore = get_string("notcompleted", "lesson");
         } else {
-            $avescore = format_float($avescore / $numofattempts, 2) . '%';
+            $data->avescore = format_float($data->avescore, 2) . '%';
         }
-        if ($highscore === null) {
-            $highscore = get_string("notcompleted", "lesson");
+        if ($data->highscore === null) {
+            $data->highscore = get_string("notcompleted", "lesson");
         } else {
-            $highscore .= '%';
+            $data->highscore .= '%';
         }
-        if ($lowscore === null) {
-            $lowscore = get_string("notcompleted", "lesson");
+        if ($data->lowscore === null) {
+            $data->lowscore = get_string("notcompleted", "lesson");
         } else {
-            $lowscore .= '%';
+            $data->lowscore .= '%';
         }
 
         // Display the full stats for the lesson.
@@ -514,7 +218,7 @@ if ($action === 'delete') {
         $stattable->align = array('center', 'center', 'center', 'center', 'center', 'center');
         $stattable->wrap = array('nowrap', 'nowrap', 'nowrap', 'nowrap', 'nowrap', 'nowrap');
         $stattable->attributes['class'] = 'standardtable generaltable';
-        $stattable->data[] = array($avescore, $avetime, $highscore, $lowscore, $hightime, $lowtime);
+        $stattable->data[] = array($data->avescore, $data->avetime, $data->highscore, $data->lowscore, $data->hightime, $data->lowtime);
 
     } else {
         // Display simple stats for the lesson.
@@ -525,7 +229,7 @@ if ($action === 'delete') {
         $stattable->align = array('center', 'center', 'center');
         $stattable->wrap = array('nowrap', 'nowrap', 'nowrap');
         $stattable->attributes['class'] = 'standardtable generaltable';
-        $stattable->data[] = array($avetime, $hightime, $lowtime);
+        $stattable->data[] = array($data->avetime, $data->hightime, $data->lowtime);
     }
 
     echo html_writer::table($stattable);
@@ -560,103 +264,7 @@ if ($action === 'delete') {
     $userid = optional_param('userid', null, PARAM_INT); // if empty, then will display the general detailed view
     $try    = optional_param('try', null, PARAM_INT);
 
-    if (!empty($userid)) {
-        // Apply overrides.
-        $lesson->update_effective_access($userid);
-    }
-
-    $lessonpages = $lesson->load_all_pages();
-    foreach ($lessonpages as $lessonpage) {
-        if ($lessonpage->prevpageid == 0) {
-            $pageid = $lessonpage->id;
-        }
-    }
-
-    // now gather the stats into an object
-    $firstpageid = $pageid;
-    $pagestats = array();
-    while ($pageid != 0) { // EOL
-        $page = $lessonpages[$pageid];
-        $params = array ("lessonid" => $lesson->id, "pageid" => $page->id);
-        if ($allanswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND pageid = :pageid", $params, "timeseen")) {
-            // get them ready for processing
-            $orderedanswers = array();
-            foreach ($allanswers as $singleanswer) {
-                // ordering them like this, will help to find the single attempt record that we want to keep.
-                $orderedanswers[$singleanswer->userid][$singleanswer->retry][] = $singleanswer;
-            }
-            // this is foreach user and for each try for that user, keep one attempt record
-            foreach ($orderedanswers as $orderedanswer) {
-                foreach($orderedanswer as $tries) {
-                    $page->stats($pagestats, $tries);
-                }
-            }
-        } else {
-            // no one answered yet...
-        }
-        //unset($orderedanswers);  initialized above now
-        $pageid = $page->nextpageid;
-    }
-
-    $manager = lesson_page_type_manager::get($lesson);
-    $qtypes = $manager->get_page_type_strings();
-
-    $answerpages = array();
-    $answerpage = "";
-    $pageid = $firstpageid;
-    // cycle through all the pages
-    //  foreach page, add to the $answerpages[] array all the data that is needed
-    //  from the question, the users attempt, and the statistics
-    // grayout pages that the user did not answer and Branch, end of branch, cluster
-    // and end of cluster pages
-    while ($pageid != 0) { // EOL
-        $page = $lessonpages[$pageid];
-        $answerpage = new stdClass;
-        $data ='';
-
-        $answerdata = new stdClass;
-        // Set some defaults for the answer data.
-        $answerdata->score = null;
-        $answerdata->response = null;
-        $answerdata->responseformat = FORMAT_PLAIN;
-
-        $answerpage->title = format_string($page->title);
-
-        $options = new stdClass;
-        $options->noclean = true;
-        $options->overflowdiv = true;
-        $options->context = $context;
-        $answerpage->contents = format_text($page->contents, $page->contentsformat, $options);
-
-        $answerpage->qtype = $qtypes[$page->qtype].$page->option_description_string();
-        $answerpage->grayout = $page->grayout;
-        $answerpage->context = $context;
-
-        if (empty($userid)) {
-            // there is no userid, so set these vars and display stats.
-            $answerpage->grayout = 0;
-            $useranswer = null;
-        } elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$try,"pageid"=>$page->id), "timeseen")) {
-            // get the user's answer for this page
-            // need to find the right one
-            $i = 0;
-            foreach ($useranswers as $userattempt) {
-                $useranswer = $userattempt;
-                $i++;
-                if ($lesson->maxattempts == $i) {
-                    break; // reached maxattempts, break out
-                }
-            }
-        } else {
-            // user did not answer this page, gray it out and set some nulls
-            $answerpage->grayout = 1;
-            $useranswer = null;
-        }
-        $i = 0;
-        $n = 0;
-        $answerpages[] = $page->report_answers(clone($answerpage), clone($answerdata), $useranswer, $pagestats, $i, $n);
-        $pageid = $page->nextpageid;
-    }
+    list($answerpages, $userstats) = lesson_get_user_detailed_report_data($lesson, $userid, $try);
 
     /// actually start printing something
     $table = new html_table();
@@ -676,24 +284,7 @@ if ($action === 'delete') {
         $table->align = array('right', 'left');
         $table->attributes['class'] = 'compacttable generaltable form-inline';
 
-        $params = array("lessonid"=>$lesson->id, "userid"=>$userid);
-        if (!$grades = $DB->get_records_select("lesson_grades", "lessonid = :lessonid and userid = :userid", $params, "completed", "*", $try, 1)) {
-            $grade = -1;
-            $completed = -1;
-        } else {
-            $grade = current($grades);
-            $completed = $grade->completed;
-            $grade = round($grade->grade, 2);
-        }
-
-        if (!$times = $lesson->get_user_timers($userid, 'starttime', '*', $try, 1)) {
-            $timetotake = -1;
-        } else {
-            $timetotake = current($times);
-            $timetotake = $timetotake->lessontime - $timetotake->starttime;
-        }
-
-        if ($timetotake == -1 || $completed == -1 || $grade == -1) {
+        if (empty($userstats->gradeinfo)) {
             $table->align = array("center");
 
             $table->data[] = array(get_string("notcompleted", "lesson"));
@@ -703,10 +294,10 @@ if ($action === 'delete') {
             $gradeinfo = lesson_grade($lesson, $try, $user->id);
 
             $table->data[] = array(get_string('name').':', $OUTPUT->user_picture($user, array('courseid'=>$course->id)).fullname($user, true));
-            $table->data[] = array(get_string("timetaken", "lesson").":", format_time($timetotake));
-            $table->data[] = array(get_string("completed", "lesson").":", userdate($completed));
-            $table->data[] = array(get_string('rawgrade', 'lesson').':', $gradeinfo->earned.'/'.$gradeinfo->total);
-            $table->data[] = array(get_string("grade", "lesson").":", $grade."%");
+            $table->data[] = array(get_string("timetaken", "lesson").":", format_time($userstats->timetotake));
+            $table->data[] = array(get_string("completed", "lesson").":", userdate($userstats->completed));
+            $table->data[] = array(get_string('rawgrade', 'lesson').':', $userstats->gradeinfo->earned.'/'.$userstats->gradeinfo->total);
+            $table->data[] = array(get_string("grade", "lesson").":", $userstats->grade."%");
         }
         echo html_writer::table($table);
 
index f44bf3a..e8c5052 100644 (file)
@@ -131,29 +131,28 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase {
                                 'maxanswers', 'maxattempts', 'review', 'nextpagedefault', 'feedback', 'minquestions',
                                 'maxpages', 'timelimit', 'retake', 'mediafile', 'mediafiles', 'mediaheight', 'mediawidth',
                                 'mediaclose', 'slideshow', 'width', 'height', 'bgcolor', 'displayleft', 'displayleftif',
-                                'progressbar');
+                                'progressbar', 'allowofflineattempts');
 
         // Add expected coursemodule and data.
         $lesson1 = $this->lesson;
         $lesson1->coursemodule = $lesson1->cmid;
         $lesson1->introformat = 1;
-        $lesson1->section = 0;
-        $lesson1->visible = true;
-        $lesson1->groupmode = 0;
-        $lesson1->groupingid = 0;
         $lesson1->introfiles = [];
         $lesson1->mediafiles = [];
 
         $lesson2->coursemodule = $lesson2->cmid;
         $lesson2->introformat = 1;
-        $lesson2->section = 0;
-        $lesson2->visible = true;
-        $lesson2->groupmode = 0;
-        $lesson2->groupingid = 0;
         $lesson2->introfiles = [];
         $lesson2->mediafiles = [];
 
+        $booltypes = array('practice', 'modattempts', 'usepassword', 'custom', 'ongoing', 'review', 'feedback', 'retake',
+            'slideshow', 'displayleft', 'progressbar', 'allowofflineattempts');
+
         foreach ($expectedfields as $field) {
+            if (in_array($field, $booltypes)) {
+                $lesson1->{$field} = (bool) $lesson1->{$field};
+                $lesson2->{$field} = (bool) $lesson2->{$field};
+            }
             $expected1[$field] = $lesson1->{$field};
             $expected2[$field] = $lesson2->{$field};
         }
@@ -576,26 +575,26 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase {
         $result = mod_lesson_external::get_user_attempt_grade($this->lesson->id, $attemptnumber, $this->student->id);
         $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_grade_returns(), $result);
         $this->assertCount(0, $result['warnings']);
-        $this->assertEquals(1, $result['nquestions']);
-        $this->assertEquals(1, $result['attempts']);
-        $this->assertEquals(1, $result['total']);
-        $this->assertEquals(1, $result['earned']);
-        $this->assertEquals(100, $result['grade']);
-        $this->assertEquals(0, $result['nmanual']);
-        $this->assertEquals(0, $result['manualpoints']);
+        $this->assertEquals(1, $result['grade']['nquestions']);
+        $this->assertEquals(1, $result['grade']['attempts']);
+        $this->assertEquals(1, $result['grade']['total']);
+        $this->assertEquals(1, $result['grade']['earned']);
+        $this->assertEquals(100, $result['grade']['grade']);
+        $this->assertEquals(0, $result['grade']['nmanual']);
+        $this->assertEquals(0, $result['grade']['manualpoints']);
 
         // With custom scoring, in this case, we don't retrieve any values since we are using questions without particular score.
         $DB->set_field('lesson', 'custom', 1, array('id' => $this->lesson->id));
         $result = mod_lesson_external::get_user_attempt_grade($this->lesson->id, $attemptnumber, $this->student->id);
         $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_grade_returns(), $result);
         $this->assertCount(0, $result['warnings']);
-        $this->assertEquals(1, $result['nquestions']);
-        $this->assertEquals(1, $result['attempts']);
-        $this->assertEquals(0, $result['total']);
-        $this->assertEquals(0, $result['earned']);
-        $this->assertEquals(0, $result['grade']);
-        $this->assertEquals(0, $result['nmanual']);
-        $this->assertEquals(0, $result['manualpoints']);
+        $this->assertEquals(1, $result['grade']['nquestions']);
+        $this->assertEquals(1, $result['grade']['attempts']);
+        $this->assertEquals(0, $result['grade']['total']);
+        $this->assertEquals(0, $result['grade']['earned']);
+        $this->assertEquals(0, $result['grade']['grade']);
+        $this->assertEquals(0, $result['grade']['nmanual']);
+        $this->assertEquals(0, $result['grade']['manualpoints']);
     }
 
     /**
@@ -661,6 +660,7 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase {
         $timer1->completed = 1;
         $timer1->starttime = time() - WEEKSECS;
         $timer1->lessontime = time();
+        $timer1->timemodifiedoffline = time();
         $timer1->id = $DB->insert_record("lesson_timer", $timer1);
 
         $timer2 = new stdClass;
@@ -669,6 +669,7 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase {
         $timer2->completed = 0;
         $timer2->starttime = time() - DAYSECS;
         $timer2->lessontime = time() + 1;
+        $timer2->timemodifiedoffline = time() + 1;
         $timer2->id = $DB->insert_record("lesson_timer", $timer2);
 
         // Test retrieve timers.
@@ -974,4 +975,378 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase {
         $this->setExpectedException('moodle_exception');
         $result = mod_lesson_external::get_page_data($this->lesson->id, $this->page2->id, '', false, true);
     }
+
+    /**
+     * Creates an attempt for the given userwith a correct or incorrect answer and optionally finishes it.
+     *
+     * @param  stdClass $user    Create an attempt for this user
+     * @param  boolean $correct  If the answer should be correct
+     * @param  boolean $finished If we should finish the attempt
+     * @return array the result of the attempt creation or finalisation
+     */
+    protected function create_attempt($user, $correct = true, $finished = false) {
+        global $DB;
+
+        $this->setUser($user);
+
+        // First we need to launch the lesson so the timer is on.
+        mod_lesson_external::launch_attempt($this->lesson->id);
+
+        $DB->set_field('lesson', 'feedback', 1, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'progressbar', 1, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'custom', 0, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'maxattempts', 3, array('id' => $this->lesson->id));
+
+        $answercorrect = 0;
+        $answerincorrect = 0;
+        $p2answers = $DB->get_records('lesson_answers', array('lessonid' => $this->lesson->id, 'pageid' => $this->page2->id), 'id');
+        foreach ($p2answers as $answer) {
+            if ($answer->jumpto == 0) {
+                $answerincorrect = $answer->id;
+            } else {
+                $answercorrect = $answer->id;
+            }
+        }
+
+        $data = array(
+            array(
+                'name' => 'answerid',
+                'value' => $correct ? $answercorrect : $answerincorrect,
+            ),
+            array(
+                'name' => '_qf__lesson_display_answer_form_truefalse',
+                'value' => 1,
+            )
+        );
+        $result = mod_lesson_external::process_page($this->lesson->id, $this->page2->id, $data);
+        $result = external_api::clean_returnvalue(mod_lesson_external::process_page_returns(), $result);
+
+        if ($finished) {
+            $result = mod_lesson_external::finish_attempt($this->lesson->id);
+            $result = external_api::clean_returnvalue(mod_lesson_external::finish_attempt_returns(), $result);
+        }
+        return $result;
+    }
+
+    /**
+     * Test process_page
+     */
+    public function test_process_page() {
+        global $DB;
+
+        // Attempt first with incorrect response.
+        $result = $this->create_attempt($this->student, false, false);
+
+        $this->assertEquals($this->page2->id, $result['newpageid']);    // Same page, since the answer was incorrect.
+        $this->assertFalse($result['correctanswer']);   // Incorrect answer.
+        $this->assertEquals(50, $result['progress']);
+
+        // Attempt with correct response.
+        $result = $this->create_attempt($this->student, true, false);
+
+        $this->assertEquals($this->page1->id, $result['newpageid']);    // Next page, the answer was correct.
+        $this->assertTrue($result['correctanswer']);    // Correct response.
+        $this->assertFalse($result['maxattemptsreached']);  // Still one attempt.
+        $this->assertEquals(50, $result['progress']);
+    }
+
+    /**
+     * Test finish attempt not doing anything.
+     */
+    public function test_finish_attempt_not_doing_anything() {
+
+        $this->setUser($this->student);
+        // First we need to launch the lesson so the timer is on.
+        mod_lesson_external::launch_attempt($this->lesson->id);
+
+        $result = mod_lesson_external::finish_attempt($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::finish_attempt_returns(), $result);
+
+        $this->assertCount(0, $result['warnings']);
+        $returneddata = [];
+        foreach ($result['data'] as $data) {
+            $returneddata[$data['name']] = $data['value'];
+        }
+        $this->assertEquals(1, $returneddata['gradelesson']);   // Graded lesson.
+        $this->assertEquals(1, $returneddata['welldone']);      // Finished correctly (even without grades).
+        $gradeinfo = json_decode($returneddata['gradeinfo']);
+        $expectedgradeinfo = (object) [
+            'nquestions' => 0,
+            'attempts' => 0,
+            'total' => 0,
+            'earned' => 0,
+            'grade' => 0,
+            'nmanual' => 0,
+            'manualpoints' => 0,
+        ];
+    }
+
+    /**
+     * Test finish attempt with correct answer.
+     */
+    public function test_finish_attempt_with_correct_answer() {
+        // Create a finished attempt.
+        $result = $this->create_attempt($this->student, true, true);
+
+        $this->assertCount(0, $result['warnings']);
+        $returneddata = [];
+        foreach ($result['data'] as $data) {
+            $returneddata[$data['name']] = $data['value'];
+        }
+        $this->assertEquals(1, $returneddata['gradelesson']);   // Graded lesson.
+        $this->assertEquals(1, $returneddata['numberofpagesviewed']);
+        $this->assertEquals(1, $returneddata['numberofcorrectanswers']);
+        $gradeinfo = json_decode($returneddata['gradeinfo']);
+        $expectedgradeinfo = (object) [
+            'nquestions' => 1,
+            'attempts' => 1,
+            'total' => 1,
+            'earned' => 1,
+            'grade' => 100,
+            'nmanual' => 0,
+            'manualpoints' => 0,
+        ];
+    }
+
+    /**
+     * Test get_attempts_overview
+     */
+    public function test_get_attempts_overview() {
+        global $DB;
+
+        // Create a finished attempt with incorrect answer.
+        $this->setCurrentTimeStart();
+        $this->create_attempt($this->student, false, true);
+
+        $this->setAdminUser();
+        $result = mod_lesson_external::get_attempts_overview($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_attempts_overview_returns(), $result);
+
+        // One attempt, 0 for grade (incorrect response) in overal statistics.
+        $this->assertEquals(1, $result['data']['numofattempts']);
+        $this->assertEquals(0, $result['data']['avescore']);
+        $this->assertEquals(0, $result['data']['highscore']);
+        $this->assertEquals(0, $result['data']['lowscore']);
+        // Check one student, finished attempt, 0 for grade.
+        $this->assertCount(1, $result['data']['students']);
+        $this->assertEquals($this->student->id, $result['data']['students'][0]['id']);
+        $this->assertEquals(0, $result['data']['students'][0]['bestgrade']);
+        $this->assertCount(1, $result['data']['students'][0]['attempts']);
+        $this->assertEquals(1, $result['data']['students'][0]['attempts'][0]['end']);
+        $this->assertEquals(0, $result['data']['students'][0]['attempts'][0]['grade']);
+        $this->assertTimeCurrent($result['data']['students'][0]['attempts'][0]['timestart']);
+        $this->assertTimeCurrent($result['data']['students'][0]['attempts'][0]['timeend']);
+
+        // Add a new attempt (same user).
+        sleep(1);
+        // Allow first retake.
+        $DB->set_field('lesson', 'retake', 1, array('id' => $this->lesson->id));
+        // Create a finished attempt with correct answer.
+        $this->setCurrentTimeStart();
+        $this->create_attempt($this->student, true, true);
+
+        $this->setAdminUser();
+        $result = mod_lesson_external::get_attempts_overview($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_attempts_overview_returns(), $result);
+
+        // Two attempts with maximum grade.
+        $this->assertEquals(2, $result['data']['numofattempts']);
+        $this->assertEquals(50.00, format_float($result['data']['avescore'], 2));
+        $this->assertEquals(100, $result['data']['highscore']);
+        $this->assertEquals(0, $result['data']['lowscore']);
+        // Check one student, finished two attempts, 100 for final grade.
+        $this->assertCount(1, $result['data']['students']);
+        $this->assertEquals($this->student->id, $result['data']['students'][0]['id']);
+        $this->assertEquals(100, $result['data']['students'][0]['bestgrade']);
+        $this->assertCount(2, $result['data']['students'][0]['attempts']);
+        foreach ($result['data']['students'][0]['attempts'] as $attempt) {
+            if ($attempt['try'] == 0) {
+                // First attempt, 0 for grade.
+                $this->assertEquals(0, $attempt['grade']);
+            } else {
+                $this->assertEquals(100, $attempt['grade']);
+            }
+        }
+
+        // Now, add other user failed attempt.
+        $student2 = self::getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($student2->id, $this->course->id, $this->studentrole->id, 'manual');
+        $this->create_attempt($student2, false, true);
+
+        // Now check we have two students and the statistics changed.
+        $this->setAdminUser();
+        $result = mod_lesson_external::get_attempts_overview($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_attempts_overview_returns(), $result);
+
+        // Total of 3 attempts with maximum grade.
+        $this->assertEquals(3, $result['data']['numofattempts']);
+        $this->assertEquals(33.33, format_float($result['data']['avescore'], 2));
+        $this->assertEquals(100, $result['data']['highscore']);
+        $this->assertEquals(0, $result['data']['lowscore']);
+        // Check students.
+        $this->assertCount(2, $result['data']['students']);
+    }
+
+    /**
+     * Test get_user_attempt
+     */
+    public function test_get_user_attempt() {
+        global $DB;
+
+        // Create a finished and unfinished attempt with incorrect answer.
+        $this->setCurrentTimeStart();
+        $this->create_attempt($this->student, true, true);
+
+        $DB->set_field('lesson', 'retake', 1, array('id' => $this->lesson->id));
+        sleep(1);
+        $this->create_attempt($this->student, false, false);
+
+        $this->setAdminUser();
+        // Test first attempt finished.
+        $result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 0);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
+
+        $this->assertCount(2, $result['answerpages']);  // 2 pages in the lesson.
+        $this->assertCount(2, $result['answerpages'][0]['answerdata']['answers']);  // 2 possible answers in true/false.
+        $this->assertEquals(100, $result['userstats']['grade']);    // Correct answer.
+        $this->assertEquals(1, $result['userstats']['gradeinfo']['total']);     // Total correct answers.
+        $this->assertEquals(100, $result['userstats']['gradeinfo']['grade']);   // Correct answer.
+
+        // Test second attempt unfinished.
+        $result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 1);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
+
+        $this->assertCount(2, $result['answerpages']);  // 2 pages in the lesson.
+        $this->assertCount(2, $result['answerpages'][0]['answerdata']['answers']);  // 2 possible answers in true/false.
+        $this->assertArrayNotHasKey('gradeinfo', $result['userstats']);    // No grade info since it not finished.
+
+        // Check as student I can get this information for only me.
+        $this->setUser($this->student);
+        // Test first attempt finished.
+        $result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 0);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
+
+        $this->assertCount(2, $result['answerpages']);  // 2 pages in the lesson.
+        $this->assertCount(2, $result['answerpages'][0]['answerdata']['answers']);  // 2 possible answers in true/false.
+        $this->assertEquals(100, $result['userstats']['grade']);    // Correct answer.
+        $this->assertEquals(1, $result['userstats']['gradeinfo']['total']);     // Total correct answers.
+        $this->assertEquals(100, $result['userstats']['gradeinfo']['grade']);   // Correct answer.
+
+        $this->setExpectedException('moodle_exception');
+        $result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->teacher->id, 0);
+    }
+
+    /**
+     * Test get_pages_possible_jumps
+     */
+    public function test_get_pages_possible_jumps() {
+        $this->setAdminUser();
+        $result = mod_lesson_external::get_pages_possible_jumps($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_pages_possible_jumps_returns(), $result);
+
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(3, $result['jumps']);    // 3 jumps, 2 from the question page and 1 from the content.
+        foreach ($result['jumps'] as $jump) {
+            if ($jump['answerid'] != 0) {
+                // Check only pages with answers.
+                if ($jump['jumpto'] == 0) {
+                    $this->assertEquals($jump['pageid'], $jump['calculatedjump']);    // 0 means to jump to current page.
+                } else {
+                    // Question is configured to jump to next page if correct.
+                    $this->assertEquals($this->page1->id, $jump['calculatedjump']);
+                }
+            }
+        }
+    }
+
+    /**
+     * Test get_pages_possible_jumps when offline attemps are disabled for a normal user
+     */
+    public function test_get_pages_possible_jumps_with_offlineattemps_disabled() {
+        $this->setUser($this->student->id);
+        $result = mod_lesson_external::get_pages_possible_jumps($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_pages_possible_jumps_returns(), $result);
+        $this->assertCount(0, $result['jumps']);
+    }
+
+    /**
+     * Test get_pages_possible_jumps when offline attemps are enabled for a normal user
+     */
+    public function test_get_pages_possible_jumps_with_offlineattemps_enabled() {
+        global $DB;
+
+        $DB->set_field('lesson', 'allowofflineattempts', 1, array('id' => $this->lesson->id));
+        $this->setUser($this->student->id);
+        $result = mod_lesson_external::get_pages_possible_jumps($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_pages_possible_jumps_returns(), $result);
+        $this->assertCount(3, $result['jumps']);
+    }
+
+    /*
+     * Test get_lesson user student.
+     */
+    public function test_get_lesson_user_student() {
+        // Test user with full capabilities.
+        $this->setUser($this->student);
+
+        // Lesson not using password.
+        $result = mod_lesson_external::get_lesson($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result);
+        $this->assertCount(36, $result['lesson']);  // Expect most of the fields.
+        $this->assertFalse(isset($result['password']));
+    }
+
+    /**
+     * Test get_lesson user student with missing password.
+     */
+    public function test_get_lesson_user_student_with_missing_password() {
+        global $DB;
+
+        // Test user with full capabilities.
+        $this->setUser($this->student);
+        $DB->set_field('lesson', 'usepassword', 1, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'password', 'abc', array('id' => $this->lesson->id));
+
+        // Lesson not using password.
+        $result = mod_lesson_external::get_lesson($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result);
+        $this->assertCount(5, $result['lesson']);   // Expect just this few fields.
+        $this->assertFalse(isset($result['intro']));
+    }
+
+    /**
+     * Test get_lesson user student with correct password.
+     */
+    public function test_get_lesson_user_student_with_correct_password() {
+        global $DB;
+        // Test user with full capabilities.
+        $this->setUser($this->student);
+        $password = 'abc';
+        $DB->set_field('lesson', 'usepassword', 1, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'password', $password, array('id' => $this->lesson->id));
+
+        // Lesson not using password.
+        $result = mod_lesson_external::get_lesson($this->lesson->id, $password);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result);
+        $this->assertCount(36, $result['lesson']);
+        $this->assertFalse(isset($result['intro']));
+    }
+
+    /**
+     * Test get_lesson teacher.
+     */
+    public function test_get_lesson_teacher() {
+        global $DB;
+        // Test user with full capabilities.
+        $this->setUser($this->teacher);
+        $password = 'abc';
+        $DB->set_field('lesson', 'usepassword', 1, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'password', $password, array('id' => $this->lesson->id));
+
+        // Lesson not passing a valid password (but we are teachers, we should see all the info).
+        $result = mod_lesson_external::get_lesson($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result);
+        $this->assertCount(45, $result['lesson']);  // Expect all the fields.
+        $this->assertEquals($result['lesson']['password'], $password);
+    }
 }
index a8ae0f6..4969c5e 100644 (file)
@@ -82,4 +82,86 @@ class mod_lesson_lib_testcase extends advanced_testcase {
         $this->assertEquals(2, $closepriorities[$override1->deadline]);
         $this->assertEquals(1, $closepriorities[$override2->deadline]);
     }
+
+    /**
+     * Test check_updates_since callback.
+     */
+    public function test_check_updates_since() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create user.
+        $student = self::getDataGenerator()->create_user();
+
+        // User enrolment.
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
+
+        $this->setCurrentTimeStart();
+        $record = array(
+            'course' => $course->id,
+            'custom' => 0,
+            'feedback' => 1,
+        );
+        $lessonmodule = $this->getDataGenerator()->create_module('lesson', $record);
+        // Convert to a lesson object.
+        $lesson = new lesson($lessonmodule);
+        $cm = $lesson->cm;
+        $cm = cm_info::create($cm);
+
+        // Check that upon creation, the updates are only about the new configuration created.
+        $onehourago = time() - HOURSECS;
+        $updates = lesson_check_updates_since($cm, $onehourago);
+        foreach ($updates as $el => $val) {
+            if ($el == 'configuration') {
+                $this->assertTrue($val->updated);
+                $this->assertTimeCurrent($val->timeupdated);
+            } else {
+                $this->assertFalse($val->updated);
+            }
+        }
+
+        // Set up a generator to create content.
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_lesson');
+        $tfrecord = $generator->create_question_truefalse($lesson);
+
+        // Check now for pages and answers.
+        $updates = lesson_check_updates_since($cm, $onehourago);
+        $this->assertTrue($updates->pages->updated);
+        $this->assertCount(1, $updates->pages->itemids);
+
+        $this->assertTrue($updates->answers->updated);
+        $this->assertCount(2, $updates->answers->itemids);
+
+        // Now, do something in the lesson.
+        $this->setUser($student);
+        mod_lesson_external::launch_attempt($lesson->id);
+        $data = array(
+            array(
+                'name' => 'answerid',
+                'value' => $DB->get_field('lesson_answers', 'id', array('pageid' => $tfrecord->id, 'jumpto' => -1)),
+            ),
+            array(
+                'name' => '_qf__lesson_display_answer_form_truefalse',
+                'value' => 1,
+            )
+        );
+        mod_lesson_external::process_page($lesson->id, $tfrecord->id, $data);
+        mod_lesson_external::finish_attempt($lesson->id);
+
+        $updates = lesson_check_updates_since($cm, $onehourago);
+
+        // Check question attempts, timers and new grades.
+        $this->assertTrue($updates->questionattempts->updated);
+        $this->assertCount(1, $updates->questionattempts->itemids);
+
+        $this->assertTrue($updates->grades->updated);
+        $this->assertCount(1, $updates->grades->itemids);
+
+        $this->assertTrue($updates->timers->updated);
+        $this->assertCount(1, $updates->timers->itemids);
+    }
 }
index 52f4ce9..6c6bba2 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016120510;     // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016120517;     // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2016112900;    // Requires this Moodle version
 $plugin->component = 'mod_lesson'; // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 785b920..0ca0495 100644 (file)
@@ -121,6 +121,16 @@ if (empty($pageid)) {
 
     $lastpageseen = $lesson->get_last_page_seen($retries);
 
+    // Check if the lesson was attempted in an external device like the mobile app.
+    // This check makes sense only when the lesson allows offline attempts.
+    if ($lesson->allowofflineattempts && $timers = $lesson->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1)) {
+        $timer = current($timers);
+        if (!empty($timer->timemodifiedoffline)) {
+            $lasttime = format_time(time() - $timer->timemodifiedoffline);
+            $lesson->add_message(get_string('offlinedatamessage', 'lesson', $lasttime), 'warning');
+        }
+    }
+
     // Check to see if end of lesson was reached.
     if (($lastpageseen !== false && ($lastpageseen != LESSON_EOL))) {
         // End not reached. Check if the user left.
@@ -233,183 +243,12 @@ if ($pageid != LESSON_EOL) {
 
 } else {
 
-    $lessoncontent = '';
     // End of lesson reached work out grade.
     // Used to check to see if the student ran out of time.
     $outoftime = optional_param('outoftime', '', PARAM_ALPHA);
 
-    $ntries = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$USER->id));
-    if (isset($USER->modattempts[$lesson->id])) {
-        $ntries--;  // need to look at the old attempts :)
-    }
-    $gradelesson = true;
-    $gradeinfo = lesson_grade($lesson, $ntries);
-    if ($lesson->custom && !$canmanage) {
-        // Before we calculate the custom score make sure they answered the minimum
-        // number of questions. We only need to do this for custom scoring as we can
-        // not get the miniumum score the user should achieve. If we are not using
-        // custom scoring (so all questions are valued as 1) then we simply check if
-        // they answered more than the minimum questions, if not, we mark it out of the
-        // number specified in the minimum questions setting - which is done in lesson_grade().
-        // Get the number of answers given.
-        if ($gradeinfo->nquestions < $lesson->minquestions) {
-            $gradelesson = false;
-            $a = new stdClass;
-            $a->nquestions = $gradeinfo->nquestions;
-            $a->minquestions = $lesson->minquestions;
-            $lessoncontent .= $OUTPUT->box_start('generalbox boxaligncenter');
-            $lesson->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
-        }
-    }
-    if ($gradelesson) {
-        // We are using level 3 header because the page title is a sub-heading of lesson title (MDL-30911).
-        $lessoncontent .= $OUTPUT->heading(get_string("congratulations", "lesson"), 3);
-        $lessoncontent .= $OUTPUT->box_start('generalbox boxaligncenter');
-    }
-    if (!$canmanage) {
-        if ($gradelesson) {
-            // Store this now before any modifications to pages viewed.
-            $progressbar = $lessonoutput->progress_bar($lesson);
-            // Update the clock / get time information for this user.
-            $lesson->stop_timer();
-
-            // Update completion state.
-            $completion = new completion_info($course);
-            if ($completion->is_enabled($cm) && $lesson->completionendreached) {
-                $completion->update_state($cm, COMPLETION_COMPLETE);
-            }
-
-            if ($lesson->completiontimespent > 0) {
-                $duration = $DB->get_field_sql(
-                    "SELECT SUM(lessontime - starttime)
-                                   FROM {lesson_timer}
-                                  WHERE lessonid = :lessonid
-                                    AND userid = :userid",
-                    array('userid' => $USER->id, 'lessonid' => $lesson->id));
-                if (!$duration) {
-                    $duration = 0;
-                }
-
-                // If student has not spend enough time in the lesson, display a message.
-                if ($duration < $lesson->completiontimespent) {
-                    $a = new stdClass;
-                    $a->timespent = format_time($duration);
-                    $a->timerequired = format_time($lesson->completiontimespent);
-                    $lessoncontent .= $lessonoutput->paragraph(get_string("notenoughtimespent", "lesson", $a), 'center');
-                }
-            }
-
-
-            if ($gradeinfo->attempts) {
-                if (!$lesson->custom) {
-                    $lessoncontent .= $lessonoutput->paragraph(get_string("numberofpagesviewed", "lesson", $gradeinfo->nquestions), 'center');
-                    if ($lesson->minquestions) {
-                        if ($gradeinfo->nquestions < $lesson->minquestions) {
-                            // print a warning and set nviewed to minquestions
-                            $lessoncontent .= $lessonoutput->paragraph(get_string("youshouldview", "lesson", $lesson->minquestions), 'center');
-                        }
-                    }
-                    $lessoncontent .= $lessonoutput->paragraph(get_string("numberofcorrectanswers", "lesson", $gradeinfo->earned), 'center');
-                }
-                $a = new stdClass;
-                $a->score = $gradeinfo->earned;
-                $a->grade = $gradeinfo->total;
-                if ($gradeinfo->nmanual) {
-                    $a->tempmaxgrade = $gradeinfo->total - $gradeinfo->manualpoints;
-                    $a->essayquestions = $gradeinfo->nmanual;
-                    $lessoncontent .= $OUTPUT->box(get_string("displayscorewithessays", "lesson", $a), 'center');
-                } else {
-                    $lessoncontent .= $OUTPUT->box(get_string("displayscorewithoutessays", "lesson", $a), 'center');
-                }
-                if ($lesson->grade != GRADE_TYPE_NONE) {
-                    $a = new stdClass;
-                    $a->grade = number_format($gradeinfo->grade * $lesson->grade / 100, 1);
-                    $a->total = $lesson->grade;
-                    $lessoncontent .= $lessonoutput->paragraph(get_string("yourcurrentgradeisoutof", "lesson", $a), 'center');
-                }
-
-                $grade = new stdClass();
-                $grade->lessonid = $lesson->id;
-                $grade->userid = $USER->id;
-                $grade->grade = $gradeinfo->grade;
-                $grade->completed = time();
-                if (isset($USER->modattempts[$lesson->id])) { // If reviewing, make sure update old grade record.
-                    if (!$grades = $DB->get_records("lesson_grades",
-                        array("lessonid" => $lesson->id, "userid" => $USER->id), "completed DESC", '*', 0, 1)) {
-                        print_error('cannotfindgrade', 'lesson');
-                    }
-                    $oldgrade = array_shift($grades);
-                    $grade->id = $oldgrade->id;
-                    $DB->update_record("lesson_grades", $grade);
-                } else {
-                    $newgradeid = $DB->insert_record("lesson_grades", $grade);
-                }
-            } else {
-                if ($lesson->timelimit) {
-                    if ($outoftime == 'normal') {
-                        $grade = new stdClass();
-                        $grade->lessonid = $lesson->id;
-                        $grade->userid = $USER->id;
-                        $grade->grade = 0;
-                        $grade->completed = time();
-                        $newgradeid = $DB->insert_record("lesson_grades", $grade);
-                        $lessoncontent .= $lessonoutput->paragraph(get_string("eolstudentoutoftimenoanswers", "lesson"));
-                    }
-                } else {
-                    $lessoncontent .= $lessonoutput->paragraph(get_string("welldone", "lesson"));
-                }
-            }
-
-            // update central gradebook
-            lesson_update_grades($lesson, $USER->id);
-            $lessoncontent .= $progressbar;
-        }
-    } else {
-        // display for teacher
-        if ($lesson->grade != GRADE_TYPE_NONE) {
-            $lessoncontent .= $lessonoutput->paragraph(get_string("displayofgrade", "lesson"), 'center');
-        }
-    }
-    $lessoncontent .= $OUTPUT->box_end(); //End of Lesson button to Continue.
-
-    if ($lesson->modattempts && !$canmanage) {
-        // make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time
-        // look at the attempt records to find the first QUESTION page that the user answered, then use that page id
-        // to pass to view again.  This is slick cause it wont call the empty($pageid) code
-        // $ntries is decremented above
-        if (!$attempts = $lesson->get_attempts($ntries)) {
-            $attempts = array();
-            $url = new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id));
-        } else {
-            $firstattempt = current($attempts);
-            $pageid = $firstattempt->pageid;
-            // IF the student wishes to review, need to know the last question page that the student answered.  This will help to make
-            // sure that the student can leave the lesson via pushing the continue button.
-            $lastattempt = end($attempts);
-            $USER->modattempts[$lesson->id] = $lastattempt->pageid;
-
-            $url = new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id, 'pageid'=>$pageid));
-        }
-        $lessoncontent .= html_writer::link($url, get_string('reviewlesson', 'lesson'),
-                array('class' => 'centerpadded lessonbutton standardbutton p-r-1'));
-    } elseif ($lesson->modattempts && $canmanage) {
-        $lessoncontent .= $lessonoutput->paragraph(get_string("modattemptsnoteacher", "lesson"), 'centerpadded');
-    }
-
-    if ($lesson->activitylink) {
-        $lessoncontent .= $lesson->link_for_activitylink();
-    }
-
-    $url = new moodle_url('/course/view.php', array('id'=>$course->id));
-    $lessoncontent .= html_writer::link($url, get_string('returnto', 'lesson', format_string($course->fullname, true)),
-            array('class' => 'centerpadded lessonbutton standardbutton p-r-1'));
-
-    if (has_capability('gradereport/user:view', context_course::instance($course->id))
-            && $course->showgrades && $lesson->grade != 0 && !$lesson->practice) {
-        $url = new moodle_url('/grade/index.php', array('id' => $course->id));
-        $lessoncontent .= html_writer::link($url, get_string('viewgrades', 'lesson'),
-            array('class' => 'centerpadded lessonbutton standardbutton p-r-1'));
-    }
+    $data = $lesson->process_eol_page($outoftime);
+    $lessoncontent = $lessonoutput->display_eol_page($lesson, $data);
 
     lesson_add_fake_blocks($PAGE, $cm, $lesson, $timer);
     echo $lessonoutput->header($lesson, $cm, $currenttab, $extraeditbuttons, $lessonpageid, get_string("congratulations", "lesson"));
index add6361..82a3fa8 100644 (file)
@@ -1935,7 +1935,7 @@ class qubaids_for_quiz extends qubaid_join {
         }
 
         if ($onlyfinished) {
-            $where .= ' AND state == :statefinished';
+            $where .= ' AND state = :statefinished';
             $params['statefinished'] = quiz_attempt::FINISHED;
         }
 
index 5b665ea..5d57c35 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M768 384v-128h-128v128h128zm128 128v-128h-128v128h128zm-128 128v-128h-128v128h128zm128 128v-128h-128v128h128zm700-388q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-128v128h-128v-128h-512v1536h1280zm-627-721l107 349q8 27 8 52 0 83-72.5 137.5t-183.5 54.5-183.5-54.5-72.5-137.5q0-25 8-52 21-63 120-396v-128h128v128h79q22 0 39 13t23 34zm-141 465q53 0 90.5-19t37.5-45-37.5-45-90.5-19-90.5 19-37.5 45 37.5 45 90.5 19z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M768 384v-128h-128v128h128zm128 128v-128h-128v128h128zm-128 128v-128h-128v128h128zm128 128v-128h-128v128h128zm700-388q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-128v128h-128v-128h-512v1536h1280zm-627-721l107 349q8 27 8 52 0 83-72.5 137.5t-183.5 54.5-183.5-54.5-72.5-137.5q0-25 8-52 21-63 120-396v-128h128v128h79q22 0 39 13t23 34zm-141 465q53 0 90.5-19t37.5-45-37.5-45-90.5-19-90.5 19-37.5 45 37.5 45 90.5 19z"/></svg>
\ No newline at end of file
index 4d8cddb..354aefc 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-788-814q20 8 20 30v544q0 22-20 30-8 2-12 2-12 0-23-9l-166-167h-131q-14 0-23-9t-9-23v-192q0-14 9-23t23-9h131l166-167q16-15 35-7zm417 689q31 0 50-24 129-159 129-363t-129-363q-16-21-43-24t-47 14q-21 17-23.5 43.5t14.5 47.5q100 123 100 282t-100 282q-17 21-14.5 47.5t23.5 42.5q18 15 40 15zm-211-148q27 0 47-20 87-93 87-219t-87-219q-18-19-45-20t-46 17-20 44.5 18 46.5q52 57 52 131t-52 131q-19 20-18 46.5t20 44.5q20 17 44 17z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-788-814q20 8 20 30v544q0 22-20 30-8 2-12 2-12 0-23-9l-166-167h-131q-14 0-23-9t-9-23v-192q0-14 9-23t23-9h131l166-167q16-15 35-7zm417 689q31 0 50-24 129-159 129-363t-129-363q-16-21-43-24t-47 14q-21 17-23.5 43.5t14.5 47.5q100 123 100 282t-100 282q-17 21-14.5 47.5t23.5 42.5q18 15 40 15zm-211-148q27 0 47-20 87-93 87-219t-87-219q-18-19-45-20t-46 17-20 44.5 18 46.5q52 57 52 131t-52 131q-19 20-18 46.5t20 44.5q20 17 44 17z"/></svg>
\ No newline at end of file
index bb9669b..1ebd253 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-640-896q52 0 90 38t38 90v384q0 52-38 90t-90 38h-384q-52 0-90-38t-38-90v-384q0-52 38-90t90-38h384zm492 2q20 8 20 30v576q0 22-20 30-8 2-12 2-14 0-23-9l-265-266v-90l265-266q9-9 23-9 4 0 12 2z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-640-896q52 0 90 38t38 90v384q0 52-38 90t-90 38h-384q-52 0-90-38t-38-90v-384q0-52 38-90t90-38h384zm492 2q20 8 20 30v576q0 22-20 30-8 2-12 2-14 0-23-9l-265-266v-90l265-266q9-9 23-9 4 0 12 2z"/></svg>
\ No newline at end of file
index d660e7f..d88c736 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280z"/></svg>
\ No newline at end of file
index ae2b0ca..976907a 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-128-448v320h-1024v-192l192-192 128 128 384-384zm-832-192q-80 0-136-56t-56-136 56-136 136-56 136 56 56 136-56 136-136 56z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-128-448v320h-1024v-192l192-192 128 128 384-384zm-832-192q-80 0-136-56t-56-136 56-136 136-56 136 56 56 136-56 136-136 56z"/></svg>
\ No newline at end of file
index 6c16917..9c9bb21 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-979-234v106h281v-106h-75l103-161q5-7 10-16.5t7.5-13.5 3.5-4h2q1 4 5 10 2 4 4.5 7.5t6 8 6.5 8.5l107 161h-76v106h291v-106h-68l-192-273 195-282h67v-107h-279v107h74l-103 159q-4 7-10 16.5t-9 13.5l-2 3h-2q-1-4-5-10-6-11-17-23l-106-159h76v-107h-290v107h68l189 272-194 283h-68z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-979-234v106h281v-106h-75l103-161q5-7 10-16.5t7.5-13.5 3.5-4h2q1 4 5 10 2 4 4.5 7.5t6 8 6.5 8.5l107 161h-76v106h291v-106h-68l-192-273 195-282h67v-107h-279v107h74l-103 159q-4 7-10 16.5t-9 13.5l-2 3h-2q-1-4-5-10-6-11-17-23l-106-159h76v-107h-290v107h68l189 272-194 283h-68z"/></svg>
\ No newline at end of file
index 589b57d..f85b6c2 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M512 896v512h-256v-512h256zm384-512v1024h-256v-1024h256zm1024 1152v128h-2048v-1536h128v1408h1920zm-640-896v768h-256v-768h256zm384-384v1152h-256v-1152h256z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M512 896v512h-256v-512h256zm384-512v1024h-256v-1024h256zm1024 1152v128h-2048v-1536h128v1408h1920zm-640-896v768h-256v-768h256zm384-384v1152h-256v-1152h256z"/></svg>
\ No newline at end of file
index e42a416..cac9e0c 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M896 768q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0 768q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-384q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-1152q208 0 385 34.5t280 93.5 103 128v128q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-128q0-69 103-128t280-93.5 385-34.5z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M896 768q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0 768q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-384q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-1152q208 0 385 34.5t280 93.5 103 128v128q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-128q0-69 103-128t280-93.5 385-34.5z"/></svg>
\ No newline at end of file
index 5b665ea..5d57c35 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M768 384v-128h-128v128h128zm128 128v-128h-128v128h128zm-128 128v-128h-128v128h128zm128 128v-128h-128v128h128zm700-388q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-128v128h-128v-128h-512v1536h1280zm-627-721l107 349q8 27 8 52 0 83-72.5 137.5t-183.5 54.5-183.5-54.5-72.5-137.5q0-25 8-52 21-63 120-396v-128h128v128h79q22 0 39 13t23 34zm-141 465q53 0 90.5-19t37.5-45-37.5-45-90.5-19-90.5 19-37.5 45 37.5 45 90.5 19z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M768 384v-128h-128v128h128zm128 128v-128h-128v128h128zm-128 128v-128h-128v128h128zm128 128v-128h-128v128h128zm700-388q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-128v128h-128v-128h-512v1536h1280zm-627-721l107 349q8 27 8 52 0 83-72.5 137.5t-183.5 54.5-183.5-54.5-72.5-137.5q0-25 8-52 21-63 120-396v-128h128v128h79q22 0 39 13t23 34zm-141 465q53 0 90.5-19t37.5-45-37.5-45-90.5-19-90.5 19-37.5 45 37.5 45 90.5 19z"/></svg>
\ No newline at end of file
index 70ce88b..12102e2 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1152 512v-472q22 14 36 28l408 408q14 14 28 36h-472zm-128 32q0 40 28 68t68 28h544v1056q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h800v544z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M1152 512v-472q22 14 36 28l408 408q14 14 28 36h-472zm-128 32q0 40 28 68t68 28h544v1056q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h800v544z"/></svg>
\ No newline at end of file
index ae2b0ca..976907a 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-128-448v320h-1024v-192l192-192 128 128 384-384zm-832-192q-80 0-136-56t-56-136 56-136 136-56 136 56 56 136-56 136-136 56z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-128-448v320h-1024v-192l192-192 128 128 384-384zm-832-192q-80 0-136-56t-56-136 56-136 136-56 136 56 56 136-56 136-136 56z"/></svg>
\ No newline at end of file
index 00b00a7..d39534e 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-514-593q33 26 84 56 59-7 117-7 147 0 177 49 16 22 2 52 0 1-1 2l-2 2v1q-6 38-71 38-48 0-115-20t-130-53q-221 24-392 83-153 262-242 262-15 0-28-7l-24-12q-1-1-6-5-10-10-6-36 9-40 56-91.5t132-96.5q14-9 23 6 2 2 2 4 52-85 107-197 68-136 104-262-24-82-30.5-159.5t6.5-127.5q11-40 42-40h22q23 0 35 15 18 21 9 68-2 6-4 8 1 3 1 8v30q-2 123-14 192 55 164 146 238zm-576 411q52-24 137-158-51 40-87.5 84t-49.5 74zm398-920q-15 42-2 132 1-7 7-44 0-3 7-43 1-4 4-8-1-1-1-2t-.5-1.5-.5-1.5q-1-22-13-36 0 1-1 2v2zm-124 661q135-54 284-81-2-1-13-9.5t-16-13.5q-76-67-127-176-27 86-83 197-30 56-45 83zm646-16q-24-24-140-24 76 28 124 28 14 0 18-1 0-1-2-3z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M1596 380q28 28 48 76t20 88v1152q0 40-28 68t-68 28h-1344q-40 0-68-28t-28-68v-1600q0-40 28-68t68-28h896q40 0 88 20t76 48zm-444-244v376h376q-10-29-22-41l-313-313q-12-12-41-22zm384 1528v-1024h-416q-40 0-68-28t-28-68v-416h-768v1536h1280zm-514-593q33 26 84 56 59-7 117-7 147 0 177 49 16 22 2 52 0 1-1 2l-2 2v1q-6 38-71 38-48 0-115-20t-130-53q-221 24-392 83-153 262-242 262-15 0-28-7l-24-12q-1-1-6-5-10-10-6-36 9-40 56-91.5t132-96.5q14-9 23 6 2 2 2 4 52-85 107-197 68-136 104-262-24-82-30.5-159.5t6.5-127.5q11-40 42-40h22q23 0 35 15 18 21 9 68-2 6-4 8 1 3 1 8v30q-2 123-14 192 55 164 146 238zm-576 411q52-24 137-158-51 40-87.5 84t-49.5 74zm398-920q-15 42-2 132 1-7 7-44 0-3 7-43 1-4 4-8-1-1-1-2t-.5-1.5-.5-1.5q-1-22-13-36 0 1-1 2v2zm-124 661q135-54 284-81-2-1-13-9.5t-16-13.5q-76-67-127-176-27 86-83 197-30 56-45 83zm646-16q-24-24-140-24 76 28 124 28 14 0 18-1 0-1-2-3z"/></svg>
\ No newline at end of file
index 2c1ee42..cebb326 100644 (file)