[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/cohort/ -> upload_form.php (source)

   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  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1