Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install
authorDan Poltawski <dan@moodle.com>
Fri, 31 Mar 2017 08:16:24 +0000 (09:16 +0100)
committerDan Poltawski <dan@moodle.com>
Fri, 31 Mar 2017 08:16:24 +0000 (09:16 +0100)
69 files changed:
admin/editors.php
admin/tool/task/schedule_task.php
admin/tool/uploadcourse/classes/step2_form.php
course/edit_form.php
enrol/tests/enrollib_test.php
grade/grading/form/guide/renderer.php
grade/grading/form/guide/tests/behat/edit_guide.feature
lang/en/moodle.php
lib/classes/filetypes.php
lib/coursecatlib.php
lib/datalib.php
lib/enrollib.php
lib/phpunit/classes/util.php
mod/data/classes/external.php
mod/data/classes/external/content_exporter.php [new file with mode: 0644]
mod/data/classes/external/field_exporter.php [new file with mode: 0644]
mod/data/classes/external/record_exporter.php [new file with mode: 0644]
mod/data/db/services.php
mod/data/edit.php
mod/data/field/checkbox/field.class.php
mod/data/field/date/field.class.php
mod/data/field/file/field.class.php
mod/data/field/latlong/field.class.php
mod/data/field/menu/field.class.php
mod/data/field/multimenu/field.class.php
mod/data/field/number/field.class.php
mod/data/field/picture/field.class.php
mod/data/field/radiobutton/field.class.php
mod/data/field/text/field.class.php
mod/data/field/textarea/field.class.php
mod/data/field/url/field.class.php
mod/data/lib.php
mod/data/locallib.php
mod/data/tests/externallib_test.php
mod/data/tests/lib_test.php
mod/data/upgrade.txt
mod/data/version.php
mod/data/view.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/forum/tests/lib_test.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/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/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
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/image.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 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 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 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 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 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 2639c4d..e71c98c 100644 (file)
@@ -30,6 +30,9 @@ require_once("$CFG->libdir/externallib.php");
 require_once($CFG->dirroot . "/mod/data/locallib.php");
 
 use mod_data\external\database_summary_exporter;
