Differences Between: [Versions 311 and 402] [Versions 400 and 402] [Versions 401 and 402]
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 namespace tool_brickfield; 18 19 /** 20 * Class manager 21 * @package tool_brickfield 22 * @copyright 2021 Brickfield Education Labs https://www.brickfield.ie 23 * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 class manager { 26 27 /** 28 * Defines the waiting for analysis status. 29 */ 30 const STATUS_WAITING = 0; 31 32 /** 33 * Defined the analysis in progress status. 34 */ 35 const STATUS_INPROGRESS = -1; 36 37 /** 38 * Defines the analysis has completed status. 39 */ 40 const STATUS_CHECKED = 1; 41 42 /** 43 * Defines summary error value. 44 */ 45 const SUMMARY_ERROR = 0; 46 47 /** 48 * Defines summary failed value. 49 */ 50 const SUMMARY_FAILED = 1; 51 52 /** 53 * Defines summary percent value. 54 */ 55 const SUMMARY_PERCENT = 2; 56 57 /** 58 * Default bulk record limit. 59 */ 60 const BULKRECORDLIMIT = 1000; 61 62 /** 63 * Name of this plugin. 64 */ 65 const PLUGINNAME = 'tool_brickfield'; 66 67 /** 68 * Areas table name. 69 */ 70 const DB_AREAS = self::PLUGINNAME . '_areas'; 71 72 /** 73 * Cacheacts table name. 74 */ 75 const DB_CACHEACTS = self::PLUGINNAME . '_cache_acts'; 76 77 /** 78 * Cachecheck table name. 79 */ 80 const DB_CACHECHECK = self::PLUGINNAME . '_cache_check'; 81 82 /** 83 * Checks table name. 84 */ 85 const DB_CHECKS = self::PLUGINNAME . '_checks'; 86 87 /** 88 * Content table name. 89 */ 90 const DB_CONTENT = self::PLUGINNAME . '_content'; 91 92 /** 93 * Errors table name. 94 */ 95 const DB_ERRORS = self::PLUGINNAME . '_errors'; 96 97 /** 98 * Process table name. 99 */ 100 const DB_PROCESS = self::PLUGINNAME . '_process'; 101 102 /** 103 * Results table name. 104 */ 105 const DB_RESULTS = self::PLUGINNAME . '_results'; 106 107 /** 108 * Schedule table name. 109 */ 110 const DB_SCHEDULE = self::PLUGINNAME . '_schedule'; 111 112 /** 113 * Summary table name. 114 */ 115 const DB_SUMMARY = self::PLUGINNAME . '_summary'; 116 117 /** @var string The URL to find help at. */ 118 private static $helpurl = 'https://www.brickfield.ie/moodle-help-311'; 119 120 121 /** @var array Statically stores the database checks records. */ 122 static protected $checks; 123 124 /** 125 * Returns the URL used for registration. 126 * 127 * @return \moodle_url 128 */ 129 public static function registration_url(): \moodle_url { 130 return accessibility::get_plugin_url('registration.php'); 131 } 132 133 /** 134 * Returns an appropriate message about the current registration state. 135 * 136 * @return string 137 * @throws \coding_exception 138 * @throws \dml_exception 139 * @throws \moodle_exception 140 */ 141 public static function registration_message(): string { 142 $firstline = get_string('notregistered', self::PLUGINNAME); 143 if (has_capability('moodle/site:config', \context_system::instance())) { 144 $secondline = \html_writer::link(self::registration_url(), get_string('registernow', self::PLUGINNAME)); 145 } else { 146 $secondline = get_string('contactadmin', self::PLUGINNAME); 147 } 148 return $firstline . '<br />' . $secondline; 149 } 150 151 /** 152 * Get the help page URL. 153 * @return string 154 * @throws dml_exception 155 */ 156 public static function get_helpurl(): string { 157 return self::$helpurl; 158 } 159 160 /** 161 * Return an array of system checks available, and store them statically. 162 * 163 * @return array 164 * @throws \dml_exception 165 */ 166 public static function get_checks(): array { 167 global $DB; 168 if (self::$checks === null) { 169 self::$checks = $DB->get_records(self::DB_CHECKS, [] , 'id'); 170 } 171 return self::$checks; 172 } 173 174 /** 175 * Find all available areas. 176 * 177 * @return area_base[] 178 * @throws \ReflectionException 179 */ 180 public static function get_all_areas(): array { 181 return array_filter( 182 array_map( 183 function($classname) { 184 $reflectionclass = new \ReflectionClass($classname); 185 if ($reflectionclass->isAbstract()) { 186 return false; 187 } 188 $instance = new $classname(); 189 190 if ($instance->is_available()) { 191 return $instance; 192 } else { 193 return null; 194 } 195 }, 196 array_keys(\core_component::get_component_classes_in_namespace('tool_brickfield', 'local\areas')) 197 ) 198 ); 199 } 200 201 /** 202 * Calculate contenthash of a given content string 203 * 204 * @param string|null $content 205 * @return string 206 */ 207 public static function get_contenthash(?string $content = null): string { 208 return sha1($content ?? ''); 209 } 210 211 /** 212 * Does the current area content need to be scheduled for check? 213 * 214 * It does not need to be scheduled if: 215 * - it is the current content 216 * OR 217 * - there is already schedule 218 * 219 * @param int $areaid 220 * @param string $contenthash 221 * @return bool 222 * @throws \dml_exception 223 */ 224 protected static function content_needs_scheduling(int $areaid, string $contenthash): bool { 225 global $DB; 226 return ! $DB->get_field_sql('SELECT 1 FROM {' . self::DB_CONTENT . '} '. 227 'WHERE areaid = ? 228 AND (status = 0 OR (iscurrent = 1 AND contenthash = ?))', 229 [$areaid, $contenthash], IGNORE_MULTIPLE); 230 } 231 232 /** 233 * Schedule an area for analysis if there has been changes. 234 * 235 * @param \stdClass $arearecord record with the fields from the {tool_brickfield_areas} table 236 * as returned by area_base::find_relevant_areas(). 237 * It also contains the 'content' property with the current area content 238 * @throws \dml_exception 239 */ 240 protected static function schedule_area_if_necessary(\stdClass $arearecord) { 241 global $DB; 242 243 $contenthash = static::get_contenthash($arearecord->content); 244 $searchparams = array_diff_key((array)$arearecord, ['content' => 1, 'reftable' => 1, 'refid' => 1]); 245 if ($dbrecord = $DB->get_record(self::DB_AREAS, $searchparams)) { 246 if ( ! static::content_needs_scheduling($dbrecord->id, $contenthash)) { 247 // This is already the latest content record or there is already scheduled record, nothing to do. 248 return; 249 } 250 } else { 251 $dbrecord = (object)array_diff_key((array)$arearecord, ['content' => 1]); 252 $dbrecord->id = $DB->insert_record(self::DB_AREAS, $dbrecord); 253 } 254 // Schedule the area for the check. Note that we do not record the contenthash, we will calculate it again 255 // during the actual check. 256 $DB->insert_record(self::DB_CONTENT, 257 (object)['areaid' => $dbrecord->id, 'contenthash' => '', 'timecreated' => time(), 258 'status' => self::STATUS_WAITING]); 259 } 260 261 /** 262 * Asks all area providers if they have any areas that might have changed as a result of an event and schedules them 263 * 264 * @param \core\event\base $event 265 * @throws \ReflectionException 266 * @throws \dml_exception 267 */ 268 public static function find_new_or_updated_areas(\core\event\base $event) { 269 foreach (static::get_all_areas() as $area) { 270 if ($records = $area->find_relevant_areas($event)) { 271 foreach ($records as $record) { 272 static::schedule_area_if_necessary($record); 273 } 274 $records->close(); 275 } 276 } 277 } 278 279 /** 280 * Returns the current content of the area. 281 * 282 * @param \stdClass $arearecord record from the tool_brickfield_areas table 283 * @return array|null array where the first element is the value of the field and the second element 284 * is the 'format' for this field if it is present. If the record was not found null is returned. 285 * @throws \ddl_exception 286 * @throws \ddl_table_missing_exception 287 * @throws \dml_exception 288 */ 289 protected static function get_area_content(\stdClass $arearecord): array { 290 global $DB; 291 if ($arearecord->type == area_base::TYPE_FIELD) { 292 $tablename = $arearecord->tablename; 293 $fieldname = $arearecord->fieldorarea; 294 $itemid = $arearecord->itemid; 295 296 if (!$DB->get_manager()->table_exists($tablename)) { 297 return []; 298 } 299 if (!$DB->get_manager()->field_exists($tablename, $fieldname)) { 300 return []; 301 } 302 $fields = $fieldname; 303 if ($DB->get_manager()->field_exists($tablename, $fieldname . 'format')) { 304 $fields .= ',' . $fieldname . 'format'; 305 } 306 if ($record = $DB->get_record($tablename, ['id' => $itemid], $fields)) { 307 return array_values((array)$record); 308 } 309 } 310 return []; 311 } 312 313 /** 314 * Asks all area providers if they have any areas that might have changed per courseid and schedules them. 315 * 316 * @param int $courseid 317 * @throws \ReflectionException 318 * @throws \coding_exception 319 * @throws \ddl_exception 320 * @throws \ddl_table_missing_exception 321 * @throws \dml_exception 322 */ 323 public static function find_new_or_updated_areas_per_course(int $courseid) { 324 $totalcount = 0; 325 foreach (static::get_all_areas() as $area) { 326 if ($records = $area->find_course_areas($courseid)) { 327 foreach ($records as $record) { 328 $totalcount++; 329 static::schedule_area_if_necessary($record); 330 } 331 $records->close(); 332 } 333 // For a site course request, also process the site level areas. 334 if (($courseid == SITEID) && ($records = $area->find_system_areas())) { 335 foreach ($records as $record) { 336 $totalcount++; 337 // Currently, the courseid in the area table is null if there is a category id. 338 if (!empty($record->categoryid)) { 339 $record->courseid = null; 340 } 341 static::schedule_area_if_necessary($record); 342 } 343 $records->close(); 344 } 345 } 346 // Need to run for total count of areas. 347 static::check_scheduled_areas($totalcount); 348 } 349 350 /** 351 * Finds all areas that are waiting to be checked, performs checks. Returns true if there were records processed, false if not. 352 * To be called from scheduled task 353 * 354 * @param int $batch 355 * @return bool 356 * @throws \coding_exception 357 * @throws \ddl_exception 358 * @throws \ddl_table_missing_exception 359 * @throws \dml_exception 360 */ 361 public static function check_scheduled_areas(int $batch = 0): bool { 362 global $DB; 363 364 $processingtime = 0; 365 $resultstime = 0; 366 367 $config = get_config(self::PLUGINNAME); 368 if ($batch == 0) { 369 $batch = $config->batch; 370 } 371 // Creating insert array for courseid cache reruns. 372 $recordsfound = false; 373 $batchinserts = []; 374 echo("Batch amount is ".$batch.", starttime ".time()."\n"); 375 $rs = $DB->get_recordset_sql('SELECT a.*, ch.id AS contentid 376 FROM {' . self::DB_AREAS. '} a 377 JOIN {' . self::DB_CONTENT . '} ch ON ch.areaid = a.id 378 WHERE ch.status = ? 379 ORDER BY a.id, ch.timecreated, ch.id', 380 [self::STATUS_WAITING], 0, $batch); 381 382 foreach ($rs as $arearecord) { 383 $recordsfound = true; 384 $DB->set_field(self::DB_CONTENT, 'status', self::STATUS_INPROGRESS, ['id' => $arearecord->contentid]); 385 $content = static::get_area_content($arearecord); 386 if ($content[0] == null) { 387 $content[0] = ''; 388 } 389 accessibility::run_check($content[0], $arearecord->contentid, $processingtime, $resultstime); 390 391 // Set all content 'iscurrent' fields for this areaid to 0. 392 $DB->set_field(self::DB_CONTENT, 'iscurrent', 0, ['areaid' => $arearecord->id]); 393 // Update this content record to be the current record. 394 $DB->update_record(self::DB_CONTENT, 395 (object)['id' => $arearecord->contentid, 'status' => self::STATUS_CHECKED, 'timechecked' => time(), 396 'contenthash' => static::get_contenthash($content[0]), 'iscurrent' => 1]); 397 398 // If full caching has been run, then queue, if not in queue already. 399 if (($arearecord->courseid != null) && static::is_okay_to_cache() && 400 !isset($batchinserts[$arearecord->courseid])) { 401 $batchinserts[$arearecord->courseid] = ['courseid' => $arearecord->courseid, 'item' => 'cache']; 402 } 403 } 404 405 if (count($batchinserts) > 0) { 406 $DB->insert_records(self::DB_PROCESS, $batchinserts); 407 } 408 409 mtrace('Total time in htmlchecker: ' . $processingtime . ' secs.'); 410 mtrace('Total time in results: ' . $resultstime . ' secs.'); 411 return $recordsfound; 412 } 413 414 /** 415 * Return true if analysis hasn't been disabled. 416 * @return bool 417 * @throws \dml_exception 418 */ 419 public static function is_okay_to_cache(): bool { 420 return (analysis::type_is_byrequest()); 421 } 422 423 /** 424 * Finds all areas that are waiting to be deleted, performs deletions. 425 * 426 * @param int $batch limit, can be called from runcli.php 427 * To be called from scheduled task 428 * @throws \coding_exception 429 * @throws \dml_exception 430 */ 431 public static function check_scheduled_deletions(int $batch = 0) { 432 global $DB; 433 434 $config = get_config(self::PLUGINNAME); 435 if ($batch == 0) { 436 $batch = $config->batch; 437 } 438 439 // Creating insert array for courseid cache reruns. 440 $batchinserts = []; 441 442 $rs = $DB->get_recordset(self::DB_PROCESS, ['contextid' => -1, 'timecompleted' => 0], '', '*', 0, $batch); 443 444 foreach ($rs as $record) { 445 446 if ($record->item == "core_course") { 447 $tidyparams = ['courseid' => $record->courseid]; 448 static::delete_summary_data($record->courseid); // Delete cache too. 449 } else if ($record->item == "course_categories") { 450 $tidyparams = ['component' => 'core_course', 'categoryid' => $record->innercontextid]; 451 } else if ($record->item == "course_sections") { 452 // Locate course sections, using innercontextid, contextid set to -1 for delete. 453 $tidyparams = ['courseid' => $record->courseid, 'component' => 'core_course', 454 'tablename' => $record->item, 'itemid' => $record->innercontextid]; 455 } else if ($record->item == "lesson_pages") { 456 // Locate lesson pages, using innercontextid, contextid set to -1 for delete. 457 $tidyparams = ['courseid' => $record->courseid, 'component' => 'mod_lesson', 458 'tablename' => $record->item, 'itemid' => $record->innercontextid]; 459 } else if ($record->item == "book_chapters") { 460 // Locate book chapters, using innercontextid, contextid set to -1 for delete. 461 $tidyparams = ['courseid' => $record->courseid, 'component' => 'mod_book', 462 'tablename' => $record->item, 'itemid' => $record->innercontextid]; 463 } else if ($record->item == "question") { 464 // Locate question areas, using innercontextid, contextid set to -1 for delete. 465 $tidyparams = [ 466 'courseid' => $record->courseid, 'component' => 'core_question', 467 'tablename' => $record->item, 'itemid' => $record->innercontextid 468 ]; 469 } else { 470 // Locate specific module instance, using innercontextid, contextid set to -1 for delete. 471 $tidyparams = ['courseid' => $record->courseid, 'component' => $record->item, 472 'itemid' => $record->innercontextid]; 473 } 474 475 $areas = $DB->get_records(self::DB_AREAS, $tidyparams); 476 foreach ($areas as $area) { 477 static::delete_area_tree($area); 478 } 479 480 $DB->delete_records(self::DB_PROCESS, ['id' => $record->id]); 481 482 // If full caching has been run, then queue, if not in queue already. 483 if ($record->courseid != null && static::is_okay_to_cache() && !isset($batchinserts[$record->courseid])) { 484 $batchinserts[$record->courseid] = ['courseid' => $record->courseid, 'item' => 'cache']; 485 } 486 } 487 $rs->close(); 488 489 if (count($batchinserts) > 0) { 490 $DB->insert_records(self::DB_PROCESS, $batchinserts); 491 } 492 } 493 494 /** 495 * Checks all queued course updates, and finds all relevant areas. 496 * 497 * @param int $batch limit 498 * To be called from scheduled task 499 * @throws \ReflectionException 500 * @throws \dml_exception 501 */ 502 public static function check_course_updates(int $batch = 0) { 503 global $DB; 504 505 if ($batch == 0) { 506 $config = get_config(self::PLUGINNAME); 507 $batch = $config->batch; 508 } 509 510 $recs = $DB->get_records(self::DB_PROCESS, ['item' => 'coursererun'], '', 'DISTINCT courseid', 0, $batch); 511 512 foreach ($recs as $record) { 513 static::find_new_or_updated_areas_per_course($record->courseid); 514 $DB->delete_records(self::DB_PROCESS, ['courseid' => $record->courseid, 'item' => 'coursererun']); 515 static::store_result_summary($record->courseid); 516 } 517 } 518 519 /** 520 * Finds all records for a given content area and performs deletions. 521 * 522 * To be called from scheduled task 523 * @param \stdClass $area 524 * @throws \dml_exception 525 */ 526 public static function delete_area_tree(\stdClass $area) { 527 global $DB; 528 529 $contents = $DB->get_records(self::DB_CONTENT, ['areaid' => $area->id]); 530 foreach ($contents as $content) { 531 $results = $DB->get_records(self::DB_RESULTS, ['contentid' => $content->id]); 532 foreach ($results as $result) { 533 $DB->delete_records(self::DB_ERRORS, ['resultid' => $result->id]); 534 } 535 $DB->delete_records(self::DB_RESULTS, ['contentid' => $content->id]); 536 } 537 538 // Also, delete all child areas, if existing. 539 $childparams = ['type' => $area->type, 'reftable' => $area->tablename, 540 'refid' => $area->itemid]; 541 $childareas = $DB->get_records(self::DB_AREAS, $childparams); 542 foreach ($childareas as $childarea) { 543 static::delete_area_tree($childarea); 544 } 545 546 $DB->delete_records(self::DB_CONTENT, ['areaid' => $area->id]); 547 $DB->delete_records(self::DB_AREAS, ['id' => $area->id]); 548 } 549 550 /** 551 * Finds all records which are no longer current and performs deletions. 552 * 553 * To be called from scheduled task. 554 */ 555 public static function delete_historical_data() { 556 global $DB; 557 558 $config = get_config(self::PLUGINNAME); 559 560 if ($config->deletehistoricaldata) { 561 $contents = $DB->get_records(self::DB_CONTENT, ['iscurrent' => 0, 'status' => self::STATUS_CHECKED]); 562 foreach ($contents as $content) { 563 $results = $DB->get_records(self::DB_RESULTS, ['contentid' => $content->id]); 564 foreach ($results as $result) { 565 $DB->delete_records(self::DB_ERRORS, ['resultid' => $result->id]); 566 } 567 $DB->delete_records(self::DB_RESULTS, ['contentid' => $content->id]); 568 $DB->delete_records(self::DB_CONTENT, ['id' => $content->id]); 569 } 570 } 571 } 572 573 /** 574 * Finds all summary cache records for a given courseid and performs deletions. 575 * To be called from scheduled task. 576 * 577 * @param int $courseid 578 * @throws \dml_exception 579 */ 580 public static function delete_summary_data(int $courseid) { 581 global $DB; 582 583 if ($courseid == null) { 584 mtrace('Attempting to run delete_summary_data with no courseid, returning'); 585 return; 586 } 587 588 $DB->delete_records(self::DB_SUMMARY, ['courseid' => $courseid]); 589 $DB->delete_records(self::DB_CACHECHECK, ['courseid' => $courseid]); 590 $DB->delete_records(self::DB_CACHEACTS, ['courseid' => $courseid]); 591 } 592 593 /** 594 * Finds all results required to display accessibility report and stores them in the database. 595 * 596 * To be called from scheduled task. 597 * @param int|null $courseid 598 * @throws \coding_exception 599 * @throws \dml_exception 600 */ 601 public static function store_result_summary(int $courseid = null) { 602 global $DB; 603 604 if (static::is_okay_to_cache() && ($courseid == null)) { 605 mtrace('Attempting to run update cache with no courseid, returning'); 606 return; 607 } 608 609 $extrasql = !$courseid ? "" : "AND courseid = ?"; 610 $coursesqlval = !$courseid ? [] : [$courseid]; 611 612 // Count of failed activities and count of errors by check. 613 $errorsql = "SELECT areas.courseid, chx.checkgroup, 614 COUNT(DISTINCT (".$DB->sql_concat('areas.contextid', 'areas.component').")) AS failed, 615 SUM(res.errorcount) AS errors 616 FROM {" . self::DB_AREAS . "} areas 617 INNER JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = areas.id 618 INNER JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id 619 INNER JOIN {" . self::DB_CHECKS . "} chx ON chx.id = res.checkid 620 WHERE res.errorcount > ? AND ch.iscurrent = ? ". $extrasql ." GROUP BY courseid, chx.checkgroup"; 621 622 $recordserrored = $DB->get_recordset_sql($errorsql, array_merge([0, 1], $coursesqlval)); 623 624 // Count of failed activities by course. 625 $failsql = "SELECT areas.courseid, 626 COUNT(DISTINCT (".$DB->sql_concat('areas.contextid', 'areas.component').")) AS failed, 627 SUM(res.errorcount) AS errors 628 FROM {" . self::DB_AREAS . "} areas 629 INNER JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = areas.id 630 INNER JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id 631 WHERE res.errorcount > ? AND ch.iscurrent = ? ". $extrasql ." GROUP BY courseid"; 632 633 $recordsfailed = $DB->get_recordset_sql($failsql, array_merge([0, 1], $coursesqlval)); 634 635 $extrasql = !$courseid ? "" : "WHERE courseid = ?"; 636 // Count of activities per course. 637 $countsql = "SELECT courseid, COUNT(DISTINCT (".$DB->sql_concat('areas.contextid', 'areas.component').")) AS activities 638 FROM {" . self::DB_AREAS . "} areas ". $extrasql ." GROUP BY areas.courseid"; 639 640 $recordscount = $DB->get_records_sql($countsql, $coursesqlval); 641 642 $final = []; 643 $values = []; 644 645 foreach ($recordscount as $countrecord) { 646 $final[$countrecord->courseid] = array_pad(array(), 8, 647 [self::SUMMARY_ERROR => 0, self::SUMMARY_FAILED => 0, self::SUMMARY_PERCENT => 100] 648 ) + [ 649 "activitiespassed" => $countrecord->activities, 650 "activitiesfailed" => 0, 651 "activities" => $countrecord->activities 652 ]; 653 } 654 655 foreach ($recordsfailed as $failedrecord) { 656 $final[$failedrecord->courseid]["activitiespassed"] -= $failedrecord->failed; 657 $final[$failedrecord->courseid]["activitiesfailed"] += $failedrecord->failed; 658 } 659 660 foreach ($recordserrored as $errorrecord) { 661 $final[$errorrecord->courseid][$errorrecord->checkgroup][self::SUMMARY_ERROR] = $errorrecord->errors; 662 $final[$errorrecord->courseid][$errorrecord->checkgroup][self::SUMMARY_FAILED] = $errorrecord->failed; 663 $final[$errorrecord->courseid][$errorrecord->checkgroup][self::SUMMARY_PERCENT] = round(100 * (1 - 664 ($final[$errorrecord->courseid][$errorrecord->checkgroup][self::SUMMARY_FAILED] 665 / $final[$errorrecord->courseid]["activities"]))); 666 } 667 668 foreach ($recordscount as $course) { 669 if (!$course->courseid) { 670 continue; 671 } 672 $element = [ 673 'courseid' => $course->courseid, 674 'status' => self::STATUS_CHECKED, 675 'activities' => $final[$course->courseid]["activities"], 676 'activitiespassed' => $final[$course->courseid]["activitiespassed"], 677 'activitiesfailed' => $final[$course->courseid]["activitiesfailed"], 678 'errorschecktype1' => $final[$course->courseid][area_base::CHECKGROUP_FORM][self::SUMMARY_ERROR], 679 'errorschecktype2' => $final[$course->courseid][area_base::CHECKGROUP_IMAGE][self::SUMMARY_ERROR], 680 'errorschecktype3' => $final[$course->courseid][area_base::CHECKGROUP_LAYOUT][self::SUMMARY_ERROR], 681 'errorschecktype4' => $final[$course->courseid][area_base::CHECKGROUP_LINK][self::SUMMARY_ERROR], 682 'errorschecktype5' => $final[$course->courseid][area_base::CHECKGROUP_MEDIA][self::SUMMARY_ERROR], 683 'errorschecktype6' => $final[$course->courseid][area_base::CHECKGROUP_TABLE][self::SUMMARY_ERROR], 684 'errorschecktype7' => $final[$course->courseid][area_base::CHECKGROUP_TEXT][self::SUMMARY_ERROR], 685 'failedchecktype1' => $final[$course->courseid][area_base::CHECKGROUP_FORM][self::SUMMARY_FAILED], 686 'failedchecktype2' => $final[$course->courseid][area_base::CHECKGROUP_IMAGE][self::SUMMARY_FAILED], 687 'failedchecktype3' => $final[$course->courseid][area_base::CHECKGROUP_LAYOUT][self::SUMMARY_FAILED], 688 'failedchecktype4' => $final[$course->courseid][area_base::CHECKGROUP_LINK][self::SUMMARY_FAILED], 689 'failedchecktype5' => $final[$course->courseid][area_base::CHECKGROUP_MEDIA][self::SUMMARY_FAILED], 690 'failedchecktype6' => $final[$course->courseid][area_base::CHECKGROUP_TABLE][self::SUMMARY_FAILED], 691 'failedchecktype7' => $final[$course->courseid][area_base::CHECKGROUP_TEXT][self::SUMMARY_FAILED], 692 'percentchecktype1' => $final[$course->courseid][area_base::CHECKGROUP_FORM][self::SUMMARY_PERCENT], 693 'percentchecktype2' => $final[$course->courseid][area_base::CHECKGROUP_IMAGE][self::SUMMARY_PERCENT], 694 'percentchecktype3' => $final[$course->courseid][area_base::CHECKGROUP_LAYOUT][self::SUMMARY_PERCENT], 695 'percentchecktype4' => $final[$course->courseid][area_base::CHECKGROUP_LINK][self::SUMMARY_PERCENT], 696 'percentchecktype5' => $final[$course->courseid][area_base::CHECKGROUP_MEDIA][self::SUMMARY_PERCENT], 697 'percentchecktype6' => $final[$course->courseid][area_base::CHECKGROUP_TABLE][self::SUMMARY_PERCENT], 698 'percentchecktype7' => $final[$course->courseid][area_base::CHECKGROUP_TEXT][self::SUMMARY_PERCENT] 699 ]; 700 $resultid = $DB->get_field(self::DB_SUMMARY, 'id', ['courseid' => $course->courseid]); 701 if ($resultid) { 702 $element['id'] = $resultid; 703 $DB->update_record(self::DB_SUMMARY, (object)$element); 704 continue; 705 } 706 array_push($values, $element); 707 } 708 709 $DB->insert_records(self::DB_SUMMARY, $values); 710 711 $extrasql = !$courseid ? "WHERE courseid != ?" : "WHERE courseid = ?"; 712 $coursesqlval = !$courseid ? [0] : [$courseid]; 713 // Count of failed errors per check. 714 $checkssql = "SELECT area.courseid, ".self::STATUS_CHECKED." AS status, res.checkid, 715 COUNT(res.errorcount) as checkcount, SUM(res.errorcount) AS errorcount 716 FROM {" . self::DB_AREAS . "} area 717 INNER JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = area.id AND ch.iscurrent = 1 718 INNER JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id 719 ".$extrasql." GROUP BY area.courseid, res.checkid"; 720 721 $checksresult = $DB->get_recordset_sql($checkssql, $coursesqlval); 722 723 $checkvalues = []; 724 foreach ($checksresult as $check) { 725 if ($result = $DB->get_record(self::DB_CACHECHECK, ['courseid' => $check->courseid, 'checkid' => $check->checkid])) { 726 $check->id = $result->id; 727 $DB->update_record(self::DB_CACHECHECK, $check); 728 } else { 729 array_push($checkvalues, (array)$check); 730 } 731 } 732 $DB->insert_records(self::DB_CACHECHECK, $checkvalues); 733 734 // Count of failed or passed rate per activity. 735 $activitysql = "SELECT courseid, ".self::STATUS_CHECKED." AS status, area.component, 736 COUNT(DISTINCT area.contextid) AS totalactivities, 0 AS failedactivities, 737 COUNT(DISTINCT area.contextid) AS passedactivities, 0 AS errorcount 738 FROM {" . self::DB_AREAS . "} area 739 ".$extrasql." 740 GROUP BY area.courseid, area.component"; 741 742 $activityresults = $DB->get_recordset_sql($activitysql, $coursesqlval); 743 744 $activityvalues = []; 745 746 // Count of failed errors per courseid per activity. 747 $activityfailedsql = "SELECT area.courseid, area.component, area.contextid, SUM(res.errorcount) AS errorcount 748 FROM {" . self::DB_AREAS . "} area 749 INNER JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = area.id AND ch.iscurrent = 1 750 INNER JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id 751 ".$extrasql." AND res.errorcount != 0 752 GROUP BY area.courseid, area.component, area.contextid"; 753 754 $activityfailedresults = $DB->get_recordset_sql($activityfailedsql, $coursesqlval); 755 756 foreach ($activityresults as $activity) { 757 $tmpkey = $activity->courseid.$activity->component; 758 $activityvalues[$tmpkey] = $activity; 759 } 760 761 foreach ($activityfailedresults as $failed) { 762 $tmpkey = $failed->courseid.$failed->component; 763 $activityvalues[$tmpkey]->failedactivities ++; 764 $activityvalues[$tmpkey]->passedactivities --; 765 $activityvalues[$tmpkey]->errorcount += $failed->errorcount; 766 } 767 768 $activityvaluespush = []; 769 foreach ($activityvalues as $value) { 770 if ($result = $DB->get_record(self::DB_CACHEACTS, ['courseid' => $value->courseid, 'component' => $value->component])) { 771 $value->id = $result->id; 772 $DB->update_record(self::DB_CACHEACTS, $value); 773 } else { 774 array_push($activityvaluespush, (array)$value); 775 } 776 } 777 778 $DB->insert_records(self::DB_CACHEACTS, $activityvaluespush); 779 780 $recordserrored->close(); 781 $recordsfailed->close(); 782 $checksresult->close(); 783 $activityresults->close(); 784 $activityfailedresults->close(); 785 } 786 787 /** 788 * Get course module summary information for a course. 789 * 790 * @param int $courseid 791 * @return \stdClass[] 792 */ 793 public static function get_cm_summary_for_course(int $courseid): array { 794 global $DB; 795 796 $sql = " 797 SELECT 798 area.cmid, 799 sum(errorcount) as numerrors, 800 count(errorcount) as numchecks 801 FROM {" . self::DB_AREAS . "} area 802 JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = area.id AND ch.iscurrent = 1 803 JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id 804 WHERE area.courseid = :courseid AND component != :component 805 GROUP BY cmid"; 806 807 $params = [ 808 'courseid' => $courseid, 809 'component' => 'core_course', 810 ]; 811 812 return $DB->get_records_sql($sql, $params); 813 } 814 815 /** 816 * Get section summary information for a course. 817 * 818 * @param int $courseid 819 * @return stdClass[] 820 */ 821 public static function get_section_summary_for_course(int $courseid): array { 822 global $DB; 823 824 $sql = " 825 SELECT 826 sec.section, 827 sum(errorcount) AS numerrors, 828 count(errorcount) as numchecks 829 FROM {" . self::DB_AREAS . "} area 830 JOIN {" . self::DB_CONTENT . "} ch ON ch.areaid = area.id AND ch.iscurrent = 1 831 JOIN {" . self::DB_RESULTS . "} res ON res.contentid = ch.id 832 JOIN {course_sections} sec ON area.itemid = sec.id 833 WHERE area.tablename = :tablename AND area.courseid = :courseid 834 GROUP BY sec.section"; 835 836 $params = [ 837 'courseid' => $courseid, 838 'tablename' => 'course_sections' 839 ]; 840 841 return $DB->get_records_sql($sql, $params); 842 } 843 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body