MDL-57455 mod_data: Implement tagging
[moodle.git] / mod / data / locallib.php
1 <?php
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/>.
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  */
24 defined('MOODLE_INTERNAL') || die();
26 require_once($CFG->dirroot . '/mod/data/lib.php');
27 require_once($CFG->libdir . '/portfolio/caller.php');
28 require_once($CFG->libdir . '/filelib.php');
30 /**
31  * The class to handle entry exports of a database module
32  */
33 class data_portfolio_caller extends portfolio_module_caller_base {
35     /** @var int the single record to export */
36     protected $recordid;
38     /** @var object the record from the data table */
39     private $data;
41     /**#@+ @var array the fields used and their fieldtypes */
42     private $fields;
43     private $fieldtypes;
45     /** @var object the records to export */
46     private $records;
48     /** @var int how many records are 'mine' */
49     private $minecount;
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     }
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     }
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         }
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         }
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     }
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     }
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     }
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         }
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     }
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     }
256     /**
257      *  @return string
258      */
259     public static function display_name() {
260         return get_string('modulename', 'data');
261     }
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     }
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();
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         }
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);
332         // actual replacement of the tags
333         return array(str_ireplace($patterns, $replacement, $this->data->singletemplate), $files);
334     }
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     }
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     }
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     }
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     }
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     }
404     public function get_allowed_export_config() {
405         return array('mineonly');
406     }
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;
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     }
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     }
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     }
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     }
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     }
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     }
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         }
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     }
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     }
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     }
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     }
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;
593     require_once($CFG->dirroot.'/calendar/lib.php');
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);
611             $event->timestart    = $data->timeavailablefrom;
612             $event->timesort     = $data->timeavailablefrom;
613             $event->visible      = instance_is_visible('data', $data);
614             $event->timeduration = 0;
615             $calendarevent = calendar_event::load($event->id);
616             $calendarevent->update($event);
617         } else {
618             // Calendar event is on longer needed.
619             $calendarevent = calendar_event::load($event->id);
620             $calendarevent->delete();
621         }
622     } else {
623         // Event doesn't exist so create one.
624         if (isset($data->timeavailablefrom) && $data->timeavailablefrom > 0) {
625             $event->name         = get_string('calendarstart', 'data', $data->name);
626             $event->description  = format_module_intro('data', $data, $data->coursemodule);
627             $event->courseid     = $data->course;
628             $event->groupid      = 0;
629             $event->userid       = 0;
630             $event->modulename   = 'data';
631             $event->instance     = $data->id;
632             $event->timestart    = $data->timeavailablefrom;
633             $event->timesort     = $data->timeavailablefrom;
634             $event->visible      = instance_is_visible('data', $data);
635             $event->timeduration = 0;
636             calendar_event::create($event);
637         }
638     }
640     // Data end calendar events.
641     $event = new stdClass();
642     $event->type = CALENDAR_EVENT_TYPE_ACTION;
643     $event->eventtype = DATA_EVENT_TYPE_CLOSE;
644     if ($event->id = $DB->get_field('event', 'id',
645             array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) {
646         if ($data->timeavailableto > 0) {
647             // Calendar event exists so update it.
648             $event->name         = get_string('calendarend', 'data', $data->name);
649             $event->description  = format_module_intro('data', $data, $data->coursemodule);
650             $event->timestart    = $data->timeavailableto;
651             $event->timesort     = $data->timeavailableto;
652             $event->visible      = instance_is_visible('data', $data);
653             $event->timeduration = 0;
654             $calendarevent = calendar_event::load($event->id);
655             $calendarevent->update($event);
656         } else {
657             // Calendar event is on longer needed.
658             $calendarevent = calendar_event::load($event->id);
659             $calendarevent->delete();
660         }
661     } else {
662         // Event doesn't exist so create one.
663         if (isset($data->timeavailableto) && $data->timeavailableto > 0) {
664             $event->name         = get_string('calendarend', 'data', $data->name);
665             $event->description  = format_module_intro('data', $data, $data->coursemodule);
666             $event->courseid     = $data->course;
667             $event->groupid      = 0;
668             $event->userid       = 0;
669             $event->modulename   = 'data';
670             $event->instance     = $data->id;
671             $event->timestart    = $data->timeavailableto;
672             $event->timesort     = $data->timeavailableto;
673             $event->visible      = instance_is_visible('data', $data);
674             $event->timeduration = 0;
675             calendar_event::create($event);
676         }
677     }
680 /**
681  * Check if a database is available for the current user.
682  *
683  * @param  stdClass  $data            database record
684  * @param  boolean $canmanageentries  optional, if the user can manage entries
685  * @param  stdClass  $context         Module context, required if $canmanageentries is not set
686  * @return array                      status (available or not and possible warnings)
687  * @since  Moodle 3.3
688  */
689 function data_get_time_availability_status($data, $canmanageentries = null, $context = null) {
690     $open = true;
691     $closed = false;
692     $warnings = array();
694     if ($canmanageentries === null) {
695         $canmanageentries = has_capability('mod/data:manageentries', $context);
696     }
698     if (!$canmanageentries) {
699         $timenow = time();
701         if (!empty($data->timeavailablefrom) and $data->timeavailablefrom > $timenow) {
702             $open = false;
703         }
704         if (!empty($data->timeavailableto) and $timenow > $data->timeavailableto) {
705             $closed = true;
706         }
708         if (!$open or $closed) {
709             if (!$open) {
710                 $warnings['notopenyet'] = userdate($data->timeavailablefrom);
711             }
712             if ($closed) {
713                 $warnings['expired'] = userdate($data->timeavailableto);
714             }
715             return array(false, $warnings);
716         }
717     }
719     // Database is available.
720     return array(true, $warnings);
723 /**
724  * Requires a database to be available for the current user.
725  *
726  * @param  stdClass  $data            database record
727  * @param  boolean $canmanageentries  optional, if the user can manage entries
728  * @param  stdClass  $context          Module context, required if $canmanageentries is not set
729  * @throws moodle_exception
730  * @since  Moodle 3.3
731  */
732 function data_require_time_available($data, $canmanageentries = null, $context = null) {
734     list($available, $warnings) = data_get_time_availability_status($data, $canmanageentries, $context);
736     if (!$available) {
737         $reason = current(array_keys($warnings));
738         throw new moodle_exception($reason, 'data', '', $warnings[$reason]);
739     }
742 /**
743  * Return the number of entries left to add to complete the activity.
744  *
745  * @param  stdClass $data           database object
746  * @param  int $numentries          the number of entries the current user has created
747  * @param  bool $canmanageentries   whether the user can manage entries (teachers, managers)
748  * @return int the number of entries left, 0 if no entries left or if is not required
749  * @since  Moodle 3.3
750  */
751 function data_get_entries_left_to_add($data, $numentries, $canmanageentries) {
752     if ($data->requiredentries > 0 && $numentries < $data->requiredentries && !$canmanageentries) {
753         return $data->requiredentries - $numentries;
754     }
755     return 0;
758 /**
759  * Return the number of entires left to add to view other users entries..
760  *
761  * @param  stdClass $data           database object
762  * @param  int $numentries          the number of entries the current user has created
763  * @param  bool $canmanageentries   whether the user can manage entries (teachers, managers)
764  * @return int the number of entries left, 0 if no entries left or if is not required
765  * @since  Moodle 3.3
766  */
767 function data_get_entries_left_to_view($data, $numentries, $canmanageentries) {
768     if ($data->requiredentriestoview > 0 && $numentries < $data->requiredentriestoview && !$canmanageentries) {
769         return $data->requiredentriestoview - $numentries;
770     }
771     return 0;
774 /**
775  * Returns data records tagged with a specified tag.
776  *
777  * This is a callback used by the tag area mod_data/data_records to search for data records
778  * tagged with a specific tag.
779  *
780  * @param core_tag_tag $tag
781  * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
782  *             are displayed on the page and the per-page limit may be bigger
783  * @param int $fromctx context id where the link was displayed, may be used by callbacks
784  *            to display items in the same context first
785  * @param int $ctx context id where to search for records
786  * @param bool $rec search in subcontexts as well
787  * @param int $page 0-based number of page being displayed
788  * @return \core_tag\output\tagindex
789  */
790 function mod_data_get_tagged_records($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = true, $page = 0) {
791     global $DB, $OUTPUT, $USER;
792     $perpage = $exclusivemode ? 20 : 5;
794     // Build the SQL query.
795     $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
796     $query = "SELECT dr.id, dr.dataid, dr.approved, d.timeviewfrom, d.timeviewto, dr.groupid, d.approval, dr.userid,
797                      d.requiredentriestoview, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect
798                 FROM {data_records} dr
799                 JOIN {data} d
800                   ON d.id = dr.dataid
801                 JOIN {modules} m
802                   ON m.name = 'data'
803                 JOIN {course_modules} cm
804                   ON cm.module = m.id AND cm.instance = d.id
805                 JOIN {tag_instance} tt
806                   ON dr.id = tt.itemid
807                 JOIN {course} c
808                   ON cm.course = c.id
809                 JOIN {context} ctx
810                   ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
811                WHERE tt.itemtype = :itemtype
812                  AND tt.tagid = :tagid
813                  AND tt.component = :component
814                  AND cm.deletioninprogress = 0
815                  AND dr.id %ITEMFILTER%
816                  AND c.id %COURSEFILTER%";
818     $params = array(
819         'itemtype' => 'data_records',
820         'tagid' => $tag->id,
821         'component' => 'mod_data',
822         'coursemodulecontextlevel' => CONTEXT_MODULE
823     );
825     if ($ctx) {
826         $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
827         $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
828         $params['contextid'] = $context->id;
829         $params['path'] = $context->path . '/%';
830     }
832     $query .= " ORDER BY ";
833     if ($fromctx) {
834         // In order-clause specify that modules from inside "fromctx" context should be returned first.
835         $fromcontext = context::instance_by_id($fromctx);
836         $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
837         $params['fromcontextid'] = $fromcontext->id;
838         $params['frompath'] = $fromcontext->path . '/%';
839     }
840     $query .= ' c.sortorder, cm.id, dr.id';
842     $totalpages = $page + 1;
844     // Use core_tag_index_builder to build and filter the list of items.
845     $builder = new core_tag_index_builder('mod_data', 'data_records', $query, $params, $page * $perpage, $perpage + 1);
846     $now = time();
847     $entrycount = [];
848     $activitygroupmode = [];
849     $usergroups = [];
850     $titlefields = [];
851     while ($item = $builder->has_item_that_needs_access_check()) {
852         context_helper::preload_from_record($item);
853         $modinfo = get_fast_modinfo($item->courseid);
854         $cm = $modinfo->get_cm($item->cmid);
855         $context = \context_module::instance($cm->id);
856         $courseid = $item->courseid;
858         if (!$builder->can_access_course($courseid)) {
859             $builder->set_accessible($item, false);
860             continue;
861         }
863         if (!has_capability('mod/data:viewentry', $context)) {
864             $builder->set_accessible($item, false);
865             continue;
866         }
868         if ($USER->id != $item->userid && (($item->timeviewfrom && $now < $item->timeviewfrom)
869                 || ($item->timeviewto && $now > $item->timeviewto))) {
870             $builder->set_accessible($item, false);
871             continue;
872         }
874         if ($USER->id != $item->userid && $item->approval && !$item->approved) {
875             $builder->set_accessible($item, false);
876             continue;
877         }
879         if ($item->requiredentriestoview) {
880             if (!isset($entrycount[$item->dataid])) {
881                 $entrycount[$item->dataid] = $DB->count_records('data_records', array('dataid' => $item->dataid));
882             }
883             $sufficiententries = $item->requiredentriestoview > $entrycount[$item->dataid];
884             $builder->set_accessible($item, $sufficiententries);
885         }
887         if (!isset($activitygroupmode[$cm->id])) {
888             $activitygroupmode[$cm->id] = groups_get_activity_groupmode($cm);
889         }
891         if (!isset($usergroups[$item->groupid])) {
892             $usergroups[$item->groupid] = groups_is_member($item->groupid, $USER->id);
893         }
895         if ($activitygroupmode[$cm->id] == SEPARATEGROUPS && !$usergroups[$item->groupid]) {
896             $builder->set_accessible($item, false);
897             continue;
898         }
900         $builder->set_accessible($item, true);
901     }
903     $items = $builder->get_items();
904     if (count($items) > $perpage) {
905         $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
906         array_pop($items);
907     }
909     // Build the display contents.
910     if ($items) {
911         $tagfeed = new core_tag\output\tagfeed();
912         foreach ($items as $item) {
913             context_helper::preload_from_record($item);
914             $modinfo = get_fast_modinfo($item->courseid);
915             $cm = $modinfo->get_cm($item->cmid);
916             $pageurl = new moodle_url('/mod/data/view.php', array(
917                     'rid' => $item->id,
918                     'd' => $item->dataid
919             ));
921             if (!isset($titlefields[$item->dataid])) {
922                 $titlefields[$item->dataid] = data_get_tag_title_field($item->dataid);
923             }
925             $pagename = data_get_tag_title_for_entry($titlefields[$item->dataid], $item);
926             $pagename = html_writer::link($pageurl, $pagename);
927             $courseurl = course_get_url($item->courseid, $cm->sectionnum);
928             $cmname = html_writer::link($cm->url, $cm->get_formatted_name());
929             $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
930             $coursename = html_writer::link($courseurl, $coursename);
931             $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
932             $tagfeed->add($icon, $pagename, $cmname . '<br>' . $coursename);
933         }
934         $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT));
936         return new core_tag\output\tagindex($tag, 'mod_data', 'data_records', $content, $exclusivemode,
937             $fromctx, $ctx, $rec, $page, $totalpages);
938     }
941 /**
942  * Get the title of a field to show when displaying tag results.
943  *
944  * @param int $dataid The id of the data field
945  * @return stdClass The field data from the 'data_fields' table as well as it's priority
946  */
947 function data_get_tag_title_field($dataid) {
948     global $DB, $CFG;
950     $validfieldtypes = array('text', 'textarea', 'menu', 'radiobutton', 'checkbox', 'multimenu', 'url');
951     $fields = $DB->get_records('data_fields', ['dataid' => $dataid]);
952     $template = $DB->get_field('data', 'addtemplate', ['id' => $dataid]);
954     $filteredfields = [];
956     foreach ($fields as $field) {
957         if (!in_array($field->type, $validfieldtypes)) {
958             continue;
959         }
960         $field->addtemplateposition = strpos($template, '[['.$field->name.']]');
961         if ($field->addtemplateposition === false) {
962             continue;
963         }
964         require_once($CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php');
965         $classname = 'data_field_' . $field->type;
966         $field->priority = $classname::get_priority();
967         $filteredfields[] = $field;
968     }
970     $sort = function($record1, $record2) {
971         // If a content's fieldtype is compulsory in the database than it would have priority than any other non-compulsory content.
972         if (($record1->required && $record2->required) || (!$record1->required && !$record2->required)) {
973             if ($record1->priority === $record2->priority) {
974                 return $record1->id < $record2->id ? 1 : -1;
975             }
977             return $record1->priority < $record2->priority ? -1 : 1;
978         } else if ($record1->required && !$record2->required) {
979             return 1;
980         } else {
981             return -1;
982         }
983     };
985     usort($filteredfields, $sort);
987     return array_shift($filteredfields);
990 /**
991  * Get the title of an entry to show when displaying tag results.
992  *
993  * @param stdClass $field The field from the 'data_fields' table
994  * @param stdClass $entry The entry from the 'data_records' table
995  * @return string The title of the entry
996  */
997 function data_get_tag_title_for_entry($field, $entry) {
998     global $CFG, $DB;
999     require_once($CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php');
1001     $classname = 'data_field_' . $field->type;
1002     $sql = "SELECT dc.*
1003               FROM {data_content} dc
1004         INNER JOIN {data_fields} df
1005                 ON dc.fieldid = df.id
1006              WHERE df.id = :fieldid
1007                AND dc.recordid = :recordid";
1008     $fieldcontents = $DB->get_record_sql($sql, array('recordid' => $entry->id, 'fieldid' => $field->id));
1010     return $classname::get_content_value($fieldcontents);
1013 /**
1014  * Search entries in a database.
1015  *
1016  * @param  stdClass  $data         database object
1017  * @param  stdClass  $cm           course module object
1018  * @param  stdClass  $context      context object
1019  * @param  stdClass  $mode         in which mode we are viewing the database (list, single)
1020  * @param  int  $currentgroup      the current group being used
1021  * @param  str  $search            search for this text in the entry data
1022  * @param  str  $sort              the field to sort by
1023  * @param  str  $order             the order to use when sorting
1024  * @param  int $page               for pagination, the current page
1025  * @param  int $perpage            entries per page
1026  * @param  bool  $advanced         whether we are using or not advanced search
1027  * @param  array  $searcharray     when using advanced search, the advanced data to use
1028  * @param  stdClass  $record       if we jsut want this record after doing all the access checks
1029  * @return array the entries found among other data related to the search
1030  * @since  Moodle 3.3
1031  */
1032 function data_search_entries($data, $cm, $context, $mode, $currentgroup, $search = '', $sort = null, $order = null, $page = 0,
1033         $perpage = 0, $advanced = null, $searcharray = null, $record = null) {
1034     global $DB, $USER;
1036     if ($sort === null) {
1037         $sort = $data->defaultsort;
1038     }
1039     if ($order === null) {
1040         $order = ($data->defaultsortdir == 0) ? 'ASC' : 'DESC';
1041     }
1042     if ($searcharray === null) {
1043         $searcharray = array();
1044     }
1046     if (core_text::strlen($search) < 2) {
1047         $search = '';
1048     }
1050     $approvecap = has_capability('mod/data:approve', $context);
1051     $canmanageentries = has_capability('mod/data:manageentries', $context);
1053     // If a student is not part of a group and seperate groups is enabled, we don't
1054     // want them seeing all records.
1055     $groupmode = groups_get_activity_groupmode($cm);
1056     if ($currentgroup == 0 && $groupmode == 1 && !$canmanageentries) {
1057         $canviewallrecords = false;
1058     } else {
1059         $canviewallrecords = true;
1060     }
1062     $numentries = data_numentries($data);
1063     $requiredentriesallowed = true;
1064     if (data_get_entries_left_to_view($data, $numentries, $canmanageentries)) {
1065         $requiredentriesallowed = false;
1066     }
1068     // Initialise the first group of params for advanced searches.
1069     $initialparams   = array();
1070     $params = array(); // Named params array.
1072     // Setup group and approve restrictions.
1073     if (!$approvecap && $data->approval) {
1074         if (isloggedin()) {
1075             $approveselect = ' AND (r.approved=1 OR r.userid=:myid1) ';
1076             $params['myid1'] = $USER->id;
1077             $initialparams['myid1'] = $params['myid1'];
1078         } else {
1079             $approveselect = ' AND r.approved=1 ';
1080         }
1081     } else {
1082         $approveselect = ' ';
1083     }
1085     if ($currentgroup) {
1086         $groupselect = " AND (r.groupid = :currentgroup OR r.groupid = 0)";
1087         $params['currentgroup'] = $currentgroup;
1088         $initialparams['currentgroup'] = $params['currentgroup'];
1089     } else {
1090         if ($canviewallrecords) {
1091             $groupselect = ' ';
1092         } else {
1093             // If separate groups are enabled and the user isn't in a group or
1094             // a teacher, manager, admin etc, then just show them entries for 'All participants'.
1095             $groupselect = " AND r.groupid = 0";
1096         }
1097     }
1099     // Init some variables to be used by advanced search.
1100     $advsearchselect = '';
1101     $advwhere        = '';
1102     $advtables       = '';
1103     $advparams       = array();
1104     // This is used for the initial reduction of advanced search results with required entries.
1105     $entrysql        = '';
1106     $namefields = user_picture::fields('u');
1107     // Remove the id from the string. This already exists in the sql statement.
1108     $namefields = str_replace('u.id,', '', $namefields);
1110     // Find the field we are sorting on.
1111     if ($sort <= 0 or !$sortfield = data_get_field_from_id($sort, $data)) {
1113         switch ($sort) {
1114             case DATA_LASTNAME:
1115                 $ordering = "u.lastname $order, u.firstname $order";
1116                 break;
1117             case DATA_FIRSTNAME:
1118                 $ordering = "u.firstname $order, u.lastname $order";
1119                 break;
1120             case DATA_APPROVED:
1121                 $ordering = "r.approved $order, r.timecreated $order";
1122                 break;
1123             case DATA_TIMEMODIFIED:
1124                 $ordering = "r.timemodified $order";
1125                 break;
1126             case DATA_TIMEADDED:
1127             default:
1128                 $sort     = 0;
1129                 $ordering = "r.timecreated $order";
1130         }
1132         $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields;
1133         $count = ' COUNT(DISTINCT c.recordid) ';
1134         $tables = '{data_content} c,{data_records} r, {user} u ';
1135         $where = 'WHERE c.recordid = r.id
1136                      AND r.dataid = :dataid
1137                      AND r.userid = u.id ';
1138         $params['dataid'] = $data->id;
1139         $sortorder = " ORDER BY $ordering, r.id $order";
1140         $searchselect = '';
1142         // If requiredentries is not reached, only show current user's entries.
1143         if (!$requiredentriesallowed) {
1144             $where .= ' AND u.id = :myid2 ';
1145             $entrysql = ' AND r.userid = :myid3 ';
1146             $params['myid2'] = $USER->id;
1147             $initialparams['myid3'] = $params['myid2'];
1148         }
1150         if (!empty($advanced)) {                    // If advanced box is checked.
1151             $i = 0;
1152             foreach ($searcharray as $key => $val) { // what does $searcharray hold?
1153                 if ($key == DATA_FIRSTNAME or $key == DATA_LASTNAME) {
1154                     $i++;
1155                     $searchselect .= " AND ".$DB->sql_like($val->field, ":search_flname_$i", false);
1156                     $params['search_flname_'.$i] = "%$val->data%";
1157                     continue;
1158                 }
1159                 if ($key == DATA_TIMEMODIFIED) {
1160                     $searchselect .= " AND $val->field >= :timemodified";
1161                     $params['timemodified'] = $val->data;
1162                     continue;
1163                 }
1164                 $advtables .= ', {data_content} c'.$key.' ';
1165                 $advwhere .= ' AND c'.$key.'.recordid = r.id';
1166                 $advsearchselect .= ' AND ('.$val->sql.') ';
1167                 $advparams = array_merge($advparams, $val->params);
1168             }
1169         } else if ($search) {
1170             $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)."
1171                               OR ".$DB->sql_like('u.firstname', ':search2', false)."
1172                               OR ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
1173             $params['search1'] = "%$search%";
1174             $params['search2'] = "%$search%";
1175             $params['search3'] = "%$search%";
1176         } else {
1177             $searchselect = ' ';
1178         }
1180     } else {
1182         $sortcontent = $DB->sql_compare_text('c.' . $sortfield->get_sort_field());
1183         $sortcontentfull = $sortfield->get_sort_sql($sortcontent);
1185         $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields . ',
1186                 ' . $sortcontentfull . ' AS sortorder ';
1187         $count = ' COUNT(DISTINCT c.recordid) ';
1188         $tables = '{data_content} c, {data_records} r, {user} u ';
1189         $where = 'WHERE c.recordid = r.id
1190                      AND r.dataid = :dataid
1191                      AND r.userid = u.id ';
1192         if (!$advanced) {
1193             $where .= 'AND c.fieldid = :sort';
1194         }
1195         $params['dataid'] = $data->id;
1196         $params['sort'] = $sort;
1197         $sortorder = ' ORDER BY sortorder '.$order.' , r.id ASC ';
1198         $searchselect = '';
1200         // If requiredentries is not reached, only show current user's entries.
1201         if (!$requiredentriesallowed) {
1202             $where .= ' AND u.id = :myid2';
1203             $entrysql = ' AND r.userid = :myid3';
1204             $params['myid2'] = $USER->id;
1205             $initialparams['myid3'] = $params['myid2'];
1206         }
1207         $i = 0;
1208         if (!empty($advanced)) {                      // If advanced box is checked.
1209             foreach ($searcharray as $key => $val) {   // what does $searcharray hold?
1210                 if ($key == DATA_FIRSTNAME or $key == DATA_LASTNAME) {
1211                     $i++;
1212                     $searchselect .= " AND ".$DB->sql_like($val->field, ":search_flname_$i", false);
1213                     $params['search_flname_'.$i] = "%$val->data%";
1214                     continue;
1215                 }
1216                 if ($key == DATA_TIMEMODIFIED) {
1217                     $searchselect .= " AND $val->field >= :timemodified";
1218                     $params['timemodified'] = $val->data;
1219                     continue;
1220                 }
1221                 $advtables .= ', {data_content} c'.$key.' ';
1222                 $advwhere .= ' AND c'.$key.'.recordid = r.id AND c'.$key.'.fieldid = '.$key;
1223                 $advsearchselect .= ' AND ('.$val->sql.') ';
1224                 $advparams = array_merge($advparams, $val->params);
1225             }
1226         } else if ($search) {
1227             $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR
1228                 ".$DB->sql_like('u.firstname', ':search2', false)." OR
1229                 ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
1230             $params['search1'] = "%$search%";
1231             $params['search2'] = "%$search%";
1232             $params['search3'] = "%$search%";
1233         } else {
1234             $searchselect = ' ';
1235         }
1236     }
1238     // To actually fetch the records.
1240     $fromsql    = "FROM $tables $advtables $where $advwhere $groupselect $approveselect $searchselect $advsearchselect";
1241     $allparams  = array_merge($params, $advparams);
1243     // Provide initial sql statements and parameters to reduce the number of total records.
1244     $initialselect = $groupselect . $approveselect . $entrysql;
1246     $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams);
1247     $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data->id);
1248     $selectdata = $where . $groupselect . $approveselect;
1250     if (!empty($advanced)) {
1251         $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
1252         $sqlselect = $advancedsearchsql['sql'];
1253         $allparams = array_merge($allparams, $advancedsearchsql['params']);
1254         $totalcount = count($newrecordids);
1255     } else {
1256         $sqlselect  = "SELECT $what $fromsql $sortorder";
1257         $sqlcountselect  = "SELECT $count $fromsql";
1258         $totalcount = $DB->count_records_sql($sqlcountselect, $allparams);
1259     }
1261     // Work out the paging numbers and counts.
1262     if (empty($searchselect) && empty($advsearchselect)) {
1263         $maxcount = $totalcount;
1264     } else {
1265         $maxcount = count($recordids);
1266     }
1268     if ($record) {     // We need to just show one, so where is it in context?
1269         $nowperpage = 1;
1270         $mode = 'single';
1271         $page = 0;
1272         // TODO MDL-33797 - Reduce this or consider redesigning the paging system.
1273         if ($allrecordids = $DB->get_fieldset_sql($sqlselect, $allparams)) {
1274             $page = (int)array_search($record->id, $allrecordids);
1275             unset($allrecordids);
1276         }
1277     } else if ($mode == 'single') {  // We rely on ambient $page settings
1278         $nowperpage = 1;
1280     } else {
1281         $nowperpage = $perpage;
1282     }
1284     // Get the actual records.
1285     if (!$records = $DB->get_records_sql($sqlselect, $allparams, $page * $nowperpage, $nowperpage)) {
1286         // Nothing to show!
1287         if ($record) {         // Something was requested so try to show that at least (bug 5132)
1288             if (data_can_view_record($data, $record, $currentgroup, $canmanageentries)) {
1289                 // OK, we can show this one
1290                 $records = array($record->id => $record);
1291                 $totalcount = 1;
1292             }
1293         }
1295     }
1297     return [$records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode];
1300 /**
1301  * Check if the current user can view the given record.
1302  *
1303  * @param  stdClass $data           database record
1304  * @param  stdClass $record         the record (entry) to check
1305  * @param  int $currentgroup        current group
1306  * @param  bool $canmanageentries   if the user can manage entries
1307  * @return bool true if the user can view the entry
1308  * @since  Moodle 3.3
1309  */
1310 function data_can_view_record($data, $record, $currentgroup, $canmanageentries) {
1311     global $USER;
1313     if ($canmanageentries || empty($data->approval) ||
1314              $record->approved || (isloggedin() && $record->userid == $USER->id)) {
1316         if (!$currentgroup || $record->groupid == $currentgroup || $record->groupid == 0) {
1317             return true;
1318         }
1319     }
1320     return false;
1323 /**
1324  * Return all the field instances for a given database.
1325  *
1326  * @param  stdClass $data database object
1327  * @return array field instances
1328  * @since  Moodle 3.3
1329  */
1330 function data_get_field_instances($data) {
1331     global $DB;
1333     $instances = [];
1334     if ($fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id')) {
1335         foreach ($fields as $field) {
1336             $instances[] = data_get_field($field, $data);
1337         }
1338     }
1339     return $instances;
1342 /**
1343  * Build the search array.
1344  *
1345  * @param  stdClass $data      the database object
1346  * @param  bool $paging        if paging is being used
1347  * @param  array $searcharray  the current search array (saved by session)
1348  * @param  array $defaults     default values for the searchable fields
1349  * @param  str $fn             the first name to search (optional)
1350  * @param  str $ln             the last name to search (optional)
1351  * @return array               the search array and plain search build based on the different elements
1352  * @since  Moodle 3.3
1353  */
1354 function data_build_search_array($data, $paging, $searcharray, $defaults = null, $fn = '', $ln = '') {
1355     global $DB;
1357     $search = '';
1358     $vals = array();
1359     $fields = $DB->get_records('data_fields', array('dataid' => $data->id));
1361     if (!empty($fields)) {
1362         foreach ($fields as $field) {
1363             $searchfield = data_get_field_from_id($field->id, $data);
1364             // Get field data to build search sql with.  If paging is false, get from user.
1365             // If paging is true, get data from $searcharray which is obtained from the $SESSION (see line 116).
1366             if (!$paging) {
1367                 $val = $searchfield->parse_search_field($defaults);
1368             } else {
1369                 // Set value from session if there is a value @ the required index.
1370                 if (isset($searcharray[$field->id])) {
1371                     $val = $searcharray[$field->id]->data;
1372                 } else { // If there is not an entry @ the required index, set value to blank.
1373                     $val = '';
1374                 }
1375             }
1376             if (!empty($val)) {
1377                 $searcharray[$field->id] = new stdClass();
1378                 list($searcharray[$field->id]->sql, $searcharray[$field->id]->params) = $searchfield->generate_sql('c'.$field->id, $val);
1379                 $searcharray[$field->id]->data = $val;
1380                 $vals[] = $val;
1381             } else {
1382                 // Clear it out.
1383                 unset($searcharray[$field->id]);
1384             }
1385         }
1386     }
1388     if (!$paging) {
1389         // Name searching.
1390         $fn = optional_param('u_fn', $fn, PARAM_NOTAGS);
1391         $ln = optional_param('u_ln', $ln, PARAM_NOTAGS);
1392     } else {
1393         $fn = isset($searcharray[DATA_FIRSTNAME]) ? $searcharray[DATA_FIRSTNAME]->data : '';
1394         $ln = isset($searcharray[DATA_LASTNAME]) ? $searcharray[DATA_LASTNAME]->data : '';
1395     }
1396     if (!empty($fn)) {
1397         $searcharray[DATA_FIRSTNAME] = new stdClass();
1398         $searcharray[DATA_FIRSTNAME]->sql    = '';
1399         $searcharray[DATA_FIRSTNAME]->params = array();
1400         $searcharray[DATA_FIRSTNAME]->field  = 'u.firstname';
1401         $searcharray[DATA_FIRSTNAME]->data   = $fn;
1402         $vals[] = $fn;
1403     } else {
1404         unset($searcharray[DATA_FIRSTNAME]);
1405     }
1406     if (!empty($ln)) {
1407         $searcharray[DATA_LASTNAME] = new stdClass();
1408         $searcharray[DATA_LASTNAME]->sql     = '';
1409         $searcharray[DATA_LASTNAME]->params = array();
1410         $searcharray[DATA_LASTNAME]->field   = 'u.lastname';
1411         $searcharray[DATA_LASTNAME]->data    = $ln;
1412         $vals[] = $ln;
1413     } else {
1414         unset($searcharray[DATA_LASTNAME]);
1415     }
1417     // In case we want to switch to simple search later - there might be multiple values there ;-).
1418     if ($vals) {
1419         $val = reset($vals);
1420         if (is_string($val)) {
1421             $search = $val;
1422         }
1423     }
1424     return [$searcharray, $search];
1427 /**
1428  * Approves or unapproves an entry.
1429  *
1430  * @param  int $entryid the entry to approve or unapprove.
1431  * @param  bool $approve Whether to approve or unapprove (true for approve false otherwise).
1432  * @since  Moodle 3.3
1433  */
1434 function data_approve_entry($entryid, $approve) {
1435     global $DB;
1437     $newrecord = new stdClass();
1438     $newrecord->id = $entryid;
1439     $newrecord->approved = $approve ? 1 : 0;
1440     $DB->update_record('data_records', $newrecord);
1443 /**
1444  * Populate the field contents of a new record with the submitted data.
1445  *
1446  * @param  stdClass $data           database object
1447  * @param  stdClass $context        context object
1448  * @param  int $recordid            the new record id
1449  * @param  array $fields            list of fields of the database
1450  * @param  stdClass $datarecord     the submitted data
1451  * @param  stdClass $processeddata  pre-processed submitted fields
1452  * @since  Moodle 3.3
1453  */
1454 function data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata) {
1455     global $DB;
1457     // Insert a whole lot of empty records to make sure we have them.
1458     $records = array();
1459     foreach ($fields as $field) {
1460         $content = new stdClass();
1461         $content->recordid = $recordid;
1462         $content->fieldid = $field->id;
1463         $records[] = $content;
1464     }
1466     // Bulk insert the records now. Some records may have no data but all must exist.
1467     $DB->insert_records('data_content', $records);
1469     // Add all provided content.
1470     foreach ($processeddata->fields as $fieldname => $field) {
1471         $field->update_content($recordid, $datarecord->$fieldname, $fieldname);
1472     }
1474     // Trigger an event for updating this record.
1475     $event = \mod_data\event\record_created::create(array(
1476         'objectid' => $recordid,
1477         'context' => $context,
1478         'courseid' => $data->course,
1479         'other' => array(
1480             'dataid' => $data->id
1481         )
1482     ));
1483     $event->add_record_snapshot('data', $data);
1484     $event->trigger();
1487 /**
1488  * Updates the fields contents of an existing record.
1489  *
1490  * @param  stdClass $data           database object
1491  * @param  stdClass $record         record to update object
1492  * @param  stdClass $context        context object
1493  * @param  stdClass $datarecord     the submitted data
1494  * @param  stdClass $processeddata  pre-processed submitted fields
1495  * @since  Moodle 3.3
1496  */
1497 function data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata) {
1498     global $DB;
1500     // Reset the approved flag after edit if the user does not have permission to approve their own entries.
1501     if (!has_capability('mod/data:approve', $context)) {
1502         $record->approved = 0;
1503     }
1505     // Update the parent record.
1506     $record->timemodified = time();
1507     $DB->update_record('data_records', $record);
1509     // Update all content.
1510     foreach ($processeddata->fields as $fieldname => $field) {
1511         $field->update_content($record->id, $datarecord->$fieldname, $fieldname);
1512     }
1514     // Trigger an event for updating this record.
1515     $event = \mod_data\event\record_updated::create(array(
1516         'objectid' => $record->id,
1517         'context' => $context,
1518         'courseid' => $data->course,
1519         'other' => array(
1520             'dataid' => $data->id
1521         )
1522     ));
1523     $event->add_record_snapshot('data', $data);
1524     $event->trigger();