Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * @package   mod_data
  20   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  21   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22   */
  23  
  24  defined('MOODLE_INTERNAL') || die();
  25  
  26  require_once($CFG->dirroot . '/mod/data/lib.php');
  27  require_once($CFG->libdir . '/portfolio/caller.php');
  28  require_once($CFG->libdir . '/filelib.php');
  29  
  30  /**
  31   * The class to handle entry exports of a database module
  32   */
  33  class data_portfolio_caller extends portfolio_module_caller_base {
  34  
  35      /** @var int the single record to export */
  36      protected $recordid;
  37  
  38      /** @var object the record from the data table */
  39      private $data;
  40  
  41      /**#@+ @var array the fields used and their fieldtypes */
  42      private $fields;
  43      private $fieldtypes;
  44  
  45      /** @var object the records to export */
  46      private $records;
  47  
  48      /** @var int how many records are 'mine' */
  49      private $minecount;
  50  
  51      /**
  52       * the required callback arguments for a single-record export
  53       *
  54       * @return array
  55       */
  56      public static function expected_callbackargs() {
  57          return array(
  58              'id'       => true,
  59              'recordid' => false,
  60          );
  61      }
  62  
  63      /**
  64       * @param array $callbackargs the arguments passed through
  65       */
  66      public function __construct($callbackargs) {
  67          parent::__construct($callbackargs);
  68          // set up the list of fields to export
  69          $this->selectedfields = array();
  70          foreach ($callbackargs as $key => $value) {
  71              if (strpos($key, 'field_') === 0) {
  72                  $this->selectedfields[] = substr($key, 6);
  73              }
  74          }
  75      }
  76  
  77      /**
  78       * load up the data needed for the export
  79       *
  80       * @global object $DB
  81       */
  82      public function load_data() {
  83          global $DB, $USER;
  84          if (!$this->cm = get_coursemodule_from_id('data', $this->id)) {
  85              throw new portfolio_caller_exception('invalidid', 'data');
  86          }
  87          if (!$this->data = $DB->get_record('data', array('id' => $this->cm->instance))) {
  88              throw new portfolio_caller_exception('invalidid', 'data');
  89          }
  90          $fieldrecords = $DB->get_records('data_fields', array('dataid' => $this->cm->instance), 'id');
  91          // populate objets for this databases fields
  92          $this->fields = array();
  93          foreach ($fieldrecords as $fieldrecord) {
  94              $tmp = data_get_field($fieldrecord, $this->data);
  95              $this->fields[] = $tmp;
  96              $this->fieldtypes[]  = $tmp->type;
  97          }
  98  
  99          $this->records = array();
 100          if ($this->recordid) {
 101              $tmp = $DB->get_record('data_records', array('id' => $this->recordid));
 102              $tmp->content = $DB->get_records('data_content', array('recordid' => $this->recordid));
 103              $this->records[] = $tmp;
 104          } else {
 105              $where = array('dataid' => $this->data->id);
 106              if (!has_capability('mod/data:exportallentries', context_module::instance($this->cm->id))) {
 107                  $where['userid'] = $USER->id; // get them all in case, we'll unset ones that aren't ours later if necessary
 108              }
 109              $tmp = $DB->get_records('data_records', $where);
 110              foreach ($tmp as $t) {
 111                  $t->content = $DB->get_records('data_content', array('recordid' => $t->id));
 112                  $this->records[] = $t;
 113              }
 114              $this->minecount = $DB->count_records('data_records', array('dataid' => $this->data->id, 'userid' => $USER->id));
 115          }
 116  
 117          if ($this->recordid) {
 118              list($formats, $files) = self::formats($this->fields, $this->records[0]);
 119              $this->set_file_and_format_data($files);
 120          }
 121      }
 122  
 123      /**
 124       * How long we think the export will take
 125       * Single entry is probably not too long.
 126       * But we check for filesizes
 127       * Else base it on the number of records
 128       *
 129       * @return one of PORTFOLIO_TIME_XX constants
 130       */
 131      public function expected_time() {
 132          if ($this->recordid) {
 133              return $this->expected_time_file();
 134          } else {
 135              return portfolio_expected_time_db(count($this->records));
 136          }
 137      }
 138  
 139      /**
 140       * Calculate the shal1 of this export
 141       * Dependent on the export format.
 142       * @return string
 143       */
 144      public function get_sha1() {
 145          // in the case that we're exporting a subclass of 'file' and we have a singlefile,
 146          // then we're not exporting any metadata, just the file by itself by mimetype.
 147          if ($this->exporter->get('format') instanceof portfolio_format_file && $this->singlefile) {
 148              return $this->get_sha1_file();
 149          }
 150          // otherwise we're exporting some sort of multipart content so use the data
 151          $str = '';
 152          foreach ($this->records as $record) {
 153              foreach ($record as $data) {
 154                  if (is_array($data) || is_object($data)) {
 155                      $keys = array_keys($data);
 156                      $testkey = array_pop($keys);
 157                      if (is_array($data[$testkey]) || is_object($data[$testkey])) {
 158                          foreach ($data as $d) {
 159                              $str .= implode(',', (array)$d);
 160                          }
 161                      } else {
 162                          $str .= implode(',', (array)$data);
 163                      }
 164                  } else {
 165                      $str .= $data;
 166                  }
 167              }
 168          }
 169          return sha1($str . ',' . $this->exporter->get('formatclass'));
 170      }
 171  
 172      /**
 173       * Prepare the package for export
 174       *
 175       * @return stored_file object
 176       */
 177      public function prepare_package() {
 178          global $DB;
 179          $leapwriter = null;
 180          $content = '';
 181          $filename = '';
 182          $uid = $this->exporter->get('user')->id;
 183          $users = array(); //cache
 184          $onlymine = $this->get_export_config('mineonly');
 185          if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
 186              $leapwriter = $this->exporter->get('format')->leap2a_writer();
 187              $ids = array();
 188          }
 189  
 190          if ($this->exporter->get('format') instanceof portfolio_format_file && $this->singlefile) {
 191              return $this->get('exporter')->copy_existing_file($this->singlefile);
 192          }
 193          foreach ($this->records  as $key => $record) {
 194              if ($onlymine && $record->userid != $uid) {
 195                  unset($this->records[$key]); // sha1
 196                  continue;
 197              }
 198              list($tmpcontent, $files)  = $this->exportentry($record);
 199              $content .= $tmpcontent;
 200              if ($leapwriter) {
 201                  $entry = new portfolio_format_leap2a_entry('dataentry' . $record->id, $this->data->name, 'resource', $tmpcontent);
 202                  $entry->published = $record->timecreated;
 203                  $entry->updated = $record->timemodified;
 204                  if ($record->userid != $uid) {
 205                      if (!array_key_exists($record->userid, $users)) {
 206                          $users[$record->userid] = $DB->get_record('user', array('id' => $record->userid), 'id,firstname,lastname');
 207                      }
 208                      $entry->author = $users[$record->userid];
 209                  }
 210                  $ids[] = $entry->id;
 211                  $leapwriter->link_files($entry, $files, 'dataentry' . $record->id . 'file');
 212                  $leapwriter->add_entry($entry);
 213              }
 214          }
 215          if ($leapwriter) {
 216              if (count($this->records) > 1) { // make a selection element to tie them all together
 217                  $selection = new portfolio_format_leap2a_entry('datadb' . $this->data->id,
 218                      get_string('entries', 'data') . ': ' . $this->data->name, 'selection');
 219                  $leapwriter->add_entry($selection);
 220                  $leapwriter->make_selection($selection, $ids, 'Grouping');
 221              }
 222              $filename = $this->exporter->get('format')->manifest_name();
 223              $content = $leapwriter->to_xml();
 224          } else {
 225              if (count($this->records) == 1) {
 226                  $filename = clean_filename($this->cm->name . '-entry.html');
 227              } else {
 228                  $filename = clean_filename($this->cm->name . '-full.html');
 229              }
 230          }
 231          return $this->exporter->write_new_file(
 232              $content,
 233              $filename,
 234              ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH) // if we have associate files, this is a 'manifest'
 235          );
 236      }
 237  
 238      /**
 239       * Verify the user can still export this entry
 240       *
 241       * @return bool
 242       */
 243      public function check_permissions() {
 244          if ($this->recordid) {
 245              if (data_isowner($this->recordid)) {
 246                  return has_capability('mod/data:exportownentry', context_module::instance($this->cm->id));
 247              }
 248              return has_capability('mod/data:exportentry', context_module::instance($this->cm->id));
 249          }
 250          if ($this->has_export_config() && !$this->get_export_config('mineonly')) {
 251              return has_capability('mod/data:exportallentries', context_module::instance($this->cm->id));
 252          }
 253          return has_capability('mod/data:exportownentry', context_module::instance($this->cm->id));
 254      }
 255  
 256      /**
 257       *  @return string
 258       */
 259      public static function display_name() {
 260          return get_string('modulename', 'data');
 261      }
 262  
 263      /**
 264       * @global object
 265       * @return bool|void
 266       */
 267      public function __wakeup() {
 268          global $CFG;
 269          if (empty($CFG)) {
 270              return true; // too early yet
 271          }
 272          foreach ($this->fieldtypes as $key => $field) {
 273              require_once($CFG->dirroot . '/mod/data/field/' . $field .'/field.class.php');
 274              $this->fields[$key] = unserialize(serialize($this->fields[$key]));
 275          }
 276      }
 277  
 278      /**
 279       * Prepare a single entry for export, replacing all the content etc
 280       *
 281       * @param stdclass $record the entry to export
 282       *
 283       * @return array with key 0 = the html content, key 1 = array of attachments
 284       */
 285      private function exportentry($record) {
 286      // Replacing tags
 287          $patterns = array();
 288          $replacement = array();
 289  
 290          $files = array();
 291      // Then we generate strings to replace for normal tags
 292          $format = $this->get('exporter')->get('format');
 293          foreach ($this->fields as $field) {
 294              $patterns[]='[['.$field->field->name.']]';
 295              if (is_callable(array($field, 'get_file'))) {
 296                  if (!$file = $field->get_file($record->id)) {
 297                      $replacement[] = '';
 298                      continue; // probably left empty
 299                  }
 300                  $replacement[] = $format->file_output($file);
 301                  $this->get('exporter')->copy_existing_file($file);
 302                  $files[] = $file;
 303              } else {
 304                  $replacement[] = $field->display_browse_field($record->id, 'singletemplate');
 305              }
 306          }
 307  
 308      // Replacing special tags (##Edit##, ##Delete##, ##More##)
 309          $patterns[]='##edit##';
 310          $patterns[]='##delete##';
 311          $patterns[]='##export##';
 312          $patterns[]='##more##';
 313          $patterns[]='##moreurl##';
 314          $patterns[]='##user##';
 315          $patterns[]='##approve##';
 316          $patterns[]='##disapprove##';
 317          $patterns[]='##comments##';
 318          $patterns[] = '##timeadded##';
 319          $patterns[] = '##timemodified##';
 320          $replacement[] = '';
 321          $replacement[] = '';
 322          $replacement[] = '';
 323          $replacement[] = '';
 324          $replacement[] = '';
 325          $replacement[] = '';
 326          $replacement[] = '';
 327          $replacement[] = '';
 328          $replacement[] = '';
 329          $replacement[] = userdate($record->timecreated);
 330          $replacement[] = userdate($record->timemodified);
 331  
 332          // actual replacement of the tags
 333          return array(str_ireplace($patterns, $replacement, $this->data->singletemplate), $files);
 334      }
 335  
 336      /**
 337       * Given the fields being exported, and the single record,
 338       * work out which export format(s) we can use
 339       *
 340       * @param array $fields array of field objects
 341       * @param object $record The data record object
 342       *
 343       * @uses PORTFOLIO_FORMAT_PLAINHTML
 344       * @uses PORTFOLIO_FORMAT_RICHHTML
 345       *
 346       * @return array of PORTFOLIO_XX constants
 347       */
 348      public static function formats($fields, $record) {
 349          $formats = array(PORTFOLIO_FORMAT_PLAINHTML);
 350          $includedfiles = array();
 351          foreach ($fields as $singlefield) {
 352              if (is_callable(array($singlefield, 'get_file'))) {
 353                  if ($file = $singlefield->get_file($record->id)) {
 354                      $includedfiles[] = $file;
 355                  }
 356              }
 357          }
 358          if (count($includedfiles) == 1 && count($fields) == 1) {
 359              $formats = array(portfolio_format_from_mimetype($includedfiles[0]->get_mimetype()));
 360          } else if (count($includedfiles) > 0) {
 361              $formats = array(PORTFOLIO_FORMAT_RICHHTML);
 362          }
 363          return array($formats, $includedfiles);
 364      }
 365  
 366      public static function has_files($data) {
 367          global $DB;
 368          $fieldrecords = $DB->get_records('data_fields', array('dataid' => $data->id), 'id');
 369          // populate objets for this databases fields
 370          foreach ($fieldrecords as $fieldrecord) {
 371              $field = data_get_field($fieldrecord, $data);
 372              if (is_callable(array($field, 'get_file'))) {
 373                  return true;
 374              }
 375          }
 376          return false;
 377      }
 378  
 379      /**
 380       * base supported formats before we know anything about the export
 381       */
 382      public static function base_supported_formats() {
 383          return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A);
 384      }
 385  
 386      public function has_export_config() {
 387          // if we're exporting more than just a single entry,
 388          // and we have the capability to export all entries,
 389          // then ask whether we want just our own, or all of them
 390          return (empty($this->recordid) // multi-entry export
 391              && $this->minecount > 0    // some of them are mine
 392              && $this->minecount != count($this->records) // not all of them are mine
 393              && has_capability('mod/data:exportallentries', context_module::instance($this->cm->id))); // they actually have a choice in the matter
 394      }
 395  
 396      public function export_config_form(&$mform, $instance) {
 397          if (!$this->has_export_config()) {
 398              return;
 399          }
 400          $mform->addElement('selectyesno', 'mineonly', get_string('exportownentries', 'data', (object)array('mine' => $this->minecount, 'all' => count($this->records))));
 401          $mform->setDefault('mineonly', 1);
 402      }
 403  
 404      public function get_allowed_export_config() {
 405          return array('mineonly');
 406      }
 407  }
 408  
 409  
 410  /**
 411   * Class representing the virtual node with all itemids in the file browser
 412   *
 413   * @category  files
 414   * @copyright 2012 David Mudrak <david@moodle.com>
 415   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 416   */
 417  class data_file_info_container extends file_info {
 418      /** @var file_browser */
 419      protected $browser;
 420      /** @var stdClass */
 421      protected $course;
 422      /** @var stdClass */
 423      protected $cm;
 424      /** @var string */
 425      protected $component;
 426      /** @var stdClass */
 427      protected $context;
 428      /** @var array */
 429      protected $areas;
 430      /** @var string */
 431      protected $filearea;
 432  
 433      /**
 434       * Constructor (in case you did not realize it ;-)
 435       *
 436       * @param file_browser $browser
 437       * @param stdClass $course
 438       * @param stdClass $cm
 439       * @param stdClass $context
 440       * @param array $areas
 441       * @param string $filearea
 442       */
 443      public function __construct($browser, $course, $cm, $context, $areas, $filearea) {
 444          parent::__construct($browser, $context);
 445          $this->browser = $browser;
 446          $this->course = $course;
 447          $this->cm = $cm;
 448          $this->component = 'mod_data';
 449          $this->context = $context;
 450          $this->areas = $areas;
 451          $this->filearea = $filearea;
 452      }
 453  
 454      /**
 455       * @return array with keys contextid, filearea, itemid, filepath and filename
 456       */
 457      public function get_params() {
 458          return array(
 459              'contextid' => $this->context->id,
 460              'component' => $this->component,
 461              'filearea' => $this->filearea,
 462              'itemid' => null,
 463              'filepath' => null,
 464              'filename' => null,
 465          );
 466      }
 467  
 468      /**
 469       * Can new files or directories be added via the file browser
 470       *
 471       * @return bool
 472       */
 473      public function is_writable() {
 474          return false;
 475      }
 476  
 477      /**
 478       * Should this node be considered as a folder in the file browser
 479       *
 480       * @return bool
 481       */
 482      public function is_directory() {
 483          return true;
 484      }
 485  
 486      /**
 487       * Returns localised visible name of this node
 488       *
 489       * @return string
 490       */
 491      public function get_visible_name() {
 492          return $this->areas[$this->filearea];
 493      }
 494  
 495      /**
 496       * Returns list of children nodes
 497       *
 498       * @return array of file_info instances
 499       */
 500      public function get_children() {
 501          return $this->get_filtered_children('*', false, true);
 502      }
 503  
 504      /**
 505       * Help function to return files matching extensions or their count
 506       *
 507       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 508       * @param bool|int $countonly if false returns the children, if an int returns just the
 509       *    count of children but stops counting when $countonly number of children is reached
 510       * @param bool $returnemptyfolders if true returns items that don't have matching files inside
 511       * @return array|int array of file_info instances or the count
 512       */
 513      private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
 514          global $DB;
 515          $params = array('contextid' => $this->context->id,
 516              'component' => $this->component,
 517              'filearea' => $this->filearea);
 518          $sql = 'SELECT DISTINCT itemid
 519                      FROM {files}
 520                      WHERE contextid = :contextid
 521                      AND component = :component
 522                      AND filearea = :filearea';
 523          if (!$returnemptyfolders) {
 524              $sql .= ' AND filename <> :emptyfilename';
 525              $params['emptyfilename'] = '.';
 526          }
 527          list($sql2, $params2) = $this->build_search_files_sql($extensions);
 528          $sql .= ' '.$sql2;
 529          $params = array_merge($params, $params2);
 530          if ($countonly === false) {
 531              $sql .= ' ORDER BY itemid DESC';
 532          }
 533  
 534          $rs = $DB->get_recordset_sql($sql, $params);
 535          $children = array();
 536          foreach ($rs as $record) {
 537              if ($child = $this->browser->get_file_info($this->context, 'mod_data', $this->filearea, $record->itemid)) {
 538                  $children[] = $child;
 539              }
 540              if ($countonly !== false && count($children) >= $countonly) {
 541                  break;
 542              }
 543          }
 544          $rs->close();
 545          if ($countonly !== false) {
 546              return count($children);
 547          }
 548          return $children;
 549      }
 550  
 551      /**
 552       * Returns list of children which are either files matching the specified extensions
 553       * or folders that contain at least one such file.
 554       *
 555       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 556       * @return array of file_info instances
 557       */
 558      public function get_non_empty_children($extensions = '*') {
 559          return $this->get_filtered_children($extensions, false);
 560      }
 561  
 562      /**
 563       * Returns the number of children which are either files matching the specified extensions
 564       * or folders containing at least one such file.
 565       *
 566       * @param string|array $extensions, for example '*' or array('.gif','.jpg')
 567       * @param int $limit stop counting after at least $limit non-empty children are found
 568       * @return int
 569       */
 570      public function count_non_empty_children($extensions = '*', $limit = 1) {
 571          return $this->get_filtered_children($extensions, $limit);
 572      }
 573  
 574      /**
 575       * Returns parent file_info instance
 576       *
 577       * @return file_info or null for root
 578       */
 579      public function get_parent() {
 580          return $this->browser->get_file_info($this->context);
 581      }
 582  }
 583  
 584  /**
 585   * This creates new calendar events given as timeavailablefrom and timeclose by $data.
 586   *
 587   * @param stdClass $data
 588   * @return void
 589   */
 590  function data_set_events($data) {
 591      global $DB, $CFG;
 592  
 593      require_once($CFG->dirroot.'/calendar/lib.php');
 594  
 595      // Get CMID if not sent as part of $data.
 596      if (!isset($data->coursemodule)) {
 597          $cm = get_coursemodule_from_instance('data', $data->id, $data->course);
 598          $data->coursemodule = $cm->id;
 599      }
 600      // Data start calendar events.
 601      $event = new stdClass();
 602      $event->eventtype = DATA_EVENT_TYPE_OPEN;
 603      // The DATA_EVENT_TYPE_OPEN event should only be an action event if no close time was specified.
 604      $event->type = empty($data->timeavailableto) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD;
 605      if ($event->id = $DB->get_field('event', 'id',
 606              array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) {
 607          if ($data->timeavailablefrom > 0) {
 608              // Calendar event exists so update it.
 609              $event->name         = get_string('calendarstart', 'data', $data->name);
 610              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 611              $event->format       = FORMAT_HTML;
 612              $event->timestart    = $data->timeavailablefrom;
 613              $event->timesort     = $data->timeavailablefrom;
 614              $event->visible      = instance_is_visible('data', $data);
 615              $event->timeduration = 0;
 616              $calendarevent = calendar_event::load($event->id);
 617              $calendarevent->update($event, false);
 618          } else {
 619              // Calendar event is on longer needed.
 620              $calendarevent = calendar_event::load($event->id);
 621              $calendarevent->delete();
 622          }
 623      } else {
 624          // Event doesn't exist so create one.
 625          if (isset($data->timeavailablefrom) && $data->timeavailablefrom > 0) {
 626              $event->name         = get_string('calendarstart', 'data', $data->name);
 627              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 628              $event->format       = FORMAT_HTML;
 629              $event->courseid     = $data->course;
 630              $event->groupid      = 0;
 631              $event->userid       = 0;
 632              $event->modulename   = 'data';
 633              $event->instance     = $data->id;
 634              $event->timestart    = $data->timeavailablefrom;
 635              $event->timesort     = $data->timeavailablefrom;
 636              $event->visible      = instance_is_visible('data', $data);
 637              $event->timeduration = 0;
 638              calendar_event::create($event, false);
 639          }
 640      }
 641  
 642      // Data end calendar events.
 643      $event = new stdClass();
 644      $event->type = CALENDAR_EVENT_TYPE_ACTION;
 645      $event->eventtype = DATA_EVENT_TYPE_CLOSE;
 646      if ($event->id = $DB->get_field('event', 'id',
 647              array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) {
 648          if ($data->timeavailableto > 0) {
 649              // Calendar event exists so update it.
 650              $event->name         = get_string('calendarend', 'data', $data->name);
 651              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 652              $event->format       = FORMAT_HTML;
 653              $event->timestart    = $data->timeavailableto;
 654              $event->timesort     = $data->timeavailableto;
 655              $event->visible      = instance_is_visible('data', $data);
 656              $event->timeduration = 0;
 657              $calendarevent = calendar_event::load($event->id);
 658              $calendarevent->update($event, false);
 659          } else {
 660              // Calendar event is on longer needed.
 661              $calendarevent = calendar_event::load($event->id);
 662              $calendarevent->delete();
 663          }
 664      } else {
 665          // Event doesn't exist so create one.
 666          if (isset($data->timeavailableto) && $data->timeavailableto > 0) {
 667              $event->name         = get_string('calendarend', 'data', $data->name);
 668              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 669              $event->format       = FORMAT_HTML;
 670              $event->courseid     = $data->course;
 671              $event->groupid      = 0;
 672              $event->userid       = 0;
 673              $event->modulename   = 'data';
 674              $event->instance     = $data->id;
 675              $event->timestart    = $data->timeavailableto;
 676              $event->timesort     = $data->timeavailableto;
 677              $event->visible      = instance_is_visible('data', $data);
 678              $event->timeduration = 0;
 679              calendar_event::create($event, false);
 680          }
 681      }
 682  }
 683  
 684  /**
 685   * Check if a database is available for the current user.
 686   *
 687   * @param  stdClass  $data            database record
 688   * @param  boolean $canmanageentries  optional, if the user can manage entries
 689   * @param  stdClass  $context         Module context, required if $canmanageentries is not set
 690   * @return array                      status (available or not and possible warnings)
 691   * @since  Moodle 3.3
 692   */
 693  function data_get_time_availability_status($data, $canmanageentries = null, $context = null) {
 694      $open = true;
 695      $closed = false;
 696      $warnings = array();
 697  
 698      if ($canmanageentries === null) {
 699          $canmanageentries = has_capability('mod/data:manageentries', $context);
 700      }
 701  
 702      if (!$canmanageentries) {
 703          $timenow = time();
 704  
 705          if (!empty($data->timeavailablefrom) and $data->timeavailablefrom > $timenow) {
 706              $open = false;
 707          }
 708          if (!empty($data->timeavailableto) and $timenow > $data->timeavailableto) {
 709              $closed = true;
 710          }
 711  
 712          if (!$open or $closed) {
 713              if (!$open) {
 714                  $warnings['notopenyet'] = userdate($data->timeavailablefrom);
 715              }
 716              if ($closed) {
 717                  $warnings['expired'] = userdate($data->timeavailableto);
 718              }
 719              return array(false, $warnings);
 720          }
 721      }
 722  
 723      // Database is available.
 724      return array(true, $warnings);
 725  }
 726  
 727  /**
 728   * Requires a database to be available for the current user.
 729   *
 730   * @param  stdClass  $data            database record
 731   * @param  boolean $canmanageentries  optional, if the user can manage entries
 732   * @param  stdClass  $context          Module context, required if $canmanageentries is not set
 733   * @throws moodle_exception
 734   * @since  Moodle 3.3
 735   */
 736  function data_require_time_available($data, $canmanageentries = null, $context = null) {
 737  
 738      list($available, $warnings) = data_get_time_availability_status($data, $canmanageentries, $context);
 739  
 740      if (!$available) {
 741          $reason = current(array_keys($warnings));
 742          throw new moodle_exception($reason, 'data', '', $warnings[$reason]);
 743      }
 744  }
 745  
 746  /**
 747   * Return the number of entries left to add to complete the activity.
 748   *
 749   * @param  stdClass $data           database object
 750   * @param  int $numentries          the number of entries the current user has created
 751   * @param  bool $canmanageentries   whether the user can manage entries (teachers, managers)
 752   * @return int the number of entries left, 0 if no entries left or if is not required
 753   * @since  Moodle 3.3
 754   */
 755  function data_get_entries_left_to_add($data, $numentries, $canmanageentries) {
 756      if ($data->requiredentries > 0 && $numentries < $data->requiredentries && !$canmanageentries) {
 757          return $data->requiredentries - $numentries;
 758      }
 759      return 0;
 760  }
 761  
 762  /**
 763   * Return the number of entires left to add to view other users entries..
 764   *
 765   * @param  stdClass $data           database object
 766   * @param  int $numentries          the number of entries the current user has created
 767   * @param  bool $canmanageentries   whether the user can manage entries (teachers, managers)
 768   * @return int the number of entries left, 0 if no entries left or if is not required
 769   * @since  Moodle 3.3
 770   */
 771  function data_get_entries_left_to_view($data, $numentries, $canmanageentries) {
 772      if ($data->requiredentriestoview > 0 && $numentries < $data->requiredentriestoview && !$canmanageentries) {
 773          return $data->requiredentriestoview - $numentries;
 774      }
 775      return 0;
 776  }
 777  
 778  /**
 779   * Returns data records tagged with a specified tag.
 780   *
 781   * This is a callback used by the tag area mod_data/data_records to search for data records
 782   * tagged with a specific tag.
 783   *
 784   * @param core_tag_tag $tag
 785   * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
 786   *             are displayed on the page and the per-page limit may be bigger
 787   * @param int $fromctx context id where the link was displayed, may be used by callbacks
 788   *            to display items in the same context first
 789   * @param int $ctx context id where to search for records
 790   * @param bool $rec search in subcontexts as well
 791   * @param int $page 0-based number of page being displayed
 792   * @return \core_tag\output\tagindex
 793   */
 794  function mod_data_get_tagged_records($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = true, $page = 0) {
 795      global $DB, $OUTPUT, $USER;
 796      $perpage = $exclusivemode ? 20 : 5;
 797  
 798      // Build the SQL query.
 799      $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
 800      $query = "SELECT dr.id, dr.dataid, dr.approved, d.timeviewfrom, d.timeviewto, dr.groupid, d.approval, dr.userid,
 801                       d.requiredentriestoview, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect
 802                  FROM {data_records} dr
 803                  JOIN {data} d
 804                    ON d.id = dr.dataid
 805                  JOIN {modules} m
 806                    ON m.name = 'data'
 807                  JOIN {course_modules} cm
 808                    ON cm.module = m.id AND cm.instance = d.id
 809                  JOIN {tag_instance} tt
 810                    ON dr.id = tt.itemid
 811                  JOIN {course} c
 812                    ON cm.course = c.id
 813                  JOIN {context} ctx
 814                    ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
 815                 WHERE tt.itemtype = :itemtype
 816                   AND tt.tagid = :tagid
 817                   AND tt.component = :component
 818                   AND cm.deletioninprogress = 0
 819                   AND dr.id %ITEMFILTER%
 820                   AND c.id %COURSEFILTER%";
 821  
 822      $params = array(
 823          'itemtype' => 'data_records',
 824          'tagid' => $tag->id,
 825          'component' => 'mod_data',
 826          'coursemodulecontextlevel' => CONTEXT_MODULE
 827      );
 828  
 829      if ($ctx) {
 830          $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
 831          $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
 832          $params['contextid'] = $context->id;
 833          $params['path'] = $context->path . '/%';
 834      }
 835  
 836      $query .= " ORDER BY ";
 837      if ($fromctx) {
 838          // In order-clause specify that modules from inside "fromctx" context should be returned first.
 839          $fromcontext = context::instance_by_id($fromctx);
 840          $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
 841          $params['fromcontextid'] = $fromcontext->id;
 842          $params['frompath'] = $fromcontext->path . '/%';
 843      }
 844      $query .= ' c.sortorder, cm.id, dr.id';
 845  
 846      $totalpages = $page + 1;
 847  
 848      // Use core_tag_index_builder to build and filter the list of items.
 849      $builder = new core_tag_index_builder('mod_data', 'data_records', $query, $params, $page * $perpage, $perpage + 1);
 850      $now = time();
 851      $entrycount = [];
 852      $activitygroupmode = [];
 853      $usergroups = [];
 854      $titlefields = [];
 855      while ($item = $builder->has_item_that_needs_access_check()) {
 856          context_helper::preload_from_record($item);
 857          $modinfo = get_fast_modinfo($item->courseid);
 858          $cm = $modinfo->get_cm($item->cmid);
 859          $context = \context_module::instance($cm->id);
 860          $courseid = $item->courseid;
 861  
 862          if (!$builder->can_access_course($courseid)) {
 863              $builder->set_accessible($item, false);
 864              continue;
 865          }
 866  
 867          if (!$cm->uservisible) {
 868              $builder->set_accessible($item, false);
 869              continue;
 870          }
 871  
 872          if (!has_capability('mod/data:viewentry', $context)) {
 873              $builder->set_accessible($item, false);
 874              continue;
 875          }
 876  
 877          if ($USER->id != $item->userid && (($item->timeviewfrom && $now < $item->timeviewfrom)
 878                  || ($item->timeviewto && $now > $item->timeviewto))) {
 879              $builder->set_accessible($item, false);
 880              continue;
 881          }
 882  
 883          if ($USER->id != $item->userid && $item->approval && !$item->approved) {
 884              $builder->set_accessible($item, false);
 885              continue;
 886          }
 887  
 888          if ($item->requiredentriestoview) {
 889              if (!isset($entrycount[$item->dataid])) {
 890                  $entrycount[$item->dataid] = $DB->count_records('data_records', array('dataid' => $item->dataid));
 891              }
 892              $sufficiententries = $item->requiredentriestoview > $entrycount[$item->dataid];
 893              $builder->set_accessible($item, $sufficiententries);
 894          }
 895  
 896          if (!isset($activitygroupmode[$cm->id])) {
 897              $activitygroupmode[$cm->id] = groups_get_activity_groupmode($cm);
 898          }
 899  
 900          if (!isset($usergroups[$item->groupid])) {
 901              $usergroups[$item->groupid] = groups_is_member($item->groupid, $USER->id);
 902          }
 903  
 904          if ($activitygroupmode[$cm->id] == SEPARATEGROUPS && !$usergroups[$item->groupid]) {
 905              $builder->set_accessible($item, false);
 906              continue;
 907          }
 908  
 909          $builder->set_accessible($item, true);
 910      }
 911  
 912      $items = $builder->get_items();
 913      if (count($items) > $perpage) {
 914          $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
 915          array_pop($items);
 916      }
 917  
 918      // Build the display contents.
 919      if ($items) {
 920          $tagfeed = new core_tag\output\tagfeed();
 921          foreach ($items as $item) {
 922              context_helper::preload_from_record($item);
 923              $modinfo = get_fast_modinfo($item->courseid);
 924              $cm = $modinfo->get_cm($item->cmid);
 925              $pageurl = new moodle_url('/mod/data/view.php', array(
 926                      'rid' => $item->id,
 927                      'd' => $item->dataid
 928              ));
 929  
 930              if (!isset($titlefields[$item->dataid])) {
 931                  $titlefields[$item->dataid] = data_get_tag_title_field($item->dataid);
 932              }
 933  
 934              $pagename = data_get_tag_title_for_entry($titlefields[$item->dataid], $item);
 935              $pagename = html_writer::link($pageurl, $pagename);
 936              $courseurl = course_get_url($item->courseid, $cm->sectionnum);
 937              $cmname = html_writer::link($cm->url, $cm->get_formatted_name());
 938              $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
 939              $coursename = html_writer::link($courseurl, $coursename);
 940              $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
 941              $tagfeed->add($icon, $pagename, $cmname . '<br>' . $coursename);
 942          }
 943          $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT));
 944  
 945          return new core_tag\output\tagindex($tag, 'mod_data', 'data_records', $content, $exclusivemode,
 946              $fromctx, $ctx, $rec, $page, $totalpages);
 947      }
 948  }
 949  
 950  /**
 951   * Get the title of a field to show when displaying tag results.
 952   *
 953   * @param int $dataid The id of the data field
 954   * @return stdClass The field data from the 'data_fields' table as well as it's priority
 955   */
 956  function data_get_tag_title_field($dataid) {
 957      global $DB, $CFG;
 958  
 959      $validfieldtypes = array('text', 'textarea', 'menu', 'radiobutton', 'checkbox', 'multimenu', 'url');
 960      $fields = $DB->get_records('data_fields', ['dataid' => $dataid]);
 961      $template = $DB->get_field('data', 'addtemplate', ['id' => $dataid]);
 962  
 963      $filteredfields = [];
 964  
 965      foreach ($fields as $field) {
 966          if (!in_array($field->type, $validfieldtypes)) {
 967              continue;
 968          }
 969          $field->addtemplateposition = strpos($template, '[['.$field->name.']]');
 970          if ($field->addtemplateposition === false) {
 971              continue;
 972          }
 973          require_once($CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php');
 974          $classname = 'data_field_' . $field->type;
 975          $field->priority = $classname::get_priority();
 976          $filteredfields[] = $field;
 977      }
 978  
 979      $sort = function($record1, $record2) {
 980          // If a content's fieldtype is compulsory in the database than it would have priority than any other non-compulsory content.
 981          if (($record1->required && $record2->required) || (!$record1->required && !$record2->required)) {
 982              if ($record1->priority === $record2->priority) {
 983                  return $record1->id < $record2->id ? 1 : -1;
 984              }
 985  
 986              return $record1->priority < $record2->priority ? -1 : 1;
 987          } else if ($record1->required && !$record2->required) {
 988              return 1;
 989          } else {
 990              return -1;
 991          }
 992      };
 993  
 994      usort($filteredfields, $sort);
 995  
 996      return array_shift($filteredfields);
 997  }
 998  
 999  /**
1000   * Get the title of an entry to show when displaying tag results.
1001   *
1002   * @param stdClass $field The field from the 'data_fields' table
1003   * @param stdClass $entry The entry from the 'data_records' table
1004   * @return string The title of the entry
1005   */
1006  function data_get_tag_title_for_entry($field, $entry) {
1007      global $CFG, $DB;
1008      require_once($CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php');
1009  
1010      $classname = 'data_field_' . $field->type;
1011      $sql = "SELECT dc.*
1012                FROM {data_content} dc
1013          INNER JOIN {data_fields} df
1014                  ON dc.fieldid = df.id
1015               WHERE df.id = :fieldid
1016                 AND dc.recordid = :recordid";
1017      $fieldcontents = $DB->get_record_sql($sql, array('recordid' => $entry->id, 'fieldid' => $field->id));
1018  
1019      return $classname::get_content_value($fieldcontents);
1020  }
1021  
1022  /**
1023   * Search entries in a database.
1024   *
1025   * @param  stdClass  $data         database object
1026   * @param  stdClass  $cm           course module object
1027   * @param  stdClass  $context      context object
1028   * @param  stdClass  $mode         in which mode we are viewing the database (list, single)
1029   * @param  int  $currentgroup      the current group being used
1030   * @param  str  $search            search for this text in the entry data
1031   * @param  str  $sort              the field to sort by
1032   * @param  str  $order             the order to use when sorting
1033   * @param  int $page               for pagination, the current page
1034   * @param  int $perpage            entries per page
1035   * @param  bool  $advanced         whether we are using or not advanced search
1036   * @param  array  $searcharray     when using advanced search, the advanced data to use
1037   * @param  stdClass  $record       if we jsut want this record after doing all the access checks
1038   * @return array the entries found among other data related to the search
1039   * @since  Moodle 3.3
1040   */
1041  function data_search_entries($data, $cm, $context, $mode, $currentgroup, $search = '', $sort = null, $order = null, $page = 0,
1042          $perpage = 0, $advanced = null, $searcharray = null, $record = null) {
1043      global $DB, $USER;
1044  
1045      if ($sort === null) {
1046          $sort = $data->defaultsort;
1047      }
1048      if ($order === null) {
1049          $order = ($data->defaultsortdir == 0) ? 'ASC' : 'DESC';
1050      }
1051      if ($searcharray === null) {
1052          $searcharray = array();
1053      }
1054  
1055      if (core_text::strlen($search) < 2) {
1056          $search = '';
1057      }
1058  
1059      $approvecap = has_capability('mod/data:approve', $context);
1060      $canmanageentries = has_capability('mod/data:manageentries', $context);
1061  
1062      // If a student is not part of a group and seperate groups is enabled, we don't
1063      // want them seeing all records.
1064      $groupmode = groups_get_activity_groupmode($cm);
1065      if ($currentgroup == 0 && $groupmode == 1 && !$canmanageentries) {
1066          $canviewallrecords = false;
1067      } else {
1068          $canviewallrecords = true;
1069      }
1070  
1071      $numentries = data_numentries($data);
1072      $requiredentriesallowed = true;
1073      if (data_get_entries_left_to_view($data, $numentries, $canmanageentries)) {
1074          $requiredentriesallowed = false;
1075      }
1076  
1077      // Initialise the first group of params for advanced searches.
1078      $initialparams   = array();
1079      $params = array(); // Named params array.
1080  
1081      // Setup group and approve restrictions.
1082      if (!$approvecap && $data->approval) {
1083          if (isloggedin()) {
1084              $approveselect = ' AND (r.approved=1 OR r.userid=:myid1) ';
1085              $params['myid1'] = $USER->id;
1086              $initialparams['myid1'] = $params['myid1'];
1087          } else {
1088              $approveselect = ' AND r.approved=1 ';
1089          }
1090      } else {
1091          $approveselect = ' ';
1092      }
1093  
1094      if ($currentgroup) {
1095          $groupselect = " AND (r.groupid = :currentgroup OR r.groupid = 0)";
1096          $params['currentgroup'] = $currentgroup;
1097          $initialparams['currentgroup'] = $params['currentgroup'];
1098      } else {
1099          if ($canviewallrecords) {
1100              $groupselect = ' ';
1101          } else {
1102              // If separate groups are enabled and the user isn't in a group or
1103              // a teacher, manager, admin etc, then just show them entries for 'All participants'.
1104              $groupselect = " AND r.groupid = 0";
1105          }
1106      }
1107  
1108      // Init some variables to be used by advanced search.
1109      $advsearchselect = '';
1110      $advwhere        = '';
1111      $advtables       = '';
1112      $advparams       = array();
1113      // This is used for the initial reduction of advanced search results with required entries.
1114      $entrysql        = '';
1115      $namefields = user_picture::fields('u');
1116      // Remove the id from the string. This already exists in the sql statement.
1117      $namefields = str_replace('u.id,', '', $namefields);
1118  
1119      // Find the field we are sorting on.
1120      if ($sort <= 0 or !$sortfield = data_get_field_from_id($sort, $data)) {
1121  
1122          switch ($sort) {
1123              case DATA_LASTNAME:
1124                  $ordering = "u.lastname $order, u.firstname $order";
1125                  break;
1126              case DATA_FIRSTNAME:
1127                  $ordering = "u.firstname $order, u.lastname $order";
1128                  break;
1129              case DATA_APPROVED:
1130                  $ordering = "r.approved $order, r.timecreated $order";
1131                  break;
1132              case DATA_TIMEMODIFIED:
1133                  $ordering = "r.timemodified $order";
1134                  break;
1135              case DATA_TIMEADDED:
1136              default:
1137                  $sort     = 0;
1138                  $ordering = "r.timecreated $order";
1139          }
1140  
1141          $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields;
1142          $count = ' COUNT(DISTINCT c.recordid) ';
1143          $tables = '{data_content} c,{data_records} r, {user} u ';
1144          $where = 'WHERE c.recordid = r.id
1145                       AND r.dataid = :dataid
1146                       AND r.userid = u.id ';
1147          $params['dataid'] = $data->id;
1148          $sortorder = " ORDER BY $ordering, r.id $order";
1149          $searchselect = '';
1150  
1151          // If requiredentries is not reached, only show current user's entries.
1152          if (!$requiredentriesallowed) {
1153              $where .= ' AND u.id = :myid2 ';
1154              $entrysql = ' AND r.userid = :myid3 ';
1155              $params['myid2'] = $USER->id;
1156              $initialparams['myid3'] = $params['myid2'];
1157          }
1158  
1159          if ($search) {
1160              $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)."
1161                                OR ".$DB->sql_like('u.firstname', ':search2', false)."
1162                                OR ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
1163              $params['search1'] = "%$search%";
1164              $params['search2'] = "%$search%";
1165              $params['search3'] = "%$search%";
1166          } else {
1167              $searchselect = ' ';
1168          }
1169  
1170      } else {
1171  
1172          $sortcontent = $DB->sql_compare_text('c.' . $sortfield->get_sort_field());
1173          $sortcontentfull = $sortfield->get_sort_sql($sortcontent);
1174  
1175          $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields . ',
1176                  ' . $sortcontentfull . ' AS sortorder ';
1177          $count = ' COUNT(DISTINCT c.recordid) ';
1178          $tables = '{data_content} c, {data_records} r, {user} u ';
1179          $where = 'WHERE c.recordid = r.id
1180                       AND r.dataid = :dataid
1181                       AND r.userid = u.id ';
1182          if (!$advanced) {
1183              $where .= 'AND c.fieldid = :sort';
1184          }
1185          $params['dataid'] = $data->id;
1186          $params['sort'] = $sort;
1187          $sortorder = ' ORDER BY sortorder '.$order.' , r.id ASC ';
1188          $searchselect = '';
1189  
1190          // If requiredentries is not reached, only show current user's entries.
1191          if (!$requiredentriesallowed) {
1192              $where .= ' AND u.id = :myid2';
1193              $entrysql = ' AND r.userid = :myid3';
1194              $params['myid2'] = $USER->id;
1195              $initialparams['myid3'] = $params['myid2'];
1196          }
1197  
1198          if ($search) {
1199              $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR
1200                  ".$DB->sql_like('u.firstname', ':search2', false)." OR
1201                  ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
1202              $params['search1'] = "%$search%";
1203              $params['search2'] = "%$search%";
1204              $params['search3'] = "%$search%";
1205          } else {
1206              $searchselect = ' ';
1207          }
1208      }
1209  
1210      // To actually fetch the records.
1211  
1212      $fromsql    = "FROM $tables $advtables $where $advwhere $groupselect $approveselect $searchselect $advsearchselect";
1213      $allparams  = array_merge($params, $advparams);
1214  
1215      // Provide initial sql statements and parameters to reduce the number of total records.
1216      $initialselect = $groupselect . $approveselect . $entrysql;
1217  
1218      $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams);
1219      $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data->id);
1220      $selectdata = $where . $groupselect . $approveselect;
1221  
1222      if (!empty($advanced)) {
1223          $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
1224          $sqlselect = $advancedsearchsql['sql'];
1225          $allparams = array_merge($allparams, $advancedsearchsql['params']);
1226          $totalcount = count($newrecordids);
1227      } else {
1228          $sqlselect  = "SELECT $what $fromsql $sortorder";
1229          $sqlcountselect  = "SELECT $count $fromsql";
1230          $totalcount = $DB->count_records_sql($sqlcountselect, $allparams);
1231      }
1232  
1233      // Work out the paging numbers and counts.
1234      if (empty($searchselect) && empty($advsearchselect)) {
1235          $maxcount = $totalcount;
1236      } else {
1237          $maxcount = count($recordids);
1238      }
1239  
1240      if ($record) {     // We need to just show one, so where is it in context?
1241          $nowperpage = 1;
1242          $mode = 'single';
1243          $page = 0;
1244          // TODO MDL-33797 - Reduce this or consider redesigning the paging system.
1245          if ($allrecordids = $DB->get_fieldset_sql($sqlselect, $allparams)) {
1246              $page = (int)array_search($record->id, $allrecordids);
1247              unset($allrecordids);
1248          }
1249      } else if ($mode == 'single') {  // We rely on ambient $page settings
1250          $nowperpage = 1;
1251  
1252      } else {
1253          $nowperpage = $perpage;
1254      }
1255  
1256      // Get the actual records.
1257      if (!$records = $DB->get_records_sql($sqlselect, $allparams, $page * $nowperpage, $nowperpage)) {
1258          // Nothing to show!
1259          if ($record) {         // Something was requested so try to show that at least (bug 5132)
1260              if (data_can_view_record($data, $record, $currentgroup, $canmanageentries)) {
1261                  // OK, we can show this one
1262                  $records = array($record->id => $record);
1263                  $totalcount = 1;
1264              }
1265          }
1266  
1267      }
1268  
1269      return [$records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode];
1270  }
1271  
1272  /**
1273   * Check if the current user can view the given record.
1274   *
1275   * @param  stdClass $data           database record
1276   * @param  stdClass $record         the record (entry) to check
1277   * @param  int $currentgroup        current group
1278   * @param  bool $canmanageentries   if the user can manage entries
1279   * @return bool true if the user can view the entry
1280   * @since  Moodle 3.3
1281   */
1282  function data_can_view_record($data, $record, $currentgroup, $canmanageentries) {
1283      global $USER;
1284  
1285      if ($canmanageentries || empty($data->approval) ||
1286               $record->approved || (isloggedin() && $record->userid == $USER->id)) {
1287  
1288          if (!$currentgroup || $record->groupid == $currentgroup || $record->groupid == 0) {
1289              return true;
1290          }
1291      }
1292      return false;
1293  }
1294  
1295  /**
1296   * Return all the field instances for a given database.
1297   *
1298   * @param  stdClass $data database object
1299   * @return array field instances
1300   * @since  Moodle 3.3
1301   */
1302  function data_get_field_instances($data) {
1303      global $DB;
1304  
1305      $instances = [];
1306      if ($fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id')) {
1307          foreach ($fields as $field) {
1308              $instances[] = data_get_field($field, $data);
1309          }
1310      }
1311      return $instances;
1312  }
1313  
1314  /**
1315   * Build the search array.
1316   *
1317   * @param  stdClass $data      the database object
1318   * @param  bool $paging        if paging is being used
1319   * @param  array $searcharray  the current search array (saved by session)
1320   * @param  array $defaults     default values for the searchable fields
1321   * @param  str $fn             the first name to search (optional)
1322   * @param  str $ln             the last name to search (optional)
1323   * @return array               the search array and plain search build based on the different elements
1324   * @since  Moodle 3.3
1325   */
1326  function data_build_search_array($data, $paging, $searcharray, $defaults = null, $fn = '', $ln = '') {
1327      global $DB;
1328  
1329      $search = '';
1330      $vals = array();
1331      $fields = $DB->get_records('data_fields', array('dataid' => $data->id));
1332  
1333      if (!empty($fields)) {
1334          foreach ($fields as $field) {
1335              $searchfield = data_get_field_from_id($field->id, $data);
1336              // Get field data to build search sql with.  If paging is false, get from user.
1337              // If paging is true, get data from $searcharray which is obtained from the $SESSION (see line 116).
1338              if (!$paging) {
1339                  $val = $searchfield->parse_search_field($defaults);
1340              } else {
1341                  // Set value from session if there is a value @ the required index.
1342                  if (isset($searcharray[$field->id])) {
1343                      $val = $searcharray[$field->id]->data;
1344                  } else { // If there is not an entry @ the required index, set value to blank.
1345                      $val = '';
1346                  }
1347              }
1348              if (!empty($val)) {
1349                  $searcharray[$field->id] = new stdClass();
1350                  list($searcharray[$field->id]->sql, $searcharray[$field->id]->params) = $searchfield->generate_sql('c'.$field->id, $val);
1351                  $searcharray[$field->id]->data = $val;
1352                  $vals[] = $val;
1353              } else {
1354                  // Clear it out.
1355                  unset($searcharray[$field->id]);
1356              }
1357          }
1358      }
1359  
1360      $rawtagnames = optional_param_array('tags', false, PARAM_TAGLIST);
1361  
1362      if ($rawtagnames) {
1363          $searcharray[DATA_TAGS] = new stdClass();
1364          $searcharray[DATA_TAGS]->params = [];
1365          $searcharray[DATA_TAGS]->rawtagnames = $rawtagnames;
1366          $searcharray[DATA_TAGS]->sql = '';
1367      } else {
1368          unset($searcharray[DATA_TAGS]);
1369      }
1370  
1371      if (!$paging) {
1372          // Name searching.
1373          $fn = optional_param('u_fn', $fn, PARAM_NOTAGS);
1374          $ln = optional_param('u_ln', $ln, PARAM_NOTAGS);
1375      } else {
1376          $fn = isset($searcharray[DATA_FIRSTNAME]) ? $searcharray[DATA_FIRSTNAME]->data : '';
1377          $ln = isset($searcharray[DATA_LASTNAME]) ? $searcharray[DATA_LASTNAME]->data : '';
1378      }
1379      if (!empty($fn)) {
1380          $searcharray[DATA_FIRSTNAME] = new stdClass();
1381          $searcharray[DATA_FIRSTNAME]->sql    = '';
1382          $searcharray[DATA_FIRSTNAME]->params = array();
1383          $searcharray[DATA_FIRSTNAME]->field  = 'u.firstname';
1384          $searcharray[DATA_FIRSTNAME]->data   = $fn;
1385          $vals[] = $fn;
1386      } else {
1387          unset($searcharray[DATA_FIRSTNAME]);
1388      }
1389      if (!empty($ln)) {
1390          $searcharray[DATA_LASTNAME] = new stdClass();
1391          $searcharray[DATA_LASTNAME]->sql     = '';
1392          $searcharray[DATA_LASTNAME]->params = array();
1393          $searcharray[DATA_LASTNAME]->field   = 'u.lastname';
1394          $searcharray[DATA_LASTNAME]->data    = $ln;
1395          $vals[] = $ln;
1396      } else {
1397          unset($searcharray[DATA_LASTNAME]);
1398      }
1399  
1400      // In case we want to switch to simple search later - there might be multiple values there ;-).
1401      if ($vals) {
1402          $val = reset($vals);
1403          if (is_string($val)) {
1404              $search = $val;
1405          }
1406      }
1407      return [$searcharray, $search];
1408  }
1409  
1410  /**
1411   * Approves or unapproves an entry.
1412   *
1413   * @param  int $entryid the entry to approve or unapprove.
1414   * @param  bool $approve Whether to approve or unapprove (true for approve false otherwise).
1415   * @since  Moodle 3.3
1416   */
1417  function data_approve_entry($entryid, $approve) {
1418      global $DB;
1419  
1420      $newrecord = new stdClass();
1421      $newrecord->id = $entryid;
1422      $newrecord->approved = $approve ? 1 : 0;
1423      $DB->update_record('data_records', $newrecord);
1424  }
1425  
1426  /**
1427   * Populate the field contents of a new record with the submitted data.
1428   * An event has been previously triggered upon the creation of the new record in data_add_record().
1429   *
1430   * @param  stdClass $data           database object
1431   * @param  stdClass $context        context object
1432   * @param  int $recordid            the new record id
1433   * @param  array $fields            list of fields of the database
1434   * @param  stdClass $datarecord     the submitted data
1435   * @param  stdClass $processeddata  pre-processed submitted fields
1436   * @since  Moodle 3.3
1437   */
1438  function data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata) {
1439      global $DB;
1440  
1441      // Insert a whole lot of empty records to make sure we have them.
1442      $records = array();
1443      foreach ($fields as $field) {
1444          $content = new stdClass();
1445          $content->recordid = $recordid;
1446          $content->fieldid = $field->id;
1447          $records[] = $content;
1448      }
1449  
1450      // Bulk insert the records now. Some records may have no data but all must exist.
1451      $DB->insert_records('data_content', $records);
1452  
1453      // Add all provided content.
1454      foreach ($processeddata->fields as $fieldname => $field) {
1455          $field->update_content($recordid, $datarecord->$fieldname, $fieldname);
1456      }
1457  }
1458  
1459  /**
1460   * Updates the fields contents of an existing record.
1461   *
1462   * @param  stdClass $data           database object
1463   * @param  stdClass $record         record to update object
1464   * @param  stdClass $context        context object
1465   * @param  stdClass $datarecord     the submitted data
1466   * @param  stdClass $processeddata  pre-processed submitted fields
1467   * @since  Moodle 3.3
1468   */
1469  function data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata) {
1470      global $DB;
1471  
1472      // Reset the approved flag after edit if the user does not have permission to approve their own entries.
1473      if (!has_capability('mod/data:approve', $context)) {
1474          $record->approved = 0;
1475      }
1476  
1477      // Update the parent record.
1478      $record->timemodified = time();
1479      $DB->update_record('data_records', $record);
1480  
1481      // Update all content.
1482      foreach ($processeddata->fields as $fieldname => $field) {
1483          $field->update_content($record->id, $datarecord->$fieldname, $fieldname);
1484      }
1485  
1486      // Trigger an event for updating this record.
1487      $event = \mod_data\event\record_updated::create(array(
1488          'objectid' => $record->id,
1489          'context' => $context,
1490          'courseid' => $data->course,
1491          'other' => array(
1492              'dataid' => $data->id
1493          )
1494      ));
1495      $event->add_record_snapshot('data', $data);
1496      $event->trigger();
1497  }