// 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':
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;
$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;
$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;
* @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');
*/
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.
$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'));
$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)) {
$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);
+ }
}
foreach ($comments as $id => $comment) {
$commentoption = new stdClass();
$commentoption->id = $id;
- $commentoption->description = s($comment['description']);
+ $commentoption->description = $comment['description'];
$commentoptions[] = $commentoption;
}
| 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"
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
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
# 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"
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
$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';
'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',
'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')),
'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'),
}
// 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];
}
$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";
}
$ue->modifierid = $USER->id;
+ $ue->timemodified = time();
$DB->update_record('user_enrolments', $ue);
context_course::instance($instance->courseid)->mark_dirty(); // reset enrol caches
<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
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
)
);
}
+
+ /**
+ * 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()
+ )
+ );
+ }
}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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',
+ );
+ }
+}
--- /dev/null
+<?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;
+ }
+}
'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)
+ ),
);
*/
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");
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,
// 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(
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 '';
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;
+ }
}
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();
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;
+ }
}
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) {
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;
+ }
}
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) {
// 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;
+ }
}
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;
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;
+ }
}
}
- 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 '';
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;
+ }
}
'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?
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;
+ }
}
'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) {
}
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;
+ }
}
return $str;
}
- function display_search_field($value = '') {
+ function display_search_field($value = '') {
global $CFG, $DB;
$varcharcontent = $DB->sql_compare_text('content', 255);
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) {
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;
+ }
}
'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) {
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;
+ }
}
'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) {
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;
+ }
}
' 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) {
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;
+ }
}
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;
+ }
}
$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##';
*/
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) {
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%";
'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;
+}
}
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();
+}
$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);
// 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.
$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);
}
/**
$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']);
$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, []);
+ }
}
$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);
+ }
}
* 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 ===
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;
///////////////////////////////////////////////////////////////////////////
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);
} 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');
}
$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();
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
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) {
*
* @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;
}
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
)
);
}
+
+ /**
+ * 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(),
+ )
+ );
+ }
}
--- /dev/null
+<?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.',
+ ),
+ );
+ }
+}
'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)
+ ),
);
// 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;
}
}
}
}
+
+ /**
+ * 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']);
+ }
}
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;
$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);
'mediafile', 'mediaheight', 'mediawidth', 'mediaclose', 'slideshow',
'width', 'height', 'bgcolor', 'displayleft', 'displayleftif', 'progressbar',
'available', 'deadline', 'timemodified',
- 'completionendreached', 'completiontimespent'
+ 'completionendreached', 'completiontimespent', 'allowofflineattempts'
));
// The lesson_pages table
// 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');
require_once($CFG->libdir . '/externallib.php');
require_once($CFG->dirroot . '/mod/lesson/locallib.php');
+use mod_lesson\external\lesson_summary_exporter;
+
/**
* Lesson external functions
*
*/
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.
*
* @since Moodle 3.3
*/
public static function get_lessons_by_courses($courseids = array()) {
- global $USER;
+ global $PAGE;
$warnings = array();
$returnedlessons = array();
// 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();
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(),
)
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);
}
/**
);
$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.
$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();
$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'])) {
$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'])) {
);
}
+ /**
+ * 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.
*
$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'])) {
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;
}
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(),
)
);
$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'])) {
$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'])) {
'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'
)
$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();
$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;
$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'];
'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};
$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();
$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();
)
);
}
+
+ /**
+ * 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(),
+ )
+ );
+ }
}
--- /dev/null
+<?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',
+ ];
+ }
+}
<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"/>
'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),
+ ),
);
// 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;
}
$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';
$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.';
'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;
+}
$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&action=reportdetail&userid=".$try['userid']
+ .'&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 .= " ".userdate($try["timestart"]);
+ $temp .= ", (".format_time($timetotake).")</a>";
+ } else {
+ if ($try["end"]) {
+ // User finished the lesson but has no grade. (Happens when there are only content pages).
+ $temp .= " ".userdate($try["timestart"]);
+ $timetotake = $try["timeend"] - $try["timestart"];
+ $temp .= ", (".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 .= " ".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