From e2bc3f2d4dcee6d5de8c4515c130c222e877f4fc Mon Sep 17 00:00:00 2001 From: Dirk Eschler Date: Wed, 11 Jul 2012 12:57:37 +0000 Subject: [PATCH] =?UTF-8?q?Added=20a=20new=20management=20command=20sync?= =?UTF-8?q?=5Fdatabase=5Ffields=20to=20sync=20the=20database=20after=20a?= =?UTF-8?q?=20new=20model=20has=20been=20registered=20or=20a=20new=20langu?= =?UTF-8?q?age=20has=20been=20added.=20Resolves=20issue=2062=20(thanks=20t?= =?UTF-8?q?o=20S=C3=A9bastien=20Fievet=20and=20the=20authors=20of=20django?= =?UTF-8?q?-transmeta).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUTHORS.txt | 1 + CHANGELOG.txt | 11 +- .../commands/sync_translation_fields.py | 137 ++++++++++++++++++ 3 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 modeltranslation/management/commands/sync_translation_fields.py diff --git a/AUTHORS.txt b/AUTHORS.txt index 3523ac9..cde65ab 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -8,3 +8,4 @@ CONTRIBUTORS Carl J. Meyer Jaap Roes Bojan Mihelac +Sébastien Fievet diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 17543db..4046466 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,14 +1,18 @@ v0.4.0-alpha1 ============= - FIXED: Dynamic TranslationOptions model name. + ADDED: New management command sync_database_fields to sync the database after + a new model has been registered or a new language has been added. + (thanks to Sébastien Fievet and the authors of django-transmeta, + resolves issue 62) + CHANGED: Use app-level translation files in favour of a single project-level one. Adds an autoregister feature similiar to the one provided by Django's admin. A new setting MODELTRANSLATION_TRANSLATION_FILES keeps backwards compatibility with older versions. See documentation for details. This is basically a merge from both django-modeltranslation-wrapper and hyperweek's branch at github. - (thanks to Jacek Tomaszewski, hyperweek and haineault) + (thanks to Jacek Tomaszewski, Sébastien Fievet and Maxime Haineault, resolves issues 19, 58 and 71) CHANGED: Moved tests to separate folder and added tests for TranslationAdmin. To run the tests the settings provided in model.tests.modeltranslation @@ -20,6 +24,8 @@ CHANGED: Major refactoring of the admin integration. Subclassed BaseModelAdmin and get_fieldsets. This should resolve several problems with the exclude and fieldsets options and properly support options in inlines. (resolves issue 72) + + FIXED: Dynamic TranslationOptions model name. FIXED: Widgets for translated fields are not properly copied from original fields. (thanks to boris-chervenkov, resolves issue 74) @@ -35,6 +41,7 @@ Date: 2012-02-23 CHANGED: jQuery search path in tabbed_translation_fields.js. This allows use of a version of jQuery other than the one provided by Django. Users who want to force the use of Django's jQuery can include force_jquery.js. + FIXED: Another attempt to include static files during installation. (resolves reopened issue 61) diff --git a/modeltranslation/management/commands/sync_translation_fields.py b/modeltranslation/management/commands/sync_translation_fields.py new file mode 100644 index 0000000..059a686 --- /dev/null +++ b/modeltranslation/management/commands/sync_translation_fields.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +""" +Detect new translatable fields in all models and sync database structure. + +You will need to execute this command in two cases: + + 1. When you add new languages to settings.LANGUAGES. + 2. When you add new translatable fields to your models. + +Credits: Heavily inspired by django-transmeta's sync_transmeta_db command. +""" +from django.conf import settings +from django.core.management.base import BaseCommand +from django.core.management.color import no_style +from django.db import connection, transaction +from django.db.models import get_models + +from modeltranslation.translator import translator, NotRegistered +from modeltranslation.utils import build_localized_fieldname + + +def ask_for_confirmation(sql_sentences, model_full_name): + print '\nSQL to synchronize "%s" schema:' % model_full_name + for sentence in sql_sentences: + print ' %s' % sentence + while True: + prompt = ('\nAre you sure that you want to execute the previous SQL: ' + '(y/n) [n]: ') + answer = raw_input(prompt).strip() + if answer == '': + return False + elif answer not in ('y', 'n', 'yes', 'no'): + print 'Please answer yes or no' + elif answer == 'y' or answer == 'yes': + return True + else: + return False + + +def print_missing_langs(missing_langs, field_name, model_name): + print 'Missing languages in "%s" field from "%s" model: %s' % ( + field_name, model_name, ", ".join(missing_langs)) + + +class Command(BaseCommand): + help = ('Detect new translatable fields or new available languages and ' + 'sync database structure') + + def handle(self, *args, **options): + """ + Command execution. + """ + self.cursor = connection.cursor() + self.introspection = connection.introspection + + all_models = get_models() + found_missing_fields = False + for model in all_models: + try: + options = translator.get_options_for_model(model) + # options returns full-wide spectrum of localized fields but + # we only to synchronize the local fields attached to the model. + local_field_names = [field.name for field + in model._meta.local_fields] + translatable_fields = [field for field + in options.localized_fieldnames + if field in local_field_names] + model_full_name = '%s.%s' % (model._meta.app_label, + model._meta.module_name) + db_table = model._meta.db_table + for field_name in translatable_fields: + missing_langs = list( + self.get_missing_languages(field_name, db_table)) + if missing_langs: + found_missing_fields = True + print_missing_langs( + missing_langs, field_name, model_full_name) + sql_sentences = self.get_sync_sql( + field_name, missing_langs, model) + execute_sql = ask_for_confirmation( + sql_sentences, model_full_name) + if execute_sql: + print 'Executing SQL...', + for sentence in sql_sentences: + self.cursor.execute(sentence) + print 'Done' + else: + print 'SQL not executed' + except NotRegistered: + pass + + transaction.commit_unless_managed() + + if not found_missing_fields: + print 'No new translatable fields detected' + + def get_table_fields(self, db_table): + """ + Gets table fields from schema. + """ + db_table_desc = self.introspection.get_table_description( + self.cursor, db_table) + return [t[0] for t in db_table_desc] + + def get_missing_languages(self, field_name, db_table): + """ + Gets only missings fields. + """ + db_table_fields = self.get_table_fields(db_table) + for lang_code, lang_name in settings.LANGUAGES: + if build_localized_fieldname( + field_name, lang_code) not in db_table_fields: + yield lang_code + + def get_sync_sql(self, field_name, missing_langs, model): + """ + Returns SQL needed for sync schema for a new translatable field. + """ + qn = connection.ops.quote_name + style = no_style() + sql_output = [] + db_table = model._meta.db_table + for lang in missing_langs: + new_field = build_localized_fieldname(field_name, lang) + f = model._meta.get_field(new_field) + col_type = f.db_type(connection) + field_sql = [style.SQL_FIELD(qn(f.column)), + style.SQL_COLTYPE(col_type)] + # column creation + sql_output.append( + "ALTER TABLE %s ADD COLUMN %s;" % ( + qn(db_table), ' '.join(field_sql))) + if not f.null and lang == settings.LANGUAGE_CODE: + sql_output.append("ALTER TABLE %s MODIFY COLUMN %s %s %s;" % \ + (qn(db_table), qn(f.column), col_type, + style.SQL_KEYWORD('NOT NULL'))) + return sql_output