├── ImportPagesCSV.module └── README.md /ImportPagesCSV.module: -------------------------------------------------------------------------------- 1 | 'Import Pages from CSV', 43 | 'version' => 108, 44 | 'summary' => 'Import CSV files to create ProcessWire pages.', 45 | 'author' => 'Ryan Cramer', 46 | 'icon' => 'table', 47 | 'page' => array( 48 | 'name' => 'import-pages-csv', 49 | 'parent' => 'setup', 50 | 'title' => 'Import Pages CSV' 51 | ), 52 | 'requires' => 'ProcessWire>=3.0.123' 53 | ); 54 | } 55 | 56 | /** 57 | * Constants for the csvDuplicate session var 58 | * 59 | */ 60 | const csvDuplicateSkip = 0; 61 | const csvDuplicateNew = 1; 62 | const csvDuplicateModify = 2; 63 | 64 | /** 65 | * Filename with path to CSV file 66 | * 67 | */ 68 | protected $csvFilename = ''; 69 | 70 | /** 71 | * Instance of Template, used for imported pages 72 | * 73 | * @var Template|null 74 | * 75 | */ 76 | protected $template = null; 77 | 78 | /** 79 | * Instance of Page, representing the parent Page for imported pages 80 | * 81 | * @var Page|null 82 | * 83 | */ 84 | protected $parent = null; 85 | 86 | /** 87 | * List of Fieldtypes that we support importing to 88 | * 89 | */ 90 | protected $allowedFieldtypes = array( 91 | 'Checkbox', 92 | 'Datetime', 93 | 'Email', 94 | 'File', 95 | 'Float', 96 | 'Integer', 97 | 'Options', 98 | 'Page', 99 | 'PageTitle', 100 | 'Text', 101 | 'Textarea', 102 | 'Toggle', 103 | 'URL', 104 | ); 105 | 106 | /** 107 | * Initialize the module 108 | * 109 | */ 110 | public function init() { 111 | parent::init(); 112 | ini_set('auto_detect_line_endings', true); 113 | } 114 | 115 | /** 116 | * Executed when root url for module is accessed 117 | * 118 | */ 119 | public function ___execute() { 120 | $form = $this->buildForm1(); 121 | if($this->input->post('submit')) { 122 | if($this->processForm1($form)) $this->session->redirect('./fields/'); 123 | } 124 | return $form->render(); 125 | } 126 | 127 | /** 128 | * Executed when ./fields/ url for module is accessed 129 | * 130 | */ 131 | public function ___executeFields() { 132 | 133 | $this->template = $this->templates->get($this->sessionGet('csvTemplate')); 134 | $this->parent = $this->pages->get($this->sessionGet('csvParent', new NullPage())); 135 | $this->csvFilename = $this->sessionGet('csvFilename'); 136 | $error = ''; 137 | 138 | if(!$this->template || !$this->parent->id || !$this->csvFilename) { 139 | $error = "Missing required fields"; 140 | } else if(!$this->parent->editable()) { 141 | $error = "Selected parent page is not editable"; 142 | } 143 | 144 | if($error) { 145 | $this->error($error); 146 | $this->session->redirect("../"); 147 | } 148 | $this->message("Using template: {$this->template}"); 149 | $this->message("Using parent: {$this->parent->path}"); 150 | 151 | $form = $this->buildForm2(); 152 | 153 | if($this->input->post('submit')) { 154 | return $this->processForm2($form); 155 | } else { 156 | return $form->render(); 157 | } 158 | } 159 | 160 | /** 161 | * Build the "Step 1" form 162 | * 163 | * @return InputfieldForm 164 | * 165 | */ 166 | protected function buildForm1() { 167 | 168 | /** @var InputfieldForm $form */ 169 | $form = $this->modules->get("InputfieldForm"); 170 | $form->description = "Step 1: Define source and destination"; 171 | 172 | /** @var InputfieldSelect $f */ 173 | $f = $this->modules->get("InputfieldSelect"); 174 | $f->name = 'template'; 175 | $f->label = 'Template'; 176 | $f->description = 'The pages you import will use the selected template.'; 177 | $f->required = true; 178 | $f->icon = 'cubes'; 179 | $f->addOption(''); 180 | foreach($this->templates as $t) { 181 | $f->addOption($t->id, $t->name); 182 | } 183 | $value = $this->sessionGet('csvTemplate'); 184 | if($value) $f->attr('value', $value); 185 | $form->add($f); 186 | 187 | /** @var InputfieldPageListSelect $f */ 188 | $f = $this->modules->get("InputfieldPageListSelect"); 189 | $f->name = 'parent_id'; 190 | $f->label = 'Parent Page'; 191 | $f->icon = 'sitemap'; 192 | $f->required = true; 193 | $f->description = "The pages you import will be given this parent."; 194 | $value = $this->sessionGet('csvParent'); 195 | if($value) $f->attr('value', $value); 196 | $form->add($f); 197 | 198 | /** @var InputfieldFile $f */ 199 | $f = $this->modules->get("InputfieldFile"); 200 | $f->name = 'csv_file'; 201 | $f->label = 'CSV File'; 202 | $f->icon = 'file-text'; 203 | $f->extensions = 'csv txt'; 204 | $f->maxFiles = 1; 205 | $f->descriptionRows = 0; 206 | $f->overwrite = true; 207 | $f->required = false; 208 | $f->description = 209 | "The list of field names must be provided as the first row in the CSV file. " . 210 | "UTF-8 compatible encoding is assumed. File must have the extension '.csv' or '.txt'. " . 211 | "If you prefer, you may instead paste in CSV data in the 'More Options' section below. "; 212 | $form->add($f); 213 | 214 | /** @var InputfieldFieldset $fieldset */ 215 | $fieldset = $this->modules->get("InputfieldFieldset"); 216 | $fieldset->attr('id', 'csv_advanced_options'); 217 | $fieldset->label = "More Options"; 218 | $fieldset->collapsed = Inputfield::collapsedYes; 219 | $fieldset->icon = 'sliders'; 220 | $form->add($fieldset); 221 | 222 | /** @var InputfieldRadios $f */ 223 | $f = $this->modules->get("InputfieldRadios"); 224 | $f->name = 'csv_delimeter'; 225 | $f->label = 'Fields delimited by'; 226 | $f->addOption(1, 'Commas'); 227 | $f->addOption(2, 'Tabs'); 228 | $value = $this->sessionGet('csvDelimeter'); 229 | if(strlen($value)) { 230 | $f->attr('value', $value === "\t" ? 2 : 1); 231 | } else { 232 | $f->attr('value', 1); 233 | } 234 | $f->columnWidth = 33; 235 | $fieldset->add($f); 236 | 237 | /** @var InputfieldText $f */ 238 | $f = $this->modules->get("InputfieldText"); 239 | $f->name = 'csv_enclosure'; 240 | $f->label = 'Fields enclosed by'; 241 | $f->description = "If you aren't sure, it is recommended you leave it at the default (\")."; 242 | $f->attr('value', $this->sessionGet('csvEnclosure', '"')); 243 | $f->attr('maxlength', 1); 244 | $f->attr('size', 1); 245 | $f->columnWidth = 33; 246 | $fieldset->add($f); 247 | 248 | /** @var InputfieldInteger $f */ 249 | $f = $this->modules->get("InputfieldInteger"); 250 | $f->name = 'csv_max_rows'; 251 | $f->label = 'Max rows to import'; 252 | $f->description = "0 = no limit"; 253 | $f->attr('value', (int) $this->sessionGet('csvMaxRows', 0)); 254 | $f->attr('size', 5); 255 | $f->columnWidth = 34; 256 | $fieldset->add($f); 257 | 258 | /** @var InputfieldRadios $f */ 259 | $f = $this->modules->get("InputfieldRadios"); 260 | $f->name = 'csv_duplicate'; 261 | $f->label = 'What to do with duplicate page names'; 262 | $f->description = "When a row in a CSV file will result in a page with the same 'name' as one that's already there, what do you want to do?"; 263 | $f->addOption(self::csvDuplicateSkip, 'Skip it'); 264 | $f->addOption(self::csvDuplicateNew, 'Make the name unique and import new page'); 265 | $f->addOption(self::csvDuplicateModify, 'Modify the existing page'); 266 | $f->attr('value', (int) $this->sessionGet('csvDuplicate')); 267 | $fieldset->add($f); 268 | 269 | /** @var InputfieldRadios $f */ 270 | $f = $this->modules->get("InputfieldRadios"); 271 | $f->name = 'csv_add_page_refs'; 272 | $f->label = 'Create new pages for Page references that do not exist?'; 273 | $f->description = 274 | "When importing, if an existing Page for a FieldtypePage field cannot be found by title or name, " . 275 | "it can optionally be created during import. This requires that the column being imported to the " . 276 | "FieldtypePage field contains a title or name for the Page. It also requires that the FieldtypePage " . 277 | "field is already configured to specify both the parent and template that it should use."; 278 | $f->notes = 279 | "Note that only the title and name properties are populated to created Page reference pages. " . 280 | "If there are more properties you want to populate, create or import those pages ahead of time."; 281 | $f->addOption(1, 'Yes, create new pages for Page references that do not already exist'); 282 | $f->addOption(0, 'No, do not create new pages for missing page references'); 283 | $f->attr('value', (int) $this->sessionGet('csvAddPageRefs')); 284 | $fieldset->add($f); 285 | 286 | /** @var InputfieldTextarea $f */ 287 | $f = $this->modules->get("InputfieldTextarea"); 288 | $f->name = 'csv_data'; 289 | $f->label = 'Paste in CSV Data'; 290 | $f->icon = 'code'; 291 | $f->description = 292 | "If you prefer, you may paste in the CSV data here rather than uploading a file above. " . 293 | "You should use one or the other, but not both."; 294 | $f->collapsed = Inputfield::collapsedBlank; 295 | $fieldset->add($f); 296 | 297 | $this->addSubmit($form, 'Continue to Step 2'); 298 | 299 | return $form; 300 | } 301 | 302 | /** 303 | * Process the "Step 1" form and populate session variables with the results 304 | * 305 | * @param InputfieldForm $form 306 | * @return bool 307 | * 308 | */ 309 | protected function processForm1(InputfieldForm $form) { 310 | 311 | $form->processInput($this->input->post); 312 | if(count($form->getErrors())) return false; 313 | 314 | $this->sessionSet('csvTemplate', (int) $form->getChildByName('template')->value); 315 | $this->sessionSet('csvParent', (int) $form->getChildByName('parent_id')->value); 316 | $this->sessionSet('csvDelimeter', $form->getChildByName('csv_delimeter')->value == 2 ? "\t" : ","); 317 | $this->sessionSet('csvEnclosure', substr($form->getChildByName('csv_enclosure')->value, 0, 1)); 318 | $this->sessionSet('csvDuplicate', (int) $form->getChildByName('csv_duplicate')->value); 319 | $this->sessionSet('csvMaxRows', (int) $form->getChildByName('csv_max_rows')->value); 320 | $this->sessionSet('csvAddPageRefs', (int) $form->getChildByName('csv_add_page_refs')->value); 321 | 322 | /** @var Pagefiles|Pagefile $csvFile */ 323 | $csvFile = $form->getChildByName('csv_file')->value; 324 | $csvData = $form->getChildByName('csv_data')->value; 325 | 326 | $csvBasename = 'data-' . $this->user->id . '.csv'; 327 | $csvFilename = $this->page->filesManager()->path() . $csvBasename; 328 | 329 | if(count($csvFile)) { 330 | $csvFile = $csvFile->first(); 331 | $csvFile->rename($csvBasename); 332 | $csvFilename = $csvFile->filename; 333 | 334 | } else if(strlen($csvData)) { 335 | file_put_contents($csvFilename, $csvData); 336 | $this->wire('files')->chmod($csvFilename); 337 | 338 | } else { 339 | $csvFilename = ''; 340 | } 341 | 342 | if(!$csvFilename || !is_file($csvFilename)) { 343 | $this->error("Missing required CSV file/data"); 344 | return false; 345 | } 346 | 347 | $this->sessionSet('csvFilename', $csvFilename); 348 | 349 | return true; 350 | } 351 | 352 | /** 353 | * Build the "Step 2" form to connect the fields 354 | * 355 | * @return InputfieldForm 356 | * 357 | */ 358 | protected function buildForm2() { 359 | 360 | /** @var InputfieldForm $form */ 361 | $form = $this->modules->get("InputfieldForm"); 362 | $form->description = "Step 2: Connect the fields"; 363 | $form->value = "

" . 364 | "Below is a list of columns found in in the header of your CSV file. " . 365 | "For each of them, select the field it should import to. " . 366 | "Leave any fields you want to exclude blank. " . 367 | "Once finished, click “Start Import” at the bottom of this page. " . 368 | "Note: any field names in your CSV file that match those in your site " . 369 | "will be automatically selected." . 370 | "

"; 371 | 372 | $fp = fopen($this->csvFilename, "r"); 373 | if($fp === false) throw new WireException("Unable to open CSV file"); 374 | 375 | $data = fgetcsv($fp, 0, $this->sessionGet('csvDelimeter'), $this->sessionGet('csvEnclosure')); 376 | 377 | foreach($data as $key => $value) { 378 | 379 | /** @var InputfieldSelect $f */ 380 | $f = $this->modules->get('InputfieldSelect'); 381 | $f->name = "csv" . $key; 382 | $f->label = $value; 383 | $f->addOption(''); 384 | 385 | foreach($this->template->fieldgroup as $field) { 386 | if(!$this->isAllowedField($field)) continue; 387 | $label = "$field->name – $field->label (" . $field->type->shortName . ")"; 388 | $f->addOption($field->name, $label); 389 | if($field->name == $value) $f->attr('value', $field->name); 390 | } 391 | 392 | $form->add($f); 393 | } 394 | 395 | fclose($fp); 396 | 397 | $this->addSubmit($form, 'Start Import'); 398 | 399 | return $form; 400 | } 401 | 402 | /** 403 | * Process the "Step 2" form and perform the import 404 | * 405 | * @param InputfieldForm $form 406 | * @return string 407 | * 408 | */ 409 | protected function processForm2(InputfieldForm $form) { 410 | 411 | $form->processInput($this->input->post); 412 | 413 | $csvFilename = $this->csvFilename; 414 | $fp = fopen($csvFilename, "r"); 415 | if($fp === false) throw new WireException('Unable to open CSV file'); 416 | 417 | $numImported = 0; 418 | $rowNum = 0; 419 | $maxRows = $this->sessionGet('csvMaxRows'); 420 | $csvDelimeter = $this->sessionGet('csvDelimeter', ','); 421 | $csvEnclosure = $this->sessionGet('csvEnclosure', '"'); 422 | 423 | while(($data = fgetcsv($fp, 0, $csvDelimeter, $csvEnclosure)) !== false) { 424 | $cnt = count($data); 425 | 426 | // skip blank lines 427 | if(!$cnt || ($cnt == 1 && empty($data[0]))) continue; 428 | 429 | $rowNum++; 430 | 431 | // only start importing on second line (if $n) 432 | if($rowNum > 1 && $this->importPage($data, $form)) { 433 | $numImported++; 434 | } 435 | 436 | if($maxRows && $rowNum > $maxRows) break; 437 | } 438 | 439 | fclose($fp); 440 | 441 | $this->wire('files')->unlink($csvFilename); 442 | 443 | return $this->processForm2Markup($numImported); 444 | } 445 | 446 | /** 447 | * Provide the completion output markup for processForm2 448 | * 449 | * @param int $numImported 450 | * @return string 451 | * 452 | */ 453 | protected function processForm2Markup($numImported) { 454 | return 455 | "

Imported $numImported pages

" . 456 | "

See the imported pages

" . 457 | "

Import more pages

"; 458 | } 459 | 460 | /** 461 | * Import a single page 462 | * 463 | * @param array $data 464 | * @param InputfieldForm $form 465 | * @return bool 466 | * 467 | */ 468 | protected function importPage(array $data, InputfieldForm $form) { 469 | 470 | $page = $this->wire('pages')->newPage(array('template' => $this->template)); 471 | $page->parent = $this->parent; 472 | $page->set('ImportPagesCSVData', array()); // data to set after page is saved 473 | $page->setTrackChanges(true); 474 | $fieldNames = array(); 475 | 476 | foreach($form as $f) { 477 | if(!preg_match('/^csv(\d+)$/', $f->name, $matches)) continue; 478 | $key = (int) $matches[1]; 479 | $value = $data[$key]; 480 | $name = $f->value; 481 | if(!$name) continue; 482 | $this->importPageValue($page, $name, $value); 483 | $fieldNames[] = $name; 484 | } 485 | 486 | if(!$page->name) { 487 | $this->error( 488 | "Unable to import page because it has no required 'title' field or it is blank.
" . 489 | "
" . print_r($data, true) . "
", 490 | Notice::allowMarkup 491 | ); 492 | return false; 493 | } 494 | 495 | $existingPage = $this->wire('pages')->get("parent_id=$this->parent, name=$page->name"); 496 | 497 | if($existingPage->id) { 498 | // existing page 499 | if($this->sessionGet('csvDuplicate') == self::csvDuplicateNew) { 500 | $page->name = $this->getUniquePageName($page->name); 501 | $page = $this->savePage($page, true); 502 | 503 | } else if($this->sessionGet('csvDuplicate') == self::csvDuplicateModify) { 504 | $page = $this->modifyPage($existingPage, $page, $fieldNames); 505 | 506 | } else { 507 | $this->message("Skipping row with duplicate name '$page->name'"); 508 | } 509 | } else { 510 | // new page 511 | $page = $this->savePage($page, true); 512 | } 513 | 514 | // import post-save data, like files 515 | if($page->id && count($page->get('ImportPagesCSVData'))) { 516 | foreach($page->get('ImportPagesCSVData') as $name => $value) { 517 | $page->set($name, $value); 518 | } 519 | $page->save(); 520 | } 521 | 522 | return $page->id > 0; 523 | } 524 | 525 | /** 526 | * Assign a value to a page field 527 | * 528 | * @param Page $page Page being imported to 529 | * @param string $name Field name or page property name 530 | * @param mixed $value Value to set 531 | * @return bool 532 | * 533 | */ 534 | protected function ___importPageValue(Page $page, $name, $value) { 535 | 536 | $field = $this->fields->get($name); 537 | if(!$field) return false; 538 | 539 | if($field->type instanceof FieldtypeFile) { 540 | 541 | $value = trim($value); 542 | // split delimeted data to an array 543 | $value = preg_split('/[\r\n\t|]+/', $value); 544 | if($field->get('maxFiles') == 1) $value = array_shift($value); 545 | $data = $page->get('ImportPagesCSVData'); 546 | $data[$name] = $value; 547 | $page->set('ImportPagesCSVData', $data); 548 | 549 | } else if($field->type instanceof FieldtypePage) { 550 | 551 | // $oldValue = $page->id ? (string) $page->get($name) : null; 552 | 553 | if($this->sessionGet('csvAddPageRefs')) { 554 | $field->setQuietly('_sanitizeValueString', 'create'); 555 | $page->set($name, $value); 556 | $field->offsetUnset('_sanitizeValueString'); 557 | } else { 558 | $page->set($name, $value); 559 | } 560 | 561 | } else if($name === 'title') { 562 | $page->set($name, $value); 563 | if(!$page->name) $page->name = $this->sanitizer->pageName($value, Sanitizer::translate); 564 | 565 | } else { 566 | $page->set($name, $value); 567 | } 568 | 569 | return true; 570 | } 571 | 572 | /** 573 | * Modify an existing page with CSV data 574 | * 575 | * @param Page $existingPage 576 | * @param Page $newPage 577 | * @param array $fieldNames 578 | * @return bool|Page 579 | * 580 | */ 581 | protected function modifyPage(Page $existingPage, Page $newPage, array $fieldNames) { 582 | 583 | if($existingPage->template->id != $newPage->template->id) { 584 | $this->error("Unable to modify '$existingPage->name' because it uses a different template '$existingPage->template'"); 585 | return false; 586 | } 587 | 588 | /** @var array $data */ 589 | $data = $newPage->get('ImportPagesCSVData'); 590 | 591 | foreach($fieldNames as $fieldName) { 592 | if(isset($data[$fieldName])) { 593 | $value = $data[$fieldName]; 594 | } else { 595 | $value = $newPage->get($fieldName); 596 | } 597 | 598 | $field = $this->wire('fields')->get($fieldName); 599 | $existingValue = $existingPage->get($fieldName); 600 | $existingPage->set($fieldName, $value); 601 | 602 | if($field->type instanceof FieldtypePage) { 603 | if(((string) $existingValue) === ((string) $newPage->get($fieldName))) { 604 | $existingPage->untrackChange($fieldName); 605 | } 606 | } 607 | 608 | } 609 | 610 | return $this->savePage($existingPage); 611 | } 612 | 613 | /** 614 | * Wrapper to PW's page save to capture exceptions so importPage can try name variations if necessary 615 | * 616 | * @param Page $page 617 | * @param bool $reportErrors 618 | * @return Page 619 | * 620 | */ 621 | protected function savePage(Page $page, $reportErrors = true) { 622 | 623 | try { 624 | $label = $page->id ? "Modified" : "Created"; 625 | $changes = implode(', ', $page->getChanges()); 626 | if(strlen($changes)) { 627 | $changes = "($changes)"; 628 | $page->save(); 629 | $this->message("$label $page->path $changes"); 630 | $page->setQuietly('_csvSaved', true); 631 | } else { 632 | $page->setQuietly('_csvSaved', false); 633 | } 634 | 635 | } catch(\Exception $e) { 636 | if($reportErrors) $this->error($e->getMessage()); 637 | } 638 | 639 | return $page; 640 | } 641 | 642 | /** 643 | * Given a page name, check that it is unique and return it or a unique numbered variation of it 644 | * 645 | * @param string $pageName 646 | * @return string 647 | * 648 | */ 649 | protected function getUniquePageName($pageName) { 650 | 651 | return $this->wire('pages')->names()->uniquePageName(array( 652 | 'name' => $pageName, 653 | 'parent' => $this->parent 654 | )); 655 | 656 | /* 657 | * Original method for reference 658 | $n = 0; 659 | do { 660 | $testName = $pageName . "-" . (++$n); 661 | $test = $this->parent->child("name=$testName, include=all"); 662 | if(!$test->id) break; 663 | } while(1); 664 | return $testName; 665 | */ 666 | } 667 | 668 | /** 669 | * Add a submit button, moved to a function so we don't have to do this twice 670 | * 671 | * @param InputfieldForm $form 672 | * @param string $value 673 | * 674 | */ 675 | protected function addSubmit(InputfieldForm $form, $value = 'Submit') { 676 | /** @var InputfieldSubmit $f */ 677 | $f = $this->modules->get("InputfieldSubmit"); 678 | $f->name = 'submit'; 679 | $f->value = $value; 680 | $form->add($f); 681 | } 682 | 683 | /** 684 | * Is given Field allowed for importing? 685 | * 686 | * @param Field $field 687 | * @return bool 688 | * 689 | */ 690 | protected function ___isAllowedField(Field $field) { 691 | $valid = false; 692 | foreach($this->allowedFieldtypes as $name) { 693 | if(wireInstanceOf($field->type, "Fieldtype$name")) $valid = true; 694 | if($valid) break; 695 | } 696 | return $valid; 697 | } 698 | 699 | /** 700 | * Get session value 701 | * 702 | * @param string $key 703 | * @param null $fallback 704 | * @return string|int|null 705 | * 706 | */ 707 | protected function sessionGet($key, $fallback = null) { 708 | $value = $this->session->getFor($this, $key); 709 | if($value === null) $value = $fallback; 710 | return $value; 711 | } 712 | 713 | /** 714 | * Set session value 715 | * 716 | * @param string $key 717 | * @param string|int $value 718 | * 719 | */ 720 | protected function sessionSet($key, $value) { 721 | $this->session->setFor($this, $key, $value); 722 | } 723 | } 724 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImportPagesCSV: Import CSV file to pages 2 | 3 | This is a ProcessWire module that enables you to import CSV files to create pages 4 | or modify existing pages. The module requires ProcessWire 3.0.123 or newer. 5 | This is an admin/development tool that is recommended only for use by the 6 | superuser or developer. 7 | 8 | The following Fieldtypes are supported for importing, as well as most types 9 | derived from them: 10 | 11 | - Checkbox 12 | - Datetime 13 | - Email 14 | - File 15 | - Float 16 | - Image 17 | - Integer 18 | - Options 19 | - Page 20 | - PageTitle 21 | - Text 22 | - Textarea 23 | - URL 24 | 25 | ## To install: 26 | 27 | 1. Place the file ImportPagesCSV.module in a /site/modules/ImportPagesCSV/ directory. 28 | 2. In ProcessWire admin, click to 'Modules' and 'Check for new modules'. 29 | 3. Click 'install next to the 'Import Pages CSV' module (under heading 'Import'). 30 | 31 | Once installed, the module can be found on your admin Setup menu under the title "Import 32 | Pages CSV". 33 | 34 | ## Importing file/image fields 35 | 36 | CSV column should contain full URL (or diskpath and filename) to the file you want to import. 37 | For fields that support multiple files, place each filename or URL on its own line, OR separate 38 | them by | (pipe) OR tab. 39 | 40 | ## Importing page reference fields 41 | 42 | For single-value page fields the CSV imported value can be the page id, path, title, or name. 43 | For multi-value page fields, the value can be the same but multiple-values should be separated by 44 | either a newline in the column, or a pipe "|" character. Please make sure that your Page reference 45 | field has one or more pages selected for the "Parent" setting on the Details tab. If you want the 46 | import to be able to create paes, there must also be a single Template selected on the "Template" 47 | setting. 48 | 49 | 50 | --- 51 | Copyright 2011-2021 by Ryan Cramer for ProcessWire 52 | 53 | --------------------------------------------------------------------------------