See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * This file contains the definition for the library class for file feedback plugin 19 * 20 * 21 * @package assignfeedback_file 22 * @copyright 2012 NetSpot {@link http://www.netspot.com.au} 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 // File areas for file feedback assignment. 29 define('ASSIGNFEEDBACK_FILE_FILEAREA', 'feedback_files'); 30 define('ASSIGNFEEDBACK_FILE_BATCH_FILEAREA', 'feedback_files_batch'); 31 define('ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA', 'feedback_files_import'); 32 define('ASSIGNFEEDBACK_FILE_MAXSUMMARYFILES', 5); 33 define('ASSIGNFEEDBACK_FILE_MAXSUMMARYUSERS', 5); 34 define('ASSIGNFEEDBACK_FILE_MAXFILEUNZIPTIME', 120); 35 36 /** 37 * Library class for file feedback plugin extending feedback plugin base class. 38 * 39 * @package assignfeedback_file 40 * @copyright 2012 NetSpot {@link http://www.netspot.com.au} 41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 42 */ 43 class assign_feedback_file extends assign_feedback_plugin { 44 45 /** 46 * Get the name of the file feedback plugin. 47 * 48 * @return string 49 */ 50 public function get_name() { 51 return get_string('file', 'assignfeedback_file'); 52 } 53 54 /** 55 * Get file feedback information from the database. 56 * 57 * @param int $gradeid 58 * @return mixed 59 */ 60 public function get_file_feedback($gradeid) { 61 global $DB; 62 return $DB->get_record('assignfeedback_file', array('grade'=>$gradeid)); 63 } 64 65 /** 66 * File format options. 67 * 68 * @return array 69 */ 70 private function get_file_options() { 71 global $COURSE; 72 73 $fileoptions = array('subdirs'=>1, 74 'maxbytes'=>$COURSE->maxbytes, 75 'accepted_types'=>'*', 76 'return_types'=>FILE_INTERNAL); 77 return $fileoptions; 78 } 79 80 /** 81 * Has the feedback file been modified? 82 * 83 * @param stdClass $grade Grade object. 84 * @param stdClass $data Form data. 85 * @return boolean True if the file area has been modified, else false. 86 */ 87 public function is_feedback_modified(stdClass $grade, stdClass $data) { 88 global $USER; 89 90 $filekey = null; 91 $draftareainfo = null; 92 foreach ($data as $key => $value) { 93 if (strpos($key, 'files_') === 0 && strpos($key, '_filemanager')) { 94 $filekey = $key; 95 } 96 } 97 if (isset($filekey)) { 98 $draftareainfo = file_get_draft_area_info($data->$filekey); 99 $filecount = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); 100 if ($filecount != $draftareainfo['filecount']) { 101 return true; 102 } else { 103 // We need to check that the files in the draft area are the same as in the file area. 104 $usercontext = context_user::instance($USER->id); 105 $fs = get_file_storage(); 106 $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $data->$filekey, 'id', true); 107 $files = $fs->get_area_files($this->assignment->get_context()->id, 108 'assignfeedback_file', 109 ASSIGNFEEDBACK_FILE_FILEAREA, 110 $grade->id, 111 'id', 112 false); 113 foreach ($files as $key => $file) { 114 // Flag for recording if we have a matching file. 115 $matchflag = false; 116 foreach ($draftfiles as $draftkey => $draftfile) { 117 if (!$file->is_directory()) { 118 // File name is the same, but it could be a different file with the same name. 119 if ($draftfile->get_filename() == $file->get_filename()) { 120 // If the file name is the same but the content hash is different, or 121 // The file path for the file has changed, then we have a modification. 122 if ($draftfile->get_contenthash() != $file->get_contenthash() || 123 $draftfile->get_filepath() != $file->get_filepath()) { 124 return true; 125 } 126 // These files match. Check the next file. 127 $matchflag = true; 128 // We have a match on the file name so we can move to the next file and not 129 // proceed through the other draftfiles. 130 break; 131 } 132 } 133 } 134 // If the file does not match then there has been a modification. 135 if (!$matchflag) { 136 return true; 137 } 138 } 139 } 140 } 141 return false; 142 } 143 144 /** 145 * Copy all the files from one file area to another. 146 * 147 * @param file_storage $fs - The source context id 148 * @param int $fromcontextid - The source context id 149 * @param string $fromcomponent - The source component 150 * @param string $fromfilearea - The source filearea 151 * @param int $fromitemid - The source item id 152 * @param int $tocontextid - The destination context id 153 * @param string $tocomponent - The destination component 154 * @param string $tofilearea - The destination filearea 155 * @param int $toitemid - The destination item id 156 * @return boolean 157 */ 158 private function copy_area_files(file_storage $fs, 159 $fromcontextid, 160 $fromcomponent, 161 $fromfilearea, 162 $fromitemid, 163 $tocontextid, 164 $tocomponent, 165 $tofilearea, 166 $toitemid) { 167 168 $newfilerecord = new stdClass(); 169 $newfilerecord->contextid = $tocontextid; 170 $newfilerecord->component = $tocomponent; 171 $newfilerecord->filearea = $tofilearea; 172 $newfilerecord->itemid = $toitemid; 173 174 if ($files = $fs->get_area_files($fromcontextid, $fromcomponent, $fromfilearea, $fromitemid)) { 175 foreach ($files as $file) { 176 if ($file->is_directory() and $file->get_filepath() === '/') { 177 // We need a way to mark the age of each draft area. 178 // By not copying the root dir we force it to be created 179 // automatically with current timestamp. 180 continue; 181 } 182 183 $existingfile = $fs->get_file( 184 $newfilerecord->contextid, 185 $newfilerecord->component, 186 $newfilerecord->filearea, 187 $newfilerecord->itemid, 188 $file->get_filepath(), 189 $file->get_filename() 190 ); 191 if ($existingfile) { 192 // If the file already exists, remove it so it can be updated. 193 $existingfile->delete(); 194 } 195 196 $newfile = $fs->create_file_from_storedfile($newfilerecord, $file); 197 } 198 } 199 return true; 200 } 201 202 /** 203 * Get form elements for grading form. 204 * 205 * @param stdClass $grade 206 * @param MoodleQuickForm $mform 207 * @param stdClass $data 208 * @param int $userid The userid we are currently grading 209 * @return bool true if elements were added to the form 210 */ 211 public function get_form_elements_for_user($grade, MoodleQuickForm $mform, stdClass $data, $userid) { 212 213 $fileoptions = $this->get_file_options(); 214 $gradeid = $grade ? $grade->id : 0; 215 $elementname = 'files_' . $userid; 216 217 $data = file_prepare_standard_filemanager($data, 218 $elementname, 219 $fileoptions, 220 $this->assignment->get_context(), 221 'assignfeedback_file', 222 ASSIGNFEEDBACK_FILE_FILEAREA, 223 $gradeid); 224 $mform->addElement('filemanager', $elementname . '_filemanager', $this->get_name(), null, $fileoptions); 225 226 return true; 227 } 228 229 /** 230 * Count the number of files. 231 * 232 * @param int $gradeid 233 * @param string $area 234 * @return int 235 */ 236 private function count_files($gradeid, $area) { 237 238 $fs = get_file_storage(); 239 $files = $fs->get_area_files($this->assignment->get_context()->id, 240 'assignfeedback_file', 241 $area, 242 $gradeid, 243 'id', 244 false); 245 246 return count($files); 247 } 248 249 /** 250 * Update the number of files in the file area. 251 * 252 * @param stdClass $grade The grade record 253 * @return bool - true if the value was saved 254 */ 255 public function update_file_count($grade) { 256 global $DB; 257 258 $filefeedback = $this->get_file_feedback($grade->id); 259 if ($filefeedback) { 260 $filefeedback->numfiles = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); 261 return $DB->update_record('assignfeedback_file', $filefeedback); 262 } else { 263 $filefeedback = new stdClass(); 264 $filefeedback->numfiles = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); 265 $filefeedback->grade = $grade->id; 266 $filefeedback->assignment = $this->assignment->get_instance()->id; 267 return $DB->insert_record('assignfeedback_file', $filefeedback) > 0; 268 } 269 } 270 271 /** 272 * Save the feedback files. 273 * 274 * @param stdClass $grade 275 * @param stdClass $data 276 * @return bool 277 */ 278 public function save(stdClass $grade, stdClass $data) { 279 $fileoptions = $this->get_file_options(); 280 281 // The element name may have been for a different user. 282 foreach ($data as $key => $value) { 283 if (strpos($key, 'files_') === 0 && strpos($key, '_filemanager')) { 284 $elementname = substr($key, 0, strpos($key, '_filemanager')); 285 } 286 } 287 288 $data = file_postupdate_standard_filemanager($data, 289 $elementname, 290 $fileoptions, 291 $this->assignment->get_context(), 292 'assignfeedback_file', 293 ASSIGNFEEDBACK_FILE_FILEAREA, 294 $grade->id); 295 296 return $this->update_file_count($grade); 297 } 298 299 /** 300 * Display the list of files in the feedback status table. 301 * 302 * @param stdClass $grade 303 * @param bool $showviewlink - Set to true to show a link to see the full list of files 304 * @return string 305 */ 306 public function view_summary(stdClass $grade, & $showviewlink) { 307 308 $count = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); 309 310 // Show a view all link if the number of files is over this limit. 311 $showviewlink = $count > ASSIGNFEEDBACK_FILE_MAXSUMMARYFILES; 312 313 if ($count <= ASSIGNFEEDBACK_FILE_MAXSUMMARYFILES) { 314 return $this->assignment->render_area_files('assignfeedback_file', 315 ASSIGNFEEDBACK_FILE_FILEAREA, 316 $grade->id); 317 } else { 318 return get_string('countfiles', 'assignfeedback_file', $count); 319 } 320 } 321 322 /** 323 * Display the list of files in the feedback status table. 324 * 325 * @param stdClass $grade 326 * @return string 327 */ 328 public function view(stdClass $grade) { 329 return $this->assignment->render_area_files('assignfeedback_file', 330 ASSIGNFEEDBACK_FILE_FILEAREA, 331 $grade->id); 332 } 333 334 /** 335 * The assignment has been deleted - cleanup. 336 * 337 * @return bool 338 */ 339 public function delete_instance() { 340 global $DB; 341 // Will throw exception on failure. 342 $DB->delete_records('assignfeedback_file', 343 array('assignment'=>$this->assignment->get_instance()->id)); 344 345 return true; 346 } 347 348 /** 349 * Return true if there are no feedback files. 350 * 351 * @param stdClass $grade 352 */ 353 public function is_empty(stdClass $grade) { 354 return $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA) == 0; 355 } 356 357 /** 358 * Get file areas returns a list of areas this plugin stores files. 359 * 360 * @return array - An array of fileareas (keys) and descriptions (values) 361 */ 362 public function get_file_areas() { 363 return array(ASSIGNFEEDBACK_FILE_FILEAREA=>$this->get_name()); 364 } 365 366 /** 367 * Return true if this plugin can upgrade an old Moodle 2.2 assignment of this type 368 * and version. 369 * 370 * @param string $type old assignment subtype 371 * @param int $version old assignment version 372 * @return bool True if upgrade is possible 373 */ 374 public function can_upgrade($type, $version) { 375 if (($type == 'upload' || $type == 'uploadsingle') && $version >= 2011112900) { 376 return true; 377 } 378 return false; 379 } 380 381 /** 382 * Upgrade the settings from the old assignment to the new plugin based one. 383 * 384 * @param context $oldcontext - the context for the old assignment 385 * @param stdClass $oldassignment - the data for the old assignment 386 * @param string $log - can be appended to by the upgrade 387 * @return bool was it a success? (false will trigger a rollback) 388 */ 389 public function upgrade_settings(context $oldcontext, stdClass $oldassignment, & $log) { 390 // First upgrade settings (nothing to do). 391 return true; 392 } 393 394 /** 395 * Upgrade the feedback from the old assignment to the new one. 396 * 397 * @param context $oldcontext - the database for the old assignment context 398 * @param stdClass $oldassignment The data record for the old assignment 399 * @param stdClass $oldsubmission The data record for the old submission 400 * @param stdClass $grade The data record for the new grade 401 * @param string $log Record upgrade messages in the log 402 * @return bool true or false - false will trigger a rollback 403 */ 404 public function upgrade(context $oldcontext, 405 stdClass $oldassignment, 406 stdClass $oldsubmission, 407 stdClass $grade, 408 & $log) { 409 global $DB; 410 411 // Now copy the area files. 412 $this->assignment->copy_area_files_for_upgrade($oldcontext->id, 413 'mod_assignment', 414 'response', 415 $oldsubmission->id, 416 $this->assignment->get_context()->id, 417 'assignfeedback_file', 418 ASSIGNFEEDBACK_FILE_FILEAREA, 419 $grade->id); 420 421 // Now count them! 422 $filefeedback = new stdClass(); 423 $filefeedback->numfiles = $this->count_files($grade->id, ASSIGNFEEDBACK_FILE_FILEAREA); 424 $filefeedback->grade = $grade->id; 425 $filefeedback->assignment = $this->assignment->get_instance()->id; 426 if (!$DB->insert_record('assignfeedback_file', $filefeedback) > 0) { 427 $log .= get_string('couldnotconvertgrade', 'mod_assign', $grade->userid); 428 return false; 429 } 430 return true; 431 } 432 433 /** 434 * Return a list of the batch grading operations performed by this plugin. 435 * This plugin supports batch upload files and upload zip. 436 * 437 * @return array The list of batch grading operations 438 */ 439 public function get_grading_batch_operations() { 440 return array('uploadfiles'=>get_string('uploadfiles', 'assignfeedback_file')); 441 } 442 443 /** 444 * Upload files and send them to multiple users. 445 * 446 * @param array $users - An array of user ids 447 * @return string - The response html 448 */ 449 public function view_batch_upload_files($users) { 450 global $CFG, $DB, $USER; 451 452 require_capability('mod/assign:grade', $this->assignment->get_context()); 453 require_once($CFG->dirroot . '/mod/assign/feedback/file/batchuploadfilesform.php'); 454 require_once($CFG->dirroot . '/mod/assign/renderable.php'); 455 456 $formparams = array('cm'=>$this->assignment->get_course_module()->id, 457 'users'=>$users, 458 'context'=>$this->assignment->get_context()); 459 460 $usershtml = ''; 461 462 $usercount = 0; 463 foreach ($users as $userid) { 464 if ($usercount >= ASSIGNFEEDBACK_FILE_MAXSUMMARYUSERS) { 465 $moreuserscount = count($users) - ASSIGNFEEDBACK_FILE_MAXSUMMARYUSERS; 466 $usershtml .= get_string('moreusers', 'assignfeedback_file', $moreuserscount); 467 break; 468 } 469 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); 470 471 $usersummary = new assign_user_summary($user, 472 $this->assignment->get_course()->id, 473 has_capability('moodle/site:viewfullnames', 474 $this->assignment->get_course_context()), 475 $this->assignment->is_blind_marking(), 476 $this->assignment->get_uniqueid_for_user($user->id), 477 get_extra_user_fields($this->assignment->get_context())); 478 $usershtml .= $this->assignment->get_renderer()->render($usersummary); 479 $usercount += 1; 480 } 481 482 $formparams['usershtml'] = $usershtml; 483 484 $mform = new assignfeedback_file_batch_upload_files_form(null, $formparams); 485 486 if ($mform->is_cancelled()) { 487 redirect(new moodle_url('view.php', 488 array('id'=>$this->assignment->get_course_module()->id, 489 'action'=>'grading'))); 490 return; 491 } else if ($data = $mform->get_data()) { 492 // Copy the files from the draft area to a temporary import area. 493 $data = file_postupdate_standard_filemanager($data, 494 'files', 495 $this->get_file_options(), 496 $this->assignment->get_context(), 497 'assignfeedback_file', 498 ASSIGNFEEDBACK_FILE_BATCH_FILEAREA, 499 $USER->id); 500 $fs = get_file_storage(); 501 502 // Now copy each of these files to the users feedback file area. 503 foreach ($users as $userid) { 504 $grade = $this->assignment->get_user_grade($userid, true); 505 $this->assignment->notify_grade_modified($grade); 506 507 $this->copy_area_files($fs, 508 $this->assignment->get_context()->id, 509 'assignfeedback_file', 510 ASSIGNFEEDBACK_FILE_BATCH_FILEAREA, 511 $USER->id, 512 $this->assignment->get_context()->id, 513 'assignfeedback_file', 514 ASSIGNFEEDBACK_FILE_FILEAREA, 515 $grade->id); 516 517 $filefeedback = $this->get_file_feedback($grade->id); 518 if ($filefeedback) { 519 $filefeedback->numfiles = $this->count_files($grade->id, 520 ASSIGNFEEDBACK_FILE_FILEAREA); 521 $DB->update_record('assignfeedback_file', $filefeedback); 522 } else { 523 $filefeedback = new stdClass(); 524 $filefeedback->numfiles = $this->count_files($grade->id, 525 ASSIGNFEEDBACK_FILE_FILEAREA); 526 $filefeedback->grade = $grade->id; 527 $filefeedback->assignment = $this->assignment->get_instance()->id; 528 $DB->insert_record('assignfeedback_file', $filefeedback); 529 } 530 } 531 532 // Now delete the temporary import area. 533 $fs->delete_area_files($this->assignment->get_context()->id, 534 'assignfeedback_file', 535 ASSIGNFEEDBACK_FILE_BATCH_FILEAREA, 536 $USER->id); 537 538 redirect(new moodle_url('view.php', 539 array('id'=>$this->assignment->get_course_module()->id, 540 'action'=>'grading'))); 541 return; 542 } else { 543 544 $header = new assign_header($this->assignment->get_instance(), 545 $this->assignment->get_context(), 546 false, 547 $this->assignment->get_course_module()->id, 548 get_string('batchuploadfiles', 'assignfeedback_file')); 549 $o = ''; 550 $o .= $this->assignment->get_renderer()->render($header); 551 $o .= $this->assignment->get_renderer()->render(new assign_form('batchuploadfiles', $mform)); 552 $o .= $this->assignment->get_renderer()->render_footer(); 553 } 554 555 return $o; 556 } 557 558 /** 559 * User has chosen a custom grading batch operation and selected some users. 560 * 561 * @param string $action - The chosen action 562 * @param array $users - An array of user ids 563 * @return string - The response html 564 */ 565 public function grading_batch_operation($action, $users) { 566 567 if ($action == 'uploadfiles') { 568 return $this->view_batch_upload_files($users); 569 } 570 return ''; 571 } 572 573 /** 574 * View the upload zip form. 575 * 576 * @return string - The html response 577 */ 578 public function view_upload_zip() { 579 global $CFG, $USER; 580 581 require_capability('mod/assign:grade', $this->assignment->get_context()); 582 require_once($CFG->dirroot . '/mod/assign/feedback/file/uploadzipform.php'); 583 require_once($CFG->dirroot . '/mod/assign/feedback/file/importziplib.php'); 584 require_once($CFG->dirroot . '/mod/assign/feedback/file/importzipform.php'); 585 586 $formparams = array('context'=>$this->assignment->get_context(), 587 'cm'=>$this->assignment->get_course_module()->id); 588 $mform = new assignfeedback_file_upload_zip_form(null, $formparams); 589 590 $o = ''; 591 592 $confirm = optional_param('confirm', 0, PARAM_BOOL); 593 $renderer = $this->assignment->get_renderer(); 594 595 // Delete any existing files. 596 $importer = new assignfeedback_file_zip_importer(); 597 $contextid = $this->assignment->get_context()->id; 598 599 if ($mform->is_cancelled()) { 600 $importer->delete_import_files($contextid); 601 $urlparams = array('id'=>$this->assignment->get_course_module()->id, 602 'action'=>'grading'); 603 $url = new moodle_url('view.php', $urlparams); 604 redirect($url); 605 return; 606 } else if ($confirm) { 607 $params = array('assignment'=>$this->assignment, 'importer'=>$importer); 608 609 $mform = new assignfeedback_file_import_zip_form(null, $params); 610 if ($mform->is_cancelled()) { 611 $importer->delete_import_files($contextid); 612 $urlparams = array('id'=>$this->assignment->get_course_module()->id, 613 'action'=>'grading'); 614 $url = new moodle_url('view.php', $urlparams); 615 redirect($url); 616 return; 617 } 618 619 $o .= $importer->import_zip_files($this->assignment, $this); 620 $importer->delete_import_files($contextid); 621 } else if (($data = $mform->get_data()) && 622 ($zipfile = $mform->save_stored_file('feedbackzip', 623 $contextid, 624 'assignfeedback_file', 625 ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA, 626 $USER->id, 627 '/', 628 'import.zip', 629 true))) { 630 631 $importer->extract_files_from_zip($zipfile, $contextid); 632 633 $params = array('assignment'=>$this->assignment, 'importer'=>$importer); 634 635 $mform = new assignfeedback_file_import_zip_form(null, $params); 636 637 $header = new assign_header($this->assignment->get_instance(), 638 $this->assignment->get_context(), 639 false, 640 $this->assignment->get_course_module()->id, 641 get_string('confirmuploadzip', 'assignfeedback_file')); 642 $o .= $renderer->render($header); 643 $o .= $renderer->render(new assign_form('confirmimportzip', $mform)); 644 $o .= $renderer->render_footer(); 645 646 } else { 647 648 $header = new assign_header($this->assignment->get_instance(), 649 $this->assignment->get_context(), 650 false, 651 $this->assignment->get_course_module()->id, 652 get_string('uploadzip', 'assignfeedback_file')); 653 $o .= $renderer->render($header); 654 $o .= $renderer->render(new assign_form('uploadfeedbackzip', $mform)); 655 $o .= $renderer->render_footer(); 656 } 657 658 return $o; 659 } 660 661 /** 662 * Called by the assignment module when someone chooses something from the 663 * grading navigation or batch operations list. 664 * 665 * @param string $action - The page to view 666 * @return string - The html response 667 */ 668 public function view_page($action) { 669 if ($action == 'uploadfiles') { 670 $users = required_param('selectedusers', PARAM_SEQUENCE); 671 return $this->view_batch_upload_files(explode(',', $users)); 672 } 673 if ($action == 'uploadzip') { 674 return $this->view_upload_zip(); 675 } 676 677 return ''; 678 } 679 680 /** 681 * Return a list of the grading actions performed by this plugin. 682 * This plugin supports upload zip. 683 * 684 * @return array The list of grading actions 685 */ 686 public function get_grading_actions() { 687 return array('uploadzip'=>get_string('uploadzip', 'assignfeedback_file')); 688 } 689 690 /** 691 * Return a description of external params suitable for uploading a feedback file from a webservice. 692 * 693 * @return external_description|null 694 */ 695 public function get_external_parameters() { 696 return array( 697 'files_filemanager' => new external_value( 698 PARAM_INT, 699 'The id of a draft area containing files for this feedback.', 700 VALUE_OPTIONAL 701 ) 702 ); 703 } 704 705 /** 706 * Return the plugin configs for external functions. 707 * 708 * @return array the list of settings 709 * @since Moodle 3.2 710 */ 711 public function get_config_for_external() { 712 return (array) $this->get_config(); 713 } 714 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body