array(
'title' => t('Merge terms'),
'description' => t('Gives the user the ability to merge terms.'),
)
);
}
/**
* Implements hook_menu():
*/
function term_merge_menu() {
$items = array(
'admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/merge' => array(
'title' => 'Merge terms',
'page callback' => 'drupal_get_form',
'page arguments' => array('term_merge_term_merge_form', 3),
'access arguments' => array('merge terms'),
'type' => MENU_LOCAL_TASK,
)
);
return $items;
}
/**
* Implements hook_help().
*/
function term_merge_help($path, $arg) {
switch ($path) {
// Main module help for the block module
case 'admin/help#term_merge':
return '
Allows you to merge multiple terms into one and and at the same time update all fields referencing to the old ones.
';
/*case 'admin/structure/taxonomy/%/merge':
return 'This form simply lets you merge multiple terms into one and at the same time update all fields referencing to the old ones.
';*/
}
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Inserts the Merge button into the actions of the form.
*/
function term_merge_form_taxonomy_overview_terms_alter(&$form, &$form_state, $form_id) {
if ($form['#total_entries'] > 0) {
$form['actions'][] = array(
'merge' => array(
'#type' => 'submit',
'#value' => t('Merge'),
'#submit' => array('term_merge_form_taxonomy_overview_terms_merge_submit'),
'#weight' => 1,
)
);
$form['actions']['submit']['#weight'] = 0;
$form['actions']['reset_alphabetical']['#weight'] = 2;
}
}
/**
* Submit function for the merge button in the alteration of form form_taxonomy_overview_terms (see above).
* Redirects the user to the term merge form/page.
*/
function term_merge_form_taxonomy_overview_terms_merge_submit($form, &$form_state) {
unset($form_state['storage']);
$form_state['redirect'] = 'admin/structure/taxonomy/' . $form['#vocabulary']->machine_name . '/merge';
}
/**
* Implements hook_form().
*
* Menu callback; displays components to let user select one or more terms for merging and
* let them be merged into another, specified term.
*/
function term_merge_term_merge_form($form, $form_state, $vocabulary) {
if (!isset($form_state['storage']['term_list'])) { // Create th merge form.
$form = array(
'term_list' => array(
'#type' => 'select',
'#title' => t('Merge terms'),
'#options' => _term_delete_form_replacement_term_options($vocabulary->vid, 0, FALSE),
'#multiple' => TRUE,
'#required' => TRUE,
'#attributes' => array('size' => 15),
'#prefix' => '',
),
'replacement_term' => array(
'#type' => 'select',
'#title' => t('Into'),
'#description' => t('Cannot be any of the terms selected in the list to the left.'),
'#options' => _term_delete_form_replacement_term_options($vocabulary->vid, 0, TRUE),
'#prefix' => '
',
),
'replacement_term_new' => array(
'#type' => 'textfield',
'#title' => t('Name of new term'),
'#description' => t('A new term will be created with the specified name.'),
'#suffix' => '
',
),
'keep_merged' => array(
'#type' => 'checkbox',
'#title' => t('Only merge occurrences'),
'#description' => t('Check this if you only want to merge occurrences of the merged terms leaving them as they are in the vocabulary.'),
'#prefix' => '
',
),
'#vocabulary' => $vocabulary,
'merge_redirect_path' => array(
'#type' => 'value',
'#value' => 'admin/structure/taxonomy/' . $vocabulary->machine_name,
),
);
// Adds the .js and .css used by form.
$form['#after_build'] = array('_term_merge_form_attach');
}
else { // Let the user confirm the deletion.
$form = array(
'confirm_markup' => array(
'#markup' => _term_merge_get_confirm_merge_markup($form_state),
'#prefix' => '',
'#suffix' => '
',
)
);
}
$form['actions'] = array (
'submit' => array(
'#type' => 'submit',
'#value' => t('Merge'),
),
'cancel' => array(
'#type' => 'link',
'#title' => t('Cancel'),
'#href' => 'admin/structure/taxonomy/' . $vocabulary->machine_name,
)
);
return $form;
}
/**
* Validation handler for term_merge_term_merge_form().
*/
function term_merge_term_merge_form_validate($form, &$form_state) {
$form_values = $form_state['values'];
if (!isset($form_state['storage']['term_list'])) {
// Convert the array of terms to merge into a normally keyed array.
$form_values['term_list'] = array_values($form_values['term_list']);
if ($form_values['replacement_term'] == '0' &&
empty($form_values['replacement_term_new'])) {
form_set_error('replacement_term_new', t('You must enter a name for the new term.'));
}
elseif ($pos = array_search($form_values['replacement_term'], $form_values['term_list'])) {
array_splice($form_values['term_list'], $pos, 1);
}
}
}
/**
* Submit handler for term_merge_term_merge_form().
*
* Collects data about what terms to merge into what term and sends it to term_merge().
*/
function term_merge_term_merge_form_submit($form, &$form_state) {
if (!isset($form_state['storage']['term_list'])) { // Store values and rebuild form for merge confirmation.
$form_values = $form_state['values'];
$form_state['storage']['term_list'] = $form_values['term_list'];
$form_state['storage']['replacement_term'] = $form_values['replacement_term'];
$form_state['storage']['replacement_term_new'] = $form_values['replacement_term_new'];
$form_state['storage']['merge_redirect_path'] = $form_values['merge_redirect_path'];
$form_state['storage']['keep_merged'] = $form_values['keep_merged'];
$form_state['storage']['working_vocabulary'] = $form['#vocabulary'];
$form_state['rebuild'] = TRUE;
}
else { // Merge confirmed, do it!
$replacement_term = $form_state['storage']['replacement_term'];
$merged_terms = $form_state['storage']['term_list'];
$replacement_term_new = $form_state['storage']['replacement_term_new'];
// Create a new tid id the value of $replacement_term is equal to 0.
if ($replacement_term == '0') {
$new_term = _term_merge_term_create($replacement_term_new, $form_state['storage']['working_vocabulary'], $merged_terms);
$dest_tid = $new_term->tid;
}
else { // Else, the value of $replacement_term contains the tid of the new term.
$dest_tid = $replacement_term;
}
// Redirect user after merge.
$form_state['redirect'] = $form_state['storage']['merge_redirect_path'];
// Now merge.
term_merge($merged_terms, $dest_tid, $form_state['storage']['keep_merged']);
}
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Add fields to allow term merging on the confirm deletion state of the form_taxonomy_form_term form.
*/
function term_merge_form_taxonomy_form_term_alter(&$form, &$form_state, $form_id) {
// Only modify the form if it is in the "Confirm Delete" states.
if (isset($form_state['confirm_delete']) && $form_state['confirm_delete'] === TRUE && user_access('term merge')) {
$form['description']['#markup'] .= '
' . t('You can replace any occurrences of the deleted term with another term by specifying the replacement below. This is useful when correcting typos, or to consolidate multiple, synonymous terms.') . '
';
$form['replacement_term_replace'] = array(
'#type' => 'checkbox',
'#title' => t('Replace deleted term with:'),
'#prefix' => '',
);
$form['replacement_term'] = array(
'#type' => 'select',
'#options' => _term_delete_form_replacement_term_options($form['#vocabulary']->vid, $form['#term']->tid, TRUE),
'#suffix' => '
',
);
$form['replacement_term_new'] = array(
'#type' => 'textfield',
'#title' => t('Name of new term'),
);
$form['#after_build'] = array('_term_merge_form_attach');
$form['#submit'] = array('term_merge_form_taxonomy_form_term_submit');
$form['#validate'] = array('term_merge_form_taxonomy_form_term_validate');
}
}
/**
* Validation function for override of form_taxonomy_form_term (see above).
*
* If the user want's to create a new term, check to see if he has entered a name for it.
*/
function term_merge_form_taxonomy_form_term_validate($form, &$form_state) {
$form_values = $form_state['values'];
if ($form_values['replacement_term'] == '0' &&
empty($form_state['values']['replacement_term_new'])) {
form_set_error('replacement_term_new', t('You must enter a name for the new term.'));
}
}
/**
* Submit function for alteration of form_taxonomy_form_term (see above).
*
* If the user choose to create a new term for merging, create and save a term with the entered name.
* Call the merge function to merge the replacement term into the deleted term.
*/
function term_merge_form_taxonomy_form_term_submit($form, &$form_state) {
if ($form_state['values']['replacement_term_replace']) {
$replacement_term = $form_state['values']['replacement_term'];
/// Create a new tid id the value of $replacement_term is equal to 0.
if ($replacement_term == '0') {
$new_term = _term_merge_term_create(check_plain($form_state['values']['replacement_term_new']), $form['#vocabulary'], $form['#term']->tid);
$dest_tid = $new_term->tid;
}
else { // Else, the value of $replacement_term contains the tid of the new term.
$dest_tid = $replacement_term;
}
// Merge.
term_merge($form['#term']->tid, $dest_tid, TRUE);
}
else {
// Continue as the confirm deletion form would normally.
taxonomy_term_confirm_delete_submit($form, $form_state);
}
}
/**
* Merge source terms into one destination term.
* @param $source
* A single term tid or an array of term tids to be merged.
* @param $dest
* The tid of the term to merge sources into.
*/
function term_merge($source, $dest, $keep_merged) {
// Create an array of source if it isn't already one.
if (!is_array($source)) {
$source = array($source);
}
$source = array_values($source);
// Create a skeleton for the merging batch.
$update_batch = array(
'title' => t('Merging terms'),
'operations' => array(
array('_term_merge_batch_store_merged_terms', array(
$keep_merged ? array() : $source,
count($source),
))
),
'finished' => '_term_merge_finished',
);
// For every field existing on the site.
foreach (field_info_fields() as $key => $field) {
if ($field['type'] == 'taxonomy_term_reference') {
$table_name = 'field_data_' . $key;
$tid_field = $key . '_tid';
$update_data = array();
foreach ($source as $source_tid) {
// Get data from fields previously containing a reference to the merged term.
$fields_result = db_select($table_name, 'df')
->fields('df', array('entity_type', 'entity_id', 'delta'))
->condition($tid_field, $source_tid)
->execute();
// Collect update data form the batch process and group them by their entity.
foreach ($fields_result as $field_row) {
$batch_key = $field_row->entity_type . '_' . $field_row->entity_id;
$update_data[$batch_key][] = array(
'field_name' => $key,
'source_tid' => $source_tid,
'dest_tid' => $dest,
);
}
}
// Create a batch processing step for every entity that needs an update.
foreach ($update_data as $key => $batch_data) {
$delimiter_pos = strripos($key, '_');
$type = drupal_substr($key, 0, $delimiter_pos);
$id = drupal_substr($key, $delimiter_pos + 1);
$batch_data = array(
'entity_type' => $type,
'entity_id' => $id,
'data' => $batch_data
);
$update_batch['operations'][] = array(
'_term_merge_insert_field_values',
array($batch_data)
);
}
}
}
// Initialize the batch process.
batch_set($update_batch);
}
/**
* Creates a new term with the specified name. The new term will have the same
* parent as the merged terms as long as long as they all a common parent.
*/
function _term_merge_term_create($name, $vocabulary, $merged_terms) {
$new_term = new stdClass();
if (!is_array($merged_terms)) {
$merged_terms = array(0 => $merged_terms);
}
// Get parent of merged terms.
$merged_parent_query = db_select('taxonomy_term_hierarchy', 'tth')
->fields('tth', array('parent'));
$conditions = db_or();
foreach ($merged_terms as $merged_tid) {
$conditions->condition('tid', $merged_tid);
}
$merged_parent_result = $merged_parent_query->condition($conditions)
->execute()->fetchCol();
if (count($parent = array_unique($merged_parent_result)) == 1) {
$parent_tid = $parent[0];
}
else {
$parent_tid = 0;
}
$term = new stdClass();
$term->name = $name;
$term->vid = $vocabulary->vid;
$term->parent = $parent_tid;
$term->vocabulary_machine_name = $vocabulary->machine_name;
$term->description = '';
taxonomy_term_save((object) $term);
return $term;
}
/**
* Stores the merged terms in the context of the batch operation,
* I need them in the finish function of the batch operation.
*/
function _term_merge_batch_store_merged_terms($merged_terms_delete, $num_merged_terms, &$context) {
$context['results']['merged_terms_delete'] = $merged_terms_delete;
$context['results']['num_merged_terms'] = $num_merged_terms;
}
/**
* Batch operation for adding field values into entities.
*
* Loads the entity specified in $update_data and add the term reference
* to all fields specified in $update_data['data'].
*/
function _term_merge_insert_field_values($update_data, &$context) {
// Extract entity information.
$entity_type = $update_data['entity_type'];
$entity_id = $update_data['entity_id'];
// Load the entity.
$entity = entity_load($entity_type, array($entity_id));
$entity = $entity[$entity_id];
$context['message'] = 'Merging terms in fields of ' . $entity_type . ' ' . $entity->title . '.';
$target_merged = FALSE;
foreach ($update_data['data'] as $i => &$update_data_arr) {
$update_data_arr = array_reverse($update_data_arr);
$field_lang = field_language('node', $entity, $update_data_arr['field_name']);
$field_values = &$entity->{$update_data_arr['field_name']}[$field_lang];
foreach ($field_values as $i => $value) {
if ($value['tid'] == $update_data_arr['source_tid']) {
if (!$target_merged) {
$field_values[$i]['tid'] = $update_data_arr['dest_tid']->tid;
$target_merged = TRUE;
}
// Any more occurrences of the source tid will simply be removed.
// We don't want duplicates.
else {
array_splice($field_values, $i, 1);
}
// If target tid is one of the source tids, remove it from the array
// of terms that will be removed at the end of the process.
if($update_data_arr['source_tid']== $update_data_arr['dest_tid']->tid) {
foreach ($context['results']['merged_terms_delete'] as $i => $merged_term) {
if ($merged_term == $update_data_arr['source_tid']) {
array_splice($context['results']['merged_terms_delete'], $i, 1);
}
}
}
$context['results'][] = 'Merged ' . $update_data_arr['source_tid'] . ' with '
. $update_data_arr['dest_tid']->tid . ' in ' . $update_data_arr['field_name']
. ' of ' . $entity_type . ' ' . $entity_id;
break;
}
}
}
if (empty($context['sandbox'])) {
$context['sandbox']['progress'] = 0;
}
$context['sandbox']['progress']++;
$context['finished'] = $context['sandbox']['progress']/$context['results']['num_merged_terms'];
// Save the new field values if something has changed.
if ($target_merged) {
field_attach_update($entity_type, $entity);
}
}
/**
* The merge batch operation was finished.
*/
function _term_merge_finished($success, $results, $operations) {
if ($success) {
$message = format_plural($results['num_merged_terms'], 'One term merged.', '@count terms merged.');
$message .= '
';
$message .= format_plural(count($results) - 2, 'One term occurrence merged.', '@count term occurrences merged.');
if (count($results['merged_terms_delete']) > 0) {
$message .= '
';
$message .= format_plural(count($results['merged_terms_delete']), 'One term deleted', '@count terms deleted.');
}
}
else {
$message = t('Finished with an error.');
}
drupal_set_message($message);
// Delete merged terms on success. We don't want to do this earlier if something went wrong during the batch process.
if ($success) {
foreach ($results['merged_terms_delete'] as $merged_term) {
taxonomy_term_delete($merged_term);
}
}
}
/**
* Returns the markup text for the confirmation message of term merging.
*/
function _term_merge_get_confirm_merge_markup($form_state) {
$form_values = $form_state['values'];
$terms = taxonomy_term_load_multiple($form_values['term_list']);
$dest_term_name = ($dest_term = taxonomy_term_load($form_values['replacement_term'])) ?
$dest_term->name : $form_values['replacement_term_new'];
$merged_names = '';
foreach (array_values($terms) as $i => $term) {
$merged_names .= $term->name;
if ($i < count($terms) - 2) {
$merged_names .= ', ';
}
else if ($i == count($terms) - 2) {
$merged_names .= ' & ';
}
}
$output = 'Are you sure you want to merge %term_names with %destination_term'
. (($form_state['storage']['replacement_term_new']) ? ' (new)' : '')
. ' ? ';
if ($form_values['keep_merged']) {
$output .= 'Due to your choice, the merged terms will not be deleted from their vocabulary.
';
}
else {
$output .= '
All merged terms will be deleted!
';
}
$output = t($output, array('%term_names' => $merged_names, '%destination_term' => $dest_term_name));
return $output;
}
/**
* Returns values for the replacement_term select box in the form (see form builder at top).
* Don't return the value for the term that will be merged.
*/
function _term_delete_form_replacement_term_options($vid, $merged_term_tid, $add_new_option) {
$tree = taxonomy_get_tree($vid);
foreach ($tree as $i => $term) {
if ($merged_term_tid != $term->tid) {
$options[$term->tid] = $term->name;
}
}
if ($add_new_option) {
$options['0'] = t('New term, specified below');
}
return $options;
}
/**
* Add necessary JavaScript and css to the form.
*/
function _term_merge_form_attach($form_element) {
if ($form_element['#id'] == 'term-merge-term-merge-form') {
drupal_add_js(drupal_get_path('module', 'term_merge') . '/term_merge.js');
}
elseif ($form_element['#id'] == 'taxonomy-form-term') {
drupal_add_js(drupal_get_path('module', 'term_merge') . '/term_merge_deletion.js');
}
drupal_add_css(drupal_get_path('module', 'term_merge') . '/term_merge.css');
return $form_element;
}