Index: skeleton_sync.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/skeleton/skeleton_sync.inc,v
retrieving revision 1.1
diff -u -r1.1 skeleton_sync.inc
--- skeleton_sync.inc	12 Mar 2009 18:22:29 -0000	1.1
+++ skeleton_sync.inc	30 Apr 2009 02:40:50 -0000
@@ -3,14 +3,33 @@
 /**
  * Syncronize form. Contains lists of updated, added, and deleted templates.
  *
+ * @return
+ *   The rendered HTML of the page.
+ */
+function skeleton_sync_page() {
+  drupal_add_js(drupal_get_path('module', 'skeleton') . '/skeleton-admin.js');
+  $output .= drupal_get_form('skeleton_sync_content_form');
+  $output .= drupal_get_form('skeleton_sync_add_template_form');
+  $output .= drupal_get_form('skeleton_sync_delete_template_form');
+  return $output;
+}
+
+/**
+ * Syncronize content form.
+ *
  * @param $form_state
  *   The state of the form.
  * @return
  *   The form array.
  */
-function skeleton_sync_form($form_state) {
+function skeleton_sync_content_form($form_state) {
   $form = array();
-  $form['help'] = array(
+  $form['content'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Syncronize template content'),
+    '#collapsible' => TRUE,
+  );
+  $form['content']['help'] = array(
     '#prefix' => '<p>',
     '#suffix' => '</p>',
     '#value' => t('Use this form to syncronize changed templates with pages which have all ready been created.'),
@@ -25,33 +44,30 @@
     $updated[$node->nid] = t('<a href="@node-url">%title</a>, in the <a href="@book-url">%book-title</a> book, created from the <a href="@template-url">%template</a> template.', array('@node-url' => url('node/' . $node->nid), '%title' => $node->title, '@book-url' => url('node/' . $book->nid), '%book-title' => $book->title, '%template' => $eligble_node->template, '@template-url' => url('admin/content/skeleton/template/' . $eligble_node->template_id . '/view')));
   }
   if (!empty($updated)) {
-    $form['nodes'] = array(
+    $form['content']['#collapsed'] = FALSE;
+    $form['content']['nodes'] = array(
       '#type' => 'checkboxes',
       '#title' => t('Select the nodes you wish to update'),
       '#options' => $updated,
     );
-    $form['submit'] = array(
+    $form['content']['submit'] = array(
       '#type' => 'submit',
       '#value' => t('Synchronize'),
     );
   }
   else {
-    $form['no_nodes'] = array(
+    $form['content']['#collapsed'] = TRUE;
+    $form['content']['no_nodes'] = array(
       '#prefix' => '<p>',
       '#suffix' => '</p>',
-      '#value' => t('There are no nodes eligible for synchronizing.')
+      '#value' => t('There are no nodes eligible for content synchronizing.')
     );
   }
-
-  // TODO: Show a seperate list for new templates added to a skeleton.
-
-  // TODO: Show a seperate list for templates removed from a skeleton.
-
   return $form;
 }
 
 /**
- * Submit handler for the syncronize form.
+ * Submit handler for the syncronize content form.
  *
  * @param $form
  *   The form being submitted.
@@ -59,7 +75,7 @@
  * @param $form_state
  *   The state of the form.
  */
-function skeleton_sync_form_submit($form, $form_state) {
+function skeleton_sync_content_form_submit($form, $form_state) {
   $batch = array(
     'title' => t('Updating content'),
     'init_message' => t('Content updating is starting.'),
@@ -74,6 +90,189 @@
 }
 
 /**
+ * Form for pushing out new templates to existing skeletons.
+ */
+function skeleton_sync_add_template_form($form_state) {
+  $form = array();
+  $form['templates'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Syncronize templates'),
+    '#collapsible' => TRUE,
+  );
+  // Find all copies of instantiated skeletons which do not contain a template
+  // now associated with the same skeleton.
+  $skeleton_result = db_query("SELECT DISTINCT skeleton_id FROM {skeleton_template_node}");
+  while ($skeleton_id = db_result($skeleton_result)) {
+    $templates = array();
+    $updated = array();
+    $skeleton = skeleton_load($skeleton_id);
+    $form['templates']['skeletons'] = array(
+      '#tree' => TRUE,
+    );
+    $form['templates']['skeletons'][$skeleton_id] = array();
+    $template_result = db_query("SELECT DISTINCT template_id FROM {skeleton_template_node} WHERE skeleton_id = %d", $skeleton_id);
+    while ($template_id = db_result($template_result)) {
+      $templates[] = $template_id;
+    }
+    $updated_result = db_query("SELECT template_id FROM {skeleton_data} WHERE skeleton_id = %d AND template_id NOT IN (" . implode(',', $templates) . ")", $skeleton_id);
+    while ($updated_id = db_result($updated_result)) {
+      $template = skeleton_template_load($updated_id);
+      $updated[$updated_id] = l($template->template, 'admin/content/skeleton/template/' . $template->template_id . '/view');
+    }
+    if (!empty($updated)) {
+      $form['templates']['skeletons'][$skeleton_id] = array(
+        '#type' => 'checkboxes',
+        '#title' => t('Create new pages from new templates assigned to <a href="@skeleton-url">%skeleton</a>', array('@skeleton-url' => url('admin/content/skeleton/skeleton/' . $skeleton->skeleton_id . '/edit'), '%skeleton' => $skeleton->skeleton)),
+        '#options' => $updated,
+      );
+      $form['templates']['submit'] = array(
+        '#type' => 'submit', 
+        '#value' => t('Syncronize'),
+      );
+    }
+    else {
+      $form['templates']['#collapsed'] = TRUE;
+      $form['templates']['no_templates'] = array(
+        '#prefix' => '<p>',
+        '#suffix' => '</p>',
+        '#value' => t('There are no templates eligible for content synchronizing.')
+      );
+    }
+  }
+  return $form;
+}
+
+/**
+ * Submit handler for pushing out new templates to existing books.
+ */
+function skeleton_sync_add_template_form_submit($form, &$form_state) {
+  module_load_include('inc', 'node', 'node.pages');
+  module_load_include('inc', 'skeleton', 'skeleton_token');
+  foreach($form_state['values']['skeletons'] as $skeleton_id => $templates) {
+    $templates = array_filter($templates);
+    foreach($templates as $template_id) {
+      // Find the template parent of this template
+      // $template = skeleton_template_load($template_id);
+      // If the parent is 0, we place it under the top-most book node.
+      // Otherwise, place it under the parent template in the book.
+      $parent = db_result(db_query("SELECT parent FROM {skeleton_data} WHERE skeleton_id = %d AND template_id = %d", $skeleton_id, $template_id));
+      if ($parent == 0) {
+        // TODO!
+      }
+      else {
+        $parent_nids_result = db_query("SELECT nid, tokens FROM {skeleton_template_node} WHERE skeleton_id = %d AND template_id = %d", $skeleton_id, $parent);
+        while($parent_info = db_fetch_object($parent_nids_result)) {
+          $parent_info->tokens = unserialize($parent_info->tokens);
+          $data = db_result(db_query("SELECT node_data FROM {skeleton_template} WHERE template_id = %d", $template_id));
+          $node = array();
+          $node['values'] = unserialize($data);
+          $parent_node = node_load($parent_info->nid);
+          $node['values']['book'] = array();
+          $node['values']['book']['bid'] = $parent_node->book['bid'];
+          $node['values']['book']['plid'] = $parent_node->book['mlid'];
+          $node['values']['book']['options'] = array();
+    
+          // Replace custom tokens in the title and body fields. Replace skeleton
+          // tokens first so they may be used as node tokens such as [title].
+          if (!empty($parent_info->tokens)) {
+            $node['values']['title'] = token_replace($node['values']['title'], 'skeleton', $parent_info->tokens);
+            $node['values']['body'] = token_replace($node['values']['body'], 'skeleton', $parent_info->tokens);
+          }
+    
+          $node['values']['title'] = token_replace($node['values']['title'], 'node', (object)$node['values']);
+          $node['values']['body'] = token_replace($node['values']['body'], 'node', (object)$node['values']);
+    
+          $node['values']['uid'] = $parent_node->uid;
+          $node['values']['name'] = $parent_node->name;
+    
+          $node['values']['tokens'] = $parent_info->tokens;
+   
+          // We have to add the skeleton ID here as we can't save it with the
+          // template as a template may have multiple skeletons.
+          $node['values']['skeleton_id'] = $skeleton_id;
+    
+          $node['values']['op'] = t('Save');
+          drupal_execute($node['values']['type'] .'_node_form', $node, (object)$node['values']);
+          // TODO: This will probably need to be removed if this call is integrated
+          // into the book module. See http://drupal.org/node/364529 for details.
+          menu_rebuild();
+          $form_state['nids'][] = $node['nid'];
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Form for deleting instantiated templates where the source template has been
+ * deleted.
+ */
+function skeleton_sync_delete_template_form($form_state) {
+  $form = array();
+  $form['delete'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Delete instantiated template nodes'),
+    '#collapsible' => TRUE,
+  );
+  $form['delete']['help'] = array(
+    '#prefix' => '<p>',
+    '#suffix' => '</p>',
+    '#value' => t("Use this form to delete nodes where the associated template has also been deleted."),
+  );
+  $form['delete']['nodes'] = array(
+    '#tree' => TRUE,
+  );
+
+  // Find all nodes where the template it was created from has been deleted.
+  $result = db_query("SELECT nid from {skeleton_template_node} stn LEFT JOIN {skeleton_template} st ON stn.template_id = st.template_id WHERE st.template_id IS NULL");
+  $options = array();
+  while ($node = node_load(db_result($result))) {
+    $book = node_load($node->book['bid']);
+    $options[$node->nid] = t('<a href="@node-url">%title</a>, in the <a href="@book-url">%book-title</a> book.', array('@node-url' => url('node/' . $node->nid), '%title' => $node->title, '@book-url' => url('node/' . $book->nid), '%book-title' => $book->title));
+  }
+  if (!empty($options)) {
+    $form['delete']['nodes'] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Select the nodes you wish to delete'),
+      '#options' => $options,
+    );
+    $form['delete']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Delete selected nodes'),
+    );
+  }
+  else {
+    $form['delete']['#collapsed'] = TRUE;
+    $form['delete']['no_nodes'] = array(
+      '#prefix' => '<p>',
+      '#suffix' => '</p>',
+      '#value' => t('There are no nodes with deleted templates.')
+    );
+  }
+  return $form;
+}
+
+/**
+ * Submit handler for deleting nodes where the associated template has also
+ * been deleted.
+ */
+function skeleton_sync_delete_template_form_submit($form, &$form_state) {
+  $batch = array(
+    'title' => t('Deleting content'),
+    'init_message' => t('Content deletion is starting.'),
+    'progress_message' => t('Processed @current out of @total.'),
+    'error_message' => t('There was an error while updating content.'),
+    'file' => drupal_get_path('module', 'skeleton'). '/skeleton_sync.inc',
+  );
+  foreach (array_filter($form_state['values']['nodes']) as $nid) {
+    $batch['operations'][] = array('skeleton_sync_delete_node', array($nid));
+  }
+  if (!empty($batch['operations'])) {
+    batch_set($batch);
+  }
+}
+
+/**
  * Update an instantiated node based on an updated template.
  *
  * @param $nid
@@ -114,4 +313,20 @@
   node_save($node);
   db_query("UPDATE {skeleton_template_node} SET template_status = 'synced' WHERE nid = %d", $node->nid);
   $context['results'][] = t('Updated %title', array('%title' => $node->title));
-}
\ No newline at end of file
+}
+
+/**
+ * Delete an instantiated node. This is used to delete nodes where it's
+ * template has also been deleted, but could also be used to delete any node.
+ *
+ * @param $nid
+ *   The node ID to delete.
+ *
+ * @param &$context
+ *   The current context of the batch operation.
+ */
+function skeleton_sync_delete_node($nid, &$context) {
+  $node = node_load($nid);
+  node_delete($nid);
+  $context['results'][] = t('Deleted %title', array('%title' => $node->title));
+}
Index: skeleton_template.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/skeleton/skeleton_template.inc,v
retrieving revision 1.16
diff -u -r1.16 skeleton_template.inc
--- skeleton_template.inc	30 Apr 2009 00:25:43 -0000	1.16
+++ skeleton_template.inc	30 Apr 2009 02:40:50 -0000
@@ -181,7 +181,6 @@
 function skeleton_delete_template_submit($form, &$form_state) {
   db_query("DELETE from {skeleton_template} WHERE template_id = %d", $form_state['values']['template_id']);
   db_query("DELETE from {skeleton_data} WHERE template_id = %d", $form_state['values']['template_id']);
-  db_query("DELETE from {skeleton_template_node} WHERE template_id = %d", $form_state['values']['template_id']);
   drupal_set_message(t('Template deleted.'));
   drupal_goto('admin/content/skeleton/template');
 }
Index: skeleton.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/skeleton/skeleton.module,v
retrieving revision 1.18
diff -u -r1.18 skeleton.module
--- skeleton.module	12 Mar 2009 18:27:26 -0000	1.18
+++ skeleton.module	30 Apr 2009 02:40:50 -0000
@@ -71,8 +71,7 @@
     'file' => 'skeleton_admin.inc',
   );
   $items['admin/content/skeleton/sync'] = array(
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('skeleton_sync_form'),
+    'page callback' => 'skeleton_sync_page',
     'access arguments' => array('configure skeleton outlines'),
     'type' => MENU_LOCAL_TASK,
     'title' => 'Synchronize',
@@ -333,7 +332,7 @@
 function skeleton_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
   switch ($op) {
     case 'load':
-      if ($template = db_fetch_object(db_query("SELECT skeleton_id, template_id, tokens FROM {skeleton_template_node} WHERE nid = %d", $node->nid))) {
+      if ($template = db_fetch_object(db_query("SELECT skeleton_id, template_id, tokens FROM {skeleton_template_node} WHERE nid = %d AND template_status != 'overridden'", $node->nid))) {
         $node->skeleton_template = new stdClass();
         $node->skeleton_template->skeleton_id = $template->skeleton_id;
         $node->skeleton_template->template_id = $template->template_id;
@@ -351,11 +350,13 @@
       }
       break;
     case 'update':
-    case 'delete':
       if (!$node->skeleton_template->keep_connected) {
-        db_query("DELETE FROM {skeleton_template_node} WHERE nid = %d", $node->nid);
+        db_query("UPDATE {skeleton_template_node} SET template_status = '%s' WHERE nid = %d", 'overridden', $node->nid);
       }
       break;
+    case 'delete':
+      db_query("DELETE FROM {skeleton_template_node} WHERE nid = %d", $node->nid);
+      break;
   }
 }
 
@@ -376,13 +377,14 @@
     module_load_include('inc', 'skeleton', 'skeleton_template');
     skeleton_alter_node_form($form, $form_state, $form_id);
   }
-  else if ($form_node == $form_id && arg(0) == 'admin' && arg(2) == 'skeleton' && arg(3) == 'skeleton' && arg(5) == 'create') {
+  else if ($form_node == $form_id && (arg(0) == 'admin' && arg(2) == 'skeleton' && arg(3) == 'skeleton' && arg(5) == 'create')
+    || (arg(0) == 'admin' && arg(2) == 'skeleton' && arg(3) == 'sync')) {
     // We need skeleton_id to be defined so forms validate properly. This value
     // should always be provided by the function instantiating the skeleton.
     $form['skeleton_id'] = array('#type' => 'hidden', '#value' => $form_state['values']['skeleton_id']);
     $form['template_id'] = array('#type' => 'hidden', '#value' => $form_state['values']['template_id']);
     $form['tokens'] = array('#type' => 'hidden', '#value' => $form_state['values']['tokens']);
-  }  
+  }
 }
 
 /**
@@ -396,7 +398,7 @@
  *   FALSE otherwise. 
  */
 function _skeleton_template_connected($node) {
-  return user_access('configure skeleton outlines') && db_result(db_query("SELECT COUNT(1) FROM {skeleton_template_node} WHERE nid = %d", $node->nid));
+  return user_access('configure skeleton outlines') && db_result(db_query("SELECT COUNT(1) FROM {skeleton_template_node} WHERE nid = %d AND template_status != 'overridden'", $node->nid));
 }
 
 /**
Index: skeleton-admin.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/skeleton/skeleton-admin.js,v
retrieving revision 1.2
diff -u -r1.2 skeleton-admin.js
--- skeleton-admin.js	27 Jan 2009 20:06:14 -0000	1.2
+++ skeleton-admin.js	30 Apr 2009 02:40:49 -0000
@@ -10,3 +10,47 @@
     return false;
   });
 });
+
+/**
+ * Add a checkbox to lists of nodes on the Syncronize page to select or clear
+ * all checkboxes at once.
+ */
+Drupal.behaviors.skeletonAutoCheckbox = function(context) {
+  var strings = { 'selectAll': Drupal.t('Select all rows in this table'), 'selectNone': Drupal.t('Deselect all rows in this table') };
+  $('#skeleton-sync-content-form .form-item .form-checkboxes:not(.skeletonAutoCheckbox-processed)').prepend(
+    $('<label></label>').html(Drupal.t('Select all nodes')).prepend($('<input type="checkbox" class="form-checkbox"/>').attr('title', strings.selectAll).click(function() {
+      if (this.checked) {
+        $('#skeleton-sync-content-form .skeletonAutoCheckbox-processed input[type=checkbox]').attr('checked', true);
+      }
+      else {
+        $('#skeleton-sync-content-form .skeletonAutoCheckbox-processed input[type=checkbox]').attr('checked', false);
+      }
+    }))
+  );
+
+  $('#skeleton-sync-content-form .form-item .form-checkboxes').addClass('skeletonAutoCheckbox-processed');
+
+  $('#skeleton-sync-add-template-form .form-item .form-checkboxes:not(.skeletonAutoCheckbox-processed)').prepend(
+    $('<label></label>').html(Drupal.t('Select all nodes')).prepend($('<input type="checkbox" class="form-checkbox"/>').attr('title', strings.selectAll).click(function() {
+      if (this.checked) {
+        $('#skeleton-sync-add-template-form .skeletonAutoCheckbox-processed input[type=checkbox]').attr('checked', true);
+      }
+      else {
+        $('#skeleton-sync-add-template-form .skeletonAutoCheckbox-processed input[type=checkbox]').attr('checked', false);
+      }
+    }))
+  );
+  $('#skeleton-sync-add-template-form .form-item .form-checkboxes').addClass('skeletonAutoCheckbox-processed');
+
+  $('#skeleton-sync-delete-template-form .form-item .form-checkboxes:not(.skeletonAutoCheckbox-processed)').prepend(
+    $('<label></label>').html(Drupal.t('Select all nodes')).prepend($('<input type="checkbox" class="form-checkbox"/>').attr('title', strings.selectAll).click(function() {
+      if (this.checked) {
+        $('#skeleton-sync-delete-template-form .skeletonAutoCheckbox-processed input[type=checkbox]').attr('checked', true);
+      }
+      else {
+        $('#skeleton-sync-delete-template-form .skeletonAutoCheckbox-processed input[type=checkbox]').attr('checked', false);
+      }
+    }))
+  );
+  $('#skeleton-sync-delete-template-form .form-item .form-checkboxes').addClass('skeletonAutoCheckbox-processed');
+}
