Index: feeds.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds.module,v
retrieving revision 1.47
diff -u -r1.47 feeds.module
--- feeds.module	19 Jun 2010 20:34:31 -0000	1.47
+++ feeds.module	12 Jul 2010 08:55:54 -0000
@@ -65,10 +65,10 @@
  */
 function feeds_forms() {
   $forms = array();
-  $forms['FeedsImporter_feeds_config_form']['callback'] = 'feeds_config_form';
+  $forms['FeedsImporter_feeds_form']['callback'] = 'feeds_form';
   $plugins = feeds_get_plugins();
   foreach ($plugins as $plugin) {
-    $forms[$plugin['handler']['class'] .'_feeds_config_form']['callback'] = 'feeds_config_form';
+    $forms[$plugin['handler']['class'] .'_feeds_form']['callback'] = 'feeds_form';
   }
   return $forms;
 }
@@ -103,6 +103,15 @@
         'file' => 'feeds.pages.inc',
         'type' => MENU_LOCAL_TASK,
       );
+      $items['import/'. $importer->id . '/mapping'] = array(
+      'title' => t('Mapping'),
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('feeds_import_mapping_form', 1),
+      'access callback' => 'feeds_access',
+      'access arguments' => array('import', $importer->id),
+      'file' => 'feeds.pages.inc',
+      'type' => MENU_LOCAL_TASK,
+      );
     }
     else {
       $items['node/%node/import'] = array(
@@ -125,7 +134,18 @@
         'type' => MENU_LOCAL_TASK,
         'weight' => 11,
       );
+      $items['node/%node/mapping'] = array(
+        'title' => t('Mapping'),
+        'page callback' => 'drupal_get_form',
+        'page arguments' => array('feeds_import_mapping_form', NULL, 1),
+        'access callback' => 'feeds_access',
+        'access arguments' => array('import', 1),
+        'file' => 'feeds.pages.inc',
+        'type' => MENU_LOCAL_TASK,
+        'weight' => 12,
+      );
     }
+
     $items += $importer->fetcher->menuItem();
   }
   if (count($items)) {
@@ -154,6 +174,9 @@
     'feeds_info' => array(
       'file' => 'feeds.pages.inc',
     ),
+    'feeds_mapping_form' => array(
+      'file' => 'feeds.pages.inc'
+    ),
   );
 }

@@ -301,17 +324,22 @@
         $source->addConfig($node_feeds);
         $source->save();

-        // Refresh feed if import on create is selected and suppress_import is
-        // not set.
-        if ($op == 'insert' && feeds_importer($importer_id)->config['import_on_create'] && !isset($node_feeds['suppress_import'])) {
-          feeds_batch_set(t('Importing'), 'import', $importer_id, $node->nid);
+        if ($source->importer->processor->config['mapping_on_import']) {
+          $_REQUEST['destination'] = 'node/' . $node->nid . '/mapping/' . $source->importer->id;
+        }
+        else {
+          // Refresh feed if import on create is selected and suppress_import is
+          // not set.
+          if ($op == 'insert' && feeds_importer($importer_id)->config['import_on_create'] && !isset($node_feeds['suppress_import'])) {
+            feeds_batch_set(t('Importing'), 'import', $importer_id, $node->nid);
+          }
+          // Add import to scheduler.
+          feeds_scheduler()->add($importer_id, 'import', $node->nid);
+          // Add expiry to schedule, in case this is the first feed of this
+          // configuration.
+          feeds_scheduler()->add($importer_id, 'expire');
+          $node_feeds = NULL;
         }
-        // Add import to scheduler.
-        feeds_scheduler()->add($importer_id, 'import', $node->nid);
-        // Add expiry to schedule, in case this is the first feed of this
-        // configuration.
-        feeds_scheduler()->add($importer_id, 'expire');
-        $node_feeds = NULL;
         break;
       case 'delete':
         // Remove feed from scheduler and delete source.
Index: feeds.pages.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds.pages.inc,v
retrieving revision 1.18
diff -u -r1.18 feeds.pages.inc
--- feeds.pages.inc	4 May 2010 21:24:58 -0000	1.18
+++ feeds.pages.inc	13 Jul 2010 07:35:45 -0000
@@ -59,6 +59,12 @@
     '#type' => 'submit',
     '#value' => t('Import'),
   );
+
+  // If mapping on import is selected, redirect to feeds_import_mapping_form
+  if ($source->importer->processor->config['mapping_on_import']) {
+    $form['#redirect'] = 'import/' . $importer_id . '/mapping';
+  }
+
   return $form;
 }

@@ -74,20 +80,25 @@
  * Submit handler for feeds_import_form().
  */
 function feeds_import_form_submit($form, &$form_state) {
-
   // Save source and import.
   $source = feeds_source($form['#importer_id']);
   $source->addConfig($form_state['values']['feeds']);
   $source->save();

-  // Refresh feed if import on create is selected.
-  if ($source->importer->config['import_on_create']) {
-    feeds_batch_set(t('Importing'), 'import', $form['#importer_id']);
+  if ($source->importer->processor->config['mapping_on_import']) {
+    // Handle the redirection to mapping form
+    $form_state['redirect'] = $form['#redirect'];
+  }
+  else {
+    // Refresh feed if import on create is selected.
+    if ($source->importer->config['import_on_create']) {
+      feeds_batch_set(t('Importing'), 'import', $form['#importer_id']);
+    }
+
+    // Add importer to schedule.
+    feeds_scheduler()->add($form['#importer_id'], 'import');
+    feeds_scheduler()->add($form['#importer_id'], 'expire');
   }
-
-  // Add importer to schedule.
-  feeds_scheduler()->add($form['#importer_id'], 'import');
-  feeds_scheduler()->add($form['#importer_id'], 'expire');
 }

 /**
@@ -152,3 +163,147 @@
   }
   drupal_access_denied();
 }
+
+/**
+ * Declare Feeds mapping on import form
+ */
+function feeds_import_mapping_form(&$form_state, $importer_id, $node = NULL) {
+  if (empty($node)) {
+    $feed_nid = 0;
+  }
+  else {
+    $importer_id = feeds_get_importer_id($node->type);
+    $feed_nid = empty($node) ? 0 : $node->nid;
+  }
+
+  $source = feeds_source($importer_id, $feed_nid);
+
+  // Fetch and parse the source to enable batch to provide mapping sources
+  $batch = $source->importer->fetcher->fetch($source);
+  $source->importer->parser->parse($batch, $source);
+
+  $form = $source->importer->processor->mappingForm($form_state, 'import', $batch);
+
+  // Declare the appropriate configurable and forms method for appropriate submit handler
+  $form['#feeds_form_method'] = 'mappingForm';
+  $form['#configurable'] = $source->importer->processor;
+
+  $form['#importer_id'] = $importer_id;
+  $form['#feed_nid'] = $feed_nid;
+
+  $form['import'] = array(
+    '#type' => 'submit',
+    '#value' => t('Import'),
+    '#submit' => array('feeds_import_mapping_form_import_submit'),
+  );
+  return $form;
+}
+
+/**
+ * Default submit handler for mapping on import form
+ */
+function feeds_import_mapping_form_submit($form, &$form_state) {
+  $form['#configurable']->addConfig($form_state['values']);
+  $form['#configurable']->save();
+  drupal_set_message(t('Your changes have been saved.'));
+  feeds_cache_clear(FALSE);
+}
+
+/**
+ * Import submit handler for mapping on import form
+ */
+function feeds_import_mapping_form_import_submit($form, &$form_state) {
+  // Handle the redirections
+  if (!$form['#feed_nid']) {
+    $form_state['redirect'] = 'import/' . $form['#importer_id'];
+  }
+  else {
+    $form_state['redirect'] = 'node/' . $form['#feed_nid'];
+  }
+
+  $source = feeds_source($form['#importer_id'], $form['#feed_nid']);
+
+  // Refresh feed if import on create is selected.
+  if ($source->importer->config['import_on_create']) {
+    feeds_batch_set(t('Importing'), 'import', $form['#importer_id'], $form['#feed_nid']);
+  }
+
+  // Add importer to schedule.
+  feeds_scheduler()->add($form['#importer_id'], 'import', $form['#feed_nid']);
+  feeds_scheduler()->add($form['#importer_id'], 'expire', $form['#feed_nid']);
+}
+
+/**
+ * Theme function for feeds_mapping_form().
+ */
+function theme_feeds_mapping_form($form) {
+  // Build the actual mapping table.
+  $header = array(
+    t('Source'),
+    t('Target'),
+    t('Unique target'),
+    '&nbsp;',
+  );
+  $rows = array();
+  if (is_array($form['#mappings'])) {
+    foreach ($form['#mappings'] as $i => $mapping) {
+      $rows[] = array(
+        $mapping['source'],
+        $mapping['target'],
+        drupal_render($form['unique_flags'][$i]),
+        drupal_render($form['remove_flags'][$i]),
+      );
+    }
+  }
+  if (!count($rows)) {
+    $rows[] = array(
+      array(
+        'colspan' => 4,
+        'data' => t('No mappings defined.'),
+      ),
+    );
+  }
+  $rows[] = array(
+    drupal_render($form['source']),
+    drupal_render($form['target']),
+    '',
+    drupal_render($form['add']),
+  );
+  $output = '<div class="help feeds-admin-ui""'. drupal_render($form['help']) .'</div>';
+  $output .= theme('table', $header, $rows);
+
+  // Build the help table that explains available sources.
+  $legend = '';
+  $rows = array();
+  foreach (element_children($form['legendset']['legend']['sources']) as $k) {
+    $rows[] = array(
+      drupal_render($form['legendset']['legend']['sources'][$k]['name']),
+      drupal_render($form['legendset']['legend']['sources'][$k]['description']),
+    );
+  }
+  if (count($rows)) {
+    $legend .= '<h4>'. t('Sources') .'</h4>';
+    $legend .= theme('table', array(t('Name'), t('Description')), $rows);
+  }
+
+  // Build the help table that explains available targets.
+  $rows = array();
+  foreach (element_children($form['legendset']['legend']['targets']) as $k) {
+    $rows[] = array(
+      drupal_render($form['legendset']['legend']['targets'][$k]['name']),
+      drupal_render($form['legendset']['legend']['targets'][$k]['description']),
+    );
+  }
+  $legend .= '<h4>'. t('Targets') .'</h4>';
+  $legend .= theme('table', array(t('Name'), t('Description')), $rows);
+
+  // Stick tables into collapsible fieldset.
+  $form['legendset']['legend'] = array(
+    '#value' => '<div>'. $legend .'</div>',
+  );
+
+  $output .= drupal_render($form['legendset']);
+
+  $output .= drupal_render($form);
+  return $output;
+}
Index: feeds_ui/feeds_ui.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/feeds_ui/feeds_ui.admin.inc,v
retrieving revision 1.30
diff -u -r1.30 feeds_ui.admin.inc
--- feeds_ui/feeds_ui.admin.inc	5 May 2010 23:52:48 -0000	1.30
+++ feeds_ui/feeds_ui.admin.inc	13 Jul 2010 02:30:58 -0000
@@ -36,17 +36,6 @@
 }

 /**
- * Help text for mapping.
- */
-function feeds_ui_mapping_help() {
-  return t('
-  <p>
-  Define which elements of a single item of a feed (= <em>Sources</em>) map to which content pieces in Drupal (= <em>Targets</em>). Make sure that at least one definition has a <em>Unique target</em>. A unique target means that a value for a target can only occur once. E. g. only one item with the URL <em>http://example.com/story/1</em> can exist.
-  </p>
-  ');
-}
-
-/**
  * Build overview of available configurations.
  */
 function feeds_ui_overview_form(&$form_status) {
@@ -300,18 +289,20 @@
       ctools_include('dependent');
       if (empty($plugin_key)) {
         $active_container['title'] = t('Basic settings');
-        $active_container['body'] = feeds_get_config_form($importer);
+        $active_container['body'] = feeds_get_form($importer, 'configForm');
       }
       // feeds_plugin_instance() returns a correct result because feed has been
       // instantiated previously.
       elseif (in_array($plugin_key, array_keys($plugins)) && $plugin = feeds_plugin_instance($plugin_key, $importer->id)) {
         $active_container['title'] = t('Settings for !plugin', array('!plugin' => $plugins[$plugin_key]['name']));
-        $active_container['body'] = feeds_get_config_form($plugin);
+        $active_container['body'] = feeds_get_form($plugin, 'configForm');
       }
       break;
     case 'mapping':
-      $active_container['title'] = t('Mapping for !processor', array('!processor' => $plugins[$config['processor']['plugin_key']]['name']));
-      $active_container['body'] = drupal_get_form('feeds_ui_mapping_form', $importer);
+      if (in_array($plugin_key, array_keys($plugins)) && $plugin = feeds_plugin_instance($plugin_key, $importer->id)) {
+        $active_container['title'] = t('Mapping for !processor', array('!processor' => $plugins[$config['processor']['plugin_key']]['name']));
+        $active_container['body'] = feeds_get_form($plugin, 'mappingForm');
+      }
       break;
   }

@@ -346,7 +337,7 @@
   // Fetcher.
   $fetcher = $plugins[$config['fetcher']['plugin_key']];
   $actions = array();
-  if (feeds_get_config_form($importer->fetcher)) {
+  if (feeds_get_form($importer->fetcher, 'configForm')) {
     $actions = array(l(t('Settings'), $path .'/settings/'. $config['fetcher']['plugin_key']));
   }
   $info['title'] = t('Fetcher');
@@ -363,7 +354,7 @@
   // Parser.
   $parser = $plugins[$config['parser']['plugin_key']];
   $actions = array();
-  if (feeds_get_config_form($importer->parser)) {
+  if (feeds_get_form($importer->parser, 'configForm')) {
     $actions = array(l(t('Settings'), $path .'/settings/'. $config['parser']['plugin_key']));
   }
   $info['title'] = t('Parser');
@@ -380,10 +371,10 @@
   // Processor.
   $processor = $plugins[$config['processor']['plugin_key']];
   $actions = array();
-  if (feeds_get_config_form($importer->processor)) {
+  if (feeds_get_form($importer->processor, 'configForm')) {
     $actions[] = l(t('Settings'), $path .'/settings/'. $config['processor']['plugin_key']);
+    $actions[] = l(t('Mapping'), $path .'/mapping/'. $config['processor']['plugin_key']);
   }
-  $actions[] = l(t('Mapping'), $path .'/mapping');
   $info['title'] = t('Processor');
   $info['body'] = array(
     array(
@@ -473,174 +464,6 @@
 }

 /**
- * Edit mapping.
- *
- * @todo Completely merge this into config form handling. This is just a
- *   shared form of configuration, most of the common functionality can live in
- *   FeedsProcessor, a flag can tell whether mapping is supported or not.
- */
-function feeds_ui_mapping_form(&$form_state, $importer) {
-  drupal_add_js(drupal_get_path('module', 'feeds_ui') .'/feeds_ui.js');
-
-  $form = array();
-  $form['#importer'] = $importer;
-  $form['help']['#value'] = feeds_ui_mapping_help();
-
-  // Get mapping sources from parsers and targets from processor, format them
-  // for output.
-  // Some parsers do not define mapping sources but let them define on the fly.
-  if ($sources = $importer->parser->getMappingSources()) {
-    $source_options = _feeds_ui_format_options($sources);
-    foreach ($sources as $k => $source) {
-      $legend['sources'][$k]['name']['#value'] = empty($source['name']) ? $k : $source['name'];
-      $legend['sources'][$k]['description']['#value'] = empty($source['description']) ? '' : $source['description'];
-    }
-  }
-  else {
-    $legend['sources']['#value'] = t('This parser supports free source definitions. Enter the name of the source field in lower case into the Source text field above.');
-  }
-  $targets = $importer->processor->getMappingTargets();
-  $target_options = _feeds_ui_format_options($targets);
-  foreach ($targets as $k => $target) {
-    $legend['targets'][$k]['name']['#value'] = empty($target['name']) ? $k : $target['name'];
-    $legend['targets'][$k]['description']['#value'] = empty($target['description']) ? '' : $target['description'];
-  }
-
-  $form['legendset'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Legend'),
-    '#collapsible' => TRUE,
-    '#collapsed' => TRUE,
-    '#tree' => TRUE,
-  );
-  $form['legendset']['legend'] = $legend;
-
-  // Add unique and remove forms to mappings.
-  $mappings = $importer->processor->getMappings();
-  $form['unique_flags'] = $form['remove_flags'] = array(
-    '#tree' => TRUE,
-  );
-
-  if (is_array($mappings)) {
-    foreach ($mappings as $i => $mapping) {
-
-      $mappings[$i]['source'] = isset($source_options) ? $source_options[$mappings[$i]['source']] : $mappings[$i]['source'];
-      $mappings[$i]['target'] = $target_options[$mappings[$i]['target']];
-      $param = array(
-        'processor' => $importer->processor,
-        'mapping' => $mapping,
-      );
-
-      if (isset($targets[$mapping['target']]['optional_unique']) && $targets[$mapping['target']]['optional_unique'] === TRUE) {
-
-        $form['unique_flags'][$i] = array(
-          '#type' => 'checkbox',
-          '#default_value' => !empty($mapping['unique']),
-          '#attributes' => array('class' => 'feeds-ui-trigger-submit'),
-        );
-      }
-      $form['remove_flags'][$i] = array(
-        '#type' => 'checkbox',
-        '#title' => t('Remove'),
-        '#prefix' => '<div class="feeds-ui-checkbox-link">',
-        '#suffix' => '</div>',
-      );
-    }
-  }
-
-  $form['#mappings'] = $mappings;
-  $form['#targets'] = $targets;
-  if ($sources) {
-    $form['source'] = array(
-      '#type' => 'select',
-      '#options' => array('' => t('Select a source')) + $source_options,
-    );
-  }
-  else {
-    $form['source'] = array(
-      '#type' => 'textfield',
-      '#size' => 20,
-      '#default_value' => t('Name of source field'),
-      '#attributes' => array('class' => 'hide-text-on-focus'),
-    );
-  }
-  $form['target'] = array(
-    '#type' => 'select',
-    '#options' => array('' => t('Select a target')) + _feeds_ui_format_options($targets),
-  );
-  $form['add'] = array(
-    '#type' => 'submit',
-    '#value' => t('Add'),
-    '#submit' => array('feeds_ui_mapping_form_add_submit'),
-  );
-  $form['save'] = array(
-    '#type' => 'submit',
-    '#value' => t('Save'),
-    '#attributes' => array('class' => 'feeds-ui-hidden-submit'),
-  );
-  return $form;
-}
-
-/**
- * Submit handler for add button on feeds_ui_mapping_form().
- */
-function feeds_ui_mapping_form_add_submit($form, &$form_state) {
-  $importer = $form['#importer'];
-  try {
-    $importer->processor->addMapping($form_state['values']['source'], $form_state['values']['target']);
-    $importer->processor->save();
-    drupal_set_message(t('Mapping has been added.'));
-  }
-  catch (Exception $e) {
-    drupal_set_message($e->getMessage(), 'error');
-  }
-}
-
-/**
- * Submit handler for save button on feeds_ui_mapping_form().
- */
-function feeds_ui_mapping_form_submit($form, &$form_state) {
-  $processor = $form['#importer']->processor;
-  $mappings = $processor->config['mappings'];
-  // We may set some unique flags to mappings that we remove in the subsequent
-  // step, that's fine.
-  if (isset($form_state['values']['unique_flags'])) {
-    foreach ($form_state['values']['unique_flags'] as $k => $v) {
-      $processor->setUnique($mappings[$k]['source'], $mappings[$k]['target'], $v);
-    }
-  }
-
-  foreach ($form_state['values']['remove_flags'] as $k => $v) {
-    if ($v) {
-      $processor->removeMapping($mappings[$k]['source'], $mappings[$k]['target']);
-    }
-  }
-  drupal_set_message(t('Your changes have been saved.'));
-  $processor->save();
-}
-
-/**
- * Walk the result of FeedsParser::getMappingSources() or
- * FeedsProcessor::getMappingTargets() and format them into
- * a Form API options array.
- */
-function _feeds_ui_format_options($options) {
-  $result = array();
-  foreach ($options as $k => $v) {
-    if (is_array($v) && !empty($v['name'])) {
-      $result[$k] = $v['name'];
-    }
-    elseif (is_array($v)) {
-      $result[$k] = $k;
-    }
-    else {
-      $result[$k] = $v;
-    }
-  }
-  return $result;
-}
-
-/**
  * Theme feeds_ui_overview_form().
  */
 function theme_feeds_ui_overview_form($form) {
@@ -749,79 +572,3 @@
   $output .= '</div>';
   return $output;
 }
-
-/**
- * Theme function for feeds_ui_mapping_form().
- */
-function theme_feeds_ui_mapping_form($form) {
-
-  // Build the actual mapping table.
-  $header = array(
-    t('Source'),
-    t('Target'),
-    t('Unique target'),
-    '&nbsp;',
-  );
-  $rows = array();
-  if (is_array($form['#mappings'])) {
-    foreach ($form['#mappings'] as $i => $mapping) {
-      $rows[] = array(
-        $mapping['source'],
-        $mapping['target'],
-        drupal_render($form['unique_flags'][$i]),
-        drupal_render($form['remove_flags'][$i]),
-      );
-    }
-  }
-  if (!count($rows)) {
-    $rows[] = array(
-      array(
-        'colspan' => 4,
-        'data' => t('No mappings defined.'),
-      ),
-    );
-  }
-  $rows[] = array(
-    drupal_render($form['source']),
-    drupal_render($form['target']),
-    '',
-    drupal_render($form['add']),
-  );
-  $output = '<div class="help feeds-admin-ui""'. drupal_render($form['help']) .'</div>';
-  $output .= theme('table', $header, $rows);
-
-  // Build the help table that explains available sources.
-  $legend = '';
-  $rows = array();
-  foreach (element_children($form['legendset']['legend']['sources']) as $k) {
-    $rows[] = array(
-      drupal_render($form['legendset']['legend']['sources'][$k]['name']),
-      drupal_render($form['legendset']['legend']['sources'][$k]['description']),
-    );
-  }
-  if (count($rows)) {
-    $legend .= '<h4>'. t('Sources') .'</h4>';
-    $legend .= theme('table', array(t('Name'), t('Description')), $rows);
-  }
-
-  // Build the help table that explains available targets.
-  $rows = array();
-  foreach (element_children($form['legendset']['legend']['targets']) as $k) {
-    $rows[] = array(
-      drupal_render($form['legendset']['legend']['targets'][$k]['name']),
-      drupal_render($form['legendset']['legend']['targets'][$k]['description']),
-    );
-  }
-  $legend .= '<h4>'. t('Targets') .'</h4>';
-  $legend .= theme('table', array(t('Name'), t('Description')), $rows);
-
-  // Stick tables into collapsible fieldset.
-  $form['legendset']['legend'] = array(
-    '#value' => '<div>'. $legend .'</div>',
-  );
-
-  $output .= drupal_render($form['legendset']);
-
-  $output .= drupal_render($form);
-  return $output;
-}
Index: includes/FeedsBatch.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/includes/FeedsBatch.inc,v
retrieving revision 1.12
diff -u -r1.12 FeedsBatch.inc
--- includes/FeedsBatch.inc	20 Jun 2010 01:10:29 -0000	1.12
+++ includes/FeedsBatch.inc	13 Jul 2010 02:41:40 -0000
@@ -180,4 +180,20 @@
     $this->items[] = $item;
     $this->total = count($this->items);
   }
+
+  /**
+   * Get mapping sources after the batch items has been populated
+   */
+  public function getMappingSources() {
+    $sources = array();
+    if ($this->total > 0) {
+      foreach ($this->items[0] as $k => $v) {
+        $sources[$k] = array(
+          'title' => $k,
+          'description' => '',
+        );
+      }
+    }
+    return $sources;
+  }
 }
Index: includes/FeedsConfigurable.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/includes/FeedsConfigurable.inc,v
retrieving revision 1.12
diff -u -r1.12 FeedsConfigurable.inc
--- includes/FeedsConfigurable.inc	28 Feb 2010 17:16:25 -0000	1.12
+++ includes/FeedsConfigurable.inc	13 Jul 2010 07:37:05 -0000
@@ -178,31 +178,43 @@
    */
   public function configFormValidate(&$values) {
   }
+
+  /**
+   * Submission handler for configForm().
+   *
+   * @param $values
+   */
+  public function configFormSubmit(&$values) {
+    $this->addConfig($values);
+    $this->save();
+    drupal_set_message(t('Your changes have been saved.'));
+    feeds_cache_clear(FALSE);
+  }
 }

 /**
  * Config form wrapper. Use to render the configuration form of
  * a FeedsConfigurable object.
  *
- * @param
+ * @param $configurable
  *   FeedsConfigurable object.
- * @param
- *   The parent object to perform the save on.
+ * @param $form_method
+ *   The form method that should be rendered.
  *
  * @return
  *   Rendered config form, if available. Empty string otherwise.
  */
-function feeds_get_config_form($configurable) {
+function feeds_get_form($configurable, $form_method) {
   $form_state = array();
-  if ($configurable->configForm($form_state)) {
-    return drupal_get_form(get_class($configurable) .'_feeds_config_form', $configurable);
+  if (method_exists($configurable, $form_method)) {
+    return drupal_get_form(get_class($configurable) .'_feeds_form', $configurable, $form_method);
   }
   return '';
 }

 /**
  * Config form callback. Don't call directly, but use
- * feeds_get_config_form($configurable) instead.
+ * feeds_get_form($configurable, 'method') instead.
  *
  * @param
  *   FormAPI $form_state.
@@ -210,12 +222,15 @@
  *   FeedsConfigurable object.
  * @param
  *   The object to perform the save() operation on.
+ * @param $form_method
+ *   The $form_method that should be rendered.
  */
-function feeds_config_form(&$form_state, $configurable) {
-  $form = $configurable->configForm($form_state);
+function feeds_form(&$form_state, $configurable, $form_method) {
+  $form = $configurable->$form_method($form_state);
   $form['#configurable'] = $configurable;
-  $form['#validate'] = array('feeds_config_form_validate');
-  $form['#submit'] = array('feeds_config_form_submit');
+  $form['#feeds_form_method'] = $form_method;
+  $form['#validate'] = array('feeds_form_validate');
+  $form['#submit'] = array('feeds_form_submit');
   $form['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
@@ -225,18 +240,21 @@
 }

 /**
- * Validation handler for feeds_config_form().
+ * Validation handler for feeds_form().
  */
-function feeds_config_form_validate($form, &$form_state) {
-  $form['#configurable']->configFormValidate($form_state['values']);
+function feeds_form_validate($form, &$form_state) {
+  $validate_method = $form['#feeds_form_method'] .'Validate';
+  if (method_exists($form['#configurable'], $validate_method)) {
+    $form['#configurable']->$validate_method($form_state['values']);
+  }
 }

 /**
- * Submit handler for feeds_config_form().
+ * Submit handler for feeds_form().
  */
-function feeds_config_form_submit($form, &$form_state) {
-  $form['#configurable']->addConfig($form_state['values']);
-  $form['#configurable']->save();
-  drupal_set_message(t('Your changes have been saved.'));
-  feeds_cache_clear(FALSE);
+function feeds_form_submit($form, &$form_state) {
+  $submit_method = $form['#feeds_form_method'] .'Submit';
+  if (method_exists($form['#configurable'], $submit_method)) {
+    $form['#configurable']->$submit_method($form_state['values']);
+  }
 }
Index: includes/FeedsSource.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/includes/FeedsSource.inc,v
retrieving revision 1.13
diff -u -r1.13 FeedsSource.inc
--- includes/FeedsSource.inc	19 Jun 2010 19:40:43 -0000	1.13
+++ includes/FeedsSource.inc	13 Jul 2010 07:35:50 -0000
@@ -106,6 +106,9 @@
   protected function __construct($importer_id, $feed_nid) {
     $this->feed_nid = $feed_nid;
     $this->importer = feeds_importer($importer_id);
+
+    // Add source into the processor object
+    $this->importer->processor->source = $this;
     parent::__construct($importer_id);
     $this->load();
   }
@@ -235,6 +238,23 @@
   }

   /**
+   * Overrides FeedsConfigurable::addConfig()
+   * The config in FeedsSource is hierarchical, thus deeper filtering is necessary.
+   */
+
+  public function addConfig($config) {
+    $this->config = array_merge($this->config, $config);
+    $default_keys = $this->configDefaults();
+    $this->config = array_intersect_key($this->config, $default_keys);
+
+    foreach($this->config as $k => $v) {
+      if (is_array($this->config[$k]) && is_array($default_keys[$k])) {
+        $this->config[$k] = array_intersect_key($this->config[$k], $default_keys[$k]);
+      }
+    }
+  }
+
+  /**
    * Convenience function. Returns the configuration for a specific class.
    *
    * @param FeedsSourceInterface $client
Index: plugins/FeedsCSVParser.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsCSVParser.inc,v
retrieving revision 1.9
diff -u -r1.9 FeedsCSVParser.inc
--- plugins/FeedsCSVParser.inc	28 Apr 2010 15:37:32 -0000	1.9
+++ plugins/FeedsCSVParser.inc	13 Jul 2010 06:08:41 -0000
@@ -67,7 +67,7 @@
     $form = array();
     $form['#weight'] = -10;

-    $mappings = feeds_importer($this->id)->processor->config['mappings'];
+    $mappings = feeds_importer($this->id)->processor->getMappings();
     $sources = $uniques = array();
     foreach ($mappings as $mapping) {
       $sources[] = $mapping['source'];
Index: plugins/FeedsDataProcessor.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsDataProcessor.inc,v
retrieving revision 1.13
diff -u -r1.13 FeedsDataProcessor.inc
--- plugins/FeedsDataProcessor.inc	16 May 2010 21:51:58 -0000	1.13
+++ plugins/FeedsDataProcessor.inc	12 Jul 2010 08:55:54 -0000
@@ -255,6 +255,7 @@
       'update_existing' => 0,
       'expire' => FEEDS_EXPIRE_NEVER, // Don't expire items by default.
       'mappings' => array(),
+      'mapping_on_import' => 0,
     );
   }

Index: plugins/FeedsFeedNodeProcessor.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsFeedNodeProcessor.inc,v
retrieving revision 1.12
diff -u -r1.12 FeedsFeedNodeProcessor.inc
--- plugins/FeedsFeedNodeProcessor.inc	28 Apr 2010 22:18:30 -0000	1.12
+++ plugins/FeedsFeedNodeProcessor.inc	12 Jul 2010 08:55:54 -0000
@@ -107,6 +107,7 @@
       'content_type' => '',
       'update_existing' => 0,
       'mappings' => array(),
+      'mapping_on_import' => 0,
     );
   }

Index: plugins/FeedsNodeProcessor.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsNodeProcessor.inc,v
retrieving revision 1.38
diff -u -r1.38 FeedsNodeProcessor.inc
--- plugins/FeedsNodeProcessor.inc	19 Jun 2010 18:03:41 -0000	1.38
+++ plugins/FeedsNodeProcessor.inc	12 Jul 2010 08:55:54 -0000
@@ -146,6 +146,7 @@
       'expire' => FEEDS_EXPIRE_NEVER,
       'mappings' => array(),
       'author' => 0,
+      'mapping_on_import' => 0,
     );
   }

Index: plugins/FeedsProcessor.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsProcessor.inc,v
retrieving revision 1.11
diff -u -r1.11 FeedsProcessor.inc
--- plugins/FeedsProcessor.inc	19 Jun 2010 15:57:13 -0000	1.11
+++ plugins/FeedsProcessor.inc	13 Jul 2010 07:35:40 -0000
@@ -104,7 +104,10 @@
     If the mapping specifies a callback method, use the callback instead of
     setTargetElement().
     */
-    foreach ($this->config['mappings'] as $mapping) {
+
+    $mappings = $this->getMappings();
+
+    foreach ($mappings as $mapping) {
       $value = $parser->getSourceElement($source_item, $mapping['source']);

       if (is_array($targets[$this->id][$mapping['target']]) && isset($targets[$this->id][$mapping['target']]['callback']) && function_exists($targets[$this->id][$mapping['target']]['callback'])) {
@@ -130,62 +133,28 @@
    * Declare default configuration.
    */
   public function configDefaults() {
-    return array('mappings' => array());
+    return array(
+      'mappings' => array(),
+      'mapping_on_import' => 0,
+    );
   }

   /**
-   * Add a mapping to existing mappings.
-   *
-   * @param $source
-   *   A string that identifies a source element.
-   * @param $target
-   *   A string that identifies a target element.
-   * @param $unique
-   *   A boolean that defines whether the target value should be unique. If
-   *   TRUE only one item with a given target value can exist on the local
-   *   system. Compare with existingItemId() and uniqueTargets().
-   */
-  public function addMapping($source, $target, $unique = FALSE) {
-    if (!empty($source) && !empty($target)) {
-      $this->config['mappings'][] = array(
-        'source' => $source,
-        'target' => $target,
-        'unique' => $unique,
-      );
-    }
-  }
-
-  /**
-   * Set unique state of a mapping target.
+   * Get mappings.
+   * Mappings must not be got directly by using $this->config['mappings'] but instead via
+   * this function.
    */
-  public function setUnique($source, $target, $unique) {
-    if (!empty($source) && !empty($target)) {
-      foreach ($this->config['mappings'] as $k => $mapping) {
-        if ($mapping['source'] == $source && $mapping['target'] == $target) {
-          $this->config['mappings'][$k]['unique'] = $unique;
-        }
-      }
+  public function getMappings() {
+    // If the source is not empty, use the source instead.
+    if (isset($this->source)) {
+      $config = $this->source->getConfigFor($this);
+      $mappings = isset($config['mappings']) ? $config['mappings'] : $this->config['mappings'];
     }
-  }
-
-  /**
-   * Remove a mapping.
-   */
-  public function removeMapping($source, $target) {
-    foreach ($this->config['mappings'] as $k => $mapping) {
-      if ($mapping['source'] == $source && $mapping['target'] == $target) {
-        unset($this->config['mappings'][$k]);
-      }
+    else {
+      $mappings = $this->config['mappings'];
     }
-    // Keep or keys clean.
-    $this->config['mappings'] = array_values($this->config['mappings']);
-  }

-  /**
-   * Get mappings.
-   */
-  public function getMappings() {
-    return isset($this->config['mappings']) ? $this->config['mappings'] : array();
+    return $mappings;
   }

   /**
@@ -239,7 +208,7 @@
   protected function uniqueTargets($source_item) {
     $parser = feeds_importer($this->id)->parser;
     $targets = array();
-    foreach ($this->config['mappings'] as $mapping) {
+    foreach ($this->getMappings() as $mapping) {
       if ($mapping['unique']) {
         // Invoke the parser's getSourceElement to retrieve the value for this
         // mapping's source.
@@ -273,4 +242,260 @@
     }
     $loaded = TRUE;
   }
+
+  /**
+   * configDefaults for FeedsSource
+   */
+  function sourceDefaults() {
+    return array ('mappings' => $this->config['mappings']);
+  }
+
+  /**
+   * FeedsProcessor has source config
+   * default: mappings
+   */
+  function hasSourceConfig() {
+    return TRUE;
+  }
+
+  /**
+   * Render the mapping form.
+   * This function should NOT be overwritten.
+   * @param
+   *   The location from where this function is called. For now, it will be passed either
+   *   'import' or 'config'
+   * @param
+   *   The batch object that contains populated items that has been fetched and parsed.
+   *   This will only be handled if the first parameter is NOT 'config'
+   */
+  public function mappingForm(&$form_state, $loc_id = 'config', $batch = NULL) {
+    drupal_add_js(drupal_get_path('module', 'feeds_ui') .'/feeds_ui.js');
+
+    // Get the FeedsImporter object
+    $importer = feeds_importer($this->id);
+
+    $form = array();
+    if ($loc_id == 'config') {
+      // Mapping on import checkbox
+      $form['mapping_on_import'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Define mapping on import'),
+        '#description' => t('If this is checked, the mapping form here will only act as a default mapping. User can further specify the preferred mapping in the import page.'),
+        '#weight' => -100,
+        '#default_value' => $this->config['mapping_on_import'],
+      );
+    }
+
+    $form['#theme'] = 'feeds_mapping_form';
+    $form['help']['#value'] = feeds_mapping_form_help();
+
+    // Get mapping sources from parsers or batch and targets from processor, format them
+    // for output.
+    // Some parsers do not define mapping sources but let them define on the fly.
+    if ($loc_id == 'config' || empty($batch)) {
+      $sources = $importer->parser->getMappingSources();
+    }
+    else {
+      $sources = $batch->getMappingSources();
+    }
+
+    if ($sources) {
+      $source_options = _feeds_mapping_form_format_options($sources);
+      foreach ($sources as $k => $source) {
+        $legend['sources'][$k]['name']['#value'] = empty($source['name']) ? $k : $source['name'];
+        $legend['sources'][$k]['description']['#value'] = empty($source['description']) ? '' : $source['description'];
+      }
+    }
+    else {
+      $legend['sources']['#value'] = t('This parser supports free source definitions. Enter the name of the source field in lower case into the Source text field above.');
+    }
+    $targets = $importer->processor->getMappingTargets();
+    $target_options = _feeds_mapping_form_format_options($targets);
+    foreach ($targets as $k => $target) {
+      $legend['targets'][$k]['name']['#value'] = empty($target['name']) ? $k : $target['name'];
+      $legend['targets'][$k]['description']['#value'] = empty($target['description']) ? '' : $target['description'];
+    }
+
+    $form['legendset'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Legend'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+      '#tree' => TRUE,
+    );
+    $form['legendset']['legend'] = $legend;
+
+    $mappings = $this->getMappings();
+
+    // Add unique and remove forms to mappings.
+    $form['unique_flags'] = $form['remove_flags'] = array(
+      '#tree' => TRUE,
+    );
+
+    if (is_array($mappings)) {
+      $form['mappings']['#tree'] = TRUE;
+
+      foreach ($mappings as $i => $mapping) {
+        $mappings[$i]['source'] = isset($source_options) ? $source_options[$mappings[$i]['source']] : $mappings[$i]['source'];
+        $mappings[$i]['target'] = $target_options[$mappings[$i]['target']];
+        $param = array(
+          'processor' => $importer->processor,
+          'mapping' => $mapping,
+        );
+
+        if (isset($targets[$mapping['target']]['optional_unique']) && $targets[$mapping['target']]['optional_unique'] === TRUE) {
+
+          $form['unique_flags'][$i] = array(
+            '#type' => 'checkbox',
+            '#default_value' => !empty($mapping['unique']),
+            '#attributes' => array('class' => 'feeds-ui-trigger-submit'),
+          );
+        }
+        $form['remove_flags'][$i] = array(
+          '#type' => 'checkbox',
+          '#title' => t('Remove'),
+          '#prefix' => '<div class="feeds-ui-checkbox-link">',
+          '#suffix' => '</div>',
+        );
+
+        // Declare the mappings data in the hidden field, so later
+        // the full mapping data will be submitted.
+        foreach($mapping as $k => $v) {
+          $form['mappings'][$i][$k] = array (
+            '#type' => 'hidden',
+            '#value' => $v,
+          );
+        }
+      }
+
+      $form['#mappings'] = $mappings;
+      $form['#targets'] = $targets;
+      if ($sources) {
+        $form['source'] = array(
+          '#type' => 'select',
+          '#options' => array('' => t('Select a source')) + $source_options,
+        );
+      }
+      else {
+        $form['source'] = array(
+          '#type' => 'textfield',
+          '#size' => 20,
+          '#default_value' => t('Name of source field'),
+          '#attributes' => array('class' => 'hide-text-on-focus'),
+        );
+      }
+      $form['target'] = array(
+        '#type' => 'select',
+        '#options' => array('' => t('Select a target')) + _feeds_mapping_form_format_options($targets),
+      );
+      $form['add'] = array(
+        '#type' => 'submit',
+        '#value' => t('Add'),
+        '#submit' => array('feeds_form_submit'),
+      );
+    }
+
+    $form['save'] = array(
+      '#type' => 'submit',
+      '#value' => t('Save'),
+      '#attributes' => array('class' => 'feeds-ui-hidden-submit'),
+      '#submit' => array('feeds_form_submit'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * Submit handler for FeedsProcessor::mappingForm()
+   */
+  function mappingFormSubmit(&$values) {
+    // If add new mappings submit is invoked.
+    if ($values['op'] == 'Add') {
+      if (!empty($values['source']) && !empty($values['target'])) {
+        $values['mappings'][] = array(
+          'source' => $values['source'],
+          'target' => $values['target'],
+          'unique' => FALSE,
+        );
+      }
+    }
+    // If other submit is invoked.
+    else {
+      // We may set some unique flags to mappings that we remove in the subsequent
+      // step, that's fine.
+      if (isset($values['unique_flags'])) {
+        foreach ($values['unique_flags'] as $k => $v) {
+          $values['mappings'][$k]['unique'] = $v;
+        }
+      }
+
+      if (isset($values['remove_flags'])) {
+        foreach ($values['remove_flags'] as $k => $v) {
+          if ($v) {
+            unset($values['mappings'][$k]);
+            // Keep or keys clean.
+            $values['mappings'] = array_values($values['mappings']);
+          }
+        }
+      }
+    }
+
+    if ($this->config['mapping_on_import'] && !empty($this->source)) {
+      $source_values[get_class($this)] = $values;
+      $this->source->addConfig($source_values);
+      $this->source->save();
+    }
+    else {
+      $this->addConfig($values);
+      $this->save();
+    }
+
+    drupal_set_message(t('Your changes have been saved.'));
+    feeds_cache_clear(FALSE);
+  }
+
+  /**
+   * Validation handler for mappingForm()
+   */
+  function mappingFormValidate(&$values) {
+  }
+}
+
+/**
+ * Walk the result of FeedsParser::getMappingSources() or
+ * FeedsProcessor::getMappingTargets() and format them into
+ * a Form API options array.
+ */
+function _feeds_mapping_form_format_options($options) {
+  $result = array();
+  foreach ($options as $k => $v) {
+    if (is_array($v) && !empty($v['name'])) {
+      $result[$k] = $v['name'];
+    }
+    elseif (is_array($v)) {
+      $result[$k] = $k;
+    }
+    else {
+      $result[$k] = $v;
+    }
+  }
+  return $result;
+}
+
+/**
+ * Help text for mapping.
+ */
+function feeds_mapping_form_help() {
+  return
+    '<p>' .
+    t('Define which elements of a single item of a feed') .
+    '(= <em>' . t('Sources') . '</em>) ' .
+    t('map to which content pieces in Drupal') .
+    ' (= <em>' . t('Targets') . '</em>). ' .
+    t('Make sure that at least one definition has a') .
+    ' <em>' . t('Unique target') . '</em>. ' .
+    t('A unique target means that a value for a target can only occur once. E. g. only one item with the URL ') .
+    '<em>http://example.com/story/1</em> ' .
+    t('can exist.') .
+    '</p>';
 }
Index: plugins/FeedsTermProcessor.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsTermProcessor.inc,v
retrieving revision 1.9
diff -u -r1.9 FeedsTermProcessor.inc
--- plugins/FeedsTermProcessor.inc	28 Apr 2010 22:18:30 -0000	1.9
+++ plugins/FeedsTermProcessor.inc	12 Jul 2010 08:55:54 -0000
@@ -126,6 +126,7 @@
       'vocabulary' => 0,
       'update_existing' => 0,
       'mappings' => array(),
+      'mapping_on_import' => 0,
     );
   }

Index: plugins/FeedsUserProcessor.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsUserProcessor.inc,v
retrieving revision 1.12
diff -u -r1.12 FeedsUserProcessor.inc
--- plugins/FeedsUserProcessor.inc	19 Jun 2010 15:57:13 -0000	1.12
+++ plugins/FeedsUserProcessor.inc	12 Jul 2010 08:55:54 -0000
@@ -117,6 +117,7 @@
       'update_existing' => FALSE,
       'status' => 1,
       'mappings' => array(),
+      'mapping_on_import' => 0,
     );
   }


