Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

       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      $userfieldsapi = \core_user\fields::for_userpic()->excluding('id');
    1116      $namefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
    1117  
    1118      // Find the field we are sorting on.
    1119      if ($sort <= 0 or !$sortfield = data_get_field_from_id($sort, $data)) {
    1120  
    1121          switch ($sort) {
    1122              case DATA_LASTNAME:
    1123                  $ordering = "u.lastname $order, u.firstname $order";
    1124                  break;
    1125              case DATA_FIRSTNAME:
    1126                  $ordering = "u.firstname $order, u.lastname $order";
    1127                  break;
    1128              case DATA_APPROVED:
    1129                  $ordering = "r.approved $order, r.timecreated $order";
    1130                  break;
    1131              case DATA_TIMEMODIFIED:
    1132                  $ordering = "r.timemodified $order";
    1133                  break;
    1134              case DATA_TIMEADDED:
    1135              default:
    1136                  $sort     = 0;
    1137                  $ordering = "r.timecreated $order";
    1138          }
    1139  
    1140          $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields;
    1141          $count = ' COUNT(DISTINCT c.recordid) ';
    1142          $tables = '{data_content} c,{data_records} r, {user} u ';
    1143          $where = 'WHERE c.recordid = r.id
    1144                       AND r.dataid = :dataid
    1145                       AND r.userid = u.id ';
    1146          $params['dataid'] = $data->id;
    1147          $sortorder = " ORDER BY $ordering, r.id $order";
    1148          $searchselect = '';
    1149  
    1150          // If requiredentries is not reached, only show current user's entries.
    1151          if (!$requiredentriesallowed) {
    1152              $where .= ' AND u.id = :myid2 ';
    1153              $entrysql = ' AND r.userid = :myid3 ';
    1154              $params['myid2'] = $USER->id;
    1155              $initialparams['myid3'] = $params['myid2'];
    1156          }
    1157  
    1158          if ($search) {
    1159              $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)."
    1160                                OR ".$DB->sql_like('u.firstname', ':search2', false)."
    1161                                OR ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
    1162              $params['search1'] = "%$search%";
    1163              $params['search2'] = "%$search%";
    1164              $params['search3'] = "%$search%";
    1165          } else {
    1166              $searchselect = ' ';
    1167          }
    1168  
    1169      } else {
    1170  
    1171          $sortcontent = $DB->sql_compare_text('c.' . $sortfield->get_sort_field());
    1172          $sortcontentfull = $sortfield->get_sort_sql($sortcontent);
    1173  
    1174          $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields . ',
    1175                  ' . $sortcontentfull . ' AS sortorder ';
    1176          $count = ' COUNT(DISTINCT c.recordid) ';
    1177          $tables = '{data_content} c, {data_records} r, {user} u ';
    1178          $where = 'WHERE c.recordid = r.id
    1179                       AND r.dataid = :dataid
    1180                       AND r.userid = u.id ';
    1181          if (!$advanced) {
    1182              $where .= 'AND c.fieldid = :sort';
    1183          }
    1184          $params['dataid'] = $data->id;
    1185          $params['sort'] = $sort;
    1186          $sortorder = ' ORDER BY sortorder '.$order.' , r.id ASC ';
    1187          $searchselect = '';
    1188  
    1189          // If requiredentries is not reached, only show current user's entries.
    1190          if (!$requiredentriesallowed) {
    1191              $where .= ' AND u.id = :myid2';
    1192              $entrysql = ' AND r.userid = :myid3';
    1193              $params['myid2'] = $USER->id;
    1194              $initialparams['myid3'] = $params['myid2'];
    1195          }
    1196  
    1197          if ($search) {
    1198              $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR
    1199                  ".$DB->sql_like('u.firstname', ':search2', false)." OR
    1200                  ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
    1201              $params['search1'] = "%$search%";
    1202              $params['search2'] = "%$search%";
    1203              $params['search3'] = "%$search%";
    1204          } else {
    1205              $searchselect = ' ';
    1206          }
    1207      }
    1208  
    1209      // To actually fetch the records.
    1210  
    1211      $fromsql    = "FROM $tables $advtables $where $advwhere $groupselect $approveselect $searchselect $advsearchselect";
    1212      $allparams  = array_merge($params, $advparams);
    1213  
    1214      // Provide initial sql statements and parameters to reduce the number of total records.
    1215      $initialselect = $groupselect . $approveselect . $entrysql;
    1216  
    1217      $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams);
    1218      $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data->id);
    1219      $selectdata = $where . $groupselect . $approveselect;
    1220  
    1221      if (!empty($advanced)) {
    1222          $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
    1223          $sqlselect = $advancedsearchsql['sql'];
    1224          $allparams = array_merge($allparams, $advancedsearchsql['params']);
    1225          $totalcount = count($newrecordids);
    1226      } else {
    1227          $sqlselect  = "SELECT $what $fromsql $sortorder";
    1228          $sqlcountselect  = "SELECT $count $fromsql";
    1229          $totalcount = $DB->count_records_sql($sqlcountselect, $allparams);
    1230      }
    1231  
    1232      // Work out the paging numbers and counts.
    1233      if (empty($searchselect) && empty($advsearchselect)) {
    1234          $maxcount = $totalcount;
    1235      } else {
    1236          $maxcount = count($recordids);
    1237      }
    1238  
    1239      if ($record) {     // We need to just show one, so where is it in context?
    1240          $nowperpage = 1;
    1241          $mode = 'single';
    1242          $page = 0;
    1243          // TODO MDL-33797 - Reduce this or consider redesigning the paging system.
    1244          if ($allrecordids = $DB->get_fieldset_sql($sqlselect, $allparams)) {
    1245              $page = (int)array_search($record->id, $allrecordids);
    1246              unset($allrecordids);
    1247          }
    1248      } else if ($mode == 'single') {  // We rely on ambient $page settings
    1249          $nowperpage = 1;
    1250  
    1251      } else {
    1252          $nowperpage = $perpage;
    1253      }
    1254  
    1255      // Get the actual records.
    1256      if (!$records = $DB->get_records_sql($sqlselect, $allparams, $page * $nowperpage, $nowperpage)) {
    1257          // Nothing to show!
    1258          if ($record) {         // Something was requested so try to show that at least (bug 5132)
    1259              if (data_can_view_record($data, $record, $currentgroup, $canmanageentries)) {
    1260                  // OK, we can show this one
    1261                  $records = array($record->id => $record);
    1262                  $totalcount = 1;
    1263              }
    1264          }
    1265  
    1266      }
    1267  
    1268      return [$records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode];
    1269  }
    1270  
    1271  /**
    1272   * Check if the current user can view the given record.
    1273   *
    1274   * @param  stdClass $data           database record
    1275   * @param  stdClass $record         the record (entry) to check
    1276   * @param  int $currentgroup        current group
    1277   * @param  bool $canmanageentries   if the user can manage entries
    1278   * @return bool true if the user can view the entry
    1279   * @since  Moodle 3.3
    1280   */
    1281  function data_can_view_record($data, $record, $currentgroup, $canmanageentries) {
    1282      global $USER;
    1283  
    1284      if ($canmanageentries || empty($data->approval) ||
    1285               $record->approved || (isloggedin() && $record->userid == $USER->id)) {
    1286  
    1287          if (!$currentgroup || $record->groupid == $currentgroup || $record->groupid == 0) {
    1288              return true;
    1289          }
    1290      }
    1291      return false;
    1292  }
    1293  
    1294  /**
    1295   * Return all the field instances for a given database.
    1296   *
    1297   * @param  stdClass $data database object
    1298   * @return array field instances
    1299   * @since  Moodle 3.3
    1300   */
    1301  function data_get_field_instances($data) {
    1302      global $DB;
    1303  
    1304      $instances = [];
    1305      if ($fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id')) {
    1306          foreach ($fields as $field) {
    1307              $instances[] = data_get_field($field, $data);
    1308          }
    1309      }
    1310      return $instances;
    1311  }
    1312  
    1313  /**
    1314   * Build the search array.
    1315   *
    1316   * @param  stdClass $data      the database object
    1317   * @param  bool $paging        if paging is being used
    1318   * @param  array $searcharray  the current search array (saved by session)
    1319   * @param  array $defaults     default values for the searchable fields
    1320   * @param  str $fn             the first name to search (optional)
    1321   * @param  str $ln             the last name to search (optional)
    1322   * @return array               the search array and plain search build based on the different elements
    1323   * @since  Moodle 3.3
    1324   */
    1325  function data_build_search_array($data, $paging, $searcharray, $defaults = null, $fn = '', $ln = '') {
    1326      global $DB;
    1327  
    1328      $search = '';
    1329      $vals = array();
    1330      $fields = $DB->get_records('data_fields', array('dataid' => $data->id));
    1331  
    1332      if (!empty($fields)) {
    1333          foreach ($fields as $field) {
    1334              $searchfield = data_get_field_from_id($field->id, $data);
    1335              // Get field data to build search sql with.  If paging is false, get from user.
    1336              // If paging is true, get data from $searcharray which is obtained from the $SESSION (see line 116).
    1337              if (!$paging) {
    1338                  $val = $searchfield->parse_search_field($defaults);
    1339              } else {
    1340                  // Set value from session if there is a value @ the required index.
    1341                  if (isset($searcharray[$field->id])) {
    1342                      $val = $searcharray[$field->id]->data;
    1343                  } else { // If there is not an entry @ the required index, set value to blank.
    1344                      $val = '';
    1345                  }
    1346              }
    1347              if (!empty($val)) {
    1348                  $searcharray[$field->id] = new stdClass();
    1349                  list($searcharray[$field->id]->sql, $searcharray[$field->id]->params) = $searchfield->generate_sql('c'.$field->id, $val);
    1350                  $searcharray[$field->id]->data = $val;
    1351                  $vals[] = $val;
    1352              } else {
    1353                  // Clear it out.
    1354                  unset($searcharray[$field->id]);
    1355              }
    1356          }
    1357      }
    1358  
    1359      $rawtagnames = optional_param_array('tags', false, PARAM_TAGLIST);
    1360  
    1361      if ($rawtagnames) {
    1362          $searcharray[DATA_TAGS] = new stdClass();
    1363          $searcharray[DATA_TAGS]->params = [];
    1364          $searcharray[DATA_TAGS]->rawtagnames = $rawtagnames;
    1365          $searcharray[DATA_TAGS]->sql = '';
    1366      } else {
    1367          unset($searcharray[DATA_TAGS]);
    1368      }
    1369  
    1370      if (!$paging) {
    1371          // Name searching.
    1372          $fn = optional_param('u_fn', $fn, PARAM_NOTAGS);
    1373          $ln = optional_param('u_ln', $ln, PARAM_NOTAGS);
    1374      } else {
    1375          $fn = isset($searcharray[DATA_FIRSTNAME]) ? $searcharray[DATA_FIRSTNAME]->data : '';
    1376          $ln = isset($searcharray[DATA_LASTNAME]) ? $searcharray[DATA_LASTNAME]->data : '';
    1377      }
    1378      if (!empty($fn)) {
    1379          $searcharray[DATA_FIRSTNAME] = new stdClass();
    1380          $searcharray[DATA_FIRSTNAME]->sql    = '';
    1381          $searcharray[DATA_FIRSTNAME]->params = array();
    1382          $searcharray[DATA_FIRSTNAME]->field  = 'u.firstname';
    1383          $searcharray[DATA_FIRSTNAME]->data   = $fn;
    1384          $vals[] = $fn;
    1385      } else {
    1386          unset($searcharray[DATA_FIRSTNAME]);
    1387      }
    1388      if (!empty($ln)) {
    1389          $searcharray[DATA_LASTNAME] = new stdClass();
    1390          $searcharray[DATA_LASTNAME]->sql     = '';
    1391          $searcharray[DATA_LASTNAME]->params = array();
    1392          $searcharray[DATA_LASTNAME]->field   = 'u.lastname';
    1393          $searcharray[DATA_LASTNAME]->data    = $ln;
    1394          $vals[] = $ln;
    1395      } else {
    1396          unset($searcharray[DATA_LASTNAME]);
    1397      }
    1398  
    1399      // In case we want to switch to simple search later - there might be multiple values there ;-).
    1400      if ($vals) {
    1401          $val = reset($vals);
    1402          if (is_string($val)) {
    1403              $search = $val;
    1404          }
    1405      }
    1406      return [$searcharray, $search];
    1407  }
    1408  
    1409  /**
    1410   * Approves or unapproves an entry.
    1411   *
    1412   * @param  int $entryid the entry to approve or unapprove.
    1413   * @param  bool $approve Whether to approve or unapprove (true for approve false otherwise).
    1414   * @since  Moodle 3.3
    1415   */
    1416  function data_approve_entry($entryid, $approve) {
    1417      global $DB;
    1418  
    1419      $newrecord = new stdClass();
    1420      $newrecord->id = $entryid;
    1421      $newrecord->approved = $approve ? 1 : 0;
    1422      $DB->update_record('data_records', $newrecord);
    1423  }
    1424  
    1425  /**
    1426   * Populate the field contents of a new record with the submitted data.
    1427   * An event has been previously triggered upon the creation of the new record in data_add_record().
    1428   *
    1429   * @param  stdClass $data           database object
    1430   * @param  stdClass $context        context object
    1431   * @param  int $recordid            the new record id
    1432   * @param  array $fields            list of fields of the database
    1433   * @param  stdClass $datarecord     the submitted data
    1434   * @param  stdClass $processeddata  pre-processed submitted fields
    1435   * @since  Moodle 3.3
    1436   */
    1437  function data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata) {
    1438      global $DB;
    1439  
    1440      // Insert a whole lot of empty records to make sure we have them.
    1441      $records = array();
    1442      foreach ($fields as $field) {
    1443          $content = new stdClass();
    1444          $content->recordid = $recordid;
    1445          $content->fieldid = $field->id;
    1446          $records[] = $content;
    1447      }
    1448  
    1449      // Bulk insert the records now. Some records may have no data but all must exist.
    1450      $DB->insert_records('data_content', $records);
    1451  
    1452      // Add all provided content.
    1453      foreach ($processeddata->fields as $fieldname => $field) {
    1454          $field->update_content($recordid, $datarecord->$fieldname, $fieldname);
    1455      }
    1456  }
    1457  
    1458  /**
    1459   * Updates the fields contents of an existing record.
    1460   *
    1461   * @param  stdClass $data           database object
    1462   * @param  stdClass $record         record to update object
    1463   * @param  stdClass $context        context object
    1464   * @param  stdClass $datarecord     the submitted data
    1465   * @param  stdClass $processeddata  pre-processed submitted fields
    1466   * @since  Moodle 3.3
    1467   */
    1468  function data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata) {
    1469      global $DB;
    1470  
    1471      // Reset the approved flag after edit if the user does not have permission to approve their own entries.
    1472      if (!has_capability('mod/data:approve', $context)) {
    1473          $record->approved = 0;
    1474      }
    1475  
    1476      // Update the parent record.
    1477      $record->timemodified = time();
    1478      $DB->update_record('data_records', $record);
    1479  
    1480      // Update all content.
    1481      foreach ($processeddata->fields as $fieldname => $field) {
    1482          $field->update_content($record->id, $datarecord->$fieldname, $fieldname);
    1483      }
    1484  
    1485      // Trigger an event for updating this record.
    1486      $event = \mod_data\event\record_updated::create(array(
    1487          'objectid' => $record->id,
    1488          'context' => $context,
    1489          'courseid' => $data->course,
    1490          'other' => array(
    1491              'dataid' => $data->id
    1492          )
    1493      ));
    1494      $event->add_record_snapshot('data', $data);
    1495      $event->trigger();
    1496  }