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