. /** * @package mod_data * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/data/lib.php'); require_once($CFG->libdir . '/portfolio/caller.php'); require_once($CFG->libdir . '/filelib.php'); /** * The class to handle entry exports of a database module */ class data_portfolio_caller extends portfolio_module_caller_base { /** @var int the single record to export */ protected $recordid; /** @var object the record from the data table */ private $data; /**#@+ @var array the fields used and their fieldtypes */ private $fields; private $fieldtypes; /** @var object the records to export */ private $records; /** @var int how many records are 'mine' */ private $minecount; /** * the required callback arguments for a single-record export * * @return array */ public static function expected_callbackargs() { return array( 'id' => true, 'recordid' => false, ); } /** * @param array $callbackargs the arguments passed through */ public function __construct($callbackargs) { parent::__construct($callbackargs); // set up the list of fields to export $this->selectedfields = array(); foreach ($callbackargs as $key => $value) { if (strpos($key, 'field_') === 0) { $this->selectedfields[] = substr($key, 6); } } } /** * load up the data needed for the export * * @global object $DB */ public function load_data() { global $DB, $USER; if (!$this->cm = get_coursemodule_from_id('data', $this->id)) { throw new portfolio_caller_exception('invalidid', 'data'); } if (!$this->data = $DB->get_record('data', array('id' => $this->cm->instance))) { throw new portfolio_caller_exception('invalidid', 'data'); } $fieldrecords = $DB->get_records('data_fields', array('dataid' => $this->cm->instance), 'id'); // populate objets for this databases fields $this->fields = array(); foreach ($fieldrecords as $fieldrecord) { $tmp = data_get_field($fieldrecord, $this->data); $this->fields[] = $tmp; $this->fieldtypes[] = $tmp->type; } $this->records = array(); if ($this->recordid) { $tmp = $DB->get_record('data_records', array('id' => $this->recordid)); $tmp->content = $DB->get_records('data_content', array('recordid' => $this->recordid)); $this->records[] = $tmp; } else { $where = array('dataid' => $this->data->id); if (!has_capability('mod/data:exportallentries', context_module::instance($this->cm->id))) { $where['userid'] = $USER->id; // get them all in case, we'll unset ones that aren't ours later if necessary } $tmp = $DB->get_records('data_records', $where); foreach ($tmp as $t) { $t->content = $DB->get_records('data_content', array('recordid' => $t->id)); $this->records[] = $t; } $this->minecount = $DB->count_records('data_records', array('dataid' => $this->data->id, 'userid' => $USER->id)); } if ($this->recordid) { list($formats, $files) = self::formats($this->fields, $this->records[0]); $this->set_file_and_format_data($files); } } /** * How long we think the export will take * Single entry is probably not too long. * But we check for filesizes * Else base it on the number of records * * @return one of PORTFOLIO_TIME_XX constants */ public function expected_time() { if ($this->recordid) { return $this->expected_time_file(); } else { return portfolio_expected_time_db(count($this->records)); } } /** * Calculate the shal1 of this export * Dependent on the export format. * @return string */ public function get_sha1() { // in the case that we're exporting a subclass of 'file' and we have a singlefile, // then we're not exporting any metadata, just the file by itself by mimetype. if ($this->exporter->get('format') instanceof portfolio_format_file && $this->singlefile) { return $this->get_sha1_file(); } // otherwise we're exporting some sort of multipart content so use the data $str = ''; foreach ($this->records as $record) { foreach ($record as $data) { if (is_array($data) || is_object($data)) { $keys = array_keys($data); $testkey = array_pop($keys); if (is_array($data[$testkey]) || is_object($data[$testkey])) { foreach ($data as $d) { $str .= implode(',', (array)$d); } } else { $str .= implode(',', (array)$data); } } else { $str .= $data; } } } return sha1($str . ',' . $this->exporter->get('formatclass')); } /** * Prepare the package for export * * @return stored_file object */ public function prepare_package() { global $DB; $leapwriter = null; $content = ''; $filename = ''; $uid = $this->exporter->get('user')->id; $users = array(); //cache $onlymine = $this->get_export_config('mineonly'); if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { $leapwriter = $this->exporter->get('format')->leap2a_writer(); $ids = array(); } if ($this->exporter->get('format') instanceof portfolio_format_file && $this->singlefile) { return $this->get('exporter')->copy_existing_file($this->singlefile); } foreach ($this->records as $key => $record) { if ($onlymine && $record->userid != $uid) { unset($this->records[$key]); // sha1 continue; } list($tmpcontent, $files) = $this->exportentry($record); $content .= $tmpcontent; if ($leapwriter) { $entry = new portfolio_format_leap2a_entry('dataentry' . $record->id, $this->data->name, 'resource', $tmpcontent); $entry->published = $record->timecreated; $entry->updated = $record->timemodified; if ($record->userid != $uid) { if (!array_key_exists($record->userid, $users)) { $users[$record->userid] = $DB->get_record('user', array('id' => $record->userid), 'id,firstname,lastname'); } $entry->author = $users[$record->userid]; } $ids[] = $entry->id; $leapwriter->link_files($entry, $files, 'dataentry' . $record->id . 'file'); $leapwriter->add_entry($entry); } } if ($leapwriter) { if (count($this->records) > 1) { // make a selection element to tie them all together $selection = new portfolio_format_leap2a_entry('datadb' . $this->data->id, get_string('entries', 'data') . ': ' . $this->data->name, 'selection'); $leapwriter->add_entry($selection); $leapwriter->make_selection($selection, $ids, 'Grouping'); } $filename = $this->exporter->get('format')->manifest_name(); $content = $leapwriter->to_xml(); } else { if (count($this->records) == 1) { $filename = clean_filename($this->cm->name . '-entry.html'); } else { $filename = clean_filename($this->cm->name . '-full.html'); } } return $this->exporter->write_new_file( $content, $filename, ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH) // if we have associate files, this is a 'manifest' ); } /** * Verify the user can still export this entry * * @return bool */ public function check_permissions() { if ($this->recordid) { if (data_isowner($this->recordid)) { return has_capability('mod/data:exportownentry', context_module::instance($this->cm->id)); } return has_capability('mod/data:exportentry', context_module::instance($this->cm->id)); } if ($this->has_export_config() && !$this->get_export_config('mineonly')) { return has_capability('mod/data:exportallentries', context_module::instance($this->cm->id)); } return has_capability('mod/data:exportownentry', context_module::instance($this->cm->id)); } /** * @return string */ public static function display_name() { return get_string('modulename', 'data'); } /** * @global object * @return bool|void */ public function __wakeup() { global $CFG; if (empty($CFG)) { return true; // too early yet } foreach ($this->fieldtypes as $key => $field) { require_once($CFG->dirroot . '/mod/data/field/' . $field .'/field.class.php'); $this->fields[$key] = unserialize(serialize($this->fields[$key])); } } /** * Prepare a single entry for export, replacing all the content etc * * @param stdclass $record the entry to export * * @return array with key 0 = the html content, key 1 = array of attachments */ private function exportentry($record) { // Replacing tags $patterns = array(); $replacement = array(); $files = array(); // Then we generate strings to replace for normal tags $format = $this->get('exporter')->get('format'); foreach ($this->fields as $field) { $patterns[]='[['.$field->field->name.']]'; if (is_callable(array($field, 'get_file'))) { if (!$file = $field->get_file($record->id)) { $replacement[] = ''; continue; // probably left empty } $replacement[] = $format->file_output($file); $this->get('exporter')->copy_existing_file($file); $files[] = $file; } else { $replacement[] = $field->display_browse_field($record->id, 'singletemplate'); } } // Replacing special tags (##Edit##, ##Delete##, ##More##) $patterns[]='##edit##'; $patterns[]='##delete##'; $patterns[]='##export##'; $patterns[]='##more##'; $patterns[]='##moreurl##'; $patterns[]='##user##'; $patterns[]='##approve##'; $patterns[]='##disapprove##'; $patterns[]='##comments##'; $patterns[] = '##timeadded##'; $patterns[] = '##timemodified##'; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = userdate($record->timecreated); $replacement[] = userdate($record->timemodified); // actual replacement of the tags return array(str_ireplace($patterns, $replacement, $this->data->singletemplate), $files); } /** * Given the fields being exported, and the single record, * work out which export format(s) we can use * * @param array $fields array of field objects * @param object $record The data record object * * @uses PORTFOLIO_FORMAT_PLAINHTML * @uses PORTFOLIO_FORMAT_RICHHTML * * @return array of PORTFOLIO_XX constants */ public static function formats($fields, $record) { $formats = array(PORTFOLIO_FORMAT_PLAINHTML); $includedfiles = array(); foreach ($fields as $singlefield) { if (is_callable(array($singlefield, 'get_file'))) { if ($file = $singlefield->get_file($record->id)) { $includedfiles[] = $file; } } } if (count($includedfiles) == 1 && count($fields) == 1) { $formats = array(portfolio_format_from_mimetype($includedfiles[0]->get_mimetype())); } else if (count($includedfiles) > 0) { $formats = array(PORTFOLIO_FORMAT_RICHHTML); } return array($formats, $includedfiles); } public static function has_files($data) { global $DB; $fieldrecords = $DB->get_records('data_fields', array('dataid' => $data->id), 'id'); // populate objets for this databases fields foreach ($fieldrecords as $fieldrecord) { $field = data_get_field($fieldrecord, $data); if (is_callable(array($field, 'get_file'))) { return true; } } return false; } /** * base supported formats before we know anything about the export */ public static function base_supported_formats() { return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A); } public function has_export_config() { // if we're exporting more than just a single entry, // and we have the capability to export all entries, // then ask whether we want just our own, or all of them return (empty($this->recordid) // multi-entry export && $this->minecount > 0 // some of them are mine && $this->minecount != count($this->records) // not all of them are mine && has_capability('mod/data:exportallentries', context_module::instance($this->cm->id))); // they actually have a choice in the matter } public function export_config_form(&$mform, $instance) { if (!$this->has_export_config()) { return; } $mform->addElement('selectyesno', 'mineonly', get_string('exportownentries', 'data', (object)array('mine' => $this->minecount, 'all' => count($this->records)))); $mform->setDefault('mineonly', 1); } public function get_allowed_export_config() { return array('mineonly'); } } /** * Class representing the virtual node with all itemids in the file browser * * @category files * @copyright 2012 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_file_info_container extends file_info { /** @var file_browser */ protected $browser; /** @var stdClass */ protected $course; /** @var stdClass */ protected $cm; /** @var string */ protected $component; /** @var stdClass */ protected $context; /** @var array */ protected $areas; /** @var string */ protected $filearea; /** * Constructor (in case you did not realize it ;-) * * @param file_browser $browser * @param stdClass $course * @param stdClass $cm * @param stdClass $context * @param array $areas * @param string $filearea */ public function __construct($browser, $course, $cm, $context, $areas, $filearea) { parent::__construct($browser, $context); $this->browser = $browser; $this->course = $course; $this->cm = $cm; $this->component = 'mod_data'; $this->context = $context; $this->areas = $areas; $this->filearea = $filearea; } /** * @return array with keys contextid, filearea, itemid, filepath and filename */ public function get_params() { return array( 'contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea, 'itemid' => null, 'filepath' => null, 'filename' => null, ); } /** * Can new files or directories be added via the file browser * * @return bool */ public function is_writable() { return false; } /** * Should this node be considered as a folder in the file browser * * @return bool */ public function is_directory() { return true; } /** * Returns localised visible name of this node * * @return string */ public function get_visible_name() { return $this->areas[$this->filearea]; } /** * Returns list of children nodes * * @return array of file_info instances */ public function get_children() { return $this->get_filtered_children('*', false, true); } /** * Help function to return files matching extensions or their count * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @param bool|int $countonly if false returns the children, if an int returns just the * count of children but stops counting when $countonly number of children is reached * @param bool $returnemptyfolders if true returns items that don't have matching files inside * @return array|int array of file_info instances or the count */ private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { global $DB; $params = array('contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea); $sql = 'SELECT DISTINCT itemid FROM {files} WHERE contextid = :contextid AND component = :component AND filearea = :filearea'; if (!$returnemptyfolders) { $sql .= ' AND filename <> :emptyfilename'; $params['emptyfilename'] = '.'; } list($sql2, $params2) = $this->build_search_files_sql($extensions); $sql .= ' '.$sql2; $params = array_merge($params, $params2); if ($countonly === false) { $sql .= ' ORDER BY itemid DESC'; } $rs = $DB->get_recordset_sql($sql, $params); $children = array(); foreach ($rs as $record) { if ($child = $this->browser->get_file_info($this->context, 'mod_data', $this->filearea, $record->itemid)) { $children[] = $child; } if ($countonly !== false && count($children) >= $countonly) { break; } } $rs->close(); if ($countonly !== false) { return count($children); } return $children; } /** * Returns list of children which are either files matching the specified extensions * or folders that contain at least one such file. * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @return array of file_info instances */ public function get_non_empty_children($extensions = '*') { return $this->get_filtered_children($extensions, false); } /** * Returns the number of children which are either files matching the specified extensions * or folders containing at least one such file. * * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @param int $limit stop counting after at least $limit non-empty children are found * @return int */ public function count_non_empty_children($extensions = '*', $limit = 1) { return $this->get_filtered_children($extensions, $limit); } /** * Returns parent file_info instance * * @return file_info or null for root */ public function get_parent() { return $this->browser->get_file_info($this->context); } } /** * This creates new calendar events given as timeavailablefrom and timeclose by $data. * * @param stdClass $data * @return void */ function data_set_events($data) { global $DB, $CFG; require_once($CFG->dirroot.'/calendar/lib.php'); // Get CMID if not sent as part of $data. if (!isset($data->coursemodule)) { $cm = get_coursemodule_from_instance('data', $data->id, $data->course); $data->coursemodule = $cm->id; } // Data start calendar events. $event = new stdClass(); $event->eventtype = DATA_EVENT_TYPE_OPEN; // The DATA_EVENT_TYPE_OPEN event should only be an action event if no close time was specified. $event->type = empty($data->timeavailableto) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) { if ($data->timeavailablefrom > 0) { // Calendar event exists so update it. $event->name = get_string('calendarstart', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule); $event->timestart = $data->timeavailablefrom; $event->timesort = $data->timeavailablefrom; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is on longer needed. $calendarevent = calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if (isset($data->timeavailablefrom) && $data->timeavailablefrom > 0) { $event->name = get_string('calendarstart', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule); $event->courseid = $data->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'data'; $event->instance = $data->id; $event->timestart = $data->timeavailablefrom; $event->timesort = $data->timeavailablefrom; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; calendar_event::create($event, false); } } // Data end calendar events. $event = new stdClass(); $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = DATA_EVENT_TYPE_CLOSE; if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) { if ($data->timeavailableto > 0) { // Calendar event exists so update it. $event->name = get_string('calendarend', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule); $event->timestart = $data->timeavailableto; $event->timesort = $data->timeavailableto; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is on longer needed. $calendarevent = calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if (isset($data->timeavailableto) && $data->timeavailableto > 0) { $event->name = get_string('calendarend', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule); $event->courseid = $data->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'data'; $event->instance = $data->id; $event->timestart = $data->timeavailableto; $event->timesort = $data->timeavailableto; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; calendar_event::create($event, false); } } } /** * Check if a database is available for the current user. * * @param stdClass $data database record * @param boolean $canmanageentries optional, if the user can manage entries * @param stdClass $context Module context, required if $canmanageentries is not set * @return array status (available or not and possible warnings) * @since Moodle 3.3 */ function data_get_time_availability_status($data, $canmanageentries = null, $context = null) { $open = true; $closed = false; $warnings = array(); if ($canmanageentries === null) { $canmanageentries = has_capability('mod/data:manageentries', $context); } if (!$canmanageentries) { $timenow = time(); if (!empty($data->timeavailablefrom) and $data->timeavailablefrom > $timenow) { $open = false; } if (!empty($data->timeavailableto) and $timenow > $data->timeavailableto) { $closed = true; } if (!$open or $closed) { if (!$open) { $warnings['notopenyet'] = userdate($data->timeavailablefrom); } if ($closed) { $warnings['expired'] = userdate($data->timeavailableto); } return array(false, $warnings); } } // Database is available. return array(true, $warnings); } /** * Requires a database to be available for the current user. * * @param stdClass $data database record * @param boolean $canmanageentries optional, if the user can manage entries * @param stdClass $context Module context, required if $canmanageentries is not set * @throws moodle_exception * @since Moodle 3.3 */ function data_require_time_available($data, $canmanageentries = null, $context = null) { list($available, $warnings) = data_get_time_availability_status($data, $canmanageentries, $context); if (!$available) { $reason = current(array_keys($warnings)); throw new moodle_exception($reason, 'data', '', $warnings[$reason]); } } /** * Return the number of entries left to add to complete the activity. * * @param stdClass $data database object * @param int $numentries the number of entries the current user has created * @param bool $canmanageentries whether the user can manage entries (teachers, managers) * @return int the number of entries left, 0 if no entries left or if is not required * @since Moodle 3.3 */ function data_get_entries_left_to_add($data, $numentries, $canmanageentries) { if ($data->requiredentries > 0 && $numentries < $data->requiredentries && !$canmanageentries) { return $data->requiredentries - $numentries; } return 0; } /** * Return the number of entires left to add to view other users entries.. * * @param stdClass $data database object * @param int $numentries the number of entries the current user has created * @param bool $canmanageentries whether the user can manage entries (teachers, managers) * @return int the number of entries left, 0 if no entries left or if is not required * @since Moodle 3.3 */ function data_get_entries_left_to_view($data, $numentries, $canmanageentries) { if ($data->requiredentriestoview > 0 && $numentries < $data->requiredentriestoview && !$canmanageentries) { return $data->requiredentriestoview - $numentries; } return 0; } /** * Returns data records tagged with a specified tag. * * This is a callback used by the tag area mod_data/data_records to search for data records * tagged with a specific tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function mod_data_get_tagged_records($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = true, $page = 0) { global $DB, $OUTPUT, $USER; $perpage = $exclusivemode ? 20 : 5; // Build the SQL query. $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); $query = "SELECT dr.id, dr.dataid, dr.approved, d.timeviewfrom, d.timeviewto, dr.groupid, d.approval, dr.userid, d.requiredentriestoview, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect FROM {data_records} dr JOIN {data} d ON d.id = dr.dataid JOIN {modules} m ON m.name = 'data' JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = d.id JOIN {tag_instance} tt ON dr.id = tt.itemid JOIN {course} c ON cm.course = c.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component AND cm.deletioninprogress = 0 AND dr.id %ITEMFILTER% AND c.id %COURSEFILTER%"; $params = array( 'itemtype' => 'data_records', 'tagid' => $tag->id, 'component' => 'mod_data', 'coursemodulecontextlevel' => CONTEXT_MODULE ); if ($ctx) { $context = $ctx ? context::instance_by_id($ctx) : context_system::instance(); $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid'; $params['contextid'] = $context->id; $params['path'] = $context->path . '/%'; } $query .= " ORDER BY "; if ($fromctx) { // In order-clause specify that modules from inside "fromctx" context should be returned first. $fromcontext = context::instance_by_id($fromctx); $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; $params['fromcontextid'] = $fromcontext->id; $params['frompath'] = $fromcontext->path . '/%'; } $query .= ' c.sortorder, cm.id, dr.id'; $totalpages = $page + 1; // Use core_tag_index_builder to build and filter the list of items. $builder = new core_tag_index_builder('mod_data', 'data_records', $query, $params, $page * $perpage, $perpage + 1); $now = time(); $entrycount = []; $activitygroupmode = []; $usergroups = []; $titlefields = []; while ($item = $builder->has_item_that_needs_access_check()) { context_helper::preload_from_record($item); $modinfo = get_fast_modinfo($item->courseid); $cm = $modinfo->get_cm($item->cmid); $context = \context_module::instance($cm->id); $courseid = $item->courseid; if (!$builder->can_access_course($courseid)) { $builder->set_accessible($item, false); continue; } if (!$cm->uservisible) { $builder->set_accessible($item, false); continue; } if (!has_capability('mod/data:viewentry', $context)) { $builder->set_accessible($item, false); continue; } if ($USER->id != $item->userid && (($item->timeviewfrom && $now < $item->timeviewfrom) || ($item->timeviewto && $now > $item->timeviewto))) { $builder->set_accessible($item, false); continue; } if ($USER->id != $item->userid && $item->approval && !$item->approved) { $builder->set_accessible($item, false); continue; } if ($item->requiredentriestoview) { if (!isset($entrycount[$item->dataid])) { $entrycount[$item->dataid] = $DB->count_records('data_records', array('dataid' => $item->dataid)); } $sufficiententries = $item->requiredentriestoview > $entrycount[$item->dataid]; $builder->set_accessible($item, $sufficiententries); } if (!isset($activitygroupmode[$cm->id])) { $activitygroupmode[$cm->id] = groups_get_activity_groupmode($cm); } if (!isset($usergroups[$item->groupid])) { $usergroups[$item->groupid] = groups_is_member($item->groupid, $USER->id); } if ($activitygroupmode[$cm->id] == SEPARATEGROUPS && !$usergroups[$item->groupid]) { $builder->set_accessible($item, false); continue; } $builder->set_accessible($item, true); } $items = $builder->get_items(); if (count($items) > $perpage) { $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. array_pop($items); } // Build the display contents. if ($items) { $tagfeed = new core_tag\output\tagfeed(); foreach ($items as $item) { context_helper::preload_from_record($item); $modinfo = get_fast_modinfo($item->courseid); $cm = $modinfo->get_cm($item->cmid); $pageurl = new moodle_url('/mod/data/view.php', array( 'rid' => $item->id, 'd' => $item->dataid )); if (!isset($titlefields[$item->dataid])) { $titlefields[$item->dataid] = data_get_tag_title_field($item->dataid); } $pagename = data_get_tag_title_for_entry($titlefields[$item->dataid], $item); $pagename = html_writer::link($pageurl, $pagename); $courseurl = course_get_url($item->courseid, $cm->sectionnum); $cmname = html_writer::link($cm->url, $cm->get_formatted_name()); $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid))); $coursename = html_writer::link($courseurl, $coursename); $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url()))); $tagfeed->add($icon, $pagename, $cmname . '
' . $coursename); } $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT)); return new core_tag\output\tagindex($tag, 'mod_data', 'data_records', $content, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } } /** * Get the title of a field to show when displaying tag results. * * @param int $dataid The id of the data field * @return stdClass The field data from the 'data_fields' table as well as it's priority */ function data_get_tag_title_field($dataid) { global $DB, $CFG; $validfieldtypes = array('text', 'textarea', 'menu', 'radiobutton', 'checkbox', 'multimenu', 'url'); $fields = $DB->get_records('data_fields', ['dataid' => $dataid]); $template = $DB->get_field('data', 'addtemplate', ['id' => $dataid]); $filteredfields = []; foreach ($fields as $field) { if (!in_array($field->type, $validfieldtypes)) { continue; } $field->addtemplateposition = strpos($template, '[['.$field->name.']]'); if ($field->addtemplateposition === false) { continue; } require_once($CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php'); $classname = 'data_field_' . $field->type; $field->priority = $classname::get_priority(); $filteredfields[] = $field; } $sort = function($record1, $record2) { // If a content's fieldtype is compulsory in the database than it would have priority than any other non-compulsory content. if (($record1->required && $record2->required) || (!$record1->required && !$record2->required)) { if ($record1->priority === $record2->priority) { return $record1->id < $record2->id ? 1 : -1; } return $record1->priority < $record2->priority ? -1 : 1; } else if ($record1->required && !$record2->required) { return 1; } else { return -1; } }; usort($filteredfields, $sort); return array_shift($filteredfields); } /** * Get the title of an entry to show when displaying tag results. * * @param stdClass $field The field from the 'data_fields' table * @param stdClass $entry The entry from the 'data_records' table * @return string The title of the entry */ function data_get_tag_title_for_entry($field, $entry) { global $CFG, $DB; require_once($CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php'); $classname = 'data_field_' . $field->type; $sql = "SELECT dc.* FROM {data_content} dc INNER JOIN {data_fields} df ON dc.fieldid = df.id WHERE df.id = :fieldid AND dc.recordid = :recordid"; $fieldcontents = $DB->get_record_sql($sql, array('recordid' => $entry->id, 'fieldid' => $field->id)); return $classname::get_content_value($fieldcontents); } /** * 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 ($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']; } 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); $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']); $totalcount = count($newrecordids); } else { $sqlselect = "SELECT $what $fromsql $sortorder"; $sqlcountselect = "SELECT $count $fromsql"; $totalcount = $DB->count_records_sql($sqlcountselect, $allparams); } // 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]); } } } $rawtagnames = optional_param_array('tags', false, PARAM_TAGLIST); if ($rawtagnames) { $searcharray[DATA_TAGS] = new stdClass(); $searcharray[DATA_TAGS]->params = []; $searcharray[DATA_TAGS]->rawtagnames = $rawtagnames; $searcharray[DATA_TAGS]->sql = ''; } else { unset($searcharray[DATA_TAGS]); } 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(); }