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