Move Page translation fields to Page table (breaking changes!)

- Bumps version to 0.8.0-alpha
- Introduces 'makemigrations_translation' command to avoid generating
wagtailcore_page migrations
- Adds 'sync_page_translation_fields' which mimicks
'sync_translation_fields' but restricts it to Page model
- This change introduces breaking changes and has no migration script to
move from 0.6 to 0.8
- Updates tests
This commit is contained in:
Dario Marcelino 2017-12-21 18:58:07 +00:00
parent 1a656b98da
commit 333c392dc8
8 changed files with 127 additions and 83 deletions

View file

@ -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'),

View file

@ -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 '

View file

@ -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."

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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 = ()

View file

@ -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',
)