Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 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 * External completion functions unit tests 19 * 20 * @package core_completion 21 * @category external 22 * @copyright 2015 Juan Leyva <juan@moodle.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 * @since Moodle 2.9 25 */ 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 global $CFG; 30 31 require_once($CFG->dirroot . '/webservice/tests/helpers.php'); 32 33 /** 34 * External completion functions unit tests 35 * 36 * @package core_completion 37 * @category external 38 * @copyright 2015 Juan Leyva <juan@moodle.com> 39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 40 * @since Moodle 2.9 41 */ 42 class core_completion_externallib_testcase extends externallib_advanced_testcase { 43 44 /** 45 * Test update_activity_completion_status_manually 46 */ 47 public function test_update_activity_completion_status_manually() { 48 global $DB, $CFG; 49 50 $this->resetAfterTest(true); 51 52 $CFG->enablecompletion = true; 53 $user = $this->getDataGenerator()->create_user(); 54 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 55 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), 56 array('completion' => 1)); 57 $cm = get_coursemodule_from_id('data', $data->cmid); 58 59 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 60 $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); 61 62 $this->setUser($user); 63 64 $result = core_completion_external::update_activity_completion_status_manually($data->cmid, true); 65 // We need to execute the return values cleaning process to simulate the web service server. 66 $result = external_api::clean_returnvalue( 67 core_completion_external::update_activity_completion_status_manually_returns(), $result); 68 69 // Check in DB. 70 $this->assertEquals(1, $DB->get_field('course_modules_completion', 'completionstate', 71 array('coursemoduleid' => $data->cmid))); 72 73 // Check using the API. 74 $completion = new completion_info($course); 75 $completiondata = $completion->get_data($cm); 76 $this->assertEquals(1, $completiondata->completionstate); 77 $this->assertTrue($result['status']); 78 79 $result = core_completion_external::update_activity_completion_status_manually($data->cmid, false); 80 // We need to execute the return values cleaning process to simulate the web service server. 81 $result = external_api::clean_returnvalue( 82 core_completion_external::update_activity_completion_status_manually_returns(), $result); 83 84 $this->assertEquals(0, $DB->get_field('course_modules_completion', 'completionstate', 85 array('coursemoduleid' => $data->cmid))); 86 $completiondata = $completion->get_data($cm); 87 $this->assertEquals(0, $completiondata->completionstate); 88 $this->assertTrue($result['status']); 89 } 90 91 /** 92 * Test update_activity_completion_status 93 */ 94 public function test_get_activities_completion_status() { 95 global $DB, $CFG; 96 97 $this->resetAfterTest(true); 98 99 $CFG->enablecompletion = true; 100 $student = $this->getDataGenerator()->create_user(); 101 $teacher = $this->getDataGenerator()->create_user(); 102 103 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1, 104 'groupmode' => SEPARATEGROUPS, 105 'groupmodeforce' => 1)); 106 availability_completion\condition::wipe_static_cache(); 107 108 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), 109 array('completion' => 1)); 110 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), 111 array('completion' => 1)); 112 $availability = '{"op":"&","c":[{"type":"completion","cm":' . $forum->cmid .',"e":1}],"showc":[true]}'; 113 $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['availability' => $availability]); 114 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), 115 array('completion' => 1, 'visible' => 0)); 116 117 $cmdata = get_coursemodule_from_id('data', $data->cmid); 118 $cmforum = get_coursemodule_from_id('forum', $forum->cmid); 119 120 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 121 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 122 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 123 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 124 125 $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); 126 $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); 127 128 // Teacher and student in different groups initially. 129 groups_add_member($group1->id, $student->id); 130 groups_add_member($group2->id, $teacher->id); 131 132 $this->setUser($student); 133 // Forum complete. 134 $completion = new completion_info($course); 135 $completion->update_state($cmforum, COMPLETION_COMPLETE); 136 137 $result = core_completion_external::get_activities_completion_status($course->id, $student->id); 138 // We need to execute the return values cleaning process to simulate the web service server. 139 $result = external_api::clean_returnvalue( 140 core_completion_external::get_activities_completion_status_returns(), $result); 141 142 // We added 4 activities, but only 3 with completion enabled and one of those is hidden. 143 $this->assertCount(2, $result['statuses']); 144 145 $activitiesfound = 0; 146 foreach ($result['statuses'] as $status) { 147 if ($status['cmid'] == $forum->cmid and $status['modname'] == 'forum' and $status['instance'] == $forum->id) { 148 $activitiesfound++; 149 $this->assertEquals(COMPLETION_COMPLETE, $status['state']); 150 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); 151 $this->assertTrue($status['valueused']); 152 } else if ($status['cmid'] == $data->cmid and $status['modname'] == 'data' and $status['instance'] == $data->id) { 153 $activitiesfound++; 154 $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); 155 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); 156 $this->assertFalse($status['valueused']); 157 } 158 } 159 $this->assertEquals(2, $activitiesfound); 160 161 // Teacher should see students status, they are in different groups but the teacher can access all groups. 162 $this->setUser($teacher); 163 $result = core_completion_external::get_activities_completion_status($course->id, $student->id); 164 // We need to execute the return values cleaning process to simulate the web service server. 165 $result = external_api::clean_returnvalue( 166 core_completion_external::get_activities_completion_status_returns(), $result); 167 168 // We added 4 activities, but only 3 with completion enabled and one of those is hidden. 169 $this->assertCount(3, $result['statuses']); 170 171 // Override status by teacher. 172 $completion->update_state($cmforum, COMPLETION_INCOMPLETE, $student->id, true); 173 174 $result = core_completion_external::get_activities_completion_status($course->id, $student->id); 175 // We need to execute the return values cleaning process to simulate the web service server. 176 $result = external_api::clean_returnvalue( 177 core_completion_external::get_activities_completion_status_returns(), $result); 178 179 // Check forum has been overriden by the teacher. 180 foreach ($result['statuses'] as $status) { 181 if ($status['cmid'] == $forum->cmid) { 182 $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); 183 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); 184 $this->assertEquals($teacher->id, $status['overrideby']); 185 break; 186 } 187 } 188 189 // Teacher should see his own completion status. 190 191 // Forum complete for teacher. 192 $completion = new completion_info($course); 193 $completion->update_state($cmforum, COMPLETION_COMPLETE); 194 195 $result = core_completion_external::get_activities_completion_status($course->id, $teacher->id); 196 // We need to execute the return values cleaning process to simulate the web service server. 197 $result = external_api::clean_returnvalue( 198 core_completion_external::get_activities_completion_status_returns(), $result); 199 200 // We added 4 activities, but only 3 with completion enabled (one of those is hidden but the teacher can see it). 201 $this->assertCount(3, $result['statuses']); 202 203 $activitiesfound = 0; 204 foreach ($result['statuses'] as $status) { 205 if ($status['cmid'] == $forum->cmid and $status['modname'] == 'forum' and $status['instance'] == $forum->id) { 206 $activitiesfound++; 207 $this->assertEquals(COMPLETION_COMPLETE, $status['state']); 208 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); 209 } else { 210 $activitiesfound++; 211 $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); 212 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); 213 } 214 } 215 $this->assertEquals(3, $activitiesfound); 216 217 // Change teacher role capabilities (disable access all groups). 218 $context = context_course::instance($course->id); 219 assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $teacherrole->id, $context); 220 accesslib_clear_all_caches_for_unit_testing(); 221 222 try { 223 $result = core_completion_external::get_activities_completion_status($course->id, $student->id); 224 $this->fail('Exception expected due to groups permissions.'); 225 } catch (moodle_exception $e) { 226 $this->assertEquals('accessdenied', $e->errorcode); 227 } 228 229 // Now add the teacher in the same group. 230 groups_add_member($group1->id, $teacher->id); 231 $result = core_completion_external::get_activities_completion_status($course->id, $student->id); 232 // We need to execute the return values cleaning process to simulate the web service server. 233 $result = external_api::clean_returnvalue( 234 core_completion_external::get_activities_completion_status_returns(), $result); 235 // We added 4 activities, but only 3 with completion enabled and one of those is hidden. 236 $this->assertCount(3, $result['statuses']); 237 } 238 239 /** 240 * Test override_activity_completion_status 241 */ 242 public function test_override_activity_completion_status() { 243 global $DB, $CFG; 244 $this->resetAfterTest(true); 245 246 // Create course with teacher and student enrolled. 247 $CFG->enablecompletion = true; 248 $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); 249 $student = $this->getDataGenerator()->create_user(); 250 $teacher = $this->getDataGenerator()->create_user(); 251 $studentrole = $DB->get_record('role', ['shortname' => 'student']); 252 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 253 $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); 254 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 255 256 // Create 2 activities, one with manual completion (data), one with automatic completion triggered by viewing it (forum). 257 $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id], ['completion' => 1]); 258 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id], 259 ['completion' => 2, 'completionview' => 1]); 260 $cmdata = get_coursemodule_from_id('data', $data->cmid); 261 $cmforum = get_coursemodule_from_id('forum', $forum->cmid); 262 263 // Manually complete the data activity as the student. 264 $this->setUser($student); 265 $completion = new completion_info($course); 266 $completion->update_state($cmdata, COMPLETION_COMPLETE); 267 268 // Test overriding the status of the manual-completion-activity 'incomplete'. 269 $this->setUser($teacher); 270 $result = core_completion_external::override_activity_completion_status($student->id, $data->cmid, COMPLETION_INCOMPLETE); 271 $result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result); 272 $this->assertEquals($result['state'], COMPLETION_INCOMPLETE); 273 $completiondata = $completion->get_data($cmdata, false, $student->id); 274 $this->assertEquals(COMPLETION_INCOMPLETE, $completiondata->completionstate); 275 276 // Test overriding the status of the manual-completion-activity back to 'complete'. 277 $result = core_completion_external::override_activity_completion_status($student->id, $data->cmid, COMPLETION_COMPLETE); 278 $result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result); 279 $this->assertEquals($result['state'], COMPLETION_COMPLETE); 280 $completiondata = $completion->get_data($cmdata, false, $student->id); 281 $this->assertEquals(COMPLETION_COMPLETE, $completiondata->completionstate); 282 283 // Test overriding the status of the auto-completion-activity to 'complete'. 284 $result = core_completion_external::override_activity_completion_status($student->id, $forum->cmid, COMPLETION_COMPLETE); 285 $result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result); 286 $this->assertEquals($result['state'], COMPLETION_COMPLETE); 287 $completionforum = $completion->get_data($cmforum, false, $student->id); 288 $this->assertEquals(COMPLETION_COMPLETE, $completionforum->completionstate); 289 290 // Test overriding the status of the auto-completion-activity to 'incomplete'. 291 $result = core_completion_external::override_activity_completion_status($student->id, $forum->cmid, COMPLETION_INCOMPLETE); 292 $result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result); 293 $this->assertEquals($result['state'], COMPLETION_INCOMPLETE); 294 $completionforum = $completion->get_data($cmforum, false, $student->id); 295 $this->assertEquals(COMPLETION_INCOMPLETE, $completionforum->completionstate); 296 297 // Test overriding the status of the auto-completion-activity to an invalid state. 298 $this->expectException('moodle_exception'); 299 core_completion_external::override_activity_completion_status($student->id, $forum->cmid, 3); 300 } 301 302 /** 303 * Test overriding the activity completion status as a user without the capability to do so. 304 */ 305 public function test_override_status_user_without_capability() { 306 global $DB, $CFG; 307 $this->resetAfterTest(true); 308 309 // Create course with teacher and student enrolled. 310 $CFG->enablecompletion = true; 311 $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); 312 $student = $this->getDataGenerator()->create_user(); 313 $teacher = $this->getDataGenerator()->create_user(); 314 $studentrole = $DB->get_record('role', ['shortname' => 'student']); 315 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 316 $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); 317 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 318 $coursecontext = context_course::instance($course->id); 319 320 // Create an activity with automatic completion (a forum). 321 $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id], 322 ['completion' => 2, 'completionview' => 1]); 323 324 // Test overriding the status of the activity for a user without the capability. 325 $this->setUser($teacher); 326 assign_capability('moodle/course:overridecompletion', CAP_PREVENT, $teacherrole->id, $coursecontext); 327 $this->expectException('required_capability_exception'); 328 core_completion_external::override_activity_completion_status($student->id, $forum->cmid, COMPLETION_COMPLETE); 329 } 330 331 /** 332 * Test get_course_completion_status 333 */ 334 public function test_get_course_completion_status() { 335 global $DB, $CFG, $COMPLETION_CRITERIA_TYPES; 336 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php'); 337 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php'); 338 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_unenrol.php'); 339 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php'); 340 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_duration.php'); 341 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_grade.php'); 342 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_role.php'); 343 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_course.php'); 344 345 $this->resetAfterTest(true); 346 347 $CFG->enablecompletion = true; 348 $student = $this->getDataGenerator()->create_user(); 349 $teacher = $this->getDataGenerator()->create_user(); 350 351 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1, 352 'groupmode' => SEPARATEGROUPS, 353 'groupmodeforce' => 1)); 354 355 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), 356 array('completion' => 1)); 357 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), 358 array('completion' => 1)); 359 $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); 360 361 $cmdata = get_coursemodule_from_id('data', $data->cmid); 362 $cmforum = get_coursemodule_from_id('forum', $forum->cmid); 363 364 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 365 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); 366 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 367 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); 368 369 $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); 370 $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); 371 // Teacher and student in different groups initially. 372 groups_add_member($group1->id, $student->id); 373 groups_add_member($group2->id, $teacher->id); 374 375 // Set completion rules. 376 $completion = new completion_info($course); 377 378 // Loop through each criteria type and run its update_config() method. 379 380 $criteriadata = new stdClass(); 381 $criteriadata->id = $course->id; 382 $criteriadata->criteria_activity = array(); 383 // Some activities. 384 $criteriadata->criteria_activity[$cmdata->id] = 1; 385 $criteriadata->criteria_activity[$cmforum->id] = 1; 386 387 // In a week criteria date value. 388 $criteriadata->criteria_date_value = time() + WEEKSECS; 389 390 // Self completion. 391 $criteriadata->criteria_self = 1; 392 393 foreach ($COMPLETION_CRITERIA_TYPES as $type) { 394 $class = 'completion_criteria_'.$type; 395 $criterion = new $class(); 396 $criterion->update_config($criteriadata); 397 } 398 399 // Handle overall aggregation. 400 $aggdata = array( 401 'course' => $course->id, 402 'criteriatype' => null 403 ); 404 $aggregation = new completion_aggregation($aggdata); 405 $aggregation->setMethod(COMPLETION_AGGREGATION_ALL); 406 $aggregation->save(); 407 408 $aggdata['criteriatype'] = COMPLETION_CRITERIA_TYPE_ACTIVITY; 409 $aggregation = new completion_aggregation($aggdata); 410 $aggregation->setMethod(COMPLETION_AGGREGATION_ALL); 411 $aggregation->save(); 412 413 $this->setUser($student); 414 415 $result = core_completion_external::get_course_completion_status($course->id, $student->id); 416 // We need to execute the return values cleaning process to simulate the web service server. 417 $studentresult = external_api::clean_returnvalue( 418 core_completion_external::get_course_completion_status_returns(), $result); 419 420 // 3 different criteria. 421 $this->assertCount(3, $studentresult['completionstatus']['completions']); 422 423 $this->assertEquals(COMPLETION_AGGREGATION_ALL, $studentresult['completionstatus']['aggregation']); 424 $this->assertFalse($studentresult['completionstatus']['completed']); 425 426 $this->assertEquals('No', $studentresult['completionstatus']['completions'][0]['status']); 427 $this->assertEquals('No', $studentresult['completionstatus']['completions'][1]['status']); 428 $this->assertEquals('No', $studentresult['completionstatus']['completions'][2]['status']); 429 430 // Teacher should see students status, they are in different groups but the teacher can access all groups. 431 $this->setUser($teacher); 432 $result = core_completion_external::get_course_completion_status($course->id, $student->id); 433 // We need to execute the return values cleaning process to simulate the web service server. 434 $teacherresult = external_api::clean_returnvalue( 435 core_completion_external::get_course_completion_status_returns(), $result); 436 437 $this->assertEquals($studentresult, $teacherresult); 438 439 // Change teacher role capabilities (disable access al goups). 440 $context = context_course::instance($course->id); 441 assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $teacherrole->id, $context); 442 accesslib_clear_all_caches_for_unit_testing(); 443 444 try { 445 $result = core_completion_external::get_course_completion_status($course->id, $student->id); 446 $this->fail('Exception expected due to groups permissions.'); 447 } catch (moodle_exception $e) { 448 $this->assertEquals('accessdenied', $e->errorcode); 449 } 450 451 // Now add the teacher in the same group. 452 groups_add_member($group1->id, $teacher->id); 453 $result = core_completion_external::get_course_completion_status($course->id, $student->id); 454 // We need to execute the return values cleaning process to simulate the web service server. 455 $teacherresult = external_api::clean_returnvalue( 456 core_completion_external::get_course_completion_status_returns(), $result); 457 458 $this->assertEquals($studentresult, $teacherresult); 459 460 } 461 462 /** 463 * Test mark_course_self_completed 464 */ 465 public function test_mark_course_self_completed() { 466 global $DB, $CFG; 467 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php'); 468 469 $this->resetAfterTest(true); 470 471 $CFG->enablecompletion = true; 472 $student = $this->getDataGenerator()->create_user(); 473 $teacher = $this->getDataGenerator()->create_user(); 474 475 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); 476 477 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 478 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 479 480 // Set completion rules. 481 $completion = new completion_info($course); 482 483 $criteriadata = new stdClass(); 484 $criteriadata->id = $course->id; 485 $criteriadata->criteria_activity = array(); 486 487 // Self completion. 488 $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF; 489 $class = 'completion_criteria_self'; 490 $criterion = new $class(); 491 $criterion->update_config($criteriadata); 492 493 // Handle overall aggregation. 494 $aggdata = array( 495 'course' => $course->id, 496 'criteriatype' => null 497 ); 498 $aggregation = new completion_aggregation($aggdata); 499 $aggregation->setMethod(COMPLETION_AGGREGATION_ALL); 500 $aggregation->save(); 501 502 $this->setUser($student); 503 504 $result = core_completion_external::mark_course_self_completed($course->id); 505 // We need to execute the return values cleaning process to simulate the web service server. 506 $result = external_api::clean_returnvalue( 507 core_completion_external::mark_course_self_completed_returns(), $result); 508 509 // We expect a valid result. 510 $this->assertEquals(true, $result['status']); 511 512 $result = core_completion_external::get_course_completion_status($course->id, $student->id); 513 // We need to execute the return values cleaning process to simulate the web service server. 514 $result = external_api::clean_returnvalue( 515 core_completion_external::get_course_completion_status_returns(), $result); 516 517 // Course must be completed. 518 $this->assertEquals(COMPLETION_COMPLETE, $result['completionstatus']['completions'][0]['complete']); 519 520 try { 521 $result = core_completion_external::mark_course_self_completed($course->id); 522 $this->fail('Exception expected due course already self completed.'); 523 } catch (moodle_exception $e) { 524 $this->assertEquals('useralreadymarkedcomplete', $e->errorcode); 525 } 526 527 } 528 529 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body