diff --git a/runtests.py b/runtests.py index 6831e31..24d6a3a 100755 --- a/runtests.py +++ b/runtests.py @@ -54,8 +54,10 @@ def runtests(): 'wagtail.contrib.wagtailapi', 'wagtail_modeltranslation', - ), + # remove wagtailcore from serialization as translation columns have not been created at this point + # (which causes OperationalError: no such column) + TEST_NON_SERIALIZED_APPS=['wagtail.wagtailcore'], ROOT_URLCONF=None, # tests override urlconf, but it still needs to be defined LANGUAGES=( ('en', 'English'), diff --git a/setup.py b/setup.py index fc7d258..0dddd56 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup( name='wagtail-modeltranslation', - version='0.6.0rc2', + version='0.8.0-alpha', description='Translates Wagtail CMS models using a registration approach.', long_description=( 'The modeltranslation application can be used to translate dynamic ' diff --git a/wagtail_modeltranslation/management/commands/makemigrations_translation.py b/wagtail_modeltranslation/management/commands/makemigrations_translation.py new file mode 100644 index 0000000..85c1a23 --- /dev/null +++ b/wagtail_modeltranslation/management/commands/makemigrations_translation.py @@ -0,0 +1,18 @@ +from django.core.management.commands.makemigrations import Command as MakeMigrationsCommand +from django.db.migrations.autodetector import MigrationAutodetector + + +# decorate MigrationAutodetector.changes so we can silently remove wagtailcore changes +def changes_decorator(func): + def wrapper(self, graph, trim_to_apps=None, convert_apps=None, migration_name=None): + changes = func(self, graph, trim_to_apps, convert_apps, migration_name) + if 'wagtailcore' in changes: + del changes['wagtailcore'] + return changes + return wrapper + +MigrationAutodetector.changes = changes_decorator(MigrationAutodetector.changes) + + +class Command(MakeMigrationsCommand): + help = "Creates new migration(s) for apps except wagtailcore." diff --git a/wagtail_modeltranslation/management/commands/sync_page_translation_fields.py b/wagtail_modeltranslation/management/commands/sync_page_translation_fields.py new file mode 100644 index 0000000..ec75d1e --- /dev/null +++ b/wagtail_modeltranslation/management/commands/sync_page_translation_fields.py @@ -0,0 +1,26 @@ +from modeltranslation.management.commands.sync_translation_fields import Command as SyncTranslationsFieldsCommand +from modeltranslation.translator import translator +from wagtail.wagtailcore.models import Page + + +old_get_registered_models = translator.get_registered_models + +# Monkey patching, only return a model if it's Page +def get_page_model(self, abstract=True): + models = old_get_registered_models(abstract) + return [x for x in models if x is Page] + + +class Command(SyncTranslationsFieldsCommand): + help = ("Detect new translatable fields or new available languages and" + " sync Wagtail's Page database structure. Does not remove " + " columns of removed languages or undeclared fields.") + + def handle(self, *args, **options): + translator.get_registered_models = get_page_model.__get__(translator) + + try: + super(Command, self).handle(*args, **options) + + finally: + translator.get_registered_models = old_get_registered_models diff --git a/wagtail_modeltranslation/patch_wagtailadmin.py b/wagtail_modeltranslation/patch_wagtailadmin.py index 96a9bfa..d65dc0d 100644 --- a/wagtail_modeltranslation/patch_wagtailadmin.py +++ b/wagtail_modeltranslation/patch_wagtailadmin.py @@ -36,7 +36,6 @@ COMPOSED_PANEL_CLASSES = [MultiFieldPanel, FieldRowPanel] + CUSTOM_COMPOSED_PANE class WagtailTranslator(object): _patched_models = [] - _page_fields_tables = [] def __init__(self, model): # Check if this class was already patched @@ -52,10 +51,6 @@ class WagtailTranslator(object): WagtailTranslator._patched_models.append(model) - # compile all tables that are holding page translated fields (title_xx, slug_xx, url_path_xx) - options = translator.get_options_for_model(model) - if 'url_path' in options.local_fields.keys() and model._meta.db_table is not 'wagtailcore_page': - WagtailTranslator._page_fields_tables.append(model._meta.db_table) def _patch_page_models(self, model): # PANEL PATCHING @@ -425,40 +420,32 @@ def _patch_clean(model): def _localized_update_descendant_url_paths(self, old_url_path, new_url_path, language): localized_url_path = build_localized_fieldname('url_path', language) - for db_table in WagtailTranslator._page_fields_tables: - cursor = connection.cursor() - if connection.vendor == 'sqlite': - update_statement = """ - UPDATE {db_table} - SET {localized_url_path} = %s || substr({localized_url_path}, %s) - WHERE EXISTS (SELECT * FROM wagtailcore_page AS p - WHERE p.id = {db_table}.page_ptr_id AND p.path LIKE %s) - AND page_ptr_id <> %s - """.format(db_table=db_table, localized_url_path=localized_url_path) - elif connection.vendor == 'mysql': - update_statement = """ - UPDATE {db_table} t - JOIN wagtailcore_page p ON p.id = t.page_ptr_id - SET {localized_url_path}= CONCAT(%s, substring({localized_url_path}, %s)) - WHERE p.path LIKE %s AND t.page_ptr_id <> %s - """.format(db_table=db_table, localized_url_path=localized_url_path) - elif connection.vendor in ('mssql', 'microsoft'): - update_statement = """ - UPDATE t - SET {localized_url_path}= CONCAT(%s, (SUBSTRING({localized_url_path}, 0, %s))) - FROM {db_table} t - JOIN wagtailcore_page p - ON p.id = t.page_ptr_id - WHERE p.path LIKE %s AND t.page_ptr_id <> %s - """.format(db_table=db_table, localized_url_path=localized_url_path) - else: - update_statement = """ - UPDATE {db_table} as t - SET {localized_url_path} = %s || substring({localized_url_path} from %s) - FROM wagtailcore_page AS p - WHERE p.id = t.page_ptr_id AND p.path LIKE %s AND t.page_ptr_id <> %s - """.format(db_table=db_table, localized_url_path=localized_url_path) - cursor.execute(update_statement, [new_url_path, len(old_url_path) + 1, self.path + '%', self.page_ptr_id]) + cursor = connection.cursor() + if connection.vendor == 'sqlite': + update_statement = """ + UPDATE wagtailcore_page + SET {localized_url_path} = %s || substr({localized_url_path}, %s) + WHERE path LIKE %s AND id <> %s + """.format(localized_url_path=localized_url_path) + elif connection.vendor == 'mysql': + update_statement = """ + UPDATE wagtailcore_page + SET {localized_url_path}= CONCAT(%s, substring({localized_url_path}, %s)) + WHERE path LIKE %s AND id <> %s + """.format(localized_url_path=localized_url_path) + elif connection.vendor in ('mssql', 'microsoft'): + update_statement = """ + UPDATE wagtailcore_page + SET {localized_url_path}= CONCAT(%s, (SUBSTRING({localized_url_path}, 0, %s))) + WHERE path LIKE %s AND id <> %s + """.format(localized_url_path=localized_url_path) + else: + update_statement = """ + UPDATE wagtailcore_page + SET {localized_url_path} = %s || substring({localized_url_path} from %s) + WHERE path LIKE %s AND id <> %s + """.format(localized_url_path=localized_url_path) + cursor.execute(update_statement, [new_url_path, len(old_url_path) + 1, self.path + '%', self.id]) class LocalizedSaveDescriptor(object): diff --git a/wagtail_modeltranslation/tests/tests.py b/wagtail_modeltranslation/tests/tests.py index 8d2f892..70862e2 100755 --- a/wagtail_modeltranslation/tests/tests.py +++ b/wagtail_modeltranslation/tests/tests.py @@ -51,9 +51,20 @@ class WagtailModeltranslationTransactionTestBase(TransactionTestCase): imp.reload(translator) # reload the translation module to register the Page model + # and also edit_handlers so any patches made to Page are reapplied from wagtail_modeltranslation import translation as wag_translation, translator as wag_translator + from wagtail.wagtailadmin import edit_handlers + import sys + del cls.cache.all_models['wagtailcore'] + sys.modules.pop('wagtail_modeltranslation.translation.pagetr', None) + sys.modules.pop('wagtail.wagtailcore.models', None) imp.reload(wag_translation) imp.reload(wag_translator) + imp.reload(edit_handlers) # so Page can be repatched by edit_handlers + wagtailcore_args = [] + if django.VERSION < (1, 11): + wagtailcore_args = [cls.cache.all_models['wagtailcore']] + cls.cache.get_app_config('wagtailcore').import_models(*wagtailcore_args) # Reload the patching class to update the imported translator # in order to include the newly registered models @@ -64,10 +75,12 @@ class WagtailModeltranslationTransactionTestBase(TransactionTestCase): # have translation fields, but for languages previously defined. We want # to be sure that 'de' and 'en' are available) del cls.cache.all_models['tests'] - import sys sys.modules.pop('wagtail_modeltranslation.tests.models', None) sys.modules.pop('wagtail_modeltranslation.tests.translation', None) - cls.cache.get_app_config('tests').import_models(cls.cache.all_models['tests']) + tests_args = [] + if django.VERSION < (1, 11): + tests_args = [cls.cache.all_models['tests']] + cls.cache.get_app_config('tests').import_models(*tests_args) # 4. Autodiscover from modeltranslation.models import handle_translation_registrations @@ -75,14 +88,17 @@ class WagtailModeltranslationTransactionTestBase(TransactionTestCase): # 5. makemigrations from django.db import connections, DEFAULT_DB_ALIAS - call_command('makemigrations', verbosity=2, interactive=False, + call_command('makemigrations_translation', verbosity=2, interactive=False, database=connections[DEFAULT_DB_ALIAS].alias) # 6. Syncdb call_command('migrate', verbosity=0, migrate=False, interactive=False, run_syncdb=True, database=connections[DEFAULT_DB_ALIAS].alias, load_initial_data=False) - # 7. patch wagtail models + # 7. Make sure Page translation fields are created + call_command('sync_page_translation_fields', interactive=False, verbosity=0, database=connections[DEFAULT_DB_ALIAS].alias) + + # 8. patch wagtail models from wagtail_modeltranslation.patch_wagtailadmin import patch_wagtail_models patch_wagtail_models() @@ -90,7 +106,7 @@ class WagtailModeltranslationTransactionTestBase(TransactionTestCase): # tests app has been added into INSTALLED_APPS and loaded # (that's why this is not imported in normal import section) global models, translation - from wagtail_modeltranslation.tests import models, translation + from wagtail_modeltranslation.tests import models, translation # NOQA def setUp(self): self._old_language = get_language() @@ -355,7 +371,7 @@ class WagtailModeltranslationTest(WagtailModeltranslationTestBase): trans_real.activate('en') # fetches the correct Page using slug using non-default language - page = Page.objects.filter(slug='test-slug-de').first() + page = Page.objects.rewrite(False).filter(slug='test-slug-de').first() self.assertEqual(page.specific, root, 'The wrong page was retrieved from DB.') # save the page 2 in the non-default language @@ -369,13 +385,13 @@ class WagtailModeltranslationTest(WagtailModeltranslationTestBase): self.assertEqual(root2.slug_en, 'test-slug2-en', 'slug_en has the wrong value.') # fetches the correct Page using slug using non-default language - page = Page.objects.filter(slug='test-slug2-de').first() + page = Page.objects.rewrite(False).filter(slug='test-slug2-de').first() self.assertEqual(page.specific, root2, 'The wrong page was retrieved from DB.') trans_real.activate('de') # fetches the correct Page using slug using default language - page = Page.objects.filter(slug='test-slug2-de').first() + page = Page.objects.rewrite(False).filter(slug='test-slug2-de').first() self.assertEqual(page.specific, root2, 'The wrong page was retrieved from DB.') @@ -497,26 +513,10 @@ class WagtailModeltranslationTest(WagtailModeltranslationTestBase): self.assertEqual(child.url_path_en, '/child_en/') # We should retrieve grandchild with the below command: - # grandchild_new = models.TestSlugPage1.objects.get(id=grandchild.id) - # but it's exhibiting strange behaviour during tests. See: - # https://github.com/infoportugal/wagtail-modeltranslation/issues/103#issuecomment-352006610 - grandchild_new = models.TestSlugPage1._default_manager.raw(""" - SELECT page_ptr_id, url_path_en, url_path_de FROM {} - WHERE page_ptr_id=%s LIMIT 1 - """.format(models.TestSlugPage1._meta.db_table), [grandchild.page_ptr_id])[0] + grandchild_new = models.TestSlugPage1.objects.get(id=grandchild.id) self.assertEqual(grandchild_new.url_path_en, '/child_en/grandchild1_en/') self.assertEqual(grandchild_new.url_path_de, '/child/grandchild1/') - - def test_page_fields_tables(self): - from wagtail_modeltranslation.patch_wagtailadmin import WagtailTranslator - - self.assertIn(models.TestSlugPage1, WagtailTranslator._patched_models) - self.assertIn('tests_testslugpage1', WagtailTranslator._page_fields_tables) - self.assertIn(models.TestSlugPage1Subclass, WagtailTranslator._patched_models) - self.assertNotIn('tests_testslugpage1subclass', WagtailTranslator._page_fields_tables) - self.assertNotIn('wagtailcore_page', WagtailTranslator._page_fields_tables) - def test_fetch_translation_records(self): """ Assert that saved translation fields are retrieved correctly diff --git a/wagtail_modeltranslation/tests/translation.py b/wagtail_modeltranslation/tests/translation.py index 1282a53..138649d 100755 --- a/wagtail_modeltranslation/tests/translation.py +++ b/wagtail_modeltranslation/tests/translation.py @@ -4,38 +4,40 @@ from modeltranslation.translator import translator, register, TranslationOptions from wagtail_modeltranslation.tests.models import TestRootPage, TestSlugPage1, TestSlugPage2, PatchTestPage, \ PatchTestSnippet, FieldPanelPage, ImageChooserPanelPage, FieldRowPanelPage, MultiFieldPanelPage, InlinePanelPage, \ FieldPanelSnippet, ImageChooserPanelSnippet, FieldRowPanelSnippet, MultiFieldPanelSnippet, PageInlineModel, \ - BaseInlineModel, StreamFieldPanelPage, StreamFieldPanelSnippet, SnippetInlineModel, InlinePanelSnippet, TestSlugPage1Subclass -from wagtail_modeltranslation.translator import WagtailTranslationOptions + BaseInlineModel, StreamFieldPanelPage, StreamFieldPanelSnippet, SnippetInlineModel, InlinePanelSnippet, \ + TestSlugPage1Subclass + +from wagtail.wagtailcore.models import Page # Wagtail Models @register(TestRootPage) -class TestRootPagePageTranslationOptions(WagtailTranslationOptions): +class TestRootPagePageTranslationOptions(TranslationOptions): fields = () @register(TestSlugPage1) -class TestSlugPage1TranslationOptions(WagtailTranslationOptions): +class TestSlugPage1TranslationOptions(TranslationOptions): fields = () @register(TestSlugPage2) -class TestSlugPage2TranslationOptions(WagtailTranslationOptions): +class TestSlugPage2TranslationOptions(TranslationOptions): fields = () @register(TestSlugPage1Subclass) -class TestSlugPage1SubclassTranslationOptions(WagtailTranslationOptions): +class TestSlugPage1SubclassTranslationOptions(TranslationOptions): pass @register(PatchTestPage) -class PatchTestPageTranslationOptions(WagtailTranslationOptions): +class PatchTestPageTranslationOptions(TranslationOptions): fields = ('description',) -class PatchTestSnippetTranslationOptions(WagtailTranslationOptions): +class PatchTestSnippetTranslationOptions(TranslationOptions): fields = ('name',) @@ -44,7 +46,7 @@ translator.register(PatchTestSnippet, PatchTestSnippetTranslationOptions) # Panel Patching Models -class FieldPanelTranslationOptions(WagtailTranslationOptions): +class FieldPanelTranslationOptions(TranslationOptions): fields = ('name',) @@ -52,7 +54,7 @@ translator.register(FieldPanelPage, FieldPanelTranslationOptions) translator.register(FieldPanelSnippet, FieldPanelTranslationOptions) -class ImageChooserPanelTranslationOptions(WagtailTranslationOptions): +class ImageChooserPanelTranslationOptions(TranslationOptions): fields = ('image',) @@ -60,7 +62,7 @@ translator.register(ImageChooserPanelPage, ImageChooserPanelTranslationOptions) translator.register(ImageChooserPanelSnippet, ImageChooserPanelTranslationOptions) -class FieldRowPanelTranslationOptions(WagtailTranslationOptions): +class FieldRowPanelTranslationOptions(TranslationOptions): fields = ('other_name',) @@ -68,7 +70,7 @@ translator.register(FieldRowPanelPage, FieldRowPanelTranslationOptions) translator.register(FieldRowPanelSnippet, FieldRowPanelTranslationOptions) -class StreamFieldPanelTranslationOptions(WagtailTranslationOptions): +class StreamFieldPanelTranslationOptions(TranslationOptions): fields = ('body',) @@ -76,7 +78,7 @@ translator.register(StreamFieldPanelPage, StreamFieldPanelTranslationOptions) translator.register(StreamFieldPanelSnippet, StreamFieldPanelTranslationOptions) -class MultiFieldPanelTranslationOptions(WagtailTranslationOptions): +class MultiFieldPanelTranslationOptions(TranslationOptions): fields = () @@ -84,14 +86,14 @@ translator.register(MultiFieldPanelPage, MultiFieldPanelTranslationOptions) translator.register(MultiFieldPanelSnippet, MultiFieldPanelTranslationOptions) -class InlinePanelTranslationOptions(WagtailTranslationOptions): +class InlinePanelTranslationOptions(TranslationOptions): fields = ('field_name', 'image_chooser', 'fieldrow_name',) translator.register(BaseInlineModel, InlinePanelTranslationOptions) -class InlinePanelTranslationOptions(WagtailTranslationOptions): +class InlinePanelTranslationOptions(TranslationOptions): fields = () @@ -100,7 +102,7 @@ translator.register(SnippetInlineModel, InlinePanelTranslationOptions) @register(InlinePanelPage) -class InlinePanelModelTranslationOptions(WagtailTranslationOptions): +class InlinePanelModelTranslationOptions(TranslationOptions): fields = () diff --git a/wagtail_modeltranslation/translation.py b/wagtail_modeltranslation/translation.py index 48e0b0b..4ad36d7 100644 --- a/wagtail_modeltranslation/translation.py +++ b/wagtail_modeltranslation/translation.py @@ -7,4 +7,13 @@ from wagtail.wagtailcore.models import Page @register(Page) class PageTR(TranslationOptions): - pass + class Meta: + managed = False + + fields = ( + 'title', + 'slug', + 'seo_title', + 'search_description', + 'url_path', + )