+use mod_data\external\record_exporter;
+use mod_data\external\content_exporter;
+use mod_data\external\field_exporter;
 
 /**
  * Database module external functions
@@ -322,4 +325,866 @@ class mod_data_external extends external_api {
             )
         );
     }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_entries_parameters() {
+        return new external_function_parameters(
+            array(
+                'databaseid' => new external_value(PARAM_INT, 'data instance id'),
+                'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group',
+                                                   VALUE_DEFAULT, 0),
+                'returncontents' => new external_value(PARAM_BOOL, 'Whether to return contents or not. This will return each entry
+                                                        raw contents and the complete list view (using the template).',
+                                                        VALUE_DEFAULT, false),
+                'sort' => new external_value(PARAM_INT, 'Sort the records by this field id, reserved ids are:
+                                                0: timeadded
+                                                -1: firstname
+                                                -2: lastname
+                                                -3: approved
+                                                -4: timemodified.
+                                                Empty for using the default database setting.', VALUE_DEFAULT, null),
+                'order' => new external_value(PARAM_ALPHA, 'The direction of the sorting: \'ASC\' or \'DESC\'.
+                                                Empty for using the default database setting.', VALUE_DEFAULT, null),
+                '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),
+            )
+        );
+    }
+
+    /**
+     * Return access information for a given feedback
+     *
+     * @param int $databaseid       the data instance id
+     * @param int $groupid          (optional) group id, 0 means that the function will determine the user group
+     * @param bool $returncontents  Whether to return the entries contents or not
+     * @param str $sort             sort by this field
+     * @param int $order            the direction of the sorting
+     * @param int $page             page of records to return
+     * @param int $perpage          number of records to return per page
+     * @return array of warnings and the entries
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function get_entries($databaseid, $groupid = 0, $returncontents = false, $sort = null, $order = null,
+            $page = 0, $perpage = 0) {
+        global $PAGE, $DB;
+
+        $params = array('databaseid' => $databaseid, 'groupid' => $groupid, 'returncontents' => $returncontents ,
+                        'sort' => $sort, 'order' => $order, 'page' => $page, 'perpage' => $perpage);
+        $params = self::validate_parameters(self::get_entries_parameters(), $params);
+        $warnings = array();
+
+        if (!empty($params['order'])) {
+            $params['order'] = strtoupper($params['order']);
+            if ($params['order'] != 'ASC' && $params['order'] != 'DESC') {
+                throw new invalid_parameter_exception('Invalid value for sortdirection parameter (value: ' . $params['order'] . ')');
+            }
+        }
+
+        list($database, $course, $cm, $context) = self::validate_database($params['databaseid']);
+        // Check database is open in time.
+        data_require_time_available($database, null, $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($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) =
+            data_search_entries($database, $cm, $context, 'list', $groupid, '', $params['sort'], $params['order'],
+                $params['page'], $params['perpage']);
+
+        $entries = [];
+        $contentsids = [];  // Store here the content ids of the records returned.
+        foreach ($records as $record) {
+            $user = user_picture::unalias($record, null, 'userid');
+            $related = array('context' => $context, 'database' => $database, 'user' => $user);
+
+            $contents = $DB->get_records('data_content', array('recordid' => $record->id));
+            $contentsids = array_merge($contentsids, array_keys($contents));
+            if ($params['returncontents']) {
+                $related['contents'] = $contents;
+            } else {
+                $related['contents'] = null;
+            }
+
+            $exporter = new record_exporter($record, $related);
+            $entries[] = $exporter->export($PAGE->get_renderer('core'));
+        }
+
+        // Retrieve total files size for the records retrieved.
+        $totalfilesize = 0;
+        $fs = get_file_storage();
+        $files = $fs->get_area_files($context->id, 'mod_data', 'content');
+        foreach ($files as $file) {
+            if ($file->is_directory() || !in_array($file->get_itemid(), $contentsids)) {
+                continue;
+            }
+            $totalfilesize += $file->get_filesize();
+        }
+
+        $result = array(
+            'entries' => $entries,
+            'totalcount' => $totalcount,
+            'totalfilesize' => $totalfilesize,
+            'warnings' => $warnings
+        );
+
+        // Check if we should return the list rendered.
+        if ($params['returncontents']) {
+            ob_start();
+            // The return parameter stops the execution after the first record.
+            data_print_template('listtemplate', $records, $database, '', $page, false);
+            $result['listviewcontents'] = ob_get_contents();
+            ob_end_clean();
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.3
+     */
+    public static function get_entries_returns() {
+        return new external_single_structure(
+            array(
+                'entries' => new external_multiple_structure(
+                    record_exporter::get_read_structure()
+                ),
+                'totalcount' => new external_value(PARAM_INT, 'Total count of records.'),
+                'totalfilesize' => new external_value(PARAM_INT, 'Total size (bytes) of the files included in the records.'),
+                'listviewcontents' => new external_value(PARAM_RAW, 'The list view contents as is rendered in the site.',
+                                                            VALUE_OPTIONAL),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_entry_parameters() {
+        return new external_function_parameters(
+            array(
+                'entryid' => new external_value(PARAM_INT, 'record entry id'),
+                'returncontents' => new external_value(PARAM_BOOL, 'Whether to return contents or not.', VALUE_DEFAULT, false),
+            )
+        );
+    }
+
+    /**
+     * Return one entry record from the database, including contents optionally.
+     *
+     * @param int $entryid          the record entry id id
+     * @param bool $returncontents  whether to return the entries contents or not
+     * @return array of warnings and the entries
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function get_entry($entryid, $returncontents = false) {
+        global $PAGE, $DB;
+
+        $params = array('entryid' => $entryid, 'returncontents' => $returncontents);
+        $params = self::validate_parameters(self::get_entry_parameters(), $params);
+        $warnings = array();
+
+        $record = $DB->get_record('data_records', array('id' => $params['entryid']), '*', MUST_EXIST);
+        list($database, $course, $cm, $context) = self::validate_database($record->dataid);
+
+        // Check database is open in time.
+        $canmanageentries = has_capability('mod/data:manageentries', $context);
+        data_require_time_available($database, $canmanageentries);
+
+        if ($record->groupid !== 0) {
+            if (!groups_group_visible($record->groupid, $course, $cm)) {
+                throw new moodle_exception('notingroup');
+            }
+        }
+
+        // Check correct record entry. Group check was done before.
+        if (!data_can_view_record($database, $record, $record->groupid, $canmanageentries)) {
+            throw new moodle_exception('notapproved', 'data');
+        }
+
+        $related = array('context' => $context, 'database' => $database, 'user' => null);
+        if ($params['returncontents']) {
+            $related['contents'] = $DB->get_records('data_content', array('recordid' => $record->id));
+        } else {
+            $related['contents'] = null;
+        }
+        $exporter = new record_exporter($record, $related);
+        $entry = $exporter->export($PAGE->get_renderer('core'));
+
+        $result = array(
+            'entry' => $entry,
+            'warnings' => $warnings
+        );
+        // Check if we should return the entry rendered.
+        if ($params['returncontents']) {
+            $records = [$record];
+            $result['entryviewcontents'] = data_print_template('singletemplate', $records, $database, '', 0, true);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.3
+     */
+    public static function get_entry_returns() {
+        return new external_single_structure(
+            array(
+                'entry' => record_exporter::get_read_structure(),
+                'entryviewcontents' => new external_value(PARAM_RAW, 'The entry as is rendered in the site.', VALUE_OPTIONAL),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_fields_parameters() {
+        return new external_function_parameters(
+            array(
+                'databaseid' => new external_value(PARAM_INT, 'Database instance id.'),
+            )
+        );
+    }
+
+    /**
+     * Return the list of configured fields for the given database.
+     *
+     * @param int $databaseid the database id
+     * @return array of warnings and the fields
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function get_fields($databaseid) {
+        global $PAGE;
+
+        $params = array('databaseid' => $databaseid);
+        $params = self::validate_parameters(self::get_fields_parameters(), $params);
+        $warnings = array();
+
+        list($database, $course, $cm, $context) = self::validate_database($params['databaseid']);
+
+        // Check database is open in time.
+        $canmanageentries = has_capability('mod/data:manageentries', $context);
+        data_require_time_available($database, $canmanageentries);
+
+        $fieldinstances = data_get_field_instances($database);
+
+        foreach ($fieldinstances as $fieldinstance) {
+            $record = $fieldinstance->field;
+            // Now get the configs the user can see with his current permissions.
+            $configs = $fieldinstance->get_config_for_external();
+            foreach ($configs as $name => $value) {
+                // Overwrite.
+                $record->{$name} = $value;
+            }
+
+            $exporter = new field_exporter($record, array('context' => $context));
+            $fields[] = $exporter->export($PAGE->get_renderer('core'));
+        }
+
+        $result = array(
+            'fields' => $fields,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.3
+     */
+    public static function get_fields_returns() {
+        return new external_single_structure(
+            array(
+                'fields' => new external_multiple_structure(
+                    field_exporter::get_read_structure()
+                ),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function search_entries_parameters() {
+        return new external_function_parameters(
+            array(
+                'databaseid' => new external_value(PARAM_INT, 'data instance id'),
+                'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group',
+                                                   VALUE_DEFAULT, 0),
+                'returncontents' => new external_value(PARAM_BOOL, 'Whether to return contents or not.', VALUE_DEFAULT, false),
+                'search' => new external_value(PARAM_NOTAGS, 'search string (empty when using advanced)', VALUE_DEFAULT, ''),
+                'advsearch' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_ALPHANUMEXT, 'Field key for search.
+                                                            Use fn or ln for first or last name'),
+                            'value' => new external_value(PARAM_RAW, 'JSON encoded value for search'),
+                        )
+                    ), 'Advanced search', VALUE_DEFAULT, array()
+                ),
+                'sort' => new external_value(PARAM_INT, 'Sort the records by this field id, reserved ids are:
+                                                0: timeadded
+                                                -1: firstname
+                                                -2: lastname
+                                                -3: approved
+                                                -4: timemodified.
+                                                Empty for using the default database setting.', VALUE_DEFAULT, null),
+                'order' => new external_value(PARAM_ALPHA, 'The direction of the sorting: \'ASC\' or \'DESC\'.
+                                                Empty for using the default database setting.', VALUE_DEFAULT, null),
+                '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),
+            )
+        );
+    }
+
+    /**
+     * Return access information for a given feedback
+     *
+     * @param int $databaseid       the data instance id
+     * @param int $groupid          (optional) group id, 0 means that the function will determine the user group
+     * @param bool $returncontents  whether to return contents or not
+     * @param str $search           search text
+     * @param array $advsearch      advanced search data
+     * @param str $sort             sort by this field
+     * @param int $order            the direction of the sorting
+     * @param int $page             page of records to return
+     * @param int $perpage          number of records to return per page
+     * @return array of warnings and the entries
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function search_entries($databaseid, $groupid = 0, $returncontents = false, $search = '', $advsearch = [],
+            $sort = null, $order = null, $page = 0, $perpage = 0) {
+        global $PAGE, $DB;
+
+        $params = array('databaseid' => $databaseid, 'groupid' => $groupid, 'returncontents' => $returncontents, 'search' => $search,
+                        'advsearch' => $advsearch, 'sort' => $sort, 'order' => $order, 'page' => $page, 'perpage' => $perpage);
+        $params = self::validate_parameters(self::search_entries_parameters(), $params);
+        $warnings = array();
+
+        if (!empty($params['order'])) {
+            $params['order'] = strtoupper($params['order']);
+            if ($params['order'] != 'ASC' && $params['order'] != 'DESC') {
+                throw new invalid_parameter_exception('Invalid value for sortdirection parameter (value: ' . $params['order'] . ')');
+            }
+        }
+
+        list($database, $course, $cm, $context) = self::validate_database($params['databaseid']);
+        // Check database is open in time.
+        data_require_time_available($database, null, $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 (!empty($params['advsearch'])) {
+            $advanced = true;
+            $defaults = [];
+            $fn = $ln = ''; // Defaults for first and last name.
+            // Force defaults for advanced search.
+            foreach ($params['advsearch'] as $adv) {
+                if ($adv['name'] == 'fn') {
+                    $fn = json_decode($adv['value']);
+                    continue;
+                }
+                if ($adv['name'] == 'ln') {
+                    $ln = json_decode($adv['value']);
+                    continue;
+                }
+                $defaults[$adv['name']] = json_decode($adv['value']);
+            }
+            list($searcharray, $params['search']) = data_build_search_array($database, false, [], $defaults, $fn, $ln);
+        } else {
+            $advanced = null;
+            $searcharray = null;
+        }
+
+        list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) =
+            data_search_entries($database, $cm, $context, 'list', $groupid, $params['search'], $params['sort'], $params['order'],
+                $params['page'], $params['perpage'], $advanced, $searcharray);
+
+        $entries = [];
+        foreach ($records as $record) {
+            $user = user_picture::unalias($record, null, 'userid');
+            $related = array('context' => $context, 'database' => $database, 'user' => $user);
+            if ($params['returncontents']) {
+                $related['contents'] = $DB->get_records('data_content', array('recordid' => $record->id));
+            } else {
+                $related['contents'] = null;
+            }
+
+            $exporter = new record_exporter($record, $related);
+            $entries[] = $exporter->export($PAGE->get_renderer('core'));
+        }
+
+        $result = array(
+            'entries' => $entries,
+            'totalcount' => $totalcount,
+            'maxcount' => $maxcount,
+            'warnings' => $warnings
+        );
+
+        // Check if we should return the list rendered.
+        if ($params['returncontents']) {
+            ob_start();
+            // The return parameter stops the execution after the first record.
+            data_print_template('listtemplate', $records, $database, '', $page, false);
+            $result['listviewcontents'] = ob_get_contents();
+            ob_end_clean();
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.3
+     */
+    public static function search_entries_returns() {
+        return new external_single_structure(
+            array(
+                'entries' => new external_multiple_structure(
+                    record_exporter::get_read_structure()
+                ),
+                'totalcount' => new external_value(PARAM_INT, 'Total count of records.'),
+                'listviewcontents' => new external_value(PARAM_RAW, 'The list view contents as is rendered in the site.',
+                                                            VALUE_OPTIONAL),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function approve_entry_parameters() {
+        return new external_function_parameters(
+            array(
+                'entryid' => new external_value(PARAM_INT, 'Record entry id.'),
+                'approve' => new external_value(PARAM_BOOL, 'Whether to approve (true) or unapprove the entry.',
+                                                VALUE_DEFAULT, true),
+            )
+        );
+    }
+
+    /**
+     * Approves or unapproves an entry.
+     *
+     * @param int $entryid          the record entry id id
+     * @param bool $approve         whether to approve (true) or unapprove the entry
+     * @return array of warnings and the entries
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function approve_entry($entryid, $approve = true) {
+        global $PAGE, $DB;
+
+        $params = array('entryid' => $entryid, 'approve' => $approve);
+        $params = self::validate_parameters(self::approve_entry_parameters(), $params);
+        $warnings = array();
+
+        $record = $DB->get_record('data_records', array('id' => $params['entryid']), '*', MUST_EXIST);
+        list($database, $course, $cm, $context) = self::validate_database($record->dataid);
+        // Check database is open in time.
+        data_require_time_available($database, null, $context);
+        // Check specific capabilities.
+        require_capability('mod/data:approve', $context);
+
+        data_approve_entry($record->id, $params['approve']);
+
+        $result = array(
+            'status' => true,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.3
+     */
+    public static function approve_entry_returns() {
+        return new external_single_structure(
+            array(
+                'status' => new external_value(PARAM_BOOL, 'status: true if success'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function delete_entry_parameters() {
+        return new external_function_parameters(
+            array(
+                'entryid' => new external_value(PARAM_INT, 'Record entry id.'),
+            )
+        );
+    }
+
+    /**
+     * Deletes an entry.
+     *
+     * @param int $entryid the record entry id
+     * @return array of warnings success status
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function delete_entry($entryid) {
+        global $PAGE, $DB;
+
+        $params = array('entryid' => $entryid);
+        $params = self::validate_parameters(self::delete_entry_parameters(), $params);
+        $warnings = array();
+
+        $record = $DB->get_record('data_records', array('id' => $params['entryid']), '*', MUST_EXIST);
+        list($database, $course, $cm, $context) = self::validate_database($record->dataid);
+
+        if (data_user_can_manage_entry($record, $database, $context)) {
+            data_delete_record($record->id, $database, $course->id, $cm->id);
+        } else {
+            throw new moodle_exception('noaccess', 'data');
+        }
+
+        $result = array(
+            'status' => true,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.3
+     */
+    public static function delete_entry_returns() {
+        return new external_single_structure(
+            array(
+                'status' => new external_value(PARAM_BOOL, 'Always true. If we see this field it means that the entry was deleted.'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function add_entry_parameters() {
+        return new external_function_parameters(
+            array(
+                'databaseid' => new external_value(PARAM_INT, 'data instance id'),
+                'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group',
+                                                   VALUE_DEFAULT, 0),
+                'data' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'fieldid' => new external_value(PARAM_INT, 'The field id.'),
+                            'subfield' => new external_value(PARAM_NOTAGS, 'The subfield name (if required).', VALUE_DEFAULT, ''),
+                            'value' => new external_value(PARAM_RAW, 'The contents for the field always JSON encoded.'),
+                        )
+                    ), 'The fields data to be created'
+                ),
+            )
+        );
+    }
+
+    /**
+     * Adds a new entry to a database
+     *
+     * @param int $databaseid the data instance id
+     * @param int $groupid (optional) group id, 0 means that the function will determine the user group
+     * @param array $data the fields data to be created
+     * @return array of warnings and status result
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function add_entry($databaseid, $groupid, $data) {
+        global $DB;
+
+        $params = array('databaseid' => $databaseid, 'groupid' => $groupid, 'data' => $data);
+        $params = self::validate_parameters(self::add_entry_parameters(), $params);
+        $warnings = array();
+        $fieldnotifications = array();
+
+        list($database, $course, $cm, $context) = self::validate_database($params['databaseid']);
+        // Check database is open in time.
+        data_require_time_available($database, null, $context);
+
+        $groupmode = groups_get_activity_groupmode($cm);
+        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) {
+                $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 (!data_user_can_add_entry($database, $groupid, $groupmode, $context)) {
+            throw new moodle_exception('noaccess', 'data');
+        }
+
+        // Prepare the data as is expected by the API.
+        $datarecord = new stdClass;
+        foreach ($params['data'] as $data) {
+            $subfield = ($data['subfield'] !== '') ? '_' . $data['subfield'] : '';
+            // We ask for JSON encoded values because of multiple choice forms or checkboxes that use array parameters.
+            $datarecord->{'field_' . $data['fieldid'] . $subfield} = json_decode($data['value']);
+        }
+        // Validate to ensure that enough data was submitted.
+        $fields = $DB->get_records('data_fields', array('dataid' => $database->id));
+        $processeddata = data_process_submission($database, $fields, $datarecord);
+
+        // Format notifications.
+        if (!empty($processeddata->fieldnotifications)) {
+            foreach ($processeddata->fieldnotifications as $field => $notififications) {
+                foreach ($notififications as $notif) {
+                    $fieldnotifications[] = [
+                        'fieldname' => $field,
+                        'notification' => $notif,
+                    ];
+                }
+            }
+        }
+
+        // Create a new (empty) record.
+        $newentryid = 0;
+        if ($processeddata->validated && $recordid = data_add_record($database, $groupid)) {
+            $newentryid = $recordid;
+            // Now populate the fields contents of the new record.
+            data_add_fields_contents_to_new_record($database, $context, $recordid, $fields, $datarecord, $processeddata);
+        }
+
+        $result = array(
+            'newentryid' => $newentryid,
+            'generalnotifications' => $processeddata->generalnotifications,
+            'fieldnotifications' => $fieldnotifications,
+        );
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.3
+     */
+    public static function add_entry_returns() {
+        return new external_single_structure(
+            array(
+                'newentryid' => new external_value(PARAM_INT, 'True new created entry id. 0 if the entry was not created.'),
+                'generalnotifications' => new external_multiple_structure(
+                    new external_value(PARAM_RAW, 'General notifications')
+                ),
+                'fieldnotifications' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'fieldname' => new external_value(PARAM_TEXT, 'The field name.'),
+                            'notification' => new external_value(PARAM_RAW, 'The notification for the field.'),
+                        )
+                    )
+                ),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function update_entry_parameters() {
+        return new external_function_parameters(
+            array(
+                'entryid' => new external_value(PARAM_INT, 'The entry record id.'),
+                'data' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'fieldid' => new external_value(PARAM_INT, 'The field id.'),
+                            'subfield' => new external_value(PARAM_NOTAGS, 'The subfield name (if required).', VALUE_DEFAULT, null),
+                            'value' => new external_value(PARAM_RAW, 'The new contents for the field always JSON encoded.'),
+                        )
+                    ), 'The fields data to be updated'
+                ),
+            )
+        );
+    }
+
+    /**
+     * Updates an existing entry.
+     *
+     * @param int $entryid the data instance id
+     * @param array $data the fields data to be created
+     * @return array of warnings and status result
+     * @since Moodle 3.3
+     * @throws moodle_exception
+     */
+    public static function update_entry($entryid, $data) {
+        global $DB;
+
+        $params = array('entryid' => $entryid, 'data' => $data);
+        $params = self::validate_parameters(self::update_entry_parameters(), $params);
+        $warnings = array();
+        $fieldnotifications = array();
+        $updated = false;
+
+        $record = $DB->get_record('data_records', array('id' => $params['entryid']), '*', MUST_EXIST);
+        list($database, $course, $cm, $context) = self::validate_database($record->dataid);
+        // Check database is open in time.
+        data_require_time_available($database, null, $context);
+
+        if (!data_user_can_manage_entry($record, $database, $context)) {
+            throw new moodle_exception('noaccess', 'data');
+        }
+
+        // Prepare the data as is expected by the API.
+        $datarecord = new stdClass;
+        foreach ($params['data'] as $data) {
+            $subfield = ($data['subfield'] !== '') ? '_' . $data['subfield'] : '';
+            // We ask for JSON encoded values because of multiple choice forms or checkboxes that use array parameters.
+            $datarecord->{'field_' . $data['fieldid'] . $subfield} = json_decode($data['value']);
+        }
+        // Validate to ensure that enough data was submitted.
+        $fields = $DB->get_records('data_fields', array('dataid' => $database->id));
+        $processeddata = data_process_submission($database, $fields, $datarecord);
+
+        // Format notifications.
+        if (!empty($processeddata->fieldnotifications)) {
+            foreach ($processeddata->fieldnotifications as $field => $notififications) {
+                foreach ($notififications as $notif) {
+                    $fieldnotifications[] = [
+                        'fieldname' => $field,
+                        'notification' => $notif,
+                    ];
+                }
+            }
+        }
+
+        if ($processeddata->validated) {
+            // Now update the fields contents.
+            data_update_record_fields_contents($database, $record, $context, $datarecord, $processeddata);
+            $updated = true;
+        }
+
+        $result = array(
+            'updated' => $updated,
+            'generalnotifications' => $processeddata->generalnotifications,
+            'fieldnotifications' => $fieldnotifications,
+            'warnings' => $warnings,
+        );
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.3
+     */
+    public static function update_entry_returns() {
+        return new external_single_structure(
+            array(
+                'updated' => new external_value(PARAM_BOOL, 'True if the entry was successfully updated, false other wise.'),
+                'generalnotifications' => new external_multiple_structure(
+                    new external_value(PARAM_RAW, 'General notifications')
+                ),
+                'fieldnotifications' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'fieldname' => new external_value(PARAM_TEXT, 'The field name.'),
+                            'notification' => new external_value(PARAM_RAW, 'The notification for the field.'),
+                        )
+                    )
+                ),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
 }
diff --git a/mod/data/classes/external/content_exporter.php b/mod/data/classes/external/content_exporter.php
new file mode 100644 (file)
index 0000000..f7806eb
--- /dev/null
@@ -0,0 +1,106 @@
+<?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 content associated to a record.
+ *
+ * @package    mod_data
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_data\external;
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+use renderer_base;
+use external_files;
+use external_util;
+
+/**
+ * Class for exporting content associated to a record.
+ *
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class content_exporter extends exporter {
+
+    protected static function define_properties() {
+
+        return array(
+            'id' => array(
+                'type' => PARAM_INT,
+                'description' => 'Content id.',
+            ),
+            'fieldid' => array(
+                'type' => PARAM_INT,
+                'description' => 'The field type of the content.',
+                'default' => 0,
+            ),
+            'recordid' => array(
+                'type' => PARAM_INT,
+                'description' => 'The record this content belongs to.',
+                'default' => 0,
+            ),
+            'content' => array(
+                'type' => PARAM_RAW,
+                'description' => 'Contents.',
+                'null' => NULL_ALLOWED,
+            ),
+            'content1' => array(
+                'type' => PARAM_RAW,
+                'description' => 'Contents.',
+                'null' => NULL_ALLOWED,
+            ),
+            'content2' => array(
+                'type' => PARAM_RAW,
+                'description' => 'Contents.',
+                'null' => NULL_ALLOWED,
+            ),
+            'content3' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Contents.',
+                'null' => NULL_ALLOWED,
+            ),
+            'content4' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Contents.',
+                'null' => NULL_ALLOWED,
+            ),
+        );
+    }
+
+    protected static function define_related() {
+        return array(
+            'context' => 'context',
+        );
+    }
+
+    protected static function define_other_properties() {
+        return array(
+            'files' => array(
+                'type' => external_files::get_properties_for_exporter(),
+                'multiple' => true,
+                'optional' => true,
+            ),
+        );
+    }
+
+    protected function get_other_values(renderer_base $output) {
+        $values = ['files' => external_util::get_area_files($this->related['context']->id, 'mod_data', 'content', $this->data->id)];
+
+        return $values;
+    }
+}
diff --git a/mod/data/classes/external/field_exporter.php b/mod/data/classes/external/field_exporter.php
new file mode 100644 (file)
index 0000000..42f065e
--- /dev/null
@@ -0,0 +1,85 @@
+<?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 field data.
+ *
+ * @package    mod_data
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_data\external;
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+
+/**
+ * Class for exporting field data.
+ *
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class field_exporter extends exporter {
+
+    protected static function define_properties() {
+
+        $properties = array(
+            'id' => array(
+                'type' => PARAM_INT,
+                'description' => 'Field id.',
+            ),
+            'dataid' => array(
+                'type' => PARAM_INT,
+                'description' => 'The field type of the content.',
+                'default' => 0,
+            ),
+            'type' => array(
+                'type' => PARAM_PLUGIN,
+                'description' => 'The field type.',
+            ),
+            'name' => array(
+                'type' => PARAM_TEXT,
+                'description' => 'The field name.',
+            ),
+            'description' => array(
+                'type' => PARAM_RAW,
+                'description' => 'The field description.',
+            ),
+            'required' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Whether is a field required or not.',
+                'default' => 0,
+            ),
+        );
+        // Field possible parameters.
+        for ($i = 1; $i <= 10; $i++) {
+            $properties["param$i"] = array(
+                'type' => PARAM_RAW,
+                'description' => 'Field parameters',
+                'null' => NULL_ALLOWED,
+            );
+        }
+
+        return $properties;
+    }
+
+    protected static function define_related() {
+        // Context is required for text formatting.
+        return array(
+            'context' => 'context',
+        );
+    }
+}
diff --git a/mod/data/classes/external/record_exporter.php b/mod/data/classes/external/record_exporter.php
new file mode 100644 (file)
index 0000000..7fecd44
--- /dev/null
@@ -0,0 +1,133 @@
+<?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 record data.
+ *
+ * @package    mod_data
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_data\external;
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+use renderer_base;
+use core_user;
+
+/**
+ * Class for exporting record data.
+ *
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class record_exporter extends exporter {
+
+    protected static function define_properties() {
+
+        return array(
+            'id' => array(
+                'type' => PARAM_INT,
+                'description' => 'Record id.',
+            ),
+            'userid' => array(
+                'type' => PARAM_INT,
+                'description' => 'The id of the user who created the record.',
+                'default' => 0,
+            ),
+            'groupid' => array(
+                'type' => PARAM_INT,
+                'description' => 'The group id this record belongs to (0 for no groups).',
+                'default' => 0,
+            ),
+            'dataid' => array(
+                'type' => PARAM_INT,
+                'description' => 'The database id this record belongs to.',
+                'default' => 0,
+            ),
+            'timecreated' => array(
+                'type' => PARAM_INT,
+                'description' => 'Time the record was created.',
+                'default' => 0,
+            ),
+            'timemodified' => array(
+                'type' => PARAM_INT,
+                'description' => 'Last time the record was modified.',
+                'default' => 0,
+            ),
+            'approved' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Whether the entry has been approved (if the database is configured in that way).',
+                'default' => 0,
+            ),
+        );
+    }
+
+    protected static function define_related() {
+        return array(
+            'database' => 'stdClass',
+            'user' => 'stdClass?',
+            'context' => 'context',
+            'contents' => 'stdClass[]?',
+        );
+    }
+
+    protected static function define_other_properties() {
+        return array(
+            'canmanageentry' => array(
+                'type' => PARAM_BOOL,
+                'description' => 'Whether the current user can manage this entry',
+            ),
+            'fullname' => array(
+                'type' => PARAM_TEXT,
+                'description' => 'The user who created the entry fullname.',
+                'optional' => true,
+            ),
+            'contents' => array(
+                'type' => content_exporter::read_properties_definition(),
+                'description' => 'The record contents.',
+                'multiple' => true,
+                'optional' => true,
+            ),
+        );
+    }
+
+    protected function get_other_values(renderer_base $output) {
+        global $PAGE;
+
+        $values = array(
+            'canmanageentry' => data_user_can_manage_entry($this->data, $this->related['database'], $this->related['context']),
+        );
+
+        if (!empty($this->related['user']) and !empty($this->related['user']->id)) {
+            $values['fullname'] = fullname($this->related['user']);
+        } else if ($this->data->userid) {
+            $user = core_user::get_user($this->data->userid);
+            $values['fullname'] = fullname($user);
+        }
+
+        if (!empty($this->related['contents'])) {
+            $contents = [];
+            foreach ($this->related['contents'] as $content) {
+                $related = array('context' => $this->related['context']);
+                $exporter = new content_exporter($content, $related);
+                $contents[] = $exporter->export($PAGE->get_renderer('core'));
+            }
+            $values['contents'] = $contents;
+        }
+        return $values;
+    }
+}
index a1ae717..bf44f61 100644 (file)
@@ -51,4 +51,68 @@ $functions = array(
         'capabilities'  => 'mod/data:viewentry',
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
+    'mod_data_get_entries' => array(
+        'classname'     => 'mod_data_external',
+        'methodname'    => 'get_entries',
+        'description'   => 'Return the complete list of entries of the given database.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/data:viewentry',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_data_get_entry' => array(
+        'classname'     => 'mod_data_external',
+        'methodname'    => 'get_entry',
+        'description'   => 'Return one entry record from the database, including contents optionally.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/data:viewentry',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_data_get_fields' => array(
+        'classname'     => 'mod_data_external',
+        'methodname'    => 'get_fields',
+        'description'   => 'Return the list of configured fields for the given database.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/data:viewentry',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_data_search_entries' => array(
+        'classname'     => 'mod_data_external',
+        'methodname'    => 'search_entries',
+        'description'   => 'Search for entries in the given database.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/data:viewentry',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_data_approve_entry' => array(
+        'classname'     => 'mod_data_external',
+        'methodname'    => 'approve_entry',
+        'description'   => 'Approves or unapproves an entry.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/data:approve',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_data_delete_entry' => array(
+        'classname'     => 'mod_data_external',
+        'methodname'    => 'delete_entry',
+        'description'   => 'Deletes an entry.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/data:manageentries',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_data_add_entry' => array(
+        'classname'     => 'mod_data_external',
+        'methodname'    => 'add_entry',
+        'description'   => 'Adds a new entry.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/data:writeentry',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+    'mod_data_update_entry' => array(
+        'classname'     => 'mod_data_external',
+        'methodname'    => 'update_entry',
+        'description'   => 'Updates an existing entry.',
+        'type'          => 'write',
+        'capabilities'  => 'mod/data:writeentry',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
 );
index 4dcf608..615aa22 100644 (file)
@@ -24,7 +24,7 @@
  */
 
 require_once('../../config.php');
-require_once('lib.php');
+require_once('locallib.php');
 require_once("$CFG->libdir/rsslib.php");
 require_once("$CFG->libdir/form/filemanager.php");
 
@@ -181,34 +181,7 @@ if ($datarecord = data_submitted() and confirm_sesskey()) {
 
         if ($processeddata->validated) {
             // Enough data to update the record.
-
-            // Obtain the record to be updated.
-
-            // Reset the approved flag after edit if the user does not have permission to approve their own entries.
-            if (!has_capability('mod/data:approve', $context)) {
-                $record->approved = 0;
-            }
-
-            // Update the parent record.
-            $record->timemodified = time();
-            $DB->update_record('data_records', $record);
-
-            // Update all content.
-            foreach ($processeddata->fields as $fieldname => $field) {
-                $field->update_content($rid, $datarecord->$fieldname, $fieldname);
-            }
-
-            // Trigger an event for updating this record.
-            $event = \mod_data\event\record_updated::create(array(
-                'objectid' => $rid,
-                'context' => $context,
-                'courseid' => $course->id,
-                'other' => array(
-                    'dataid' => $data->id
-                )
-            ));
-            $event->add_record_snapshot('data', $data);
-            $event->trigger();
+            data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata);
 
             $viewurl = new moodle_url('/mod/data/view.php', array(
                 'd' => $data->id,
@@ -233,34 +206,8 @@ if ($datarecord = data_submitted() and confirm_sesskey()) {
         // Add instance to data_record.
         if ($processeddata->validated && $recordid = data_add_record($data, $currentgroup)) {
 
-            // Insert a whole lot of empty records to make sure we have them.
-            $records = array();
-            foreach ($fields as $field) {
-                $content = new stdClass();
-                $content->recordid = $recordid;
-                $content->fieldid = $field->id;
-                $records[] = $content;
-            }
-
-            // Bulk insert the records now. Some records may have no data but all must exist.
-            $DB->insert_records('data_content', $records);
-
-            // Add all provided content.
-            foreach ($processeddata->fields as $fieldname => $field) {
-                $field->update_content($recordid, $datarecord->$fieldname, $fieldname);
-            }
-
-            // Trigger an event for updating this record.
-            $event = \mod_data\event\record_created::create(array(
-                'objectid' => $rid,
-                'context' => $context,
-                'courseid' => $course->id,
-                'other' => array(
-                    'dataid' => $data->id
-                )
-            ));
-            $event->add_record_snapshot('data', $data);
-            $event->trigger();
+            // Now populate the fields contents of the new record.
+            data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata);
 
             if (!empty($datarecord->saveandview)) {
                 $viewurl = new moodle_url('/mod/data/view.php', array(
index dbfb3de..d43728b 100644 (file)
@@ -114,9 +114,17 @@ class data_field_checkbox extends data_field_base {
         return $str;
     }
 
-    function parse_search_field() {
-        $selected    = optional_param_array('f_'.$this->field->id, array(), PARAM_NOTAGS);
-        $allrequired = optional_param('f_'.$this->field->id.'_allreq', 0, PARAM_BOOL);
+    public function parse_search_field($defaults = null) {
+        $paramselected = 'f_'.$this->field->id;
+        $paramallrequired = 'f_'.$this->field->id.'_allreq';
+
+        if (empty($defaults[$paramselected])) { // One empty means the other ones are empty too.
+            $defaults = array($paramselected => array(), $paramallrequired => 0);
+        }
+
+        $selected    = optional_param_array($paramselected, $defaults[$paramselected], PARAM_NOTAGS);
+        $allrequired = optional_param($paramallrequired, $defaults[$paramallrequired], PARAM_BOOL);
+
         if (empty($selected)) {
             // no searching
             return '';
@@ -266,4 +274,19 @@ class data_field_checkbox extends data_field_base {
 
         return trim($strvalue, "\r\n ");
     }
+
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
index 319727b..c488f56 100644 (file)
@@ -93,11 +93,20 @@ class data_field_date extends data_field_base {
         return array(" ({$tablealias}.fieldid = {$this->field->id} AND $varcharcontent = :$name) ", array($name => $value['timestamp']));
     }
 
-    function parse_search_field() {
-        $day   = optional_param('f_'.$this->field->id.'_d', 0, PARAM_INT);
-        $month = optional_param('f_'.$this->field->id.'_m', 0, PARAM_INT);
-        $year  = optional_param('f_'.$this->field->id.'_y', 0, PARAM_INT);
-        $usedate = optional_param('f_'.$this->field->id.'_z', 0, PARAM_INT);
+    public function parse_search_field($defaults = null) {
+        $paramday = 'f_'.$this->field->id.'_d';
+        $parammonth = 'f_'.$this->field->id.'_m';
+        $paramyear = 'f_'.$this->field->id.'_y';
+        $paramusedate = 'f_'.$this->field->id.'_z';
+        if (empty($defaults[$paramday])) {  // One empty means the other ones are empty too.
+            $defaults = array($paramday => 0, $parammonth => 0, $paramyear => 0, $paramusedate => 0);
+        }
+
+        $day   = optional_param($paramday, $defaults[$paramday], PARAM_INT);
+        $month = optional_param($parammonth, $defaults[$parammonth], PARAM_INT);
+        $year  = optional_param($paramyear, $defaults[$paramyear], PARAM_INT);
+        $usedate = optional_param($paramusedate, $defaults[$paramusedate], PARAM_INT);
+
         $data = array();
         if (!empty($day) && !empty($month) && !empty($year) && $usedate == 1) {
             $calendartype = \core_calendar\type_factory::get_calendar_instance();
@@ -167,5 +176,18 @@ class data_field_date extends data_field_base {
         return $DB->sql_cast_char2int($fieldname, true);
     }
 
-
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
index 5e6cf2f..af45571 100644 (file)
@@ -116,8 +116,12 @@ class data_field_file extends data_field_base {
         return array(" ({$tablealias}.fieldid = {$this->field->id} AND ".$DB->sql_like("{$tablealias}.content", ":$name", false).") ", array($name=>"%$value%"));
     }
 
-    function parse_search_field() {
-        return optional_param('f_'.$this->field->id, '', PARAM_NOTAGS);
+    public function parse_search_field($defaults = null) {
+        $param = 'f_'.$this->field->id;
+        if (empty($defaults[$param])) {
+            $defaults = array($param => '');
+        }
+        return optional_param($param, $defaults[$param], PARAM_NOTAGS);
     }
 
     function get_file($recordid, $content=null) {
@@ -222,4 +226,18 @@ class data_field_file extends data_field_base {
         return false;
     }
 
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
index e504bf2..f4edefa 100644 (file)
@@ -115,8 +115,12 @@ class data_field_latlong extends data_field_base {
        return $return;
     }
 
-    function parse_search_field() {
-        return optional_param('f_'.$this->field->id, '', PARAM_NOTAGS);
+    public function parse_search_field($defaults = null) {
+        $param = 'f_'.$this->field->id;
+        if (empty($defaults[$param])) {
+            $defaults = array($param => '');
+        }
+        return optional_param($param, $defaults[$param], PARAM_NOTAGS);
     }
 
     function generate_sql($tablealias, $value) {
@@ -291,4 +295,19 @@ class data_field_latlong extends data_field_base {
         // If we get here then only one field has been filled in.
         return get_string('latlongboth', 'data');
     }
+
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
index 075b033..e4ac3e0 100644 (file)
@@ -108,9 +108,13 @@ class data_field_menu extends data_field_base {
         return $return;
     }
 
-     function parse_search_field() {
-            return optional_param('f_'.$this->field->id, '', PARAM_NOTAGS);
-     }
+    public function parse_search_field($defaults = null) {
+        $param = 'f_'.$this->field->id;
+        if (empty($defaults[$param])) {
+            $defaults = array($param => '');
+        }
+        return optional_param($param, $defaults[$param], PARAM_NOTAGS);
+    }
 
     function generate_sql($tablealias, $value) {
         global $DB;
@@ -134,4 +138,18 @@ class data_field_menu extends data_field_base {
         return strval($value) !== '';
     }
 
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
index 885bbe8..fd9c16c 100644 (file)
@@ -146,9 +146,17 @@ class data_field_multimenu extends data_field_base {
 
     }
 
-    function parse_search_field() {
-        $selected    = optional_param_array('f_'.$this->field->id, array(), PARAM_NOTAGS);
-        $allrequired = optional_param('f_'.$this->field->id.'_allreq', 0, PARAM_BOOL);
+    public function parse_search_field($defaults = null) {
+        $paramselected = 'f_'.$this->field->id;
+        $paramallrequired = 'f_'.$this->field->id.'_allreq';
+
+        if (empty($defaults[$paramselected])) { // One empty means the other ones are empty too.
+            $defaults = array($paramselected => array(), $paramallrequired => 0);
+        }
+
+        $selected    = optional_param_array($paramselected, $defaults[$paramselected], PARAM_NOTAGS);
+        $allrequired = optional_param($paramallrequired, $defaults[$paramallrequired], PARAM_BOOL);
+
         if (empty($selected)) {
             // no searching
             return '';
@@ -293,4 +301,18 @@ class data_field_multimenu extends data_field_base {
         return trim($strvalue, "\r\n ");
     }
 
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
index dc11615..d1922b0 100644 (file)
@@ -75,8 +75,12 @@ class data_field_number extends data_field_base {
                'value="'.s($value).'" class="form-control d-inline"/>';
     }
 
-    function parse_search_field() {
-        return optional_param('f_'.$this->field->id, '', PARAM_NOTAGS);
+    public function parse_search_field($defaults = null) {
+        $param = 'f_'.$this->field->id;
+        if (empty($defaults[$param])) {
+            $defaults = array($param => '');
+        }
+        return optional_param($param, $defaults[$param], PARAM_NOTAGS);
     }
 
     // need to cast?
@@ -105,6 +109,21 @@ class data_field_number extends data_field_base {
     function notemptyfield($value, $name) {
         return strval($value) !== '';
     }
+
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
 
 
index fbcdf99..53e8662 100644 (file)
@@ -145,8 +145,12 @@ class data_field_picture extends data_field_base {
                'value="' . s($value) . '" class="form-control"/>';
     }
 
-    function parse_search_field() {
-        return optional_param('f_'.$this->field->id, '', PARAM_NOTAGS);
+    public function parse_search_field($defaults = null) {
+        $param = 'f_'.$this->field->id;
+        if (empty($defaults[$param])) {
+            $defaults = array($param => '');
+        }
+        return optional_param($param, $defaults[$param], PARAM_NOTAGS);
     }
 
     function generate_sql($tablealias, $value) {
@@ -324,4 +328,19 @@ class data_field_picture extends data_field_base {
         }
         return false;
     }
+
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
index ed50805..2cf8827 100644 (file)
@@ -85,7 +85,7 @@ class data_field_radiobutton extends data_field_base {
         return $str;
     }
 
-     function display_search_field($value = '') {
+    function display_search_field($value = '') {
         global $CFG, $DB;
 
         $varcharcontent = $DB->sql_compare_text('content', 255);
@@ -107,8 +107,12 @@ class data_field_radiobutton extends data_field_base {
         return $return;
     }
 
-    function parse_search_field() {
-        return optional_param('f_'.$this->field->id, '', PARAM_NOTAGS);
+    public function parse_search_field($defaults = null) {
+        $param = 'f_'.$this->field->id;
+        if (empty($defaults[$param])) {
+            $defaults = array($param => '');
+        }
+        return optional_param($param, $defaults[$param], PARAM_NOTAGS);
     }
 
     function generate_sql($tablealias, $value) {
@@ -132,5 +136,20 @@ class data_field_radiobutton extends data_field_base {
     function notemptyfield($value, $name) {
         return strval($value) !== '';
     }
+
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
 
index 4c25595..5cebe66 100644 (file)
@@ -38,8 +38,12 @@ class data_field_text extends data_field_base {
                'name="f_' . $this->field->id . '" value="' . s($value) . '" />';
     }
 
-    function parse_search_field() {
-        return optional_param('f_'.$this->field->id, '', PARAM_NOTAGS);
+    public function parse_search_field($defaults = null) {
+        $param = 'f_'.$this->field->id;
+        if (empty($defaults[$param])) {
+            $defaults = array($param => '');
+        }
+        return optional_param($param, $defaults[$param], PARAM_NOTAGS);
     }
 
     function generate_sql($tablealias, $value) {
@@ -61,6 +65,21 @@ class data_field_text extends data_field_base {
     function notemptyfield($value, $name) {
         return strval($value) !== '';
     }
+
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
 
 
index 3ae89d2..af0c94a 100644 (file)
@@ -175,8 +175,12 @@ class data_field_textarea extends data_field_base {
                'value="' . s($value) . '" class="form-control"/>';
     }
 
-    function parse_search_field() {
-        return optional_param('f_'.$this->field->id, '', PARAM_NOTAGS);
+    public function parse_search_field($defaults = null) {
+        $param = 'f_'.$this->field->id;
+        if (empty($defaults[$param])) {
+            $defaults = array($param => '');
+        }
+        return optional_param($param, $defaults[$param], PARAM_NOTAGS);
     }
 
     function generate_sql($tablealias, $value) {
@@ -294,4 +298,18 @@ class data_field_textarea extends data_field_base {
         return content_to_text($content->content, $content->content1);
     }
 
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
index 2c8f5bb..05774f0 100644 (file)
@@ -120,8 +120,12 @@ class data_field_url extends data_field_base {
                ' name="f_' . $this->field->id . '" value="' . s($value) . '" class="form-control d-inline"/>';
     }
 
-    function parse_search_field() {
-        return optional_param('f_'.$this->field->id, '', PARAM_NOTAGS);
+    public function parse_search_field($defaults = null) {
+        $param = 'f_'.$this->field->id;
+        if (empty($defaults[$param])) {
+            $defaults = array($param => '');
+        }
+        return optional_param($param, $defaults[$param], PARAM_NOTAGS);
     }
 
     function generate_sql($tablealias, $value) {
@@ -224,4 +228,18 @@ class data_field_url extends data_field_base {
         return $record->content . " " . $record->content1;
     }
 
+    /**
+     * Return the plugin configs for external functions.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the config parameters.
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = $this->field->{"param$i"};
+        }
+        return $configs;
+    }
 }
index 81cfb91..4498ca6 100644 (file)
@@ -560,6 +560,22 @@ class data_field_base {     // Base class for Database Field Types (see field/*/
     public static function get_content_value($content) {
         return trim($content->content, "\r\n ");
     }
+
+    /**
+     * Return the plugin configs for external functions,
+     * in some cases the configs will need formatting or be returned only if the current user has some capabilities enabled.
+     *
+     * @return array the list of config parameters
+     * @since Moodle 3.3
+     */
+    public function get_config_for_external() {
+        // Return all the field configs to null (maybe there is a private key for a service or something similar there).
+        $configs = [];
+        for ($i = 1; $i <= 10; $i++) {
+            $configs["param$i"] = null;
+        }
+        return $configs;
+    }
 }
 
 
@@ -1335,6 +1351,10 @@ function data_print_template($template, $records, $data, $search='', $page=0, $r
 
         $patterns[] = '##userpicture##';
         $ruser = user_picture::unalias($record, null, 'userid');
+        // If the record didn't come with user data, retrieve the user from database.
+        if (!isset($ruser->picture)) {
+            $ruser = core_user::get_user($record->userid);
+        }
         $replacement[] = $OUTPUT->user_picture($ruser, array('courseid' => $data->course));
 
         $patterns[]='##export##';
@@ -3737,7 +3757,7 @@ function data_get_advance_search_ids($recordids, $searcharray, $dataid) {
  */
 function data_get_recordids($alias, $searcharray, $dataid, $recordids) {
     global $DB;
-
+    $searchcriteria = $alias;   // Keep the criteria.
     $nestsearch = $searcharray[$alias];
     // searching for content outside of mdl_data_content
     if ($alias < 0) {
@@ -3760,7 +3780,10 @@ function data_get_recordids($alias, $searcharray, $dataid, $recordids) {
     if (count($nestsearch->params) != 0) {
         $params = array_merge($params, $nestsearch->params);
         $nestsql = $nestselect . $nestwhere . $nestsearch->sql;
-    } else {
+    } else if ($searchcriteria == DATA_TIMEMODIFIED) {
+        $nestsql = $nestselect . $nestwhere . $nestsearch->field . ' >= :timemodified GROUP BY c' . $alias . '.recordid';
+        $params['timemodified'] = $nestsearch->data;
+    } else {    // First name or last name.
         $thing = $DB->sql_like($nestsearch->field, ':search1', false);
         $nestsql = $nestselect . $nestwhere . $thing . ' GROUP BY c' . $alias . '.recordid';
         $params['search1'] = "%$nestsearch->data%";
@@ -4151,3 +4174,41 @@ function mod_data_get_fontawesome_icon_map() {
         'mod_data:field/url' => 'fa-link',
     ];
 }
+
+/*
+ * 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.2
+ */
+function data_check_updates_since(cm_info $cm, $from, $filter = array()) {
+    global $DB, $CFG;
+    require_once($CFG->dirroot . '/mod/data/locallib.php');
+
+    $updates = course_check_module_updates_since($cm, $from, array(), $filter);
+
+    // Check for new entries.
+    $updates->entries = (object) array('updated' => false);
+
+    $data = $DB->get_record('data', array('id' => $cm->instance), '*', MUST_EXIST);
+    $searcharray = [];
+    $searcharray[DATA_TIMEMODIFIED] = new stdClass();
+    $searcharray[DATA_TIMEMODIFIED]->sql     = '';
+    $searcharray[DATA_TIMEMODIFIED]->params  = array();
+    $searcharray[DATA_TIMEMODIFIED]->field   = 'r.timemodified';
+    $searcharray[DATA_TIMEMODIFIED]->data    = $from;
+
+    $currentgroup = groups_get_activity_group($cm);
+    list($entries, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) =
+        data_search_entries($data, $cm, $cm->context, 'list', $currentgroup, '', null, null, 0, 0, true, $searcharray);
+
+    if (!empty($entries)) {
+        $updates->entries->updated = true;
+        $updates->entries->itemids = array_keys($entries);
+    }
+
+    return $updates;
+}
index 276efac..8d61544 100644 (file)
@@ -764,3 +764,515 @@ function data_get_entries_left_to_view($data, $numentries, $canmanageentries) {
     }
     return 0;
 }
+
+/**
+ * Search entries in a database.
+ *
+ * @param  stdClass  $data         database object
+ * @param  stdClass  $cm           course module object
+ * @param  stdClass  $context      context object
+ * @param  stdClass  $mode         in which mode we are viewing the database (list, single)
+ * @param  int  $currentgroup      the current group being used
+ * @param  str  $search            search for this text in the entry data
+ * @param  str  $sort              the field to sort by
+ * @param  str  $order             the order to use when sorting
+ * @param  int $page               for pagination, the current page
+ * @param  int $perpage            entries per page
+ * @param  bool  $advanced         whether we are using or not advanced search
+ * @param  array  $searcharray     when using advanced search, the advanced data to use
+ * @param  stdClass  $record       if we jsut want this record after doing all the access checks
+ * @return array the entries found among other data related to the search
+ * @since  Moodle 3.3
+ */
+function data_search_entries($data, $cm, $context, $mode, $currentgroup, $search = '', $sort = null, $order = null, $page = 0,
+        $perpage = 0, $advanced = null, $searcharray = null, $record = null) {
+    global $DB, $USER;
+
+    if ($sort === null) {
+        $sort = $data->defaultsort;
+    }
+    if ($order === null) {
+        $order = ($data->defaultsortdir == 0) ? 'ASC' : 'DESC';
+    }
+    if ($searcharray === null) {
+        $searcharray = array();
+    }
+
+    if (core_text::strlen($search) < 2) {
+        $search = '';
+    }
+
+    $approvecap = has_capability('mod/data:approve', $context);
+    $canmanageentries = has_capability('mod/data:manageentries', $context);
+
+    // If a student is not part of a group and seperate groups is enabled, we don't
+    // want them seeing all records.
+    $groupmode = groups_get_activity_groupmode($cm);
+    if ($currentgroup == 0 && $groupmode == 1 && !$canmanageentries) {
+        $canviewallrecords = false;
+    } else {
+        $canviewallrecords = true;
+    }
+
+    $numentries = data_numentries($data);
+    $requiredentriesallowed = true;
+    if (data_get_entries_left_to_view($data, $numentries, $canmanageentries)) {
+        $requiredentriesallowed = false;
+    }
+
+    // Initialise the first group of params for advanced searches.
+    $initialparams   = array();
+    $params = array(); // Named params array.
+
+    // Setup group and approve restrictions.
+    if (!$approvecap && $data->approval) {
+        if (isloggedin()) {
+            $approveselect = ' AND (r.approved=1 OR r.userid=:myid1) ';
+            $params['myid1'] = $USER->id;
+            $initialparams['myid1'] = $params['myid1'];
+        } else {
+            $approveselect = ' AND r.approved=1 ';
+        }
+    } else {
+        $approveselect = ' ';
+    }
+
+    if ($currentgroup) {
+        $groupselect = " AND (r.groupid = :currentgroup OR r.groupid = 0)";
+        $params['currentgroup'] = $currentgroup;
+        $initialparams['currentgroup'] = $params['currentgroup'];
+    } else {
+        if ($canviewallrecords) {
+            $groupselect = ' ';
+        } else {
+            // If separate groups are enabled and the user isn't in a group or
+            // a teacher, manager, admin etc, then just show them entries for 'All participants'.
+            $groupselect = " AND r.groupid = 0";
+        }
+    }
+
+    // Init some variables to be used by advanced search.
+    $advsearchselect = '';
+    $advwhere        = '';
+    $advtables       = '';
+    $advparams       = array();
+    // This is used for the initial reduction of advanced search results with required entries.
+    $entrysql        = '';
+    $namefields = user_picture::fields('u');
+    // Remove the id from the string. This already exists in the sql statement.
+    $namefields = str_replace('u.id,', '', $namefields);
+
+    // Find the field we are sorting on.
+    if ($sort <= 0 or !$sortfield = data_get_field_from_id($sort, $data)) {
+
+        switch ($sort) {
+            case DATA_LASTNAME:
+                $ordering = "u.lastname $order, u.firstname $order";
+                break;
+            case DATA_FIRSTNAME:
+                $ordering = "u.firstname $order, u.lastname $order";
+                break;
+            case DATA_APPROVED:
+                $ordering = "r.approved $order, r.timecreated $order";
+                break;
+            case DATA_TIMEMODIFIED:
+                $ordering = "r.timemodified $order";
+                break;
+            case DATA_TIMEADDED:
+            default:
+                $sort     = 0;
+                $ordering = "r.timecreated $order";
+        }
+
+        $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields;
+        $count = ' COUNT(DISTINCT c.recordid) ';
+        $tables = '{data_content} c,{data_records} r, {user} u ';
+        $where = 'WHERE c.recordid = r.id
+                     AND r.dataid = :dataid
+                     AND r.userid = u.id ';
+        $params['dataid'] = $data->id;
+        $sortorder = " ORDER BY $ordering, r.id $order";
+        $searchselect = '';
+
+        // If requiredentries is not reached, only show current user's entries.
+        if (!$requiredentriesallowed) {
+            $where .= ' AND u.id = :myid2 ';
+            $entrysql = ' AND r.userid = :myid3 ';
+            $params['myid2'] = $USER->id;
+            $initialparams['myid3'] = $params['myid2'];
+        }
+
+        if (!empty($advanced)) {                    // If advanced box is checked.
+            $i = 0;
+            foreach ($searcharray as $key => $val) { // what does $searcharray hold?
+                if ($key == DATA_FIRSTNAME or $key == DATA_LASTNAME) {
+                    $i++;
+                    $searchselect .= " AND ".$DB->sql_like($val->field, ":search_flname_$i", false);
+                    $params['search_flname_'.$i] = "%$val->data%";
+                    continue;
+                }
+                if ($key == DATA_TIMEMODIFIED) {
+                    $searchselect .= " AND $val->field >= :timemodified";
+                    $params['timemodified'] = $val->data;
+                    continue;
+                }
+                $advtables .= ', {data_content} c'.$key.' ';
+                $advwhere .= ' AND c'.$key.'.recordid = r.id';
+                $advsearchselect .= ' AND ('.$val->sql.') ';
+                $advparams = array_merge($advparams, $val->params);
+            }
+        } else if ($search) {
+            $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)."
+                              OR ".$DB->sql_like('u.firstname', ':search2', false)."
+                              OR ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
+            $params['search1'] = "%$search%";
+            $params['search2'] = "%$search%";
+            $params['search3'] = "%$search%";
+        } else {
+            $searchselect = ' ';
+        }
+
+    } else {
+
+        $sortcontent = $DB->sql_compare_text('c.' . $sortfield->get_sort_field());
+        $sortcontentfull = $sortfield->get_sort_sql($sortcontent);
+
+        $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields . ',
+                ' . $sortcontentfull . ' AS sortorder ';
+        $count = ' COUNT(DISTINCT c.recordid) ';
+        $tables = '{data_content} c, {data_records} r, {user} u ';
+        $where = 'WHERE c.recordid = r.id
+                     AND r.dataid = :dataid
+                     AND r.userid = u.id ';
+        if (!$advanced) {
+            $where .= 'AND c.fieldid = :sort';
+        }
+        $params['dataid'] = $data->id;
+        $params['sort'] = $sort;
+        $sortorder = ' ORDER BY sortorder '.$order.' , r.id ASC ';
+        $searchselect = '';
+
+        // If requiredentries is not reached, only show current user's entries.
+        if (!$requiredentriesallowed) {
+            $where .= ' AND u.id = :myid2';
+            $entrysql = ' AND r.userid = :myid3';
+            $params['myid2'] = $USER->id;
+            $initialparams['myid3'] = $params['myid2'];
+        }
+        $i = 0;
+        if (!empty($advanced)) {                      // If advanced box is checked.
+            foreach ($searcharray as $key => $val) {   // what does $searcharray hold?
+                if ($key == DATA_FIRSTNAME or $key == DATA_LASTNAME) {
+                    $i++;
+                    $searchselect .= " AND ".$DB->sql_like($val->field, ":search_flname_$i", false);
+                    $params['search_flname_'.$i] = "%$val->data%";
+                    continue;
+                }
+                if ($key == DATA_TIMEMODIFIED) {
+                    $searchselect .= " AND $val->field >= :timemodified";
+                    $params['timemodified'] = $val->data;
+                    continue;
+                }
+                $advtables .= ', {data_content} c'.$key.' ';
+                $advwhere .= ' AND c'.$key.'.recordid = r.id AND c'.$key.'.fieldid = '.$key;
+                $advsearchselect .= ' AND ('.$val->sql.') ';
+                $advparams = array_merge($advparams, $val->params);
+            }
+        } else if ($search) {
+            $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR
+                ".$DB->sql_like('u.firstname', ':search2', false)." OR
+                ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
+            $params['search1'] = "%$search%";
+            $params['search2'] = "%$search%";
+            $params['search3'] = "%$search%";
+        } else {
+            $searchselect = ' ';
+        }
+    }
+
+    // To actually fetch the records.
+
+    $fromsql    = "FROM $tables $advtables $where $advwhere $groupselect $approveselect $searchselect $advsearchselect";
+    $allparams  = array_merge($params, $advparams);
+
+    // Provide initial sql statements and parameters to reduce the number of total records.
+    $initialselect = $groupselect . $approveselect . $entrysql;
+
+    $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams);
+    $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data->id);
+    $totalcount = count($newrecordids);
+    $selectdata = $where . $groupselect . $approveselect;
+
+    if (!empty($advanced)) {
+        $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
+        $sqlselect = $advancedsearchsql['sql'];
+        $allparams = array_merge($allparams, $advancedsearchsql['params']);
+    } else {
+        $sqlselect  = "SELECT $what $fromsql $sortorder";
+    }
+
+    // Work out the paging numbers and counts.
+    if (empty($searchselect) && empty($advsearchselect)) {
+        $maxcount = $totalcount;
+    } else {
+        $maxcount = count($recordids);
+    }
+
+    if ($record) {     // We need to just show one, so where is it in context?
+        $nowperpage = 1;
+        $mode = 'single';
+        $page = 0;
+        // TODO MDL-33797 - Reduce this or consider redesigning the paging system.
+        if ($allrecordids = $DB->get_fieldset_sql($sqlselect, $allparams)) {
+            $page = (int)array_search($record->id, $allrecordids);
+            unset($allrecordids);
+        }
+    } else if ($mode == 'single') {  // We rely on ambient $page settings
+        $nowperpage = 1;
+
+    } else {
+        $nowperpage = $perpage;
+    }
+
+    // Get the actual records.
+    if (!$records = $DB->get_records_sql($sqlselect, $allparams, $page * $nowperpage, $nowperpage)) {
+        // Nothing to show!
+        if ($record) {         // Something was requested so try to show that at least (bug 5132)
+            if (data_can_view_record($data, $record, $currentgroup, $canmanageentries)) {
+                // OK, we can show this one
+                $records = array($record->id => $record);
+                $totalcount = 1;
+            }
+        }
+
+    }
+
+    return [$records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode];
+}
+
+/**
+ * Check if the current user can view the given record.
+ *
+ * @param  stdClass $data           database record
+ * @param  stdClass $record         the record (entry) to check
+ * @param  int $currentgroup        current group
+ * @param  bool $canmanageentries   if the user can manage entries
+ * @return bool true if the user can view the entry
+ * @since  Moodle 3.3
+ */
+function data_can_view_record($data, $record, $currentgroup, $canmanageentries) {
+    global $USER;
+
+    if ($canmanageentries || empty($data->approval) ||
+             $record->approved || (isloggedin() && $record->userid == $USER->id)) {
+
+        if (!$currentgroup || $record->groupid == $currentgroup || $record->groupid == 0) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/**
+ * Return all the field instances for a given database.
+ *
+ * @param  stdClass $data database object
+ * @return array field instances
+ * @since  Moodle 3.3
+ */
+function data_get_field_instances($data) {
+    global $DB;
+
+    $instances = [];
+    if ($fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id')) {
+        foreach ($fields as $field) {
+            $instances[] = data_get_field($field, $data);
+        }
+    }
+    return $instances;
+}
+
+/**
+ * Build the search array.
+ *
+ * @param  stdClass $data      the database object
+ * @param  bool $paging        if paging is being used
+ * @param  array $searcharray  the current search array (saved by session)
+ * @param  array $defaults     default values for the searchable fields
+ * @param  str $fn             the first name to search (optional)
+ * @param  str $ln             the last name to search (optional)
+ * @return array               the search array and plain search build based on the different elements
+ * @since  Moodle 3.3
+ */
+function data_build_search_array($data, $paging, $searcharray, $defaults = null, $fn = '', $ln = '') {
+    global $DB;
+
+    $search = '';
+    $vals = array();
+    $fields = $DB->get_records('data_fields', array('dataid' => $data->id));
+
+    if (!empty($fields)) {
+        foreach ($fields as $field) {
+            $searchfield = data_get_field_from_id($field->id, $data);
+            // Get field data to build search sql with.  If paging is false, get from user.
+            // If paging is true, get data from $searcharray which is obtained from the $SESSION (see line 116).
+            if (!$paging) {
+                $val = $searchfield->parse_search_field($defaults);
+            } else {
+                // Set value from session if there is a value @ the required index.
+                if (isset($searcharray[$field->id])) {
+                    $val = $searcharray[$field->id]->data;
+                } else { // If there is not an entry @ the required index, set value to blank.
+                    $val = '';
+                }
+            }
+            if (!empty($val)) {
+                $searcharray[$field->id] = new stdClass();
+                list($searcharray[$field->id]->sql, $searcharray[$field->id]->params) = $searchfield->generate_sql('c'.$field->id, $val);
+                $searcharray[$field->id]->data = $val;
+                $vals[] = $val;
+            } else {
+                // Clear it out.
+                unset($searcharray[$field->id]);
+            }
+        }
+    }
+
+    if (!$paging) {
+        // Name searching.
+        $fn = optional_param('u_fn', $fn, PARAM_NOTAGS);
+        $ln = optional_param('u_ln', $ln, PARAM_NOTAGS);
+    } else {
+        $fn = isset($searcharray[DATA_FIRSTNAME]) ? $searcharray[DATA_FIRSTNAME]->data : '';
+        $ln = isset($searcharray[DATA_LASTNAME]) ? $searcharray[DATA_LASTNAME]->data : '';
+    }
+    if (!empty($fn)) {
+        $searcharray[DATA_FIRSTNAME] = new stdClass();
+        $searcharray[DATA_FIRSTNAME]->sql    = '';
+        $searcharray[DATA_FIRSTNAME]->params = array();
+        $searcharray[DATA_FIRSTNAME]->field  = 'u.firstname';
+        $searcharray[DATA_FIRSTNAME]->data   = $fn;
+        $vals[] = $fn;
+    } else {
+        unset($searcharray[DATA_FIRSTNAME]);
+    }
+    if (!empty($ln)) {
+        $searcharray[DATA_LASTNAME] = new stdClass();
+        $searcharray[DATA_LASTNAME]->sql     = '';
+        $searcharray[DATA_LASTNAME]->params = array();
+        $searcharray[DATA_LASTNAME]->field   = 'u.lastname';
+        $searcharray[DATA_LASTNAME]->data    = $ln;
+        $vals[] = $ln;
+    } else {
+        unset($searcharray[DATA_LASTNAME]);
+    }
+
+    // In case we want to switch to simple search later - there might be multiple values there ;-).
+    if ($vals) {
+        $val = reset($vals);
+        if (is_string($val)) {
+            $search = $val;
+        }
+    }
+    return [$searcharray, $search];
+}
+
+/**
+ * Approves or unapproves an entry.
+ *
+ * @param  int $entryid the entry to approve or unapprove.
+ * @param  bool $approve Whether to approve or unapprove (true for approve false otherwise).
+ * @since  Moodle 3.3
+ */
+function data_approve_entry($entryid, $approve) {
+    global $DB;
+
+    $newrecord = new stdClass();
+    $newrecord->id = $entryid;
+    $newrecord->approved = $approve ? 1 : 0;
+    $DB->update_record('data_records', $newrecord);
+}
+
+/**
+ * Populate the field contents of a new record with the submitted data.
+ *
+ * @param  stdClass $data           database object
+ * @param  stdClass $context        context object
+ * @param  int $recordid            the new record id
+ * @param  array $fields            list of fields of the database
+ * @param  stdClass $datarecord     the submitted data
+ * @param  stdClass $processeddata  pre-processed submitted fields
+ * @since  Moodle 3.3
+ */
+function data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata) {
+    global $DB;
+
+    // Insert a whole lot of empty records to make sure we have them.
+    $records = array();
+    foreach ($fields as $field) {
+        $content = new stdClass();
+        $content->recordid = $recordid;
+        $content->fieldid = $field->id;
+        $records[] = $content;
+    }
+
+    // Bulk insert the records now. Some records may have no data but all must exist.
+    $DB->insert_records('data_content', $records);
+
+    // Add all provided content.
+    foreach ($processeddata->fields as $fieldname => $field) {
+        $field->update_content($recordid, $datarecord->$fieldname, $fieldname);
+    }
+
+    // Trigger an event for updating this record.
+    $event = \mod_data\event\record_created::create(array(
+        'objectid' => $recordid,
+        'context' => $context,
+        'courseid' => $data->course,
+        'other' => array(
+            'dataid' => $data->id
+        )
+    ));
+    $event->add_record_snapshot('data', $data);
+    $event->trigger();
+}
+
+/**
+ * Updates the fields contents of an existing record.
+ *
+ * @param  stdClass $data           database object
+ * @param  stdClass $record         record to update object
+ * @param  stdClass $context        context object
+ * @param  stdClass $datarecord     the submitted data
+ * @param  stdClass $processeddata  pre-processed submitted fields
+ * @since  Moodle 3.3
+ */
+function data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata) {
+    global $DB;
+
+    // Reset the approved flag after edit if the user does not have permission to approve their own entries.
+    if (!has_capability('mod/data:approve', $context)) {
+        $record->approved = 0;
+    }
+
+    // Update the parent record.
+    $record->timemodified = time();
+    $DB->update_record('data_records', $record);
+
+    // Update all content.
+    foreach ($processeddata->fields as $fieldname => $field) {
+        $field->update_content($record->id, $datarecord->$fieldname, $fieldname);
+    }
+
+    // Trigger an event for updating this record.
+    $event = \mod_data\event\record_updated::create(array(
+        'objectid' => $record->id,
+        'context' => $context,
+        'courseid' => $data->course,
+        'other' => array(
+            'dataid' => $data->id
+        )
+    ));
+    $event->add_record_snapshot('data', $data);
+    $event->trigger();
+}
index e2c841a..df6a65b 100644 (file)
@@ -50,7 +50,10 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $this->setAdminUser();
 
         // Setup test data.
-        $this->course = $this->getDataGenerator()->create_course();
+        $course = new stdClass();
+        $course->groupmode = SEPARATEGROUPS;
+        $course->groupmodeforce = true;
+        $this->course = $this->getDataGenerator()->create_course($course);
         $this->data = $this->getDataGenerator()->create_module('data', array('course' => $this->course->id));
         $this->context = context_module::instance($this->data->cmid);
         $this->cm = get_coursemodule_from_instance('data', $this->data->id);
@@ -58,6 +61,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         // Create users.
         $this->student1 = self::getDataGenerator()->create_user();
         $this->student2 = self::getDataGenerator()->create_user();
+        $this->student3 = self::getDataGenerator()->create_user();
         $this->teacher = self::getDataGenerator()->create_user();
 
         // Users enrolments.
@@ -65,7 +69,14 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
         $this->getDataGenerator()->enrol_user($this->student1->id, $this->course->id, $this->studentrole->id, 'manual');
         $this->getDataGenerator()->enrol_user($this->student2->id, $this->course->id, $this->studentrole->id, 'manual');
+        $this->getDataGenerator()->enrol_user($this->student3->id, $this->course->id, $this->studentrole->id, 'manual');
         $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
+
+        $this->group1 = $this->getDataGenerator()->create_group(array('courseid' => $this->course->id));
+        $this->group2 = $this->getDataGenerator()->create_group(array('courseid' => $this->course->id));
+        groups_add_member($this->group1, $this->student1);
+        groups_add_member($this->group1, $this->student2);
+        groups_add_member($this->group2, $this->student3);
     }
 
     /**
@@ -275,7 +286,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $result = mod_data_external::get_data_access_information($this->data->id);
         $result = external_api::clean_returnvalue(mod_data_external::get_data_access_information_returns(), $result);
 
-        $this->assertEquals(0, $result['groupid']);
+        $this->assertEquals($this->group1->id, $result['groupid']);
 
         $this->assertFalse($result['canmanageentries']);
         $this->assertFalse($result['canapprove']);
@@ -315,4 +326,857 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals(0, $result['entrieslefttoadd']);
         $this->assertEquals(0, $result['entrieslefttoview']);
     }
+
+    /**
+     * Helper method to populate the database with some entries.
+     *
+     * @return array the entry ids created
+     */
+    public function populate_database_with_entries() {
+        global $DB;
+
+        // Force approval.
+        $DB->set_field('data', 'approval', 1, array('id' => $this->data->id));
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+        $fieldtypes = array('checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url');
+
+        $count = 1;
+        // Creating test Fields with default parameter values.
+        foreach ($fieldtypes as $fieldtype) {
+            $fieldname = 'field-' . $count;
+            $record = new StdClass();
+            $record->name = $fieldname;
+            $record->type = $fieldtype;
+            $record->required = 1;
+
+            $generator->create_field($record, $this->data);
+            $count++;
+        }
+        // Get all the fields created.
+        $fields = $DB->get_records('data_fields', array('dataid' => $this->data->id), 'id');
+
+        // Populate with contents, creating a new entry.
+        $contents = array();
+        $contents[] = array('opt1', 'opt2', 'opt3', 'opt4');
+        $contents[] = '01-01-2037'; // It should be lower than 2038, to avoid failing on 32-bit windows.
+        $contents[] = 'menu1';
+        $contents[] = array('multimenu1', 'multimenu2', 'multimenu3', 'multimenu4');
+        $contents[] = '12345';
+        $contents[] = 'radioopt1';
+        $contents[] = 'text for testing';
+        $contents[] = '<p>text area testing<br /></p>';
+        $contents[] = array('example.url', 'sampleurl');
+        $count = 0;
+        $fieldcontents = array();
+        foreach ($fields as $fieldrecord) {
+            $fieldcontents[$fieldrecord->id] = $contents[$count++];
+        }
+
+        $this->setUser($this->student1);
+        $entry11 = $generator->create_entry($this->data, $fieldcontents, $this->group1->id);
+        $this->setUser($this->student2);
+        $entry12 = $generator->create_entry($this->data, $fieldcontents, $this->group1->id);
+        $entry13 = $generator->create_entry($this->data, $fieldcontents, $this->group1->id);
+
+        $this->setUser($this->student3);
+        $entry21 = $generator->create_entry($this->data, $fieldcontents, $this->group2->id);
+
+        // Approve all except $entry13.
+        $DB->set_field('data_records', 'approved', 1, ['id' => $entry11]);
+        $DB->set_field('data_records', 'approved', 1, ['id' => $entry12]);
+        $DB->set_field('data_records', 'approved', 1, ['id' => $entry21]);
+
+        return [$entry11, $entry12, $entry13, $entry21];
+    }
+
+    /**
+     * Test get_entries
+     */
+    public function test_get_entries() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        // First of all, expect to see only my group entries (not other users in other groups ones).
+        $this->setUser($this->student1);
+        $result = mod_data_external::get_entries($this->data->id);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(2, $result['entries']);
+        $this->assertEquals(2, $result['totalcount']);
+        $this->assertEquals($entry11, $result['entries'][0]['id']);
+        $this->assertEquals($this->student1->id, $result['entries'][0]['userid']);
+        $this->assertEquals($this->group1->id, $result['entries'][0]['groupid']);
+        $this->assertEquals($this->data->id, $result['entries'][0]['dataid']);
+        $this->assertEquals($entry12, $result['entries'][1]['id']);
+        $this->assertEquals($this->student2->id, $result['entries'][1]['userid']);
+        $this->assertEquals($this->group1->id, $result['entries'][1]['groupid']);
+        $this->assertEquals($this->data->id, $result['entries'][1]['dataid']);
+        // Other user in same group.
+        $this->setUser($this->student2);
+        $result = mod_data_external::get_entries($this->data->id);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(3, $result['entries']);  // I can see my entry not approved yet.
+        $this->assertEquals(3, $result['totalcount']);
+
+        // Now try with the user in the second group that must see only one entry.
+        $this->setUser($this->student3);
+        $result = mod_data_external::get_entries($this->data->id);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals(1, $result['totalcount']);
+        $this->assertEquals($entry21, $result['entries'][0]['id']);
+        $this->assertEquals($this->student3->id, $result['entries'][0]['userid']);
+        $this->assertEquals($this->group2->id, $result['entries'][0]['groupid']);
+        $this->assertEquals($this->data->id, $result['entries'][0]['dataid']);
+
+        // Now, as teacher we should see all (we have permissions to view all groups).
+        $this->setUser($this->teacher);
+        $result = mod_data_external::get_entries($this->data->id);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(4, $result['entries']);  // I can see the not approved one.
+        $this->assertEquals(4, $result['totalcount']);
+
+        $entries = $DB->get_records('data_records', array('dataid' => $this->data->id), 'id');
+        $this->assertCount(4, $entries);
+        $count = 0;
+        foreach ($entries as $entry) {
+            $this->assertEquals($entry->id, $result['entries'][$count]['id']);
+            $count++;
+        }
+
+        // Basic test passing the parameter (instead having to calculate it).
+        $this->setUser($this->student1);
+        $result = mod_data_external::get_entries($this->data->id, $this->group1->id);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(2, $result['entries']);
+        $this->assertEquals(2, $result['totalcount']);
+
+        // Test ordering (reverse).
+        $this->setUser($this->student1);
+        $result = mod_data_external::get_entries($this->data->id, $this->group1->id, false, null, 'DESC');
+        $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(2, $result['entries']);
+        $this->assertEquals(2, $result['totalcount']);
+        $this->assertEquals($entry12, $result['entries'][0]['id']);
+
+        // Test pagination.
+        $this->setUser($this->student1);
+        $result = mod_data_external::get_entries($this->data->id, $this->group1->id, false, null, null, 0, 1);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals(2, $result['totalcount']);
+        $this->assertEquals($entry11, $result['entries'][0]['id']);
+
+        $result = mod_data_external::get_entries($this->data->id, $this->group1->id, false, null, null, 1, 1);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals(2, $result['totalcount']);
+        $this->assertEquals($entry12, $result['entries'][0]['id']);
+
+        // Now test the return contents.
+        data_generate_default_template($this->data, 'listtemplate', 0, false, true); // Generate a default list template.
+        $result = mod_data_external::get_entries($this->data->id, $this->group1->id, true, null, null, 0, 2);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(2, $result['entries']);
+        $this->assertEquals(2, $result['totalcount']);
+        $this->assertCount(9, $result['entries'][0]['contents']);
+        $this->assertCount(9, $result['entries'][1]['contents']);
+        // Search for some content.
+        $this->assertTrue(strpos($result['listviewcontents'], 'opt1') !== false);
+        $this->assertTrue(strpos($result['listviewcontents'], 'January') !== false);
+        $this->assertTrue(strpos($result['listviewcontents'], 'menu1') !== false);
+        $this->assertTrue(strpos($result['listviewcontents'], 'text for testing') !== false);
+        $this->assertTrue(strpos($result['listviewcontents'], 'sampleurl') !== false);
+    }
+
+    /**
+     * Test get_entry_visible_groups.
+     */
+    public function test_get_entry_visible_groups() {
+        global $DB;
+
+        $DB->set_field('course', 'groupmode', VISIBLEGROUPS, ['id' => $this->course->id]);
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        // Check I can see my approved group entries.
+        $this->setUser($this->student1);
+        $result = mod_data_external::get_entry($entry11);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertEquals($entry11, $result['entry']['id']);
+        $this->assertTrue($result['entry']['approved']);
+        $this->assertTrue($result['entry']['canmanageentry']); // Is mine, I can manage it.
+
+        // Entry from other group.
+        $result = mod_data_external::get_entry($entry21);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertEquals($entry21, $result['entry']['id']);
+    }
+
+    /**
+     * Test get_entry_separated_groups.
+     */
+    public function test_get_entry_separated_groups() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        // Check I can see my approved group entries.
+        $this->setUser($this->student1);
+        $result = mod_data_external::get_entry($entry11);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertEquals($entry11, $result['entry']['id']);
+        $this->assertTrue($result['entry']['approved']);
+        $this->assertTrue($result['entry']['canmanageentry']); // Is mine, I can manage it.
+
+        // Retrieve contents.
+        data_generate_default_template($this->data, 'singletemplate', 0, false, true);
+        $result = mod_data_external::get_entry($entry11, true);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(9, $result['entry']['contents']);
+        $this->assertTrue(strpos($result['entryviewcontents'], 'opt1') !== false);
+        $this->assertTrue(strpos($result['entryviewcontents'], 'January') !== false);
+        $this->assertTrue(strpos($result['entryviewcontents'], 'menu1') !== false);
+        $this->assertTrue(strpos($result['entryviewcontents'], 'text for testing') !== false);
+        $this->assertTrue(strpos($result['entryviewcontents'], 'sampleurl') !== false);
+
+        // This is in my group but I'm not the author.
+        $result = mod_data_external::get_entry($entry12);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertEquals($entry12, $result['entry']['id']);
+        $this->assertTrue($result['entry']['approved']);
+        $this->assertFalse($result['entry']['canmanageentry']); // Not mine.
+
+        $this->setUser($this->student3);
+        $result = mod_data_external::get_entry($entry21);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertEquals($entry21, $result['entry']['id']);
+        $this->assertTrue($result['entry']['approved']);
+        $this->assertTrue($result['entry']['canmanageentry']); // Is mine, I can manage it.
+
+        // As teacher I should be able to see all the entries.
+        $this->setUser($this->teacher);
+        $result = mod_data_external::get_entry($entry11);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertEquals($entry11, $result['entry']['id']);
+
+        $result = mod_data_external::get_entry($entry12);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertEquals($entry12, $result['entry']['id']);
+        // This is the not approved one.
+        $result = mod_data_external::get_entry($entry13);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertEquals($entry13, $result['entry']['id']);
+
+        $result = mod_data_external::get_entry($entry21);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertEquals($entry21, $result['entry']['id']);
+
+        // Now, try to get an entry not approved yet.
+        $this->setUser($this->student1);
+        $this->expectException('moodle_exception');
+        $result = mod_data_external::get_entry($entry13);
+    }
+
+    /**
+     * Test get_entry from other group in separated groups.
+     */
+    public function test_get_entry_other_group_separated_groups() {
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        // We should not be able to view other gropu entries (in separated groups).
+        $this->setUser($this->student1);
+        $this->expectException('moodle_exception');
+        $result = mod_data_external::get_entry($entry21);
+    }
+
+    /**
+     * Test get_fields.
+     */
+    public function test_get_fields() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        $this->setUser($this->student1);
+        $result = mod_data_external::get_fields($this->data->id);
+        $result = external_api::clean_returnvalue(mod_data_external::get_fields_returns(), $result);
+
+        // Basically compare we retrieve all the fields and the correct values.
+        $fields = $DB->get_records('data_fields', array('dataid' => $this->data->id), 'id');
+        foreach ($result['fields'] as $field) {
+            $this->assertEquals($field, (array) $fields[$field['id']]);
+        }
+    }
+
+    /**
+     * Test search_entries.
+     */
+    public function test_search_entries() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        // First do a normal text search as student 1. I should see my two group entries.
+        $this->setUser($this->student1);
+        $result = mod_data_external::search_entries($this->data->id, 0, false, 'text');
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(2, $result['entries']);
+        $this->assertEquals(2, $result['totalcount']);
+
+        // Now as the other student I should receive my not approved entry. Apply ordering here.
+        $this->setUser($this->student2);
+        $result = mod_data_external::search_entries($this->data->id, 0, false, 'text', [], DATA_APPROVED, 'ASC');
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(3, $result['entries']);
+        $this->assertEquals(3, $result['totalcount']);
+        // The not approved one should be the first.
+        $this->assertEquals($entry13, $result['entries'][0]['id']);
+
+        // Now as the other group student.
+        $this->setUser($this->student3);
+        $result = mod_data_external::search_entries($this->data->id, 0, false, 'text');
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals(1, $result['totalcount']);
+        $this->assertEquals($this->student3->id, $result['entries'][0]['userid']);
+
+        // Same normal text search as teacher.
+        $this->setUser($this->teacher);
+        $result = mod_data_external::search_entries($this->data->id, 0, false, 'text');
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(4, $result['entries']);  // I can see all groups and non approved.
+        $this->assertEquals(4, $result['totalcount']);
+
+        // Pagination.
+        $this->setUser($this->teacher);
+        $result = mod_data_external::search_entries($this->data->id, 0, false, 'text', [], DATA_TIMEADDED, 'ASC', 0, 2);
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(2, $result['entries']);  // Only 2 per page.
+        $this->assertEquals(4, $result['totalcount']);
+
+        // Now advanced search or not dinamic fields (user firstname for example).
+        $this->setUser($this->student1);
+        $advsearch = [
+            ['name' => 'fn', 'value' => json_encode($this->student2->firstname)]
+        ];
+        $result = mod_data_external::search_entries($this->data->id, 0, false, '', $advsearch);
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals(1, $result['totalcount']);
+        $this->assertEquals($this->student2->id, $result['entries'][0]['userid']);  // I only found mine!
+
+        // Advanced search for fields.
+        $field = $DB->get_record('data_fields', array('type' => 'url'));
+        $advsearch = [
+            ['name' => 'f_' . $field->id , 'value' => 'sampleurl']
+        ];
+        $result = mod_data_external::search_entries($this->data->id, 0, false, '', $advsearch);
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(2, $result['entries']);  // Found two entries matching this.
+        $this->assertEquals(2, $result['totalcount']);
+
+        // Combined search.
+        $field2 = $DB->get_record('data_fields', array('type' => 'number'));
+        $advsearch = [
+            ['name' => 'f_' . $field->id , 'value' => 'sampleurl'],
+            ['name' => 'f_' . $field2->id , 'value' => '12345'],
+            ['name' => 'ln', 'value' => json_encode($this->student2->lastname)]
+        ];
+        $result = mod_data_external::search_entries($this->data->id, 0, false, '', $advsearch);
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);  // Only one matching everything.
+        $this->assertEquals(1, $result['totalcount']);
+
+        // Combined search (no results).
+        $field2 = $DB->get_record('data_fields', array('type' => 'number'));
+        $advsearch = [
+            ['name' => 'f_' . $field->id , 'value' => 'sampleurl'],
+            ['name' => 'f_' . $field2->id , 'value' => '98780333'], // Non existent number.
+        ];
+        $result = mod_data_external::search_entries($this->data->id, 0, false, '', $advsearch);
+        $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);  // Only one matching everything.
+        $this->assertEquals(0, $result['totalcount']);
+    }
+
+    /**
+     * Test approve_entry.
+     */
+    public function test_approve_entry() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        $this->setUser($this->teacher);
+        $this->assertEquals(0, $DB->get_field('data_records', 'approved', array('id' => $entry13)));
+        $result = mod_data_external::approve_entry($entry13);
+        $result = external_api::clean_returnvalue(mod_data_external::approve_entry_returns(), $result);
+        $this->assertEquals(1, $DB->get_field('data_records', 'approved', array('id' => $entry13)));
+    }
+
+    /**
+     * Test unapprove_entry.
+     */
+    public function test_unapprove_entry() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        $this->setUser($this->teacher);
+        $this->assertEquals(1, $DB->get_field('data_records', 'approved', array('id' => $entry11)));
+        $result = mod_data_external::approve_entry($entry11, false);
+        $result = external_api::clean_returnvalue(mod_data_external::approve_entry_returns(), $result);
+        $this->assertEquals(0, $DB->get_field('data_records', 'approved', array('id' => $entry11)));
+    }
+
+    /**
+     * Test approve_entry missing permissions.
+     */
+    public function test_approve_entry_missing_permissions() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        $this->setUser($this->student1);
+        $this->expectException('moodle_exception');
+        mod_data_external::approve_entry($entry13);
+    }
+
+    /**
+     * Test delete_entry as teacher. Check I can delete any entry.
+     */
+    public function test_delete_entry_as_teacher() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        $this->setUser($this->teacher);
+        $result = mod_data_external::delete_entry($entry11);
+        $result = external_api::clean_returnvalue(mod_data_external::delete_entry_returns(), $result);
+        $this->assertEquals(0, $DB->count_records('data_records', array('id' => $entry11)));
+
+        // Entry in other group.
+        $result = mod_data_external::delete_entry($entry21);
+        $result = external_api::clean_returnvalue(mod_data_external::delete_entry_returns(), $result);
+        $this->assertEquals(0, $DB->count_records('data_records', array('id' => $entry21)));
+    }
+
+    /**
+     * Test delete_entry as student. Check I can delete my own entries.
+     */
+    public function test_delete_entry_as_student() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        $this->setUser($this->student1);
+        $result = mod_data_external::delete_entry($entry11);
+        $result = external_api::clean_returnvalue(mod_data_external::delete_entry_returns(), $result);
+        $this->assertEquals(0, $DB->count_records('data_records', array('id' => $entry11)));
+    }
+
+    /**
+     * Test delete_entry as student in read only mode period. Check I cannot delete my own entries in that period.
+     */
+    public function test_delete_entry_as_student_in_read_only_period() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+        // Set a time period.
+        $this->data->timeviewfrom = time() - HOURSECS;
+        $this->data->timeviewto = time() + HOURSECS;
+        $DB->update_record('data', $this->data);
+
+        $this->setUser($this->student1);
+        $this->expectException('moodle_exception');
+        mod_data_external::delete_entry($entry11);
+    }
+
+    /**
+     * Test delete_entry with an user missing permissions.
+     */
+    public function test_delete_entry_missing_permissions() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        $this->setUser($this->student1);
+        $this->expectException('moodle_exception');
+        mod_data_external::delete_entry($entry21);
+    }
+
+    /**
+     * Test add_entry.
+     */
+    public function test_add_entry() {
+        global $DB;
+        // First create the record structure and add some entries.
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        $this->setUser($this->student1);
+        $newentrydata = [];
+        $fields = $DB->get_records('data_fields', array('dataid' => $this->data->id), 'id');
+        // Prepare the new entry data.
+        foreach ($fields as $field) {
+            $subfield = $value = '';
+
+            switch ($field->type) {
+                case 'checkbox':
+                    $value = ['opt1', 'opt2'];
+                    break;
+                case 'date':
+                    // Add two extra.
+                    $newentrydata[] = [
+                        'fieldid' => $field->id,
+                        'subfield' => 'day',
+                        'value' => json_encode('5')
+                    ];
+                    $newentrydata[] = [
+                        'fieldid' => $field->id,
+                        'subfield' => 'month',
+                        'value' => json_encode('1')
+                    ];
+                    $subfield = 'year';
+                    $value = '1981';
+                    break;
+                case 'menu':
+                    $value = 'menu1';
+                    break;
+                case 'multimenu':
+                    $value = ['multimenu1', 'multimenu4'];
+                    break;
+                case 'number':
+                    $value = 6;
+                    break;
+                case 'radiobutton':
+                    $value = 'radioopt1';
+                    break;
+                case 'text':
+                    $value = 'some text';
+                    break;
+                case 'textarea':
+                    $newentrydata[] = [
+                        'fieldid' => $field->id,
+                        'subfield' => 'content1',
+                        'value' => json_encode(FORMAT_MOODLE)
+                    ];
+                    $newentrydata[] = [
+                        'fieldid' => $field->id,
+                        'subfield' => 'itemid',
+                        'value' => json_encode(0)
+                    ];
+                    $value = 'more text';
+                    break;
+                case 'url':
+                    $value = 'https://moodle.org';
+                    $subfield = 0;
+                    break;
+            }
+
+            $newentrydata[] = [
+                'fieldid' => $field->id,
+                'subfield' => $subfield,
+                'value' => json_encode($value)
+            ];
+        }
+        $result = mod_data_external::add_entry($this->data->id, 0, $newentrydata);
+        $result = external_api::clean_returnvalue(mod_data_external::add_entry_returns(), $result);
+
+        $newentryid = $result['newentryid'];
+        $result = mod_data_external::get_entry($newentryid, true);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertEquals($this->student1->id, $result['entry']['userid']);
+        $this->assertCount(9, $result['entry']['contents']);
+        foreach ($result['entry']['contents'] as $content) {
+            $field = $fields[$content['fieldid']];
+            // Stored content same that the one retrieved by WS.
+            $dbcontent = $DB->get_record('data_content', array('fieldid' => $field->id, 'recordid' => $newentryid));
+            $this->assertEquals($dbcontent->content, $content['content']);
+
+            // Now double check everything stored is correct.
+            if ($field->type == 'checkbox') {
+                $this->assertEquals('opt1##opt2', $content['content']);
+                continue;
+            }
+            if ($field->type == 'date') {
+                $this->assertEquals(347500800, $content['content']); // Date in gregorian format.
+                continue;
+            }
+            if ($field->type == 'menu') {
+                $this->assertEquals('menu1', $content['content']);
+                continue;
+            }
+            if ($field->type == 'multimenu') {
+                $this->assertEquals('multimenu1##multimenu4', $content['content']);
+                continue;
+            }
+            if ($field->type == 'number') {
+                $this->assertEquals(6, $content['content']);
+                continue;
+            }
+            if ($field->type == 'radiobutton') {
+                $this->assertEquals('radioopt1', $content['content']);
+                continue;
+            }
+            if ($field->type == 'text') {
+                $this->assertEquals('some text', $content['content']);
+                continue;
+            }
+            if ($field->type == 'textarea') {
+                $this->assertEquals('more text', $content['content']);
+                $this->assertEquals(FORMAT_MOODLE, $content['content1']);
+                continue;
+            }
+            if ($field->type == 'url') {
+                $this->assertEquals('https://moodle.org', $content['content']);
+                continue;
+            }
+            $this->assertEquals('multimenu1##multimenu4', $content['content']);
+        }
+
+        // Now, try to add another entry but removing some required data.
+        unset($newentrydata[0]);
+        $result = mod_data_external::add_entry($this->data->id, 0, $newentrydata);
+        $result = external_api::clean_returnvalue(mod_data_external::add_entry_returns(), $result);
+        $this->assertEquals(0, $result['newentryid']);
+        $this->assertCount(0, $result['generalnotifications']);
+        $this->assertCount(1, $result['fieldnotifications']);
+        $this->assertEquals('field-1', $result['fieldnotifications'][0]['fieldname']);
+        $this->assertEquals(get_string('errormustsupplyvalue', 'data'), $result['fieldnotifications'][0]['notification']);
+    }
+
+    /**
+     * Test add_entry empty_form.
+     */
+    public function test_add_entry_empty_form() {
+        $result = mod_data_external::add_entry($this->data->id, 0, []);
+        $result = external_api::clean_returnvalue(mod_data_external::add_entry_returns(), $result);
+        $this->assertEquals(0, $result['newentryid']);
+        $this->assertCount(1, $result['generalnotifications']);
+        $this->assertCount(0, $result['fieldnotifications']);
+        $this->assertEquals(get_string('emptyaddform', 'data'), $result['generalnotifications'][0]);
+    }
+
+    /**
+     * Test add_entry read_only_period.
+     */
+    public function test_add_entry_read_only_period() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+        // Set a time period.
+        $this->data->timeviewfrom = time() - HOURSECS;
+        $this->data->timeviewto = time() + HOURSECS;
+        $DB->update_record('data', $this->data);
+
+        $this->setUser($this->student1);
+        $this->expectExceptionMessage(get_string('noaccess', 'data'));
+        $this->expectException('moodle_exception');
+        mod_data_external::add_entry($this->data->id, 0, []);
+    }
+
+    /**
+     * Test add_entry max_num_entries.
+     */
+    public function test_add_entry_max_num_entries() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+        // Set a time period.
+        $this->data->maxentries = 1;
+        $DB->update_record('data', $this->data);
+
+        $this->setUser($this->student1);
+        $this->expectExceptionMessage(get_string('noaccess', 'data'));
+        $this->expectException('moodle_exception');
+        mod_data_external::add_entry($this->data->id, 0, []);
+    }
+
+    /**
+     * Test update_entry.
+     */
+    public function test_update_entry() {
+        global $DB;
+        // First create the record structure and add some entries.
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        $this->setUser($this->student1);
+        $newentrydata = [];
+        $fields = $DB->get_records('data_fields', array('dataid' => $this->data->id), 'id');
+        // Prepare the new entry data.
+        foreach ($fields as $field) {
+            $subfield = $value = '';
+
+            switch ($field->type) {
+                case 'checkbox':
+                    $value = ['opt1', 'opt2'];
+                    break;
+                case 'date':
+                    // Add two extra.
+                    $newentrydata[] = [
+                        'fieldid' => $field->id,
+                        'subfield' => 'day',
+                        'value' => json_encode('5')
+                    ];
+                    $newentrydata[] = [
+                        'fieldid' => $field->id,
+                        'subfield' => 'month',
+                        'value' => json_encode('1')
+                    ];
+                    $subfield = 'year';
+                    $value = '1981';
+                    break;
+                case 'menu':
+                    $value = 'menu1';
+                    break;
+                case 'multimenu':
+                    $value = ['multimenu1', 'multimenu4'];
+                    break;
+                case 'number':
+                    $value = 6;
+                    break;
+                case 'radiobutton':
+                    $value = 'radioopt2';
+                    break;
+                case 'text':
+                    $value = 'some text';
+                    break;
+                case 'textarea':
+                    $newentrydata[] = [
+                        'fieldid' => $field->id,
+                        'subfield' => 'content1',
+                        'value' => json_encode(FORMAT_MOODLE)
+                    ];
+                    $newentrydata[] = [
+                        'fieldid' => $field->id,
+                        'subfield' => 'itemid',
+                        'value' => json_encode(0)
+                    ];
+                    $value = 'more text';
+                    break;
+                case 'url':
+                    $value = 'https://moodle.org';
+                    $subfield = 0;
+                    break;
+            }
+
+            $newentrydata[] = [
+                'fieldid' => $field->id,
+                'subfield' => $subfield,
+                'value' => json_encode($value)
+            ];
+        }
+        $result = mod_data_external::update_entry($entry11, $newentrydata);
+        $result = external_api::clean_returnvalue(mod_data_external::update_entry_returns(), $result);
+        $this->assertTrue($result['updated']);
+        $this->assertCount(0, $result['generalnotifications']);
+        $this->assertCount(0, $result['fieldnotifications']);
+
+        $result = mod_data_external::get_entry($entry11, true);
+        $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result);
+        $this->assertEquals($this->student1->id, $result['entry']['userid']);
+        $this->assertCount(9, $result['entry']['contents']);
+        foreach ($result['entry']['contents'] as $content) {
+            $field = $fields[$content['fieldid']];
+            // Stored content same that the one retrieved by WS.
+            $dbcontent = $DB->get_record('data_content', array('fieldid' => $field->id, 'recordid' => $entry11));
+            $this->assertEquals($dbcontent->content, $content['content']);
+
+            // Now double check everything stored is correct.
+            if ($field->type == 'checkbox') {
+                $this->assertEquals('opt1##opt2', $content['content']);
+                continue;
+            }
+            if ($field->type == 'date') {
+                $this->assertEquals(347500800, $content['content']); // Date in gregorian format.
+                continue;
+            }
+            if ($field->type == 'menu') {
+                $this->assertEquals('menu1', $content['content']);
+                continue;
+            }
+            if ($field->type == 'multimenu') {
+                $this->assertEquals('multimenu1##multimenu4', $content['content']);
+                continue;
+            }
+            if ($field->type == 'number') {
+                $this->assertEquals(6, $content['content']);
+                continue;
+            }
+            if ($field->type == 'radiobutton') {
+                $this->assertEquals('radioopt2', $content['content']);
+                continue;
+            }
+            if ($field->type == 'text') {
+                $this->assertEquals('some text', $content['content']);
+                continue;
+            }
+            if ($field->type == 'textarea') {
+                $this->assertEquals('more text', $content['content']);
+                $this->assertEquals(FORMAT_MOODLE, $content['content1']);
+                continue;
+            }
+            if ($field->type == 'url') {
+                $this->assertEquals('https://moodle.org', $content['content']);
+                continue;
+            }
+            $this->assertEquals('multimenu1##multimenu4', $content['content']);
+        }
+
+        // Now, try to update the entry but removing some required data.
+        unset($newentrydata[0]);
+        $result = mod_data_external::update_entry($entry11, $newentrydata);
+        $result = external_api::clean_returnvalue(mod_data_external::update_entry_returns(), $result);
+        $this->assertFalse($result['updated']);
+        $this->assertCount(0, $result['generalnotifications']);
+        $this->assertCount(1, $result['fieldnotifications']);
+        $this->assertEquals('field-1', $result['fieldnotifications'][0]['fieldname']);
+        $this->assertEquals(get_string('errormustsupplyvalue', 'data'), $result['fieldnotifications'][0]['notification']);
+    }
+
+    /**
+     * Test update_entry sending empty data.
+     */
+    public function test_update_entry_empty_data() {
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+
+        $this->setUser($this->student1);
+        $result = mod_data_external::update_entry($entry11, []);
+        $result = external_api::clean_returnvalue(mod_data_external::update_entry_returns(), $result);
+        $this->assertFalse($result['updated']);
+        $this->assertCount(1, $result['generalnotifications']);
+        $this->assertCount(9, $result['fieldnotifications']);
+        $this->assertEquals(get_string('emptyaddform', 'data'), $result['generalnotifications'][0]);
+    }
+
+    /**
+     * Test update_entry in read only period.
+     */
+    public function test_update_entry_read_only_period() {
+        global $DB;
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+        // Set a time period.
+        $this->data->timeviewfrom = time() - HOURSECS;
+        $this->data->timeviewto = time() + HOURSECS;
+        $DB->update_record('data', $this->data);
+
+        $this->setUser($this->student1);
+        $this->expectExceptionMessage(get_string('noaccess', 'data'));
+        $this->expectException('moodle_exception');
+        mod_data_external::update_entry($entry11, []);
+    }
+
+    /**
+     * Test update_entry other_user.
+     */
+    public function test_update_entry_other_user() {
+        // Try to update other user entry.
+        list($entry11, $entry12, $entry13, $entry21) = self::populate_database_with_entries();
+        $this->setUser($this->student2);
+        $this->expectExceptionMessage(get_string('noaccess', 'data'));
+        $this->expectException('moodle_exception');
+        mod_data_external::update_entry($entry11, []);
+    }
 }
index 37b8de3..57527f9 100644 (file)
@@ -921,4 +921,77 @@ class mod_data_lib_testcase extends advanced_testcase {
         $completiondata = $completion->get_data($cm);
         $this->assertEquals(1, $completiondata->completionstate);
     }
+
+    /**
+     * 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,
+        );
+        $data = $this->getDataGenerator()->create_module('data', $record);
+        $cm = get_coursemodule_from_instance('data', $data->id, $course->id);
+        $cm = cm_info::create($cm);
+        $this->setUser($student);
+
+        // Check that upon creation, the updates are only about the new configuration created.
+        $onehourago = time() - HOURSECS;
+        $updates = data_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);
+            }
+        }
+
+        // Add a couple of entries.
+        $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+        $fieldtypes = array('checkbox', 'date');
+
+        $count = 1;
+        // Creating test Fields with default parameter values.
+        foreach ($fieldtypes as $fieldtype) {
+            // Creating variables dynamically.
+            $fieldname = 'field-' . $count;
+            $record = new StdClass();
+            $record->name = $fieldname;
+            $record->type = $fieldtype;
+            $record->required = 1;
+
+            ${$fieldname} = $datagenerator->create_field($record, $data);
+            $count++;
+        }
+
+        $fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id');
+
+        $contents = array();
+        $contents[] = array('opt1', 'opt2', 'opt3', 'opt4');
+        $contents[] = '01-01-2037'; // It should be lower than 2038, to avoid failing on 32-bit windows.
+        $count = 0;
+        $fieldcontents = array();
+        foreach ($fields as $fieldrecord) {
+            $fieldcontents[$fieldrecord->id] = $contents[$count++];
+        }
+
+        $datarecor1did = $datagenerator->create_entry($data, $fieldcontents);
+        $datarecor2did = $datagenerator->create_entry($data, $fieldcontents);
+        $records = $DB->get_records('data_records', array('dataid' => $data->id));
+        $this->assertCount(2, $records);
+        // Check we received the entries updated.
+        $updates = data_check_updates_since($cm, $onehourago);
+        $this->assertTrue($updates->entries->updated);
+        $this->assertEquals([$datarecor1did, $datarecor2did], $updates->entries->itemids, '', 0, 10, true);
+    }
 }
index 401dee1..7e3a1c2 100644 (file)
@@ -6,6 +6,10 @@ information provided here is intended especially for developers.
 * External function get_databases_by_courses now return more fields for users with mod/data:viewentry capability enabled:
     maxentries, rssarticles, singletemplate, listtemplate, listtemplateheader, listtemplatefooter, addtemplate,
     rsstemplate, rsstitletemplate, csstemplate, jstemplate, asearchtemplate, approval, defaultsort, defaultsortdir, manageapproved.
+* Data field classes extending data_field_base should implement the get_config_for_external method.
+    This method is used for returning the field settings for external functions.
+    You should check the user capabilities of the current user before returning any field setting value.
+    This is intended to protect field settings like private keys for external systems.
 
 === 3.2 ===
 
index 43d1f0e..1fe73d9 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016120503;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016120510;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2016112900;       // Requires this Moodle version
 $plugin->component = 'mod_data';       // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 49b6db5..f530a11 100644 (file)
@@ -23,7 +23,7 @@
 ///////////////////////////////////////////////////////////////////////////
 
     require_once(__DIR__ . '/../../config.php');
-    require_once($CFG->dirroot . '/mod/data/lib.php');
+    require_once($CFG->dirroot . '/mod/data/locallib.php');
     require_once($CFG->libdir . '/rsslib.php');
 
 /// One of these is necessary!
 
     if (!empty($advanced)) {
         $search = '';
-        $vals = array();
-        $fields = $DB->get_records('data_fields', array('dataid'=>$data->id));
 
         //Added to ammend paging error. This error would occur when attempting to go from one page of advanced
         //search results to another.  All fields were reset in the page transfer, and there was no way of determining
         else {
             $paging = true;
         }
-        if (!empty($fields)) {
-            foreach($fields as $field) {
-                $searchfield = data_get_field_from_id($field->id, $data);
-                //Get field data to build search sql with.  If paging is false, get from user.
-                //If paging is true, get data from $search_array which is obtained from the $SESSION (see line 116).
-                if(!$paging) {
-                    $val = $searchfield->parse_search_field();
-                } else {
-                    //Set value from session if there is a value @ the required index.
-                    if (isset($search_array[$field->id])) {
-                        $val = $search_array[$field->id]->data;
-                    } else {             //If there is not an entry @ the required index, set value to blank.
-                        $val = '';
-                    }
-                }
-                if (!empty($val)) {
-                    $search_array[$field->id] = new stdClass();
-                    list($search_array[$field->id]->sql, $search_array[$field->id]->params) = $searchfield->generate_sql('c'.$field->id, $val);
-                    $search_array[$field->id]->data = $val;
-                    $vals[] = $val;
-                } else {
-                    // clear it out
-                    unset($search_array[$field->id]);
-                }
-            }
-        }
-
-        if (!$paging) {
-            // name searching
-            $fn = optional_param('u_fn', '', PARAM_NOTAGS);
-            $ln = optional_param('u_ln', '', PARAM_NOTAGS);
-        } else {
-            $fn = isset($search_array[DATA_FIRSTNAME]) ? $search_array[DATA_FIRSTNAME]->data : '';
-            $ln = isset($search_array[DATA_LASTNAME]) ? $search_array[DATA_LASTNAME]->data : '';
-        }
-        if (!empty($fn)) {
-            $search_array[DATA_FIRSTNAME] = new stdClass();
-            $search_array[DATA_FIRSTNAME]->sql    = '';
-            $search_array[DATA_FIRSTNAME]->params = array();
-            $search_array[DATA_FIRSTNAME]->field  = 'u.firstname';
-            $search_array[DATA_FIRSTNAME]->data   = $fn;
-            $vals[] = $fn;
-        } else {
-            unset($search_array[DATA_FIRSTNAME]);
-        }
-        if (!empty($ln)) {
-            $search_array[DATA_LASTNAME] = new stdClass();
-            $search_array[DATA_LASTNAME]->sql     = '';
-            $search_array[DATA_LASTNAME]->params = array();
-            $search_array[DATA_LASTNAME]->field   = 'u.lastname';
-            $search_array[DATA_LASTNAME]->data    = $ln;
-            $vals[] = $ln;
-        } else {
-            unset($search_array[DATA_LASTNAME]);
-        }
 
-        $SESSION->dataprefs[$data->id]['search_array'] = $search_array;     // Make it sticky
-
-        // in case we want to switch to simple search later - there might be multiple values there ;-)
-        if ($vals) {
-            $val = reset($vals);
-            if (is_string($val)) {
-                $search = $val;
-            }
-        }
+        // Now build the advanced search array.
+        list($search_array, $search) = data_build_search_array($data, $paging, $search_array);
+        $SESSION->dataprefs[$data->id]['search_array'] = $search_array;     // Make it sticky.
 
     } else {
         $search = optional_param('search', $SESSION->dataprefs[$data->id]['search'], PARAM_NOTAGS);
         $search = '';
     }
 
-    if (core_text::strlen($search) < 2) {
-        $search = '';
-    }
     $SESSION->dataprefs[$data->id]['search'] = $search;   // Make it sticky
 
     $sort = optional_param('sort', $SESSION->dataprefs[$data->id]['sort'], PARAM_INT);
     $currentgroup = groups_get_activity_group($cm, true);
     $groupmode = groups_get_activity_groupmode($cm);
     $canmanageentries = has_capability('mod/data:manageentries', $context);
-    // If a student is not part of a group and seperate groups is enabled, we don't
-    // want them seeing all records.
-    if ($currentgroup == 0 && $groupmode == 1 && !$canmanageentries) {
-        $canviewallrecords = false;
-    } else {
-        $canviewallrecords = true;
-    }
 
-    // detect entries not approved yet and show hint instead of not found error
-    if ($record and $data->approval and !$record->approved and $record->userid != $USER->id and !$canmanageentries) {
-        if (!$currentgroup or $record->groupid == $currentgroup or $record->groupid == 0) {
-            print_error('notapproved', 'data');
-        }
+
+    // Detect entries not approved yet and show hint instead of not found error.
+    if ($record and !data_can_view_record($data, $record, $currentgroup, $canmanageentries)) {
+        print_error('notapproved', 'data');
     }
 
     echo $OUTPUT->heading(format_string($data->name), 2);
@@ -458,19 +384,14 @@ if ($showactivity) {
 
     } else {
         // Approve or disapprove any requested records
-        $params = array(); // named params array
-
         $approvecap = has_capability('mod/data:approve', $context);
 
         if (($approve || $disapprove) && confirm_sesskey() && $approvecap) {
-            $newapproved = $approve ? 1 : 0;
+            $newapproved = $approve ? true : false;
             $recordid = $newapproved ? $approve : $disapprove;
             if ($approverecord = $DB->get_record('data_records', array('id' => $recordid))) {   // Need to check this is valid
                 if ($approverecord->dataid == $data->id) {                       // Must be from this database
-                    $newrecord = new stdClass();
-                    $newrecord->id = $approverecord->id;
-                    $newrecord->approved = $newapproved;
-                    $DB->update_record('data_records', $newrecord);
+                    data_approve_entry($approverecord->id, $newapproved);
                     $msgkey = $newapproved ? 'recordapproved' : 'recorddisapproved';
                     echo $OUTPUT->notification(get_string($msgkey, 'data'), 'notifysuccess');
                 }
@@ -492,228 +413,15 @@ if ($showactivity) {
             $requiredentries_allowed = false;
         }
 
-        // Initialise the first group of params for advanced searches.
-        $initialparams   = array();
-
-    /// setup group and approve restrictions
-        if (!$approvecap && $data->approval) {
-            if (isloggedin()) {
-                $approveselect = ' AND (r.approved=1 OR r.userid=:myid1) ';
-                $params['myid1'] = $USER->id;
-                $initialparams['myid1'] = $params['myid1'];
-            } else {
-                $approveselect = ' AND r.approved=1 ';
-            }
-        } else {
-            $approveselect = ' ';
-        }
-
-        if ($currentgroup) {
-            $groupselect = " AND (r.groupid = :currentgroup OR r.groupid = 0)";
-            $params['currentgroup'] = $currentgroup;
-            $initialparams['currentgroup'] = $params['currentgroup'];
-        } else {
-            if ($canviewallrecords) {
-                $groupselect = ' ';
-            } else {
-                // If separate groups are enabled and the user isn't in a group or
-                // a teacher, manager, admin etc, then just show them entries for 'All participants'.
-                $groupselect = " AND r.groupid = 0";
-            }
-        }
-
-        // Init some variables to be used by advanced search
-        $advsearchselect = '';
-        $advwhere        = '';
-        $advtables       = '';
-        $advparams       = array();
-        // This is used for the initial reduction of advanced search results with required entries.
-        $entrysql        = '';
-        $namefields = user_picture::fields('u');
-        // Remove the id from the string. This already exists in the sql statement.
-        $namefields = str_replace('u.id,', '', $namefields);
-
-    /// Find the field we are sorting on
-        if ($sort <= 0 or !$sortfield = data_get_field_from_id($sort, $data)) {
-
-            switch ($sort) {
-                case DATA_LASTNAME:
-                    $ordering = "u.lastname $order, u.firstname $order";
-                    break;
-                case DATA_FIRSTNAME:
-                    $ordering = "u.firstname $order, u.lastname $order";
-                    break;
-                case DATA_APPROVED:
-                    $ordering = "r.approved $order, r.timecreated $order";
-                    break;
-                case DATA_TIMEMODIFIED:
-                    $ordering = "r.timemodified $order";
-                    break;
-                case DATA_TIMEADDED:
-                default:
-                    $sort     = 0;
-                    $ordering = "r.timecreated $order";
-            }
-
-            $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, ' . $namefields;
-            $count = ' COUNT(DISTINCT c.recordid) ';
-            $tables = '{data_content} c,{data_records} r, {user} u ';
-            $where =  'WHERE c.recordid = r.id
-                         AND r.dataid = :dataid
-                         AND r.userid = u.id ';
-            $params['dataid'] = $data->id;
-            $sortorder = " ORDER BY $ordering, r.id $order";
-            $searchselect = '';
-
-            // If requiredentries is not reached, only show current user's entries
-            if (!$requiredentries_allowed) {
-                $where .= ' AND u.id = :myid2 ';
-                $entrysql = ' AND r.userid = :myid3 ';
-                $params['myid2'] = $USER->id;
-                $initialparams['myid3'] = $params['myid2'];
-            }
-
-            if (!empty($advanced)) {                                                  //If advanced box is checked.
-                $i = 0;
-                foreach($search_array as $key => $val) {                              //what does $search_array hold?
-                    if ($key == DATA_FIRSTNAME or $key == DATA_LASTNAME) {
-                        $i++;
-                        $searchselect .= " AND ".$DB->sql_like($val->field, ":search_flname_$i", false);
-                        $params['search_flname_'.$i] = "%$val->data%";
-                        continue;
-                    }
-                    $advtables .= ', {data_content} c'.$key.' ';
-                    $advwhere .= ' AND c'.$key.'.recordid = r.id';
-                    $advsearchselect .= ' AND ('.$val->sql.') ';
-                    $advparams = array_merge($advparams, $val->params);
-                }
-            } else if ($search) {
-                $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)."
-                                  OR ".$DB->sql_like('u.firstname', ':search2', false)."
-                                  OR ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
-                $params['search1'] = "%$search%";
-                $params['search2'] = "%$search%";
-                $params['search3'] = "%$search%";
-            } else {
-                $searchselect = ' ';
-            }
-
-        } else {
-
-            $sortcontent = $DB->sql_compare_text('c.' . $sortfield->get_sort_field());
-            $sortcontentfull = $sortfield->get_sort_sql($sortcontent);
-
-            $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, ' . $namefields . ',
-                    ' . $sortcontentfull . ' AS sortorder ';
-            $count = ' COUNT(DISTINCT c.recordid) ';
-            $tables = '{data_content} c, {data_records} r, {user} u ';
-            $where =  'WHERE c.recordid = r.id
-                         AND r.dataid = :dataid
-                         AND r.userid = u.id ';
-            if (!$advanced) {
-                $where .=  'AND c.fieldid = :sort';
-            }
-            $params['dataid'] = $data->id;
-            $params['sort'] = $sort;
-            $sortorder = ' ORDER BY sortorder '.$order.' , r.id ASC ';
-            $searchselect = '';
-
-            // If requiredentries is not reached, only show current user's entries
-            if (!$requiredentries_allowed) {
-                $where .= ' AND u.id = :myid2';
-                $entrysql = ' AND r.userid = :myid3';
-                $params['myid2'] = $USER->id;
-                $initialparams['myid3'] = $params['myid2'];
-            }
-            $i = 0;
-            if (!empty($advanced)) {                                                  //If advanced box is checked.
-                foreach($search_array as $key => $val) {                              //what does $search_array hold?
-                    if ($key == DATA_FIRSTNAME or $key == DATA_LASTNAME) {
-                        $i++;
-                        $searchselect .= " AND ".$DB->sql_like($val->field, ":search_flname_$i", false);
-                        $params['search_flname_'.$i] = "%$val->data%";
-                        continue;
-                    }
-                    $advtables .= ', {data_content} c'.$key.' ';
-                    $advwhere .= ' AND c'.$key.'.recordid = r.id AND c'.$key.'.fieldid = '.$key;
-                    $advsearchselect .= ' AND ('.$val->sql.') ';
-                    $advparams = array_merge($advparams, $val->params);
-                }
-            } else if ($search) {
-                $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR ".$DB->sql_like('u.firstname', ':search2', false)." OR ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
-                $params['search1'] = "%$search%";
-                $params['search2'] = "%$search%";
-                $params['search3'] = "%$search%";
-            } else {
-                $searchselect = ' ';
-            }
-        }
-
-    /// To actually fetch the records
-
-        $fromsql    = "FROM $tables $advtables $where $advwhere $groupselect $approveselect $searchselect $advsearchselect";
-        $allparams  = array_merge($params, $advparams);
-
-        // Provide initial sql statements and parameters to reduce the number of total records.
-        $initialselect = $groupselect . $approveselect . $entrysql;
-
-        $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams);
-        $newrecordids = data_get_advance_search_ids($recordids, $search_array, $data->id);
-        $totalcount = count($newrecordids);
-        $selectdata = $where . $groupselect . $approveselect;
-
-        if (!empty($advanced)) {
-            $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
-            $sqlselect = $advancedsearchsql['sql'];
-            $allparams = array_merge($allparams, $advancedsearchsql['params']);
-        } else {
-            $sqlselect  = "SELECT $what $fromsql $sortorder";
-        }
-
-        /// Work out the paging numbers and counts
-        if (empty($searchselect) && empty($advsearchselect)) {
-            $maxcount = $totalcount;
-        } else {
-            $maxcount = count($recordids);
-        }
-
-        if ($record) {     // We need to just show one, so where is it in context?
-            $nowperpage = 1;
-            $mode = 'single';
-            $page = 0;
-            // TODO MDL-33797 - Reduce this or consider redesigning the paging system.
-            if ($allrecordids = $DB->get_fieldset_sql($sqlselect, $allparams)) {
-                $page = (int)array_search($record->id, $allrecordids);
-                unset($allrecordids);
-            }
-        } else if ($mode == 'single') {  // We rely on ambient $page settings
-            $nowperpage = 1;
-
-        } else {
-            $nowperpage = $perpage;
-        }
+        // Search for entries.
+        list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) =
+            data_search_entries($data, $cm, $context, $mode, $currentgroup, $search, $sort, $order, $page, $perpage, $advanced, $search_array, $record);
 
         // Advanced search form doesn't make sense for single (redirects list view).
         if ($maxcount && $mode != 'single') {
             data_print_preference_form($data, $perpage, $search, $sort, $order, $search_array, $advanced, $mode);
         }
 
-    /// Get the actual records
-
-        if (!$records = $DB->get_records_sql($sqlselect, $allparams, $page * $nowperpage, $nowperpage)) {
-            // Nothing to show!
-            if ($record) {         // Something was requested so try to show that at least (bug 5132)
-                if ($canmanageentries || empty($data->approval) ||
-                         $record->approved || (isloggedin() && $record->userid == $USER->id)) {
-                    if (!$currentgroup || $record->groupid == $currentgroup || $record->groupid == 0) {
-                        // OK, we can show this one
-                        $records = array($record->id => $record);
-                        $totalcount = 1;
-                    }
-                }
-            }
-        }
-
         if (empty($records)) {
             if ($maxcount){
                 $a = new stdClass();
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..5f4c63d 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,168 @@ 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) {
+            $perpage = false;
+            $page = false;
+        } else {
+            $perpage = $params['perpage'];
+            $page = $perpage * $params['page'];
+        }
+        $users = feedback_get_incomplete_users($cm, $groupid, $params['sort'], $page, $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 82c5ffb..72c1526 100644 (file)
@@ -2023,9 +2023,9 @@ class mod_forum_lib_testcase extends advanced_testcase {
         $cm = get_coursemodule_from_instance('forum', $forum->id);
 
         // Create groups.
-        $group1 = self::getDataGenerator()->create_group(array('courseid' => $course->id));
-        $group2 = self::getDataGenerator()->create_group(array('courseid' => $course->id));
-        $group3 = self::getDataGenerator()->create_group(array('courseid' => $course->id));
+        $group1 = self::getDataGenerator()->create_group(array('courseid' => $course->id, 'name' => 'group1'));
+        $group2 = self::getDataGenerator()->create_group(array('courseid' => $course->id, 'name' => 'group2'));
+        $group3 = self::getDataGenerator()->create_group(array('courseid' => $course->id, 'name' => 'group3'));
 
         // Add the user1 to g1 and g2 groups.
         groups_add_member($group1->id, $user1->id);
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 8cd19d8..fb4c439 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();
@@ -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};
@@ -1445,7 +1423,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']);
 
         // Update timer so the validation can check the time restrictions.
         $timer = $lesson->update_timer();
@@ -1574,7 +1552,7 @@ class mod_lesson_external extends external_api {
 
         $warnings = array();
 
-        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+        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();
@@ -1650,4 +1628,370 @@ class mod_lesson_external extends external_api {
             )
         );
     }
+
+    /**
+     * 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.')),
+                                        'User answers',
+                                        VALUE_OPTIONAL
+                                    ),
+                                ), '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 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 4e1c874..a5d176c 100644 (file)
@@ -132,4 +132,36 @@ $functions = array(
         '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 1bfaf09..4d84afc 100644 (file)
@@ -674,6 +674,485 @@ 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, $CFG;
+    require_once($CFG->dirroot . '/mod/lesson/pagetypes/branchtable.php');
+
+    $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 (ar