It would be keen if a hierarchy of pages could be migrated into a book structure. Especially importing book structures from other Drupal sites, for which there seems to be no current solution.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

btopro’s picture

+1 or menu items, book isn't that hard to do once menu's figured out

mikeryan’s picture

Version: 6.x-1.x-dev » 7.x-2.x-dev
mvc’s picture

This is actually an easier problem to solve for book nodes than for menus in general, thanks to functions like book_link_load(). I have this working in migration 6.x-2.2-rc2, using UUIDs and an XML document created by views_data_export that lists the UUID of the current node, the book parent node, and the top node in the current book's hierarchy. My code doesn't use stubs, for simplicity it assumes that parent nodes will always exist (ie, the view is sorted by Book: Hierarchy). A general solution would ideally accept these in any order. I'm not sure how else besides UUIDs to rebuild the book hierarchy; if we use menu or node IDs we'd have to save the old values from the source someplace in the destination since they would be different. But migrate probably shouldn't depend on migrate_extras, so if we go with this approach this feature couldn't live in the migrate module, even though book is a core module.

At any rate, here's the code which regenerates the hierarchy in the complete() method, in case it's useful. The field Book_Node_UUID is the top-level node for the current node's book, and Book_Parent_Node_UUID is the node's immediate parent. (I'm accessing these via $row->xml because they're stored in the XML but not in the destination node so there's no direct mapping.)

  public function complete($node, $row) {
    // based on book_nodeapi(), case 'prepare'
    $node->book = array();
    if ($row->UUID == (string) $row->xml->Book_Node_UUID) {
      // This is the base node of this book
      $node->book['bid'] = 'new';
    }
    else {
      // Handle child pages, based on "add child page" link feature
      $parent_book_node = node_get_by_uuid((string) $row->xml->Book_Parent_Node_UUID);
      $parent_book_node_mlid = $parent_book_node->book['mlid'];
      $parent = book_link_load($parent_book_node_mlid);
      if ($parent && $parent['access']) {
        $node->book['bid'] = $parent['bid'];
        $node->book['plid'] = $parent['mlid'];
        $node->book['menu_name'] = $parent['menu_name'];
      }
    }
    // Set defaults.
    $node->book += _book_link_defaults(!empty($node->nid) ? $node->nid : 'new');
    // Find the depth limit for the parent select.
    if (isset($node->book['bid']) && !isset($node->book['parent_depth_limit'])) {
      $node->book['parent_depth_limit'] = _book_parent_depth_limit($node->book);
    }
    // based on book_nodeapi(), case 'presave'
    // Always save a revision for non-administrators.
    if (!empty($node->book['bid']) && !user_access('administer nodes')) {
      $node->revision = 1;
    }
    // Make sure a new node gets a new menu link.
    $node->book['mlid'] = NULL;
    // based on book_nodeapi(), case 'insert'/'update'
    if ($node->book['bid'] == 'new') {
      // This is the base node of this book
      $node->book['bid'] = $node->nid;
    }
    $node->book['nid'] = $node->nid;
    $node->book['menu_name'] = book_menu_name($node->book['bid']);
    _book_update_outline($node);
    node_save($node);
  }
zabelc’s picture

@mvc, I believe that migrate tracks a mapping of source ID to destination ID, perhaps that could be used in place of the UUID's.

mvc’s picture

@zabelc: true, it would be much easier to use the source ID to destination ID mapping to set a temporary variable in the node object which could be used instead of the UUID to determine the parent, although you'd still have to handle the special case where the parent NID is zero (ie, the current node is the base node of a book). this wasn't possible in my use case because i had to sync data between sites in two directions. each site would export its nodes and then use migrate to import anything new from the other site. however, i'm posting this code in hopes it would be a good starting point for anyone wanting to import book hierarchies with other use cases.

also, i noticed my code didn't handle re-running the migration with --update. correction below.

  public function complete($node, $row) {
    // test if this is an update of an existing node, or an insert of a new
    // node. for simplicity, book hierarchy information is only set when
    // new nodes are inserted, and subsequent updates will only change other
    // fields for these nodes. in case of problems importing book hierarchy
    // information, it will be necessary to rollback and re-insert the nodes.
    $test_node = node_load($node->nid);
    if (!is_array($test_node->book)) {
      // STEP 1: based on book_nodeapi(), case 'prepare'
      $node->book = array();
      if ($row->UUID == (string) $row->xml->Book_Node_UUID) {
        // This is the base node of this book
        $node->book['bid'] = 'new';
      }
      else {
        // Handle child pages, based on book's "add child page" link feature
        $parent_book_node_uuid = (string) $row->xml->Book_Parent_Node_UUID;
        $parent_book_node = node_get_by_uuid((string) $row->xml->Book_Parent_Node_UUID);
        $parent_book_node_mlid = $parent_book_node->book['mlid'];
        $parent = book_link_load($parent_book_node_mlid);
        if ($parent && $parent['access']) {
          $node->book['bid'] = $parent['bid'];
          $node->book['plid'] = $parent['mlid'];
          $node->book['menu_name'] = $parent['menu_name'];
        }
      }
      // Set defaults.
      $node->book += _book_link_defaults(!empty($node->nid) ? $node->nid : 'new');
      // Find the depth limit for the parent select.
      if (isset($node->book['bid']) && !isset($node->book['parent_depth_limit'])) {
        $node->book['parent_depth_limit'] = _book_parent_depth_limit($node->book);
      }
      // STEP 2: based on book_nodeapi(), case 'presave'
      // Always save a revision for non-administrators.
      if (!empty($node->book['bid']) && !user_access('administer nodes')) {
        $node->revision = 1;
      }
      // Make sure a new node gets a new menu link.
      $node->book['mlid'] = NULL;
      // STEP 3: based on book_nodeapi(), case 'insert'/'update'
      if ($node->book['bid'] == 'new') {
        // This is the base node of this book
        $node->book['bid'] = $node->nid;
      }
      $node->book['nid'] = $node->nid;
      $node->book['menu_name'] = book_menu_name($node->book['bid']);
      _book_update_outline($node);
      // STEP 4: save our work
      node_save($node);
    }
  }
azinck’s picture

@mvc: Thanks a ton for the snippet, it saved me a lot of time.

AndyF’s picture

Issue summary: View changes
Status: Active » Needs review
FileSize
3.37 KB

I'm not sure this will be in time for the OP :p

I've made a migrate destination for book outlines by extending the menu links destination. It's the first time I've written a destination handler, feedback welcome.

Thanks

AndyF’s picture

pip0’s picture

A simple method to import all my book hierarchy is :
1 - Dump the "menu_links" table from drupal 6 by selecting only links used by my book pages
2 - Add two columns "language" and "i18n_tsid" at the end of each row from the dumping Drupal 6 "menu_links" table with there value.
3 - Import the updated "menu_links" table from Drupal 6 to Drupal 7

Example "menu_links" table row to import with Drupal 7:
INSERT IGNORE INTO `menu_links` (`menu_name`, `mlid`, `plid`, `link_path`, `router_path`, `link_title`, `options`, `module`, `hidden`, `external`, `has_children`, `expanded`, `weight`, `depth`, `customized`, `p1`, `p2`, `p3`, `p4`, `p5`, `p6`, `p7`, `p8`, `p9`, `updated`, `language`, `i18n_tsid`) VALUES ('book-toc-200', 1320, 0, 'node/200', 'node/%', 'Services', 'a:0:{}', 'book', 0, 0, 1, 0, 0, 1, 0, 1320, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'und', 0);

Hope that this can help some one else.