diff --git a/feeds.module b/feeds.module index f0dd48c..0afa6bc 100644 --- a/feeds.module +++ b/feeds.module @@ -296,6 +296,15 @@ function feeds_menu() { 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); + $items['import/%feeds_importer/mapping'] = array( + 'title' => 'Mapping', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('feeds_import_mapping_form', 1), + 'access callback' => 'feeds_access', + 'access arguments' => array('import', 1), + 'file' => 'feeds.pages.inc', + 'type' => MENU_LOCAL_TASK, + ); $items['import/%feeds_importer/delete-items'] = array( 'title' => 'Delete items', 'page callback' => 'drupal_get_form', @@ -352,6 +361,16 @@ function feeds_menu() { 'type' => MENU_LOCAL_TASK, 'weight' => 11, ); + $items['node/%node/mapping'] = array( + 'title' => '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, + ); // @todo Eliminate this step and thus eliminate clearing menu cache when // manipulating importers. foreach (feeds_importer_load_all() as $importer) { @@ -403,6 +422,10 @@ function feeds_theme() { 'file' => 'feeds.pages.inc', 'render element' => 'element', ), + 'feeds_mapping_form' => array( + 'file' => 'feeds.pages.inc', + 'render element' => 'form', + ), 'feeds_source_status' => array( 'file' => 'feeds.pages.inc', 'variables' => array( @@ -644,8 +667,12 @@ function feeds_node_insert($node) { feeds_node_update($node); if (isset($node->feeds) && $importer_id = feeds_get_importer_id($node->type)) { $source = feeds_source($importer_id, $node->nid); - // Start import if requested. - if (feeds_importer($importer_id)->config['import_on_create'] && !isset($node->feeds['suppress_import'])) { + // Redirect to mapping page if necessary. + if ($source->importer->processor->config['mapping_on_import']) { + $_REQUEST['destination'] = 'node/' . $node->nid . '/mapping/' . $importer_id; + } + // Otherwise start import if requested. + elseif (feeds_importer($importer_id)->config['import_on_create'] && !isset($node->feeds['suppress_import'])) { $source->startImport(); } // Schedule the source. @@ -1390,3 +1417,157 @@ function feeds_api_version() { $version = feeds_ctools_plugin_api('feeds', 'plugins'); return $version['version']; } + +/** + * Per mapper configuration form that is a part of feeds_mapping_form(). + */ +function feeds_mapping_settings_form($form, $form_state, $i, $mapping, $target) { + $form_state += array( + 'mapping_settings_edit' => NULL, + 'mapping_settings' => array(), + ); + + $base_button = array( + '#submit' => array('feeds_mapping_form_multistep_submit'), + '#ajax' => array( + 'callback' => 'feeds_mapping_settings_form_callback', + 'wrapper' => 'feeds-ui-mapping-form-wrapper', + 'effect' => 'fade', + 'progress' => 'none', + ), + '#i' => $i, + ); + + if (isset($form_state['mapping_settings'][$i])) { + $mapping = $form_state['mapping_settings'][$i] + $mapping; + } + + if ($form_state['mapping_settings_edit'] === $i) { + // Build the form. + if (isset($target['form_callback'])) { + $settings_form = call_user_func($target['form_callback'], $mapping, $target, $form, $form_state); + } + else { + $settings_form = array(); + } + + // Merge in the optional unique form. + $settings_form += feeds_mapping_settings_optional_unique_form($mapping, $target, $form, $form_state); + + return array( + '#type' => 'container', + 'settings' => $settings_form, + 'save_settings' => $base_button + array( + '#type' => 'submit', + '#name' => 'mapping_settings_update_' . $i, + '#value' => t('Update'), + '#op' => 'update', + ), + 'cancel_settings' => $base_button + array( + '#type' => 'submit', + '#name' => 'mapping_settings_cancel_' . $i, + '#value' => t('Cancel'), + '#op' => 'cancel', + ), + ); + } + else { + // Build the summary. + if (isset($target['summary_callback'])) { + $summary = call_user_func($target['summary_callback'], $mapping, $target, $form, $form_state); + } + else { + $summary = ''; + } + + // Append the optional unique summary. + if ($optional_unique_summary = feeds_mapping_settings_optional_unique_summary($mapping, $target, $form, $form_state)) { + $summary .= ' ' . $optional_unique_summary; + } + + if ($summary) { + return array( + 'summary' => array( + '#prefix' => '
', + '#markup' => $summary, + '#suffix' => '
', + ), + 'edit_settings' => $base_button + array( + '#type' => 'image_button', + '#name' => 'mapping_settings_edit_' . $i, + '#src' => 'misc/configure.png', + '#attributes' => array('alt' => t('Edit')), + '#op' => 'edit', + ), + ); + } + } +} + +/** + * AJAX callback that returns the whole feeds_mapping_form(). + */ +function feeds_mapping_settings_form_callback($form, $form_state) { + return $form; +} + +/** + * Submit callback for a per mapper configuration form. Switches between edit + * and summary mode. + */ +function feeds_mapping_form_multistep_submit($form, &$form_state) { + $trigger = $form_state['triggering_element']; + + switch ($trigger['#op']) { + case 'edit': + $form_state['mapping_settings_edit'] = $trigger['#i']; + break; + + case 'update': + $values = $form_state['values']['config'][$trigger['#i']]['settings']; + $form_state['mapping_settings'][$trigger['#i']] = $values; + unset($form_state['mapping_settings_edit']); + break; + + case 'cancel': + unset($form_state['mapping_settings_edit']); + break; + } + + $form_state['rebuild'] = TRUE; +} + +/** + * Per mapping settings summary callback. Shows whether a mapping is used as + * unique or not. + */ +function feeds_mapping_settings_optional_unique_summary($mapping, $target, $form, $form_state) { + if (!empty($target['optional_unique'])) { + if ($mapping['unique']) { + return t('Used as unique.'); + } + else { + return t('Not used as unique.'); + } + } +} + +/** + * Per mapping settings form callback. Lets the user choose if a target is as + * unique or not. + */ +function feeds_mapping_settings_optional_unique_form($mapping, $target, $form, $form_state) { + $settings_form = array(); + + if (!empty($target['optional_unique'])) { + $settings_form['unique'] = array( + '#type' => 'checkbox', + '#title' => t('Unique'), + '#default_value' => !empty($mapping['unique']), + ); + } + + return $settings_form; +} + + diff --git a/feeds.pages.inc b/feeds.pages.inc index eeeab90..c3b1dee 100644 --- a/feeds.pages.inc +++ b/feeds.pages.inc @@ -89,6 +89,12 @@ function feeds_import_form(array $form, array &$form_state, FeedsImporter $impor '#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'; + } + $progress = $source->progressImporting(); if ($progress !== FEEDS_BATCH_COMPLETE) { $form['submit']['#disabled'] = TRUE; @@ -118,13 +124,20 @@ function feeds_import_form_submit($form, &$form_state) { $source->save(); } - // Refresh feed if import on create is selected. - if ($source->importer->config['import_on_create']) { - $source->startImport(); + 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']) { + $source->startImport(); + } + + // Add to schedule, make sure importer is scheduled, too. + $source->schedule(); } - // Add to schedule, make sure importer is scheduled, too. - $source->schedule(); } /** @@ -377,3 +390,163 @@ function theme_feeds_upload($variables) { return $output; } + +/** + * Declare Feeds mapping on import form + */ +function feeds_import_mapping_form($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 result to provide mapping sources. + $fetcher = $source->importer->fetcher->fetch($source); + // Signify to the parser that we're only after the fieldnames for mapping. + $result = $source->importer->parser->parse($source, $fetcher); + $result->context = 'mapping'; + + $form = $source->importer->processor->mappingForm($form_state, $result); + + // 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']) { + $source->startImport(); + } + + // Add to schedule, make sure importer is scheduled, too. + $source->schedule(); + $source->importer->schedule(); +} + +/** + * Theme function for feeds_mapping_form(). + */ +function theme_feeds_mapping_form($variables) { + $form = $variables['form']; + + // Build the actual mapping table. + $header = array( + t('Source'), + t('Target'), + t('Target configuration'), + ' ', + t('Weight'), + ); + $rows = array(); + if (is_array($form['#mappings'])) { + foreach ($form['#mappings'] as $i => $mapping) { + // Some parsers do not define source options. + $source = isset($form['source']['#options'][$mapping['source']]) ? $form['source']['#options'][$mapping['source']] : $mapping['source']; + $target = isset($form['target']['#options'][$mapping['target']]) ? check_plain($form['target']['#options'][$mapping['target']]) : '' . t('Missing') . ''; + $rows[] = array( + 'data' => array( + check_plain($source), + $target, + drupal_render($form['config'][$i]), + drupal_render($form['remove_flags'][$i]), + drupal_render($form['mapping_weight'][$i]), + ), + 'class' => array('draggable', 'tabledrag-leaf'), + ); + } + } + if (!count($rows)) { + $rows[] = array( + array( + 'colspan' => 5, + 'data' => t('No mappings defined.'), + ), + ); + } + $rows[] = array( + drupal_render($form['source']), + drupal_render($form['target']), + '', + drupal_render($form['add']), + '', + ); + $output = '
' . drupal_render($form['help']) . '
'; + $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'feeds-mapping-overview'))); + + // Build the help table that explains available sources. + $legend = ''; + $rows = array(); + foreach (element_children($form['legendset']['legend']['sources']) as $k) { + $rows[] = array( + check_plain(drupal_render($form['legendset']['legend']['sources'][$k]['name'])), + check_plain(drupal_render($form['legendset']['legend']['sources'][$k]['description'])), + ); + } + if (count($rows)) { + $legend .= '

' . t('Sources') . '

'; + $legend .= theme('table', array('header' => array(t('Name'), t('Description')), 'rows' => $rows)); + } + + // Build the help table that explains available targets. + $rows = array(); + foreach (element_children($form['legendset']['legend']['targets']) as $k) { + $rows[] = array( + check_plain(drupal_render($form['legendset']['legend']['targets'][$k]['name'])), + check_plain(drupal_render($form['legendset']['legend']['targets'][$k]['description'])), + ); + } + $legend .= '

' . t('Targets') . '

'; + $legend .= theme('table', array('header' => array(t('Name'), t('Description')), 'rows' => $rows)); + + // Stick tables into collapsible fieldset. + $form['legendset']['legend'] = array( + '#markup' => '
' . $legend . '
', + ); + + $output .= drupal_render($form['legendset']); + $output .= drupal_render_children($form); + + drupal_add_tabledrag('feeds-mapping-overview', 'order', 'sibling', 'feeds-mapping-weight'); + return $output; +} + diff --git a/feeds_ui/feeds_ui.admin.inc b/feeds_ui/feeds_ui.admin.inc index f975ffe..77a8a3b 100644 --- a/feeds_ui/feeds_ui.admin.inc +++ b/feeds_ui/feeds_ui.admin.inc @@ -35,17 +35,6 @@ function feeds_ui_edit_help() { } /** - * Help text for mapping. - */ -function feeds_ui_mapping_help() { - return t(' -

- Define which elements of a single item of a feed (= Sources) map to which content pieces in Drupal (= Targets). Make sure that at least one definition has a Unique target. A unique target means that a value for a target can only occur once. E. g. only one item with the URL http://example.com/content/1 can exist. -

- '); -} - -/** * Build overview of available configurations. */ function feeds_ui_overview_form($form, &$form_status) { @@ -315,9 +304,11 @@ function feeds_ui_edit_page(FeedsImporter $importer, $active = 'help', $plugin_k break; case 'mapping': - $processor_name = isset($plugins[$config['processor']['plugin_key']]['name']) ? $plugins[$config['processor']['plugin_key']]['name'] : $plugins['FeedsMissingPlugin']['name']; - $active_container['title'] = t('Mapping for @processor', array('@processor' => $processor_name)); - $active_container['body'] = drupal_get_form('feeds_ui_mapping_form', $importer); + if (in_array($plugin_key, array_keys($plugins)) && $plugin = feeds_plugin($plugin_key, $importer->id)) { + $processor_name = isset($plugins[$config['processor']['plugin_key']]['name']) ? $plugins[$config['processor']['plugin_key']]['name'] : $plugins['FeedsMissingPlugin']['name']; + $active_container['title'] = t('Mapping for @processor', array('@processor' => $processor_name)); + $active_container['body'] = feeds_get_form($plugin, 'mappingForm'); + } break; } @@ -388,8 +379,8 @@ function feeds_ui_edit_page(FeedsImporter $importer, $active = 'help', $plugin_k $actions = array(); if ($importer->processor->hasConfigForm()) { $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( @@ -484,403 +475,6 @@ function theme_feeds_ui_plugin_form($variables) { } /** - * 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, &$form_state, $importer) { - $form['#importer'] = $importer->id; - $form['#mappings'] = $mappings = $importer->processor->getMappings(); - $form['help']['#markup'] = feeds_ui_mapping_help(); - $form['#prefix'] = '
'; - $form['#suffix'] = '
'; - - // Show message when target configuration gets changed. - if (!empty($form_state['mapping_settings'])) { - $form['#mapping_settings'] = $form_state['mapping_settings']; - $form['changed'] = array( - '#theme_wrappers' => array('container'), - '#attributes' => array('class' => array('messages', 'warning')), - '#markup' => t('* Changes made to target configuration are stored temporarily. Click Save to make your changes permanent.'), - ); - } - - // 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']['#markup'] = empty($source['name']) ? $k : $source['name']; - $legend['sources'][$k]['description']['#markup'] = empty($source['description']) ? '' : $source['description']; - } - } - else { - $legend['sources']['#markup'] = 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); - $legend['targets'] = array(); - foreach ($targets as $k => $target) { - $legend['targets'][$k]['name']['#markup'] = empty($target['name']) ? $k : $target['name']; - $legend['targets'][$k]['description']['#markup'] = empty($target['description']) ? '' : $target['description']; - } - - // Legend explaining source and target elements. - $form['legendset'] = array( - '#type' => 'fieldset', - '#title' => t('Legend'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#tree' => TRUE, - ); - $form['legendset']['legend'] = $legend; - - // Add config forms and remove flags to mappings. - $form['config'] = $form['remove_flags'] = $form['mapping_weight'] = array( - '#tree' => TRUE, - ); - if (is_array($mappings)) { - - $delta = count($mappings) + 2; - - foreach ($mappings as $i => $mapping) { - if (isset($targets[$mapping['target']])) { - $form['config'][$i] = feeds_ui_mapping_settings_form($form, $form_state, $i, $mapping, $targets[$mapping['target']]); - } - - $form['remove_flags'][$i] = array( - '#type' => 'checkbox', - '#title' => t('Remove'), - '#prefix' => '', - ); - - $form['mapping_weight'][$i] = array( - '#type' => 'weight', - '#title' => '', - '#default_value' => $i, - '#delta' => $delta, - '#attributes' => array( - 'class' => array( - 'feeds-ui-mapping-weight' - ), - ), - ); - } - } - - if (isset($source_options)) { - $form['source'] = array( - '#type' => 'select', - '#title' => t('Source'), - '#title_display' => 'invisible', - '#options' => $source_options, - '#empty_option' => t('- Select a source -'), - '#description' => t('An element from the feed.'), - ); - } - else { - $form['source'] = array( - '#type' => 'textfield', - '#title' => t('Source'), - '#title_display' => 'invisible', - '#size' => 20, - '#default_value' => '', - '#description' => t('The name of source field.'), - ); - } - $form['target'] = array( - '#type' => 'select', - '#title' => t('Target'), - '#title_display' => 'invisible', - '#options' => $target_options, - '#empty_option' => t('- Select a target -'), - '#description' => t('The field that stores the data.'), - ); - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['save'] = array( - '#type' => 'submit', - '#value' => t('Save'), - ); - return $form; -} - -/** - * Per mapper configuration form that is a part of feeds_ui_mapping_form(). - */ -function feeds_ui_mapping_settings_form($form, $form_state, $i, $mapping, $target) { - $form_state += array( - 'mapping_settings_edit' => NULL, - 'mapping_settings' => array(), - ); - - $base_button = array( - '#submit' => array('feeds_ui_mapping_form_multistep_submit'), - '#ajax' => array( - 'callback' => 'feeds_ui_mapping_settings_form_callback', - 'wrapper' => 'feeds-ui-mapping-form-wrapper', - 'effect' => 'fade', - 'progress' => 'none', - ), - '#i' => $i, - ); - - if (isset($form_state['mapping_settings'][$i])) { - $mapping = $form_state['mapping_settings'][$i] + $mapping; - } - - if ($form_state['mapping_settings_edit'] === $i) { - $settings_form = array(); - - foreach ($target['form_callbacks'] as $callback) { - $settings_form += call_user_func($callback, $mapping, $target, $form, $form_state); - } - - // Merge in the optional unique form. - $settings_form += feeds_ui_mapping_settings_optional_unique_form($mapping, $target, $form, $form_state); - - return array( - '#type' => 'container', - 'settings' => $settings_form, - 'save_settings' => $base_button + array( - '#type' => 'submit', - '#name' => 'mapping_settings_update_' . $i, - '#value' => t('Update'), - '#op' => 'update', - ), - 'cancel_settings' => $base_button + array( - '#type' => 'submit', - '#name' => 'mapping_settings_cancel_' . $i, - '#value' => t('Cancel'), - '#op' => 'cancel', - ), - ); - } - else { - // Build the summary. - $summary = array(); - - foreach ($target['summary_callbacks'] as $callback) { - $summary[] = call_user_func($callback, $mapping, $target, $form, $form_state); - } - - // Filter out empty summary values. - $summary = implode('
', array_filter($summary)); - - // Append the optional unique summary. - if ($optional_unique_summary = feeds_ui_mapping_settings_optional_unique_summary($mapping, $target, $form, $form_state)) { - $summary .= ' ' . $optional_unique_summary; - } - - if ($summary) { - return array( - 'summary' => array( - '#prefix' => '
', - '#markup' => $summary, - '#suffix' => '
', - ), - 'edit_settings' => $base_button + array( - '#type' => 'image_button', - '#name' => 'mapping_settings_edit_' . $i, - '#src' => 'misc/configure.png', - '#attributes' => array('alt' => t('Edit')), - '#op' => 'edit', - ), - ); - } - } - return array(); -} - -/** - * Submit callback for a per mapper configuration form. Switches between edit - * and summary mode. - */ -function feeds_ui_mapping_form_multistep_submit($form, &$form_state) { - $trigger = $form_state['triggering_element']; - - switch ($trigger['#op']) { - case 'edit': - $form_state['mapping_settings_edit'] = $trigger['#i']; - break; - - case 'update': - $values = $form_state['values']['config'][$trigger['#i']]['settings']; - $form_state['mapping_settings'][$trigger['#i']] = $values; - unset($form_state['mapping_settings_edit']); - break; - - case 'cancel': - unset($form_state['mapping_settings_edit']); - break; - } - - $form_state['rebuild'] = TRUE; -} - -/** - * AJAX callback that returns the whole feeds_ui_mapping_form(). - */ -function feeds_ui_mapping_settings_form_callback($form, $form_state) { - return $form; -} - -/** - * Validation handler for feeds_ui_mapping_form(). - */ -function feeds_ui_mapping_form_validate($form, &$form_state) { - if (!strlen($form_state['values']['source']) xor !strlen($form_state['values']['target'])) { - - // Check triggering_element here so we can react differently for ajax - // submissions. - switch ($form_state['triggering_element']['#name']) { - - // Regular form submission. - case 'op': - if (!strlen($form_state['values']['source'])) { - form_error($form['source'], t('You must select a mapping source.')); - } - else { - form_error($form['target'], t('You must select a mapping target.')); - } - break; - - // Be more relaxed on ajax submission. - default: - form_set_value($form['source'], '', $form_state); - form_set_value($form['target'], '', $form_state); - break; - } - } -} - -/** - * Submission handler for feeds_ui_mapping_form(). - */ -function feeds_ui_mapping_form_submit($form, &$form_state) { - $importer = feeds_importer($form['#importer']); - $processor = $importer->processor; - - $form_state += array( - 'mapping_settings' => array(), - 'mapping_settings_edit' => NULL, - ); - - // If an item is in edit mode, prepare it for saving. - if ($form_state['mapping_settings_edit'] !== NULL) { - $values = $form_state['values']['config'][$form_state['mapping_settings_edit']]['settings']; - $form_state['mapping_settings'][$form_state['mapping_settings_edit']] = $values; - } - - // We may set some settings to mappings that we remove in the subsequent step, - // that's fine. - $mappings = $form['#mappings']; - foreach ($form_state['mapping_settings'] as $k => $v) { - $mappings[$k] = array( - 'source' => $mappings[$k]['source'], - 'target' => $mappings[$k]['target'], - ) + $v; - } - - if (!empty($form_state['values']['remove_flags'])) { - $remove_flags = array_keys(array_filter($form_state['values']['remove_flags'])); - - foreach ($remove_flags as $k) { - unset($mappings[$k]); - unset($form_state['values']['mapping_weight'][$k]); - drupal_set_message(t('Mapping has been removed.'), 'status', FALSE); - } - } - - // Keep our keys clean. - $mappings = array_values($mappings); - - if (!empty($mappings)) { - array_multisort($form_state['values']['mapping_weight'], $mappings); - } - - $processor->addConfig(array('mappings' => $mappings)); - - if (strlen($form_state['values']['source']) && strlen($form_state['values']['target'])) { - try { - $mappings = $processor->getMappings(); - $mappings[] = array( - 'source' => $form_state['values']['source'], - 'target' => $form_state['values']['target'], - 'unique' => FALSE, - ); - $processor->addConfig(array('mappings' => $mappings)); - drupal_set_message(t('Mapping has been added.')); - } - catch (Exception $e) { - drupal_set_message($e->getMessage(), 'error'); - } - } - - $importer->save(); - drupal_set_message(t('Your changes have been saved.')); -} - -/** - * 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'] . ' (' . $k . ')'; - } - elseif (is_array($v)) { - $result[$k] = $k; - } - else { - $result[$k] = $v; - } - } - asort($result); - return $result; -} - -/** - * Per mapping settings summary callback. Shows whether a mapping is used as - * unique or not. - */ -function feeds_ui_mapping_settings_optional_unique_summary($mapping, $target, $form, $form_state) { - if (!empty($target['optional_unique'])) { - if ($mapping['unique']) { - return t('Used as unique.'); - } - else { - return t('Not used as unique.'); - } - } -} - -/** - * Per mapping settings form callback. Lets the user choose if a target is as - * unique or not. - */ -function feeds_ui_mapping_settings_optional_unique_form($mapping, $target, $form, $form_state) { - $settings_form = array(); - - if (!empty($target['optional_unique'])) { - $settings_form['unique'] = array( - '#type' => 'checkbox', - '#title' => t('Unique'), - '#default_value' => !empty($mapping['unique']), - ); - } - - return $settings_form; -} - -/** * Theme feeds_ui_overview_form(). */ function theme_feeds_ui_overview_form($variables) { @@ -1012,102 +606,6 @@ function theme_feeds_ui_container($variables) { } /** - * Theme function for feeds_ui_mapping_form(). - */ -function theme_feeds_ui_mapping_form($variables) { - $form = $variables['form']; - - // Build the actual mapping table. - $header = array( - t('Source'), - t('Target'), - t('Target configuration'), - ' ', - t('Weight'), - ); - $rows = array(); - if (is_array($form['#mappings'])) { - foreach ($form['#mappings'] as $i => $mapping) { - // Some parsers do not define source options. - $source = isset($form['source']['#options'][$mapping['source']]) ? $form['source']['#options'][$mapping['source']] : $mapping['source']; - $target = isset($form['target']['#options'][$mapping['target']]) ? check_plain($form['target']['#options'][$mapping['target']]) : '' . t('Missing') . ''; - // Add indicator to target if target configuration changed. - if (isset($form['#mapping_settings'][$i])) { - $target .= '*'; - } - $rows[] = array( - 'data' => array( - check_plain($source), - $target, - drupal_render($form['config'][$i]), - drupal_render($form['remove_flags'][$i]), - drupal_render($form['mapping_weight'][$i]), - ), - 'class' => array('draggable', 'tabledrag-leaf'), - ); - } - } - if (!count($rows)) { - $rows[] = array( - array( - 'colspan' => 5, - 'data' => t('No mappings defined.'), - ), - ); - } - $rows[] = array( - drupal_render($form['source']), - drupal_render($form['target']), - '', - drupal_render($form['add']), - '', - ); - $output = ''; - if (!empty($form['changed'])) { - // This form element is only available if target configuration changed. - $output .= drupal_render($form['changed']); - } - $output .= '
' . drupal_render($form['help']) . '
'; - $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'feeds-ui-mapping-overview'))); - - // Build the help table that explains available sources. - $legend = ''; - $rows = array(); - foreach (element_children($form['legendset']['legend']['sources']) as $k) { - $rows[] = array( - check_plain(drupal_render($form['legendset']['legend']['sources'][$k]['name'])), - check_plain(drupal_render($form['legendset']['legend']['sources'][$k]['description'])), - ); - } - if (count($rows)) { - $legend .= '

' . t('Sources') . '

'; - $legend .= theme('table', array('header' => array(t('Name'), t('Description')), 'rows' => $rows)); - } - - // Build the help table that explains available targets. - $rows = array(); - foreach (element_children($form['legendset']['legend']['targets']) as $k) { - $rows[] = array( - check_plain(drupal_render($form['legendset']['legend']['targets'][$k]['name']) . ' (' . $k . ')'), - check_plain(drupal_render($form['legendset']['legend']['targets'][$k]['description'])), - ); - } - $legend .= '

' . t('Targets') . '

'; - $legend .= theme('table', array('header' => array(t('Name'), t('Description')), 'rows' => $rows)); - - // Stick tables into collapsible fieldset. - $form['legendset']['legend'] = array( - '#markup' => '
' . $legend . '
', - ); - - $output .= drupal_render($form['legendset']); - $output .= drupal_render_children($form); - - drupal_add_tabledrag('feeds-ui-mapping-overview', 'order', 'sibling', 'feeds-ui-mapping-weight'); - return $output; -} - -/** * Page callback to import a Feeds importer. */ function feeds_ui_importer_import($form, &$form_state) { diff --git a/feeds_ui/feeds_ui.module b/feeds_ui/feeds_ui.module index 08cc40f..f89a0c1 100644 --- a/feeds_ui/feeds_ui.module +++ b/feeds_ui/feeds_ui.module @@ -99,10 +99,6 @@ function feeds_ui_theme() { 'render element' => 'form', 'file' => 'feeds_ui.admin.inc', ), - 'feeds_ui_mapping_form' => array( - 'render element' => 'form', - 'file' => 'feeds_ui.admin.inc', - ), 'feeds_ui_edit_page' => array( 'variables' => array('info' => NULL, 'active' => NULL), 'file' => 'feeds_ui.admin.inc', diff --git a/plugins/FeedsCSVParser.inc b/plugins/FeedsCSVParser.inc index 04d79b6..337156f 100644 --- a/plugins/FeedsCSVParser.inc +++ b/plugins/FeedsCSVParser.inc @@ -120,7 +120,7 @@ class FeedsCSVParser extends FeedsParser { $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[] = check_plain($mapping['source']); @@ -176,6 +176,7 @@ class FeedsCSVParser extends FeedsParser { return array( 'delimiter' => ',', 'no_headers' => 0, + 'mapping_on_import' => 1, ); } diff --git a/plugins/FeedsNodeProcessor.inc b/plugins/FeedsNodeProcessor.inc index 2cfdf30..cb47c56 100644 --- a/plugins/FeedsNodeProcessor.inc +++ b/plugins/FeedsNodeProcessor.inc @@ -170,6 +170,7 @@ class FeedsNodeProcessor extends FeedsProcessor { 'expire' => FEEDS_EXPIRE_NEVER, 'author' => 0, 'authorize' => TRUE, + 'mapping_on_import' => 0, ) + parent::configDefaults(); } diff --git a/plugins/FeedsParser.inc b/plugins/FeedsParser.inc index 1e1d329..d14c21a 100644 --- a/plugins/FeedsParser.inc +++ b/plugins/FeedsParser.inc @@ -14,6 +14,7 @@ class FeedsParserResult extends FeedsResult { public $link; public $items; public $current_item; + public $context; /** * Constructor. @@ -23,6 +24,7 @@ class FeedsParserResult extends FeedsResult { $this->description = ''; $this->link = ''; $this->items = $items; + $this->context = 'import'; } /** @@ -46,6 +48,22 @@ class FeedsParserResult extends FeedsResult { public function currentItem() { return empty($this->current_item) ? NULL : $this->current_item; } + + /** + * Get mapping sources after the batch items has been populated. + */ + public function getMappingSources() { + $sources = array(); + if (count($this->items) > 0) { + foreach ($this->items[0] as $k => $v) { + $sources[$k] = array( + 'title' => $k, + 'description' => '', + ); + } + } + return $sources; + } } /** diff --git a/plugins/FeedsProcessor.inc b/plugins/FeedsProcessor.inc index e8b4b49..5e6deb1 100644 --- a/plugins/FeedsProcessor.inc +++ b/plugins/FeedsProcessor.inc @@ -667,7 +667,7 @@ abstract class FeedsProcessor extends FeedsPlugin { // Many mappers add to existing fields rather than replacing them. Hence we // need to clear target elements of each item before mapping in case we are // mapping on a prepopulated item such as an existing node. - foreach ($this->config['mappings'] as $mapping) { + foreach ($this->getMappings() as $mapping) { if (isset($targets[$mapping['target']]['real_target'])) { $target_item->{$targets[$mapping['target']]['real_target']} = NULL; } @@ -775,6 +775,7 @@ abstract class FeedsProcessor extends FeedsPlugin { 'input_format' => NULL, 'skip_hash_check' => FALSE, 'bundle' => $bundle, + 'mapping_on_import' => 0, ); } @@ -851,9 +852,20 @@ abstract class FeedsProcessor extends FeedsPlugin { /** * Get mappings. + * + * Mappings must not be got directly by using $this->config['mappings'] + * but instead via this function. */ public function getMappings() { - return isset($this->config['mappings']) ? $this->config['mappings'] : array(); + // 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']; + } + else { + $mappings = $this->config['mappings']; + } + return $mappings; } /** @@ -994,7 +1006,7 @@ abstract class FeedsProcessor extends FeedsPlugin { protected function uniqueTargets(FeedsSource $source, FeedsParserResult $result) { $parser = feeds_importer($this->id)->parser; $targets = array(); - foreach ($this->config['mappings'] as $mapping) { + foreach ($this->getMappings() as $mapping) { if (!empty($mapping['unique'])) { // Invoke the parser's getSourceElement to retrieve the value for this // mapping's source. @@ -1180,6 +1192,265 @@ abstract class FeedsProcessor extends FeedsPlugin { return $dependencies; } + /** + * 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 result 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, $result = NULL) { + // Get the FeedsImporter object + $importer = feeds_importer($this->id); + + $form = array(); + if (empty($result)) { + // 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']['#markup'] = feeds_mapping_form_help(); + $form['#importer'] = $importer->id; + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + + // Get mapping sources from parsers or result and targets from processor, format them + // for output. + // Some parsers do not define mapping sources but let them define on the fly. + if (empty($result) || $result->context <> 'mapping') { + $sources = $importer->parser->getMappingSources(); + } + else { + $sources = $result->getMappingSources(); + } + + if ($sources) { + $source_options = _feeds_mapping_form_format_options($sources); + foreach ($sources as $k => $source) { + $legend['sources'][$k]['name']['#markup'] = empty($source['name']) ? $k : $source['name']; + $legend['sources'][$k]['description']['#markup'] = empty($source['description']) ? '' : $source['description']; + } + } + else { + $legend['sources']['#markup'] = 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']['#markup'] = empty($target['name']) ? $k : $target['name']; + $legend['targets'][$k]['description']['#markup'] = 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(); + $form['#mappings'] = $mappings; + + // Add config forms and remove flags to mappings. + $form['config'] = $form['remove_flags'] = $form['mapping_weight'] = array( + '#tree' => TRUE, + ); + + if (is_array($mappings)) { + $form['mappings']['#tree'] = TRUE; + + $delta = count($mappings) + 2; + + foreach ($mappings as $i => $mapping) { + if (isset($targets[$mapping['target']])) { + $form['config'][$i] = feeds_mapping_settings_form($form, $form_state, $i, $mapping, $targets[$mapping['target']]); + } + + $mappings[$i]['source'] = isset($source_options) ? $source_options[$mappings[$i]['source']] : $mappings[$i]['source']; + $mappings[$i]['target'] = $target_options[$mappings[$i]['target']]; + + $form['remove_flags'][$i] = array( + '#type' => 'checkbox', + '#title' => t('Remove'), + '#prefix' => '', + ); + + $form['mapping_weight'][$i] = array( + '#type' => 'weight', + '#title' => '', + '#default_value' => $i, + '#delta' => $delta, + '#attributes' => array( + 'class' => array( + 'feeds-ui-mapping-weight' + ), + ), + ); + + // 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, + ); + } + } + } + + if (isset($source_options)) { + $form['source'] = array( + '#type' => 'select', + '#title' => t('Source'), + '#title_display' => 'invisible', + '#options' => $source_options, + '#empty_option' => t('- Select a source -'), + '#description' => t('An element from the feed.'), + ); + } + else { + $form['source'] = array( + '#type' => 'textfield', + '#title' => t('Source'), + '#title_display' => 'invisible', + '#size' => 20, + '#default_value' => '', + '#description' => t('The name of source field.'), + ); + } + $form['target'] = array( + '#type' => 'select', + '#title' => t('Target'), + '#title_display' => 'invisible', + '#options' => $target_options, + '#empty_option' => t('- Select a target -'), + '#description' => t('The field that stores the data.'), + ); + $form['add'] = array( + '#type' => 'submit', + '#value' => t('Add'), + '#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'] == t('Add')) { + if (!empty($values['source']) && !empty($values['target'])) { + if (method_exists($this, 'addMapping')) { + $this->addMapping($values['source'], $values['target']); + } + $values['mappings'][] = array( + 'source' => $values['source'], + 'target' => $values['target'], + 'unique' => FALSE, + ); + } + } + // If other submit is invoked. + else { + if (isset($values['remove_flags'])) { + foreach ($values['remove_flags'] as $k => $v) { + if ($v) { + unset($values['mappings'][$k]); + } + } + // Keep our 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 + '

' . + t('Define which elements of a single item of a feed') . + '(= ' . t('Sources') . ') ' . + t('map to which content pieces in Drupal') . + ' (= ' . t('Targets') . '). ' . + t('Make sure that at least one definition has a') . + ' ' . t('Unique target') . '. ' . + t('A unique target means that a value for a target can only occur once. E. g. only one item with the URL ') . + 'http://example.com/story/1 ' . + t('can exist.') . + '

'; } class FeedsProcessorBundleNotDefined extends Exception {} diff --git a/plugins/FeedsTermProcessor.inc b/plugins/FeedsTermProcessor.inc index f69819f..d5dc791 100644 --- a/plugins/FeedsTermProcessor.inc +++ b/plugins/FeedsTermProcessor.inc @@ -79,6 +79,7 @@ class FeedsTermProcessor extends FeedsProcessor { public function configDefaults() { return array( 'vocabulary' => 0, + 'mapping_on_import' => 0, ) + parent::configDefaults(); } diff --git a/plugins/FeedsUserProcessor.inc b/plugins/FeedsUserProcessor.inc index 778c72b..661f149 100644 --- a/plugins/FeedsUserProcessor.inc +++ b/plugins/FeedsUserProcessor.inc @@ -107,6 +107,7 @@ class FeedsUserProcessor extends FeedsProcessor { 'roles' => array(), 'status' => 1, 'defuse_mail' => FALSE, + 'mapping_on_import' => 0, ) + parent::configDefaults(); } diff --git a/tests/feeds.test b/tests/feeds.test index b3ce145..8280253 100644 --- a/tests/feeds.test +++ b/tests/feeds.test @@ -442,7 +442,9 @@ class FeedsWebTestCase extends DrupalWebTestCase { */ public function addMappings($id, array $mappings, $test_mappings = TRUE) { - $path = "admin/structure/feeds/$id/mapping"; + $source = feeds_source($id); + $config = $source->importer->config; + $path = 'admin/structure/feeds/'. $id .'/mapping/' . $config['processor']['plugin_key']; // Iterate through all mappings and add the mapping via the form. foreach ($mappings as $i => $mapping) { diff --git a/tests/feeds_parser_sitemap.test b/tests/feeds_parser_sitemap.test index 2a16463..7aa9712 100644 --- a/tests/feeds_parser_sitemap.test +++ b/tests/feeds_parser_sitemap.test @@ -54,6 +54,7 @@ class FeedsSitemapParserTestCase extends FeedsWebTestCase { $path = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'feeds') . '/tests/feeds/'; $nid = $this->createFeedNode('sitemap', $path . 'sitemap-example.xml', 'Testing Sitemap Parser'); $this->assertText('Created 5 nodes'); + $this->show(); // Assert DB status. $count = db_query("SELECT COUNT(*) FROM {feeds_item} WHERE entity_type = 'node'")->fetchField();