Merge branch 'MDL-57455_master' of git://github.com/markn86/moodle
[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 (!$cm->uservisible) {
864             $builder->set_accessible($item, false);
865             continue;
866         }
868         if (!has_capability('mod/data:viewentry', $context)) {
869             $builder->set_accessible($item, false);
870             continue;
871         }
873         if ($USER->id != $item->userid && (($item->timeviewfrom && $now < $item->timeviewfrom)
874                 || ($item->timeviewto && $now > $item->timeviewto))) {
875             $builder->set_accessible($item, false);
876             continue;
877         }
879         if ($USER->id != $item->userid && $item->approval && !$item->approved) {
880             $builder->set_accessible($item, false);
881             continue;
882         }
884         if ($item->requiredentriestoview) {
885             if (!isset($entrycount[$item->dataid])) {
886                 $entrycount[$item->dataid] = $DB->count_records('data_records', array('dataid' => $item->dataid));
887             }
888             $sufficiententries = $item->requiredentriestoview > $entrycount[$item->dataid];
889             $builder->set_accessible($item, $sufficiententries);
890         }
892         if (!isset($activitygroupmode[$cm->id])) {
893             $activitygroupmode[$cm->id] = groups_get_activity_groupmode($cm);
894         }
896         if (!isset($usergroups[$item->groupid])) {
897             $usergroups[$item->groupid] = groups_is_member($item->groupid, $USER->id);
898         }
900         if ($activitygroupmode[$cm->id] == SEPARATEGROUPS && !$usergroups[$item->groupid]) {
901             $builder->set_accessible($item, false);
902             continue;
903         }
905         $builder->set_accessible($item, true);
906     }
908     $items = $builder->get_items();
909     if (count($items) > $perpage) {
910         $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
911         array_pop($items);
912     }
914     // Build the display contents.
915     if ($items) {
916         $tagfeed = new core_tag\output\tagfeed();
917         foreach ($items as $item) {
918             context_helper::preload_from_record($item);
919             $modinfo = get_fast_modinfo($item->courseid);
920             $cm = $modinfo->get_cm($item->cmid);
921             $pageurl = new moodle_url('/mod/data/view.php', array(
922                     'rid' => $item->id,
923                     'd' => $item->dataid
924             ));
926             if (!isset($titlefields[$item->dataid])) {
927                 $titlefields[$item->dataid] = data_get_tag_title_field($item->dataid);
928             }
930             $pagename = data_get_tag_title_for_entry($titlefields[$item->dataid], $item);
931             $pagename = html_writer::link($pageurl, $pagename);
932             $courseurl = course_get_url($item->courseid, $cm->sectionnum);
933             $cmname = html_writer::link($cm->url, $cm->get_formatted_name());
934             $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
935             $coursename = html_writer::link($courseurl, $coursename);
936             $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
937             $tagfeed->add($icon, $pagename, $cmname . '<br>' . $coursename);
938         }
939         $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT));
941         return new core_tag\output\tagindex($tag, 'mod_data', 'data_records', $content, $exclusivemode,
942             $fromctx, $ctx, $rec, $page, $totalpages);
943     }
946 /**
947  * Get the title of a field to show when displaying tag results.
948  *
949  * @param int $dataid The id of the data field
950  * @return stdClass The field data from the 'data_fields' table as well as it's priority
951  */
952 function data_get_tag_title_field($dataid) {
953     global $DB, $CFG;
955     $validfieldtypes = array('text', 'textarea', 'menu', 'radiobutton', 'checkbox', 'multimenu', 'url');
956     $fields = $DB->get_records('data_fields', ['dataid' => $dataid]);
957     $template = $DB->get_field('data', 'addtemplate', ['id' => $dataid]);
959     $filteredfields = [];
961     foreach ($fields as $field) {
962         if (!in_array($field->type, $validfieldtypes)) {
963             continue;
964         }
965         $field->addtemplateposition = strpos($template, '[['.$field->name.']]');
966         if ($field->addtemplateposition === false) {
967             continue;
968         }
969         require_once($CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php');
970         $classname = 'data_field_' . $field->type;
971         $field->priority = $classname::get_priority();
972         $filteredfields[] = $field;
973     }
975     $sort = function($record1, $record2) {
976         // If a content's fieldtype is compulsory in the database than it would have priority than any other non-compulsory content.
977         if (($record1->required && $record2->required) || (!$record1->required && !$record2->required)) {
978             if ($record1->priority === $record2->priority) {
979                 return $record1->id < $record2->id ? 1 : -1;
980             }
982             return $record1->priority < $record2->priority ? -1 : 1;
983         } else if ($record1->required && !$record2->required) {
984             return 1;
985         } else {
986             return -1;
987         }
988     };
990     usort($filteredfields, $sort);
992     return array_shift($filteredfields);
995 /**
996  * Get the title of an entry to show when displaying tag results.
997  *
998  * @param stdClass $field The field from the 'data_fields' table
999  * @param stdClass $entry The entry from the 'data_records' table
1000  * @return string The title of the entry
1001  */
1002 function data_get_tag_title_for_entry($field, $entry) {
1003     global $CFG, $DB;
1004     require_once($CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php');
1006     $classname = 'data_field_' . $field->type;
1007     $sql = "SELECT dc.*
1008               FROM {data_content} dc
1009         INNER JOIN {data_fields} df
1010                 ON dc.fieldid = df.id
1011              WHERE df.id = :fieldid
1012                AND dc.recordid = :recordid";
1013     $fieldcontents = $DB->get_record_sql($sql, array('recordid' => $entry->id, 'fieldid' => $field->id));
1015     return $classname::get_content_value($fieldcontents);
1018 /**
1019  * Search entries in a database.
1020  *
1021  * @param  stdClass  $data         database object
1022  * @param  stdClass  $cm           course module object
1023  * @param  stdClass  $context      context object
1024  * @param  stdClass  $mode         in which mode we are viewing the database (list, single)
1025  * @param  int  $currentgroup      the current group being used
1026  * @param  str  $search            search for this text in the entry data
1027  * @param  str  $sort              the field to sort by
1028  * @param  str  $order             the order to use when sorting
1029  * @param  int $page               for pagination, the current page
1030  * @param  int $perpage            entries per page
1031  * @param  bool  $advanced         whether we are using or not advanced search
1032  * @param  array  $searcharray     when using advanced search, the advanced data to use
1033  * @param  stdClass  $record       if we jsut want this record after doing all the access checks
1034  * @return array the entries found among other data related to the search
1035  * @since  Moodle 3.3
1036  */
1037 function data_search_entries($data, $cm, $context, $mode, $currentgroup, $search = '', $sort = null, $order = null, $page = 0,
1038         $perpage = 0, $advanced = null, $searcharray = null, $record = null) {
1039     global $DB, $USER;
1041     if ($sort === null) {
1042         $sort = $data->defaultsort;
1043     }
1044     if ($order === null) {
1045         $order = ($data->defaultsortdir == 0) ? 'ASC' : 'DESC';
1046     }
1047     if ($searcharray === null) {
1048         $searcharray = array();
1049     }
1051     if (core_text::strlen($search) < 2) {
1052         $search = '';
1053     }
1055     $approvecap = has_capability('mod/data:approve', $context);
1056     $canmanageentries = has_capability('mod/data:manageentries', $context);
1058     // If a student is not part of a group and seperate groups is enabled, we don't
1059     // want them seeing all records.
1060     $groupmode = groups_get_activity_groupmode($cm);
1061     if ($currentgroup == 0 && $groupmode == 1 && !$canmanageentries) {
1062         $canviewallrecords = false;
1063     } else {
1064         $canviewallrecords = true;
1065     }
1067     $numentries = data_numentries($data);
1068     $requiredentriesallowed = true;
1069     if (data_get_entries_left_to_view($data, $numentries, $canmanageentries)) {
1070         $requiredentriesallowed = false;
1071     }
1073     // Initialise the first group of params for advanced searches.
1074     $initialparams   = array();
1075     $params = array(); // Named params array.
1077     // Setup group and approve restrictions.
1078     if (!$approvecap && $data->approval) {
1079         if (isloggedin()) {
1080             $approveselect = ' AND (r.approved=1 OR r.userid=:myid1) ';
1081             $params['myid1'] = $USER->id;
1082             $initialparams['myid1'] = $params['myid1'];
1083         } else {
1084             $approveselect = ' AND r.approved=1 ';
1085         }
1086     } else {
1087         $approveselect = ' ';
1088     }
1090     if ($currentgroup) {
1091         $groupselect = " AND (r.groupid = :currentgroup OR r.groupid = 0)";
1092         $params['currentgroup'] = $currentgroup;
1093         $initialparams['currentgroup'] = $params['currentgroup'];
1094     } else {
1095         if ($canviewallrecords) {
1096             $groupselect = ' ';
1097         } else {
1098             // If separate groups are enabled and the user isn't in a group or
1099             // a teacher, manager, admin etc, then just show them entries for 'All participants'.
1100             $groupselect = " AND r.groupid = 0";
1101         }
1102     }
1104     // Init some variables to be used by advanced search.
1105     $advsearchselect = '';
1106     $advwhere        = '';
1107     $advtables       = '';
1108     $advparams       = array();
1109     // This is used for the initial reduction of advanced search results with required entries.
1110     $entrysql        = '';
1111     $namefields = user_picture::fields('u');
1112     // Remove the id from the string. This already exists in the sql statement.
1113     $namefields = str_replace('u.id,', '', $namefields);
1115     // Find the field we are sorting on.
1116     if ($sort <= 0 or !$sortfield = data_get_field_from_id($sort, $data)) {
1118         switch ($sort) {
1119             case DATA_LASTNAME:
1120                 $ordering = "u.lastname $order, u.firstname $order";
1121                 break;
1122             case DATA_FIRSTNAME:
1123                 $ordering = "u.firstname $order, u.lastname $order";
1124                 break;
1125             case DATA_APPROVED:
1126                 $ordering = "r.approved $order, r.timecreated $order";
1127                 break;
1128             case DATA_TIMEMODIFIED:
1129                 $ordering = "r.timemodified $order";
1130                 break;
1131             case DATA_TIMEADDED:
1132             default:
1133                 $sort     = 0;
1134                 $ordering = "r.timecreated $order";
1135         }
1137         $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields;
1138         $count = ' COUNT(DISTINCT c.recordid) ';
1139         $tables = '{data_content} c,{data_records} r, {user} u ';
1140         $where = 'WHERE c.recordid = r.id
1141                      AND r.dataid = :dataid
1142                      AND r.userid = u.id ';
1143         $params['dataid'] = $data->id;
1144         $sortorder = " ORDER BY $ordering, r.id $order";
1145         $searchselect = '';
1147         // If requiredentries is not reached, only show current user's entries.
1148         if (!$requiredentriesallowed) {
1149             $where .= ' AND u.id = :myid2 ';
1150             $entrysql = ' AND r.userid = :myid3 ';
1151             $params['myid2'] = $USER->id;
1152             $initialparams['myid3'] = $params['myid2'];
1153         }
1155         if ($search) {
1156             $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)."
1157                               OR ".$DB->sql_like('u.firstname', ':search2', false)."
1158                               OR ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
1159             $params['search1'] = "%$search%";
1160             $params['search2'] = "%$search%";
1161             $params['search3'] = "%$search%";
1162         } else {
1163             $searchselect = ' ';
1164         }
1166     } else {
1168         $sortcontent = $DB->sql_compare_text('c.' . $sortfield->get_sort_field());
1169         $sortcontentfull = $sortfield->get_sort_sql($sortcontent);
1171         $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields . ',
1172                 ' . $sortcontentfull . ' AS sortorder ';
1173         $count = ' COUNT(DISTINCT c.recordid) ';
1174         $tables = '{data_content} c, {data_records} r, {user} u ';
1175         $where = 'WHERE c.recordid = r.id
1176                      AND r.dataid = :dataid
1177                      AND r.userid = u.id ';
1178         if (!$advanced) {
1179             $where .= 'AND c.fieldid = :sort';
1180         }
1181         $params['dataid'] = $data->id;
1182         $params['sort'] = $sort;
1183         $sortorder = ' ORDER BY sortorder '.$order.' , r.id ASC ';
1184         $searchselect = '';
1186         // If requiredentries is not reached, only show current user's entries.
1187         if (!$requiredentriesallowed) {
1188             $where .= ' AND u.id = :myid2';
1189             $entrysql = ' AND r.userid = :myid3';
1190             $params['myid2'] = $USER->id;
1191             $initialparams['myid3'] = $params['myid2'];
1192         }
1194         if ($search) {
1195             $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR
1196                 ".$DB->sql_like('u.firstname', ':search2', false)." OR
1197                 ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
1198             $params['search1'] = "%$search%";
1199             $params['search2'] = "%$search%";
1200             $params['search3'] = "%$search%";
1201         } else {
1202             $searchselect = ' ';
1203         }
1204     }
1206     // To actually fetch the records.
1208     $fromsql    = "FROM $tables $advtables $where $advwhere $groupselect $approveselect $searchselect $advsearchselect";
1209     $allparams  = array_merge($params, $advparams);
1211     // Provide initial sql statements and parameters to reduce the number of total records.
1212     $initialselect = $groupselect . $approveselect . $entrysql;
1214     $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams);
1215     $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data->id);
1216     $selectdata = $where . $groupselect . $approveselect;
1218     if (!empty($advanced)) {
1219         $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
1220         $sqlselect = $advancedsearchsql['sql'];
1221         $allparams = array_merge($allparams, $advancedsearchsql['params']);
1222         $totalcount = count($newrecordids);
1223     } else {
1224         $sqlselect  = "SELECT $what $fromsql $sortorder";
1225         $sqlcountselect  = "SELECT $count $fromsql";
1226         $totalcount = $DB->count_records_sql($sqlcountselect, $allparams);
1227     }
1229     // Work out the paging numbers and counts.
1230     if (empty($searchselect) && empty($advsearchselect)) {
1231         $maxcount = $totalcount;
1232     } else {
1233         $maxcount = count($recordids);
1234     }
1236     if ($record) {     // We need to just show one, so where is it in context?
1237         $nowperpage = 1;
1238         $mode = 'single';
1239         $page = 0;
1240         // TODO MDL-33797 - Reduce this or consider redesigning the paging system.
1241         if ($allrecordids = $DB->get_fieldset_sql($sqlselect, $allparams)) {
1242             $page = (int)array_search($record->id, $allrecordids);
1243             unset($allrecordids);
1244         }
1245     } else if ($mode == 'single') {  // We rely on ambient $page settings
1246         $nowperpage = 1;
1248     } else {
1249         $nowperpage = $perpage;
1250     }
1252     // Get the actual records.
1253     if (!$records = $DB->get_records_sql($sqlselect, $allparams, $page * $nowperpage, $nowperpage)) {
1254         // Nothing to show!
1255         if ($record) {         // Something was requested so try to show that at least (bug 5132)
1256             if (data_can_view_record($data, $record, $currentgroup, $canmanageentries)) {
1257                 // OK, we can show this one
1258                 $records = array($record->id => $record);
1259                 $totalcount = 1;
1260             }
1261         }
1263     }
1265     return [$records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode];
1268 /**
1269  * Check if the current user can view the given record.
1270  *
1271  * @param  stdClass $data           database record
1272  * @param  stdClass $record         the record (entry) to check
1273  * @param  int $currentgroup        current group
1274  * @param  bool $canmanageentries   if the user can manage entries
1275  * @return bool true if the user can view the entry
1276  * @since  Moodle 3.3
1277  */
1278 function data_can_view_record($data, $record, $currentgroup, $canmanageentries) {
1279     global $USER;
1281     if ($canmanageentries || empty($data->approval) ||
1282              $record->approved || (isloggedin() && $record->userid == $USER->id)) {
1284         if (!$currentgroup || $record->groupid == $currentgroup || $record->groupid == 0) {
1285             return true;
1286         }
1287     }
1288     return false;
1291 /**
1292  * Return all the field instances for a given database.
1293  *
1294  * @param  stdClass $data database object
1295  * @return array field instances
1296  * @since  Moodle 3.3
1297  */
1298 function data_get_field_instances($data) {
1299     global $DB;
1301     $instances = [];
1302     if ($fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id')) {
1303         foreach ($fields as $field) {
1304             $instances[] = data_get_field($field, $data);
1305         }
1306     }
1307     return $instances;
1310 /**
1311  * Build the search array.
1312  *
1313  * @param  stdClass $data      the database object
1314  * @param  bool $paging        if paging is being used
1315  * @param  array $searcharray  the current search array (saved by session)
1316  * @param  array $defaults     default values for the searchable fields
1317  * @param  str $fn             the first name to search (optional)
1318  * @param  str $ln             the last name to search (optional)
1319  * @return array               the search array and plain search build based on the different elements
1320  * @since  Moodle 3.3
1321  */
1322 function data_build_search_array($data, $paging, $searcharray, $defaults = null, $fn = '', $ln = '') {
1323     global $DB;
1325     $search = '';
1326     $vals = array();
1327     $fields = $DB->get_records('data_fields', array('dataid' => $data->id));
1329     if (!empty($fields)) {
1330         foreach ($fields as $field) {
1331             $searchfield = data_get_field_from_id($field->id, $data);
1332             // Get field data to build search sql with.  If paging is false, get from user.
1333             // If paging is true, get data from $searcharray which is obtained from the $SESSION (see line 116).
1334             if (!$paging) {
1335                 $val = $searchfield->parse_search_field($defaults);
1336             } else {
1337                 // Set value from session if there is a value @ the required index.
1338                 if (isset($searcharray[$field->id])) {
1339                     $val = $searcharray[$field->id]->data;
1340                 } else { // If there is not an entry @ the required index, set value to blank.
1341                     $val = '';
1342                 }
1343             }
1344             if (!empty($val)) {
1345                 $searcharray[$field->id] = new stdClass();
1346                 list($searcharray[$field->id]->sql, $searcharray[$field->id]->params) = $searchfield->generate_sql('c'.$field->id, $val);
1347                 $searcharray[$field->id]->data = $val;
1348                 $vals[] = $val;
1349             } else {
1350                 // Clear it out.
1351                 unset($searcharray[$field->id]);
1352             }
1353         }
1354     }
1356     $rawtagnames = optional_param_array('tags', false, PARAM_TAGLIST);
1358     if ($rawtagnames) {
1359         $searcharray[DATA_TAGS] = new stdClass();
1360         $searcharray[DATA_TAGS]->params = [];
1361         $searcharray[DATA_TAGS]->rawtagnames = $rawtagnames;
1362         $searcharray[DATA_TAGS]->sql = '';
1363     } else {
1364         unset($searcharray[DATA_TAGS]);
1365     }
1367     if (!$paging) {
1368         // Name searching.
1369         $fn = optional_param('u_fn', $fn, PARAM_NOTAGS);
1370         $ln = optional_param('u_ln', $ln, PARAM_NOTAGS);
1371     } else {
1372         $fn = isset($searcharray[DATA_FIRSTNAME]) ? $searcharray[DATA_FIRSTNAME]->data : '';
1373         $ln = isset($searcharray[DATA_LASTNAME]) ? $searcharray[DATA_LASTNAME]->data : '';
1374     }
1375     if (!empty($fn)) {
1376         $searcharray[DATA_FIRSTNAME] = new stdClass();
1377         $searcharray[DATA_FIRSTNAME]->sql    = '';
1378         $searcharray[DATA_FIRSTNAME]->params = array();
1379         $searcharray[DATA_FIRSTNAME]->field  = 'u.firstname';
1380         $searcharray[DATA_FIRSTNAME]->data   = $fn;
1381         $vals[] = $fn;
1382     } else {
1383         unset($searcharray[DATA_FIRSTNAME]);
1384     }
1385     if (!empty($ln)) {
1386         $searcharray[DATA_LASTNAME] = new stdClass();
1387         $searcharray[DATA_LASTNAME]->sql     = '';
1388         $searcharray[DATA_LASTNAME]->params = array();
1389         $searcharray[DATA_LASTNAME]->field   = 'u.lastname';
1390         $searcharray[DATA_LASTNAME]->data    = $ln;
1391         $vals[] = $ln;
1392     } else {
1393         unset($searcharray[DATA_LASTNAME]);
1394     }
1396     // In case we want to switch to simple search later - there might be multiple values there ;-).
1397     if ($vals) {
1398         $val = reset($vals);
1399         if (is_string($val)) {
1400             $search = $val;
1401         }
1402     }
1403     return [$searcharray, $search];
1406 /**
1407  * Approves or unapproves an entry.
1408  *
1409  * @param  int $entryid the entry to approve or unapprove.
1410  * @param  bool $approve Whether to approve or unapprove (true for approve false otherwise).
1411  * @since  Moodle 3.3
1412  */
1413 function data_approve_entry($entryid, $approve) {
1414     global $DB;
1416     $newrecord = new stdClass();
1417     $newrecord->id = $entryid;
1418     $newrecord->approved = $approve ? 1 : 0;
1419     $DB->update_record('data_records', $newrecord);
1422 /**
1423  * Populate the field contents of a new record with the submitted data.
1424  *
1425  * @param  stdClass $data           database object
1426  * @param  stdClass $context        context object
1427  * @param  int $recordid            the new record id
1428  * @param  array $fields            list of fields of the database
1429  * @param  stdClass $datarecord     the submitted data
1430  * @param  stdClass $processeddata  pre-processed submitted fields
1431  * @since  Moodle 3.3
1432  */
1433 function data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata) {
1434     global $DB;
1436     // Insert a whole lot of empty records to make sure we have them.
1437     $records = array();
1438     foreach ($fields as $field) {
1439         $content = new stdClass();
1440         $content->recordid = $recordid;
1441         $content->fieldid = $field->id;
1442         $records[] = $content;
1443     }
1445     // Bulk insert the records now. Some records may have no data but all must exist.
1446     $DB->insert_records('data_content', $records);
1448     // Add all provided content.
1449     foreach ($processeddata->fields as $fieldname => $field) {
1450         $field->update_content($recordid, $datarecord->$fieldname, $fieldname);
1451     }
1453     // Trigger an event for updating this record.
1454     $event = \mod_data\event\record_created::create(array(
1455         'objectid' => $recordid,
1456         'context' => $context,
1457         'courseid' => $data->course,
1458         'other' => array(
1459             'dataid' => $data->id
1460         )
1461     ));
1462     $event->add_record_snapshot('data', $data);
1463     $event->trigger();
1466 /**
1467  * Updates the fields contents of an existing record.
1468  *
1469  * @param  stdClass $data           database object
1470  * @param  stdClass $record         record to update object
1471  * @param  stdClass $context        context object
1472  * @param  stdClass $datarecord     the submitted data
1473  * @param  stdClass $processeddata  pre-processed submitted fields
1474  * @since  Moodle 3.3
1475  */
1476 function data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata) {
1477     global $DB;
1479     // Reset the approved flag after edit if the user does not have permission to approve their own entries.
1480     if (!has_capability('mod/data:approve', $context)) {
1481         $record->approved = 0;
1482     }
1484     // Update the parent record.
1485     $record->timemodified = time();
1486     $DB->update_record('data_records', $record);
1488     // Update all content.
1489     foreach ($processeddata->fields as $fieldname => $field) {
1490         $field->update_content($record->id, $datarecord->$fieldname, $fieldname);
1491     }
1493     // Trigger an event for updating this record.
1494     $event = \mod_data\event\record_updated::create(array(
1495         'objectid' => $record->id,
1496         'context' => $context,
1497         'courseid' => $data->course,
1498         'other' => array(
1499             'dataid' => $data->id
1500         )
1501     ));
1502     $event->add_record_snapshot('data', $data);
1503     $event->trigger();