See Release Notes
Long Term Support Release
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 * A form for cohort upload. 19 * 20 * @package core_cohort 21 * @copyright 2014 Marina Glancy 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 require_once($CFG->libdir.'/formslib.php'); 28 29 /** 30 * Cohort upload form class 31 * 32 * @package core_cohort 33 * @copyright 2014 Marina Glancy 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 class cohort_upload_form extends moodleform { 37 /** @var array new cohorts that need to be created */ 38 public $processeddata = null; 39 /** @var array cached list of available contexts */ 40 protected $contextoptions = null; 41 /** @var array temporary cache for retrieved categories */ 42 protected $categoriescache = array(); 43 44 /** 45 * Form definition 46 */ 47 public function definition() { 48 $mform = $this->_form; 49 $data = (object)$this->_customdata; 50 51 $mform->addElement('header', 'cohortfileuploadform', get_string('uploadafile')); 52 53 $filepickeroptions = array(); 54 $filepickeroptions['filetypes'] = '*'; 55 $filepickeroptions['maxbytes'] = get_max_upload_file_size(); 56 $mform->addElement('filepicker', 'cohortfile', get_string('file'), null, $filepickeroptions); 57 58 $choices = csv_import_reader::get_delimiter_list(); 59 $mform->addElement('select', 'delimiter', get_string('csvdelimiter', 'tool_uploadcourse'), $choices); 60 if (array_key_exists('cfg', $choices)) { 61 $mform->setDefault('delimiter', 'cfg'); 62 } else if (get_string('listsep', 'langconfig') == ';') { 63 $mform->setDefault('delimiter', 'semicolon'); 64 } else { 65 $mform->setDefault('delimiter', 'comma'); 66 } 67 $mform->addHelpButton('delimiter', 'csvdelimiter', 'tool_uploadcourse'); 68 69 $choices = core_text::get_encodings(); 70 $mform->addElement('select', 'encoding', get_string('encoding', 'tool_uploadcourse'), $choices); 71 $mform->setDefault('encoding', 'UTF-8'); 72 $mform->addHelpButton('encoding', 'encoding', 'tool_uploadcourse'); 73 74 $options = $this->get_context_options(); 75 $mform->addElement('select', 'contextid', get_string('defaultcontext', 'cohort'), $options); 76 77 $this->add_cohort_upload_buttons(true); 78 $this->set_data($data); 79 } 80 81 /** 82 * Add buttons to the form ("Upload cohorts", "Preview", "Cancel") 83 */ 84 protected function add_cohort_upload_buttons() { 85 $mform = $this->_form; 86 87 $buttonarray = array(); 88 89 $submitlabel = get_string('uploadcohorts', 'cohort'); 90 $buttonarray[] = $mform->createElement('submit', 'submitbutton', $submitlabel); 91 92 $previewlabel = get_string('preview', 'cohort'); 93 $buttonarray[] = $mform->createElement('submit', 'previewbutton', $previewlabel); 94 $mform->registerNoSubmitButton('previewbutton'); 95 96 $buttonarray[] = $mform->createElement('cancel'); 97 98 $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); 99 $mform->closeHeaderBefore('buttonar'); 100 } 101 102 /** 103 * Process the uploaded file and allow the submit button only if it doest not have errors. 104 */ 105 public function definition_after_data() { 106 $mform = $this->_form; 107 $cohortfile = $mform->getElementValue('cohortfile'); 108 $allowsubmitform = false; 109 if ($cohortfile && ($file = $this->get_cohort_file($cohortfile))) { 110 // File was uploaded. Parse it. 111 $encoding = $mform->getElementValue('encoding')[0]; 112 $delimiter = $mform->getElementValue('delimiter')[0]; 113 $contextid = $mform->getElementValue('contextid')[0]; 114 if (!empty($contextid) && ($context = context::instance_by_id($contextid, IGNORE_MISSING))) { 115 $this->processeddata = $this->process_upload_file($file, $encoding, $delimiter, $context); 116 if ($this->processeddata && count($this->processeddata) > 1 && !$this->processeddata[0]['errors']) { 117 $allowsubmitform = true; 118 } 119 } 120 } 121 if (!$allowsubmitform) { 122 // Hide submit button. 123 $el = $mform->getElement('buttonar')->getElements()[0]; 124 $el->setValue(''); 125 $el->freeze(); 126 } else { 127 $mform->setExpanded('cohortfileuploadform', false); 128 } 129 130 } 131 132 /** 133 * Returns the list of contexts where current user can create cohorts. 134 * 135 * @return array 136 */ 137 protected function get_context_options() { 138 if ($this->contextoptions === null) { 139 $this->contextoptions = array(); 140 $displaylist = core_course_category::make_categories_list('moodle/cohort:manage'); 141 // We need to index the options array by context id instead of category id and add option for system context. 142 $syscontext = context_system::instance(); 143 if (has_capability('moodle/cohort:manage', $syscontext)) { 144 $this->contextoptions[$syscontext->id] = $syscontext->get_context_name(); 145 } 146 foreach ($displaylist as $cid => $name) { 147 $context = context_coursecat::instance($cid); 148 $this->contextoptions[$context->id] = $name; 149 } 150 } 151 return $this->contextoptions; 152 } 153 154 public function validation($data, $files) { 155 $errors = parent::validation($data, $files); 156 if (empty($errors)) { 157 if (empty($data['cohortfile']) || !($file = $this->get_cohort_file($data['cohortfile']))) { 158 $errors['cohortfile'] = get_string('required'); 159 } else { 160 if (!empty($this->processeddata[0]['errors'])) { 161 // Any value in $errors will notify that validation did not pass. The detailed errors will be shown in preview. 162 $errors['dummy'] = ''; 163 } 164 } 165 } 166 return $errors; 167 } 168 169 /** 170 * Returns the uploaded file if it is present. 171 * 172 * @param int $draftid 173 * @return stored_file|null 174 */ 175 protected function get_cohort_file($draftid) { 176 global $USER; 177 // We can not use moodleform::get_file_content() method because we need the content before the form is validated. 178 if (!$draftid) { 179 return null; 180 } 181 $fs = get_file_storage(); 182 $context = context_user::instance($USER->id); 183 if (!$files = $fs->get_area_files($context->id, 'user', 'draft', $draftid, 'id DESC', false)) { 184 return null; 185 } 186 $file = reset($files); 187 188 return $file; 189 190 } 191 192 /** 193 * Returns the list of prepared objects to be added as cohorts 194 * 195 * @return array of stdClass objects, each can be passed to {@link cohort_add_cohort()} 196 */ 197 public function get_cohorts_data() { 198 $cohorts = array(); 199 if ($this->processeddata) { 200 foreach ($this->processeddata as $idx => $line) { 201 if ($idx && !empty($line['data'])) { 202 $cohorts[] = (object)$line['data']; 203 } 204 } 205 } 206 return $cohorts; 207 } 208 209 /** 210 * Displays the preview of the uploaded file 211 */ 212 protected function preview_uploaded_cohorts() { 213 global $OUTPUT; 214 if (empty($this->processeddata)) { 215 return; 216 } 217 foreach ($this->processeddata[0]['errors'] as $error) { 218 echo $OUTPUT->notification($error); 219 } 220 foreach ($this->processeddata[0]['warnings'] as $warning) { 221 echo $OUTPUT->notification($warning, 'notifymessage'); 222 } 223 $table = new html_table(); 224 $table->id = 'previewuploadedcohorts'; 225 $columns = $this->processeddata[0]['data']; 226 $columns['contextid'] = get_string('context', 'role'); 227 228 // Add column names to the preview table. 229 $table->head = array(''); 230 foreach ($columns as $key => $value) { 231 $table->head[] = $value; 232 } 233 $table->head[] = get_string('status'); 234 235 // Add (some) rows to the preview table. 236 $previewdrows = $this->get_previewed_rows(); 237 foreach ($previewdrows as $idx) { 238 $line = $this->processeddata[$idx]; 239 $cells = array(new html_table_cell($idx)); 240 $context = context::instance_by_id($line['data']['contextid']); 241 foreach ($columns as $key => $value) { 242 if ($key === 'contextid') { 243 $text = html_writer::link(new moodle_url('/cohort/index.php', array('contextid' => $context->id)), 244 $context->get_context_name(false)); 245 } else { 246 $text = s($line['data'][$key]); 247 } 248 $cells[] = new html_table_cell($text); 249 } 250 $text = ''; 251 if ($line['errors']) { 252 $text .= html_writer::div(join('<br>', $line['errors']), 'notifyproblem'); 253 } 254 if ($line['warnings']) { 255 $text .= html_writer::div(join('<br>', $line['warnings'])); 256 } 257 $cells[] = new html_table_cell($text); 258 $table->data[] = new html_table_row($cells); 259 } 260 if ($notdisplayed = count($this->processeddata) - count($previewdrows) - 1) { 261 $cell = new html_table_cell(get_string('displayedrows', 'cohort', 262 (object)array('displayed' => count($previewdrows), 'total' => count($this->processeddata) - 1))); 263 $cell->colspan = count($columns) + 2; 264 $table->data[] = new html_table_row(array($cell)); 265 } 266 echo html_writer::table($table); 267 } 268 269 /** 270 * Find up rows to show in preview 271 * 272 * Number of previewed rows is limited but rows with errors and warnings have priority. 273 * 274 * @return array 275 */ 276 protected function get_previewed_rows() { 277 $previewlimit = 10; 278 if (count($this->processeddata) <= 1) { 279 $rows = array(); 280 } else if (count($this->processeddata) < $previewlimit + 1) { 281 // Return all rows. 282 $rows = range(1, count($this->processeddata) - 1); 283 } else { 284 // First find rows with errors and warnings (no more than 10 of each). 285 $errorrows = $warningrows = array(); 286 foreach ($this->processeddata as $rownum => $line) { 287 if ($rownum && $line['errors']) { 288 $errorrows[] = $rownum; 289 if (count($errorrows) >= $previewlimit) { 290 return $errorrows; 291 } 292 } else if ($rownum && $line['warnings']) { 293 if (count($warningrows) + count($errorrows) < $previewlimit) { 294 $warningrows[] = $rownum; 295 } 296 } 297 } 298 // Include as many error rows as possible and top them up with warning rows. 299 $rows = array_merge($errorrows, array_slice($warningrows, 0, $previewlimit - count($errorrows))); 300 // Keep adding good rows until we reach limit. 301 for ($rownum = 1; count($rows) < $previewlimit; $rownum++) { 302 if (!in_array($rownum, $rows)) { 303 $rows[] = $rownum; 304 } 305 } 306 asort($rows); 307 } 308 return $rows; 309 } 310 311 public function display() { 312 // Finalize the form definition if not yet done. 313 if (!$this->_definition_finalized) { 314 $this->_definition_finalized = true; 315 $this->definition_after_data(); 316 } 317 318 // Difference from the parent display() method is that we want to show preview above the form if applicable. 319 $this->preview_uploaded_cohorts(); 320 321 $this->_form->display(); 322 } 323 324 /** 325 * @param stored_file $file 326 * @param string $encoding 327 * @param string $delimiter 328 * @param context $defaultcontext 329 * @return array 330 */ 331 protected function process_upload_file($file, $encoding, $delimiter, $defaultcontext) { 332 global $CFG, $DB; 333 require_once($CFG->libdir . '/csvlib.class.php'); 334 335 $cohorts = array( 336 0 => array('errors' => array(), 'warnings' => array(), 'data' => array()) 337 ); 338 339 // Read and parse the CSV file using csv library. 340 $content = $file->get_content(); 341 if (!$content) { 342 $cohorts[0]['errors'][] = new lang_string('csvemptyfile', 'error'); 343 return $cohorts; 344 } 345 346 $uploadid = csv_import_reader::get_new_iid('uploadcohort'); 347 $cir = new csv_import_reader($uploadid, 'uploadcohort'); 348 $readcount = $cir->load_csv_content($content, $encoding, $delimiter); 349 unset($content); 350 if (!$readcount) { 351 $cohorts[0]['errors'][] = get_string('csvloaderror', 'error', $cir->get_error()); 352 return $cohorts; 353 } 354 $columns = $cir->get_columns(); 355 356 // Check that columns include 'name' and warn about extra columns. 357 $allowedcolumns = array('contextid', 'name', 'idnumber', 'description', 'descriptionformat', 'visible', 'theme'); 358 $additionalcolumns = array('context', 'category', 'category_id', 'category_idnumber', 'category_path'); 359 $displaycolumns = array(); 360 $extracolumns = array(); 361 $columnsmapping = array(); 362 foreach ($columns as $i => $columnname) { 363 $columnnamelower = preg_replace('/ /', '', core_text::strtolower($columnname)); 364 $columnsmapping[$i] = null; 365 if (in_array($columnnamelower, $allowedcolumns)) { 366 $displaycolumns[$columnnamelower] = $columnname; 367 $columnsmapping[$i] = $columnnamelower; 368 } else if (in_array($columnnamelower, $additionalcolumns)) { 369 $columnsmapping[$i] = $columnnamelower; 370 } else { 371 $extracolumns[] = $columnname; 372 } 373 } 374 if (!in_array('name', $columnsmapping)) { 375 $cohorts[0]['errors'][] = new lang_string('namecolumnmissing', 'cohort'); 376 return $cohorts; 377 } 378 if ($extracolumns) { 379 $cohorts[0]['warnings'][] = new lang_string('csvextracolumns', 'cohort', s(join(', ', $extracolumns))); 380 } 381 382 if (!isset($displaycolumns['contextid'])) { 383 $displaycolumns['contextid'] = 'contextid'; 384 } 385 $cohorts[0]['data'] = $displaycolumns; 386 387 // Parse data rows. 388 $cir->init(); 389 $rownum = 0; 390 $idnumbers = array(); 391 $haserrors = false; 392 $haswarnings = false; 393 while ($row = $cir->next()) { 394 $rownum++; 395 $cohorts[$rownum] = array( 396 'errors' => array(), 397 'warnings' => array(), 398 'data' => array(), 399 ); 400 $hash = array(); 401 foreach ($row as $i => $value) { 402 if ($columnsmapping[$i]) { 403 $hash[$columnsmapping[$i]] = $value; 404 } 405 } 406 $this->clean_cohort_data($hash); 407 408 $warnings = $this->resolve_context($hash, $defaultcontext); 409 $cohorts[$rownum]['warnings'] = array_merge($cohorts[$rownum]['warnings'], $warnings); 410 411 if (!empty($hash['idnumber'])) { 412 if (isset($idnumbers[$hash['idnumber']]) || $DB->record_exists('cohort', array('idnumber' => $hash['idnumber']))) { 413 $cohorts[$rownum]['errors'][] = new lang_string('duplicateidnumber', 'cohort'); 414 } 415 $idnumbers[$hash['idnumber']] = true; 416 } 417 418 if (empty($hash['name'])) { 419 $cohorts[$rownum]['errors'][] = new lang_string('namefieldempty', 'cohort'); 420 } 421 422 if (!empty($hash['theme']) && !empty($CFG->allowcohortthemes)) { 423 $availablethemes = cohort_get_list_of_themes(); 424 if (empty($availablethemes[$hash['theme']])) { 425 $cohorts[$rownum]['errors'][] = new lang_string('invalidtheme', 'cohort'); 426 } 427 } 428 429 $cohorts[$rownum]['data'] = array_intersect_key($hash, $cohorts[0]['data']); 430 $haserrors = $haserrors || !empty($cohorts[$rownum]['errors']); 431 $haswarnings = $haswarnings || !empty($cohorts[$rownum]['warnings']); 432 } 433 434 if ($haserrors) { 435 $cohorts[0]['errors'][] = new lang_string('csvcontainserrors', 'cohort'); 436 } 437 438 if ($haswarnings) { 439 $cohorts[0]['warnings'][] = new lang_string('csvcontainswarnings', 'cohort'); 440 } 441 442 return $cohorts; 443 } 444 445 /** 446 * Cleans input data about one cohort. 447 * 448 * @param array $hash 449 */ 450 protected function clean_cohort_data(&$hash) { 451 foreach ($hash as $key => $value) { 452 switch ($key) { 453 case 'contextid': $hash[$key] = clean_param($value, PARAM_INT); break; 454 case 'name': $hash[$key] = core_text::substr(clean_param($value, PARAM_TEXT), 0, 254); break; 455 case 'idnumber': $hash[$key] = core_text::substr(clean_param($value, PARAM_RAW), 0, 254); break; 456 case 'description': $hash[$key] = clean_param($value, PARAM_RAW); break; 457 case 'descriptionformat': $hash[$key] = clean_param($value, PARAM_INT); break; 458 case 'visible': 459 $tempstr = trim(core_text::strtolower($value)); 460 if ($tempstr === '') { 461 // Empty string is treated as "YES" (the default value for cohort visibility). 462 $hash[$key] = 1; 463 } else { 464 if ($tempstr === core_text::strtolower(get_string('no')) || $tempstr === 'n') { 465 // Special treatment for 'no' string that is not included in clean_param(). 466 $value = 0; 467 } 468 $hash[$key] = clean_param($value, PARAM_BOOL) ? 1 : 0; 469 } 470 break; 471 case 'theme': 472 $hash[$key] = core_text::substr(clean_param($value, PARAM_TEXT), 0, 50); 473 break; 474 } 475 } 476 } 477 478 /** 479 * Determines in which context the particular cohort will be created 480 * 481 * @param array $hash 482 * @param context $defaultcontext 483 * @return array array of warning strings 484 */ 485 protected function resolve_context(&$hash, $defaultcontext) { 486 global $DB; 487 488 $warnings = array(); 489 490 if (!empty($hash['contextid'])) { 491 // Contextid was specified, verify we can post there. 492 $contextoptions = $this->get_context_options(); 493 if (!isset($contextoptions[$hash['contextid']])) { 494 $warnings[] = new lang_string('contextnotfound', 'cohort', $hash['contextid']); 495 $hash['contextid'] = $defaultcontext->id; 496 } 497 return $warnings; 498 } 499 500 if (!empty($hash['context'])) { 501 $systemcontext = context_system::instance(); 502 if ((core_text::strtolower(trim($hash['context'])) === 503 core_text::strtolower($systemcontext->get_context_name())) || 504 ('' . $hash['context'] === '' . $systemcontext->id)) { 505 // User meant system context. 506 $hash['contextid'] = $systemcontext->id; 507 $contextoptions = $this->get_context_options(); 508 if (!isset($contextoptions[$hash['contextid']])) { 509 $warnings[] = new lang_string('contextnotfound', 'cohort', $hash['context']); 510 $hash['contextid'] = $defaultcontext->id; 511 } 512 } else { 513 // Assume it is a category. 514 $hash['category'] = trim($hash['context']); 515 } 516 } 517 518 if (!empty($hash['category_path'])) { 519 // We already have array with available categories, look up the value. 520 $contextoptions = $this->get_context_options(); 521 if (!$hash['contextid'] = array_search($hash['category_path'], $contextoptions)) { 522 $warnings[] = new lang_string('categorynotfound', 'cohort', s($hash['category_path'])); 523 $hash['contextid'] = $defaultcontext->id; 524 } 525 return $warnings; 526 } 527 528 if (!empty($hash['category'])) { 529 // Quick search by category path first. 530 // Do not issue warnings or return here, further we'll try to search by id or idnumber. 531 $contextoptions = $this->get_context_options(); 532 if ($hash['contextid'] = array_search($hash['category'], $contextoptions)) { 533 return $warnings; 534 } 535 } 536 537 // Now search by category id or category idnumber. 538 if (!empty($hash['category_id'])) { 539 $field = 'id'; 540 $value = clean_param($hash['category_id'], PARAM_INT); 541 } else if (!empty($hash['category_idnumber'])) { 542 $field = 'idnumber'; 543 $value = $hash['category_idnumber']; 544 } else if (!empty($hash['category'])) { 545 $field = is_numeric($hash['category']) ? 'id' : 'idnumber'; 546 $value = $hash['category']; 547 } else { 548 // No category field was specified, assume default category. 549 $hash['contextid'] = $defaultcontext->id; 550 return $warnings; 551 } 552 553 if (empty($this->categoriescache[$field][$value])) { 554 $record = $DB->get_record_sql("SELECT c.id, ctx.id contextid 555 FROM {context} ctx JOIN {course_categories} c ON ctx.contextlevel = ? AND ctx.instanceid = c.id 556 WHERE c.$field = ?", array(CONTEXT_COURSECAT, $value)); 557 if ($record && ($contextoptions = $this->get_context_options()) && isset($contextoptions[$record->contextid])) { 558 $contextid = $record->contextid; 559 } else { 560 $warnings[] = new lang_string('categorynotfound', 'cohort', s($value)); 561 $contextid = $defaultcontext->id; 562 } 563 // Next time when we can look up and don't search by this value again. 564 $this->categoriescache[$field][$value] = $contextid; 565 } 566 $hash['contextid'] = $this->categoriescache[$field][$value]; 567 568 return $warnings; 569 } 570 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body