diff --git a/includes/update.inc b/includes/update.inc index 1eb7a1d..747f33b 100644 --- a/includes/update.inc +++ b/includes/update.inc @@ -178,6 +178,17 @@ function update_prepare_d7_bootstrap() { $_COOKIE[session_name()] = $sid; session_id($sid); } + + // Upgrading from D6 to D7.{0,1,2,3,4,8,...} is different than upgrading + // from D6 to D7.{5,6,7} (which should be considered screwed). To be able + // to properly handle this difference in node_update_701{2,3} we should + // variable_set() something (node_update_7012() may run in another + // invocation of a batch set than we are having here). We don't have + // proper Drupal variable handling here (which is a must for + // variable_set()), so setting only this PHP variable here, and do the + // variable_set() if and when we are ready to do so. + // @see http://drupal.org/node/1164852 + $node_updates_from_d6 = TRUE; } // Create the registry tables. @@ -302,6 +313,10 @@ function update_prepare_d7_bootstrap() { // Set the timezone for this request only. $GLOBALS['conf']['date_default_timezone'] = 'UTC'; } + + // Store if we're upgrading from D6 or not. + // @see http://drupal.org/node/1164852 + variable_set('node_updates_from_d6', isset($node_updates_from_d6) ? $node_updates_from_d6 : FALSE); } /** diff --git a/modules/field/field.install b/modules/field/field.install index d56eb90..8e48155 100644 --- a/modules/field/field.install +++ b/modules/field/field.install @@ -435,5 +435,51 @@ function field_update_7001() { } /** + * Disable 'translatable' flag for all fields that do not contain language-specific field values. + */ +function field_update_7002() { + $fields = _update_7000_field_read_fields(array( + // Only currently enabled fields are affected. + 'translatable' => 1, + // Field storage engines depend on the module and hook system; we can only + // query and update fields in SQL. Alternative field storage engines have to + // implement a corresponding module update on their own. + 'storage_type' => 'field_sql_storage', + // Field configuration and values of deleted fields are irrelevant. + 'deleted' => 0, + )); + $changed_fields = array(); + foreach ($fields as $field) { + $tables = array("field_data_{$field['field_name']}", "field_revision_{$field['field_name']}"); + $has_language = FALSE; + foreach ($tables as $table) { + // 'und' denotes LANGUAGE_NONE; constant values may change over time. + $query = db_select($table)->condition('language', 'und', '<>')->range(0, 1); + $query->addExpression(1); + $has_language = $has_language || $query->execute()->fetchField(); + } + // Only in case there is no language-specific field value, update the + // field's configuration to mark it untranslatable. + // Note: There is a small chance of disabling the translatable flag for + // fields that actually have a field translation handler associated (but + // e.g., no values yet). However, since the field translation handler + // entirely depends on module hooks, it is impossible to gather this + // information in a module update. + if (!$has_language) { + $changed_fields[] = $field['field_name']; + db_update('field_config') + ->condition('id', $field['id']) + ->fields(array('translatable' => 0)) + ->execute(); + } + } + if ($changed_fields) { + drupal_set_message(t('The following fields have been changed to be no longer translatable: %field-list.', array( + '%field-list' => implode(', ', $changed_fields), + ))); + } +} + +/** * @} End of "addtogroup updates-6.x-to-7.x" */ diff --git a/modules/node/node.install b/modules/node/node.install index 2498091..ed86e5d 100644 --- a/modules/node/node.install +++ b/modules/node/node.install @@ -610,6 +610,7 @@ function node_update_7006(&$sandbox) { 'module' => 'text', 'cardinality' => 1, 'entity_types' => array('node'), + 'translatable' => TRUE, ); _update_7000_field_create_field($body_field); @@ -861,5 +862,126 @@ function node_update_7011() { } /** + * Switches body fields to untranslatable while upgrading from D6. + */ +function node_update_7012() { + // If we are upgrading from D6, then body fields should be set (back) to + // untranslatable (as D6 did not know about the idea of translating fields, + // but only nodes). Do nothing if we are upgrading from sites that started + // their life as 7.x ones, though: it _is_ a valid use case to have + // translatable body fields in D7 sites, anyways. + if (variable_get('node_updates_from_d6', FALSE)) { + // It is not acceptable to use field_update_field() throughout the upgrade + // process _and_ we do not have an update counterpart for + // _update_7000_field_create_field(). Because of this, we must do this + // terrible dance here. Remember: we are upgrading from D6 here, this code + // does not run for D7->D7 upgrades. If somebody ever wants to do + // something like this (modifying anything in the {field_config} table + // directly) in a D7->D7 upgrade, then it may lead directly to hell, and + // the gatekeeper there is chx. You have been warned. + db_update('field_config') + ->fields(array('translatable' => 0)) + ->condition('field_name', 'body') + ->execute(); + node_type_cache_reset(); + } +} + +/** * @} End of "addtogroup updates-6.x-to-7.x" */ + +/** + * Searches for localized field values of non-translatable fields and corrects them. + */ +function node_update_7013(&$sandbox) { + $sandbox['#finished'] = 0; + + if (!isset($sandbox['total'])) { + // Initial invocation: collect the tables that need to be updated, and + // count the total affected rows. + $fields = _update_7000_field_read_fields(array( + // Only currently enabled fields are affected. + 'translatable' => 0, + // Field storage engines depend on the module and hook system; we can only + // query and update fields in SQL. Alternative field storage engines have to + // implement a corresponding module update on their own. + 'storage_type' => 'field_sql_storage', + // Field configuration and values of deleted fields are irrelevant. + 'deleted' => 0, + )); + $sandbox['tables'] = array(); + $sandbox['total'] = 0; + foreach ($fields as $field) { + $tables = array("field_data_{$field['field_name']}", "field_revision_{$field['field_name']}"); + foreach ($tables as $table) { + $query = db_select($table, 'b1'); + $query->join($table, 'b2', 'b1.entity_type = b2.entity_type AND b1.entity_id = b2.entity_id AND b1.delta = b2.delta'); + $query->fields('b1', array('entity_type', 'entity_id', 'delta')); + $query->condition('b1.deleted', 0); + // 'und' denotes LANGUAGE_NONE; constant values may change over time. + $query->condition('b2.language', 'und', '<>'); + $query->groupBy('entity_type'); + $query->groupBy('entity_id'); + $query->groupBy('delta'); + $query->having('COUNT(DISTINCT b1.language) = 1'); + $affected_values_cnt = $query->countQuery()->execute()->fetchField(); + // Do not memorize the table for subsequent invocations if it does + // not have data to be corrected. + if ($affected_values_cnt) { + $sandbox['tables'][] = $table; + $sandbox['total'] += $affected_values_cnt; + } + } + } + $sandbox['count'] = 0; + // If we have nothing to do, bail out early (to avoid infinite cycles). + if (empty($sandbox['tables'])) { + $sandbox['#finished'] = 1; + } + } + + else { + // Subsequent invocations: do the actual corrections, 200 by 200. It's a + // bit tricky: the query that drives the actual update cycle returns only + // rows to be updated - which in turn means that any row that has been + // updated will not show up again in subsequent invocations of the query. + // For this same reason it's OK to (try to) update every table that's + // listed as affected. + $batch_size = 200; + $count = 0; + foreach ($sandbox['tables'] as $table) { + $query = db_select($table, 'b1'); + $query->join($table, 'b2', 'b1.entity_type = b2.entity_type AND b1.entity_id = b2.entity_id AND b1.delta = b2.delta'); + $query->fields('b1', array('entity_type', 'entity_id', 'delta')); + $query->condition('b1.deleted', 0); + $query->condition('b2.language', 'und', '<>'); + $query->groupBy('entity_type'); + $query->groupBy('entity_id'); + $query->groupBy('delta'); + $query->having('COUNT(DISTINCT b1.language) = 1'); + $query->orderBy('entity_type'); + $query->orderBy('entity_id'); + $query->orderBy('delta'); + $result = $query->execute(); + foreach ($result as $row) { + db_update($table) + ->condition('entity_type', $row->entity_type) + ->condition('entity_id', $row->entity_id) + ->condition('delta', $row->delta) + ->condition('language', 'und', '<>') // For safety purposes. + ->fields(array('language' => LANGUAGE_NONE)) + ->execute(); + $count++; + $sandbox['count']++; + if ($count >= $batch_size) { + // If we have reached our own limit (per invocation), break out of + // both the foreach() cycles. + break 2; + } + } + } + // Inform the batch API of our progress. + $sandbox['#finished'] = $sandbox['count'] / $sandbox['total']; + } +} diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info index f51804c..6b5f34d 100644 --- a/modules/simpletest/simpletest.info +++ b/modules/simpletest/simpletest.info @@ -45,5 +45,6 @@ files[] = tests/upgrade/upgrade.locale.test files[] = tests/upgrade/upgrade.menu.test files[] = tests/upgrade/upgrade.node.test files[] = tests/upgrade/upgrade.taxonomy.test +files[] = tests/upgrade/upgrade.translatable.test files[] = tests/upgrade/upgrade.upload.test files[] = tests/upgrade/upgrade.user.test diff --git a/modules/simpletest/tests/upgrade/drupal-6.translatable.database.php b/modules/simpletest/tests/upgrade/drupal-6.translatable.database.php new file mode 100644 index 0000000..5162116 --- /dev/null +++ b/modules/simpletest/tests/upgrade/drupal-6.translatable.database.php @@ -0,0 +1,125 @@ +fields(array( + 'nid', + 'vid', + 'type', + 'language', + 'title', + 'uid', + 'status', + 'created', + 'changed', + 'comment', + 'promote', + 'moderate', + 'sticky', + 'tnid', + 'translate', +)) +->values(array( + 'nid' => '53', + 'vid' => '63', + 'type' => 'translatable_page', + 'language' => 'fr', + 'title' => 'First translatable page', + 'uid' => '1', + 'status' => '1', + 'created' => '1298363952', + 'changed' => '1298363952', + 'comment' => '2', + 'promote' => '0', + 'moderate' => '0', + 'sticky' => '0', + 'tnid' => '0', + 'translate' => '0', +)) +->execute(); + +db_insert('node_revisions')->fields(array( + 'nid', + 'vid', + 'uid', + 'title', + 'body', + 'teaser', + 'log', + 'timestamp', + 'format', +)) +->values(array( + 'nid' => '53', + 'vid' => '63', + 'uid' => '1', + 'title' => 'First translatable page', + 'body' => 'Body of the first translatable page.', + 'teaser' => 'Teaser of the first translatable page.', + 'log' => '', + 'timestamp' => '1298363952', + 'format' => '1', +)) +->execute(); + +db_insert('node_comment_statistics')->fields(array( + 'nid', + 'last_comment_timestamp', + 'last_comment_name', + 'last_comment_uid', + 'comment_count', +)) +->values(array( + 'nid' => '53', + 'last_comment_timestamp' => '1298363952', + 'last_comment_name' => NULL, + 'last_comment_uid' => '1', + 'comment_count' => '0', +)) +->execute(); + +db_insert('node_type')->fields(array( + 'type', + 'name', + 'module', + 'description', + 'help', + 'has_title', + 'title_label', + 'has_body', + 'body_label', + 'min_word_count', + 'custom', + 'modified', + 'locked', + 'orig_type', +)) +->values(array( + 'type' => 'translatable_page', + 'name' => 'Translatable page', + 'module' => 'node', + 'description' => 'A translatable page is like a normal page, but with multilanguage support.', + 'help' => '', + 'has_title' => '1', + 'title_label' => 'Title', + 'has_body' => '1', + 'body_label' => 'Body', + 'min_word_count' => '0', + 'custom' => '0', + 'modified' => '0', + 'locked' => '1', + 'orig_type' => '', +)) +->execute(); + +db_insert('variable')->fields(array( + 'name', + 'value', +)) +->values(array( + 'name' => 'language_content_type_translatable_page', + 'value' => 's:1:"1";', +)) +->execute(); diff --git a/modules/simpletest/tests/upgrade/upgrade.translatable.test b/modules/simpletest/tests/upgrade/upgrade.translatable.test new file mode 100644 index 0000000..cb66e78 --- /dev/null +++ b/modules/simpletest/tests/upgrade/upgrade.translatable.test @@ -0,0 +1,51 @@ + 'Translatable upgrade path', + 'description' => 'Upgrade path tests for the translatable content types of node.module.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + // Path to the database dump files. + $this->databaseDumpFiles = array( + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.filled.database.php', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.locale.database.php', + drupal_get_path('module', 'simpletest') . '/tests/upgrade/drupal-6.translatable.database.php', + ); + parent::setUp(); + + $this->uninstallModulesExcept(array('locale')); + } + + /** + * Test a successful upgrade (no negotiation). + */ + public function testTranslatableUpgrade() { + $this->assertTrue($this->performUpgrade(), t('The upgrade was completed successfully.')); + + // The D6 database contains the translatable (English) node + // "First translatable page" with nid 53. + $nid = 53; + $title = 'First translatable page'; + $teaser = 'Teaser of the first translatable page.'; + + // Check whether the node displays properly. + $this->drupalGet("node/$nid"); + $this->assertText($body = 'Body of the first translatable page.', t('Translatable node body displays properly')); + + // Retrieve node object, ensure that both the body and the teaser has + // survived upgrade properly. + $node = $this->drupalGetNodeByTitle($title); + $this->assertTrue($node != NULL, t('Node @title was loaded', array('@title' => $title))); + $this->verbose('Upgraded node: ' . var_export($node, TRUE)); + $this->assertEqual($node->body[LANGUAGE_NONE][0]['value'], $body, 'Body of the node survided upgrade properly'); + $this->assertEqual($node->body[LANGUAGE_NONE][0]['summary'], $teaser, 'Teaser of the node survided upgrade properly'); + } +}