diff --git a/README.md b/README.md index 4b4e3ff..3f3300a 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ wagtail's snippets. This app is tested to runs with: * Django 3.2, 4.2 -* Wagtail TODO -* Parler TODO +* Wagtail 4.1, 4.2, 5.0, 5.1 +* Parler 2.3 (probably older ones to, it's just not tested) * Python 3.7, 3.9, 3.11 To ensure code quality and consistency: diff --git a/test.sqlite b/test.sqlite deleted file mode 100644 index dad1efb..0000000 Binary files a/test.sqlite and /dev/null differ diff --git a/test_fixtures.json b/test_fixtures.json index 45a27ad..b30b715 100644 --- a/test_fixtures.json +++ b/test_fixtures.json @@ -1,122 +1,164 @@ [ - { - "model": "wagtail_parler_tests.foodtranslation", - "pk": 1, - "fields": { - "language_code": "fr", - "name": "Gelée", - "summary": "Summary FR", - "content": "Content FR", - "master": 1 - } - }, - { - "model": "wagtail_parler_tests.foodtranslation", - "pk": 2, - "fields": { - "language_code": "fr", - "name": "Pudding de Noël", - "summary": "Summary FR", - "content": "Content FR", - "master": 2 - } - }, - { - "model": "wagtail_parler_tests.foodtranslation", - "pk": 3, - "fields": { - "language_code": "fr", - "name": "Omelette au fromage", - "summary": "Summary FR", - "content": "Content FR", - "master": 3 - } - }, - { - "model": "wagtail_parler_tests.foodtranslation", - "pk": 4, - "fields": { - "language_code": "fr", - "name": "Raclette", - "summary": "Summary FR", - "content": "Content FR", - "master": 4 - } - }, - { - "model": "wagtail_parler_tests.foodtranslation", - "pk": 5, - "fields": { - "language_code": "en", - "name": "Jelly", - "summary": "Summary EN", - "content": "Content EN", - "master": 1 - } - }, - { - "model": "wagtail_parler_tests.foodtranslation", - "pk": 6, - "fields": { - "language_code": "en", - "name": "Christmas Pudding", - "summary": "Summary EN", - "content": "Content EN", - "master": 2 - } - }, - { - "model": "wagtail_parler_tests.food", - "pk": 1, - "fields": { - "yum_rating": 3, - "vegetarian": true, - "vegan": true - } - }, - { - "model": "wagtail_parler_tests.food", - "pk": 2, - "fields": { - "yum_rating": 5, - "vegetarian": false, - "vegan": false - } - }, - { - "model": "wagtail_parler_tests.food", - "pk": 3, - "fields": { - "yum_rating": 8, - "vegetarian": true, - "vegan": false - } - }, - { - "model": "wagtail_parler_tests.food", - "pk": 4, - "fields": { - "yum_rating": 10, - "vegetarian": true, - "vegan": false - } - }, - { - "model": "auth.user", - "pk": 1, - "fields": { - "password": "md5$Xibmtaqa1WGE7j0V46OcOQ$8e5148fef28cc57db487f7a8978cb345", - "last_login": "2023-08-02T08:25:40.786Z", - "is_superuser": true, - "username": "admin", - "first_name": "", - "last_name": "", - "email": "admin@exemple.com", - "is_staff": true, - "is_active": true, - "date_joined": "2023-08-02T06:58:26.271Z", - "groups": [], - "user_permissions": [] - } +{ + "model": "wagtail_parler_tests.foodtranslation", + "pk": 1, + "fields": { + "language_code": "fr", + "name": "Gelée", + "summary": "Summary FR", + "content": "Content FR", + "qa": "[{\"type\": \"QaBlock\", \"value\": {\"text\": \"Pouvez-vous emmener une \\u00ab Jelly \\u00bb dans un avion ?\"}, \"id\": \"8c23eadb-60e0-4271-831d-332dd33ce36b\"}]", + "master": 1 } +}, +{ + "model": "wagtail_parler_tests.foodtranslation", + "pk": 2, + "fields": { + "language_code": "fr", + "name": "Pudding de Noël", + "summary": "Summary FR", + "content": "Content FR", + "qa": "[{\"type\": \"QaBlock\", \"value\": {\"text\": \"De quelle origine est ce plat ?\"}, \"id\": \"6b40015d-0cd9-48b4-8a06-3719cd6bce60\"}]", + "master": 2 + } +}, +{ + "model": "wagtail_parler_tests.foodtranslation", + "pk": 3, + "fields": { + "language_code": "fr", + "name": "Omelette au fromage", + "summary": "Summary FR", + "content": "Content FR", + "qa": "[{\"type\": \"QaBlock\", \"value\": {\"text\": \"De quelle s\\u00e9rie parle t-on de \\\"Omelette du fromage\\\"\"}, \"id\": \"05aeb9c2-b6e3-42cb-ac6b-195f93885af4\"}]", + "master": 3 + } +}, +{ + "model": "wagtail_parler_tests.foodtranslation", + "pk": 4, + "fields": { + "language_code": "fr", + "name": "Raclette", + "summary": "Summary FR", + "content": "Content FR", + "qa": "[{\"type\": \"QaBlock\", \"value\": {\"text\": \"Est-ce di\\u00e9t\\u00e9tique ?\"}, \"id\": \"a414bab0-97e9-4e3d-9ef1-5f2b2d165ece\"}]", + "master": 4 + } +}, +{ + "model": "wagtail_parler_tests.foodtranslation", + "pk": 5, + "fields": { + "language_code": "en", + "name": "Jelly", + "summary": "Summary EN", + "content": "Content EN", + "qa": "[{\"type\": \"QaBlock\", \"value\": {\"text\": \"Can you bring a Jelly in a plane ?\"}, \"id\": \"bbc8985f-f249-4ce2-9cab-cee966ffb4aa\"}]", + "master": 1 + } +}, +{ + "model": "wagtail_parler_tests.foodtranslation", + "pk": 6, + "fields": { + "language_code": "en", + "name": "Christmas Pudding", + "summary": "Summary EN", + "content": "Content EN", + "qa": "[{\"type\": \"QaBlock\", \"value\": {\"text\": \"What is the origin of this meal ?\"}, \"id\": \"a2a0a74c-7817-4129-a6d5-490c5af3afe9\"}]", + "master": 2 + } +}, +{ + "model": "wagtail_parler_tests.food", + "pk": 1, + "fields": { + "yum_rating": 3, + "vegetarian": true, + "vegan": true + } +}, +{ + "model": "wagtail_parler_tests.food", + "pk": 2, + "fields": { + "yum_rating": 5, + "vegetarian": false, + "vegan": false + } +}, +{ + "model": "wagtail_parler_tests.food", + "pk": 3, + "fields": { + "yum_rating": 8, + "vegetarian": true, + "vegan": false + } +}, +{ + "model": "wagtail_parler_tests.food", + "pk": 4, + "fields": { + "yum_rating": 10, + "vegetarian": true, + "vegan": false + } +}, +{ + "model": "auth.user", + "pk": 1, + "fields": { + "password": "md5$Xibmtaqa1WGE7j0V46OcOQ$8e5148fef28cc57db487f7a8978cb345", + "last_login": "2023-09-12T07:47:24.764Z", + "is_superuser": true, + "username": "admin", + "first_name": "", + "last_name": "", + "email": "admin@exemple.com", + "is_staff": true, + "is_active": true, + "date_joined": "2023-08-02T06:58:26.271Z", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "auth.group", + "pk": 1, + "fields": { + "name": "Moderators", + "permissions": [ + 1, + 2, + 3, + 5, + 4, + 6, + 7, + 9, + 8 + ] + } +}, +{ + "model": "auth.group", + "pk": 2, + "fields": { + "name": "Editors", + "permissions": [ + 1, + 2, + 3, + 5, + 4, + 6, + 7, + 9, + 8 + ] + } +} ] diff --git a/tox.ini b/tox.ini index 7cec310..d670c93 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py37-django32-wagtail{41,42,50}, py39-django42-wagtail{50,51}, py311-django42-wagtail51, - ; py{39,311}-django42-wagtailmain + py{39,311}-django42-wagtailmain qa [testenv] diff --git a/wagtail_parler/__init__.py b/wagtail_parler/__init__.py index 3dc1f76..a57e2eb 100644 --- a/wagtail_parler/__init__.py +++ b/wagtail_parler/__init__.py @@ -1 +1,2 @@ __version__ = "0.1.0" +default_app_config = "wagtail_parler.apps.WagtailParlerConfig" diff --git a/wagtail_parler/apps.py b/wagtail_parler/apps.py new file mode 100644 index 0000000..6a72502 --- /dev/null +++ b/wagtail_parler/apps.py @@ -0,0 +1,53 @@ +# Standard libs +import importlib +from typing import Optional +from typing import Tuple +import warnings + +# Django imports +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + +# Third Party +from wagtail.blocks.base import Block + + +class WagtailParlerConfig(AppConfig): + name = "wagtail_parler" + verbose_name = _("Wagtail Parler 🧀 🐦") + + def ready(self) -> None: + if Block.__reduce__.__qualname__ == "object.__reduce__": + """ + Monkey Patch wagtail to be able to Pickle Blocks. + FIXME: remove this when related PR will be merged into wagtail. + It's required when parler cache translations which can have + StreamValue fields with Blocks as values. + If Block or it's bases (but object) don't have any __reduce__ method, + we need one to be able to Pickle instances of Blocks. + """ + + def block_reduce(self: Block) -> Tuple: + path, args, kwargs = self.deconstruct() + return unreduce, (path, (args, kwargs)) + + Block.__reduce__ = block_reduce + else: # pragma: no cover + warnings.warn( + "Monkey Patch `block_reduce` is deprecated with this version of Wagtail", + DeprecationWarning, + stacklevel=2, + ) + + +def unreduce(path: str, args_and_kwargs: Optional[Tuple] = None) -> object: + path_part = path.rsplit(".", 1) + module = importlib.import_module(path_part[0]) + cls = getattr(module, path_part[1]) + args: Tuple = tuple() + kwargs = {} + if args_and_kwargs and len(args_and_kwargs) >= 1: + args = args_and_kwargs[0] + if len(args_and_kwargs) >= 2: + kwargs = args_and_kwargs[1] + return cls(*args, **kwargs) # type: ignore diff --git a/wagtail_parler_tests/migrations/0002_auto_20230907_1910.py b/wagtail_parler_tests/migrations/0002_auto_20230907_1910.py new file mode 100644 index 0000000..a92a4df --- /dev/null +++ b/wagtail_parler_tests/migrations/0002_auto_20230907_1910.py @@ -0,0 +1,108 @@ +# Generated by Django 3.2.21 on 2023-09-07 17:10 + +# Django imports +from django.db import migrations + +# Third Party +import wagtail.blocks +import wagtail.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("wagtail_parler_tests", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="foodtranslation", + name="qa", + field=wagtail.fields.StreamField( + [ + ( + "QaBlock", + wagtail.blocks.StructBlock( + [("text", wagtail.blocks.TextBlock(label="Question"))], label="QA" + ), + ) + ], + blank=True, + null=True, + use_json_field=True, + verbose_name="Some QA", + ), + ), + migrations.AddField( + model_name="foodwithedithandlertranslation", + name="qa", + field=wagtail.fields.StreamField( + [ + ( + "QaBlock", + wagtail.blocks.StructBlock( + [("text", wagtail.blocks.TextBlock(label="Question"))], label="QA" + ), + ) + ], + blank=True, + null=True, + use_json_field=True, + verbose_name="Some QA", + ), + ), + migrations.AddField( + model_name="foodwithemptyedithandlertranslation", + name="qa", + field=wagtail.fields.StreamField( + [ + ( + "QaBlock", + wagtail.blocks.StructBlock( + [("text", wagtail.blocks.TextBlock(label="Question"))], label="QA" + ), + ) + ], + blank=True, + null=True, + use_json_field=True, + verbose_name="Some QA", + ), + ), + migrations.AddField( + model_name="foodwithpanelsinsidemodeltranslation", + name="qa", + field=wagtail.fields.StreamField( + [ + ( + "QaBlock", + wagtail.blocks.StructBlock( + [("text", wagtail.blocks.TextBlock(label="Question"))], label="QA" + ), + ) + ], + blank=True, + null=True, + use_json_field=True, + verbose_name="Some QA", + ), + ), + migrations.AddField( + model_name="foodwithspecificedithandlertranslation", + name="qa", + field=wagtail.fields.StreamField( + [ + ( + "QaBlock", + wagtail.blocks.StructBlock( + [("text", wagtail.blocks.TextBlock(label="Question"))], label="QA" + ), + ) + ], + blank=True, + null=True, + use_json_field=True, + verbose_name="Some QA", + ), + ), + ] diff --git a/wagtail_parler_tests/models.py b/wagtail_parler_tests/models.py index 39e6d48..836d0a6 100644 --- a/wagtail_parler_tests/models.py +++ b/wagtail_parler_tests/models.py @@ -5,8 +5,14 @@ # Third Party from parler.models import TranslatableModel from parler.models import TranslatedFields +from wagtail import blocks from wagtail.admin.panels import FieldPanel from wagtail.admin.panels import ObjectList +from wagtail.fields import StreamField + + +class QABlock(blocks.StructBlock): + text = blocks.TextBlock(label="Question") class BaseFood(TranslatableModel): @@ -29,6 +35,16 @@ class BaseFood(TranslatableModel): "name": models.CharField(_("Nom"), max_length=255, blank=False, null=False), "summary": models.TextField(_("Résumé"), blank=False, null=False), "content": models.TextField(_("Contenu"), blank=False, null=False), + "qa": StreamField( + [ + ("QaBlock", QABlock(label="QA")), + ], + verbose_name=_("Some QA"), + blank=True, + null=True, + collapsed=True, + use_json_field=True, + ), } class Meta: diff --git a/wagtail_parler_tests/tests.py b/wagtail_parler_tests/tests.py index f2be871..c47c000 100644 --- a/wagtail_parler_tests/tests.py +++ b/wagtail_parler_tests/tests.py @@ -308,10 +308,30 @@ def test_create_translations(self: TestCase) -> None: "translations_fr_name": "Gelée", "translations_fr_summary": "Summary FR", "translations_fr_content": "Content FR", + "translations_fr_qa-count": 1, + "translations_fr_qa-0-deleted": "", + "translations_fr_qa-0-order": 0, + "translations_fr_qa-0-type": "QaBlock", + "translations_fr_qa-0-id": "8c23eadb-60e0-4271-831d-332dd33ce36b", + "translations_fr_qa-0-value-text": "Pouvez-vous emmener une « Jelly » dans un avion ?", + # EN "translations_en_name": "Jelly", "translations_en_summary": "Summary EN", "translations_en_content": "Content EN", + "translations_en_qa-count": 1, + "translations_en_qa-0-deleted": "", + "translations_en_qa-0-order": 0, + "translations_en_qa-0-type": "QaBlock", + "translations_en_qa-0-id": "bbc8985f-f249-4ce2-9cab-cee966ffb4aa", + "translations_en_qa-0-value-text": "Can you bring a Jelly in a plane ?", + # ES "translations_es_name": "Jelly ES", + "translations_es_qa-count": 1, + "translations_es_qa-0-deleted": "", + "translations_es_qa-0-order": 0, + "translations_es_qa-0-type": "QaBlock", + "translations_es_qa-0-id": "d2d0ab62-6946-4764-947a-77f58ebfd7ae", + "translations_es_qa-0-value-text": "QA ES", } resp = self.client.post(edit_url, data) self.assertEqual(resp.status_code, 302) @@ -319,6 +339,10 @@ def test_create_translations(self: TestCase) -> None: jelly = Food.objects.get(pk=1) self.assertIn("es", jelly.get_available_languages()) self.assertEqual(jelly.get_translation("es").name, "Jelly ES") + self.assertEqual( + jelly.get_translation("es").qa.raw_data[0]["value"]["text"], + "QA ES", + ) def test_update_translations(self: TestCase) -> None: """checks we can update existing translations for an instance""" @@ -334,15 +358,34 @@ def test_update_translations(self: TestCase) -> None: "translations_fr_name": "Gelée", "translations_fr_summary": "Summary FR", "translations_fr_content": "Content FR", + "translations_fr_qa-count": 1, + "translations_fr_qa-0-deleted": "", + "translations_fr_qa-0-order": 0, + "translations_fr_qa-0-type": "QaBlock", + "translations_fr_qa-0-id": "8c23eadb-60e0-4271-831d-332dd33ce36b", + "translations_fr_qa-0-value-text": "Pouvez-vous emmener une « Jelly » dans un avion ?", + # EN "translations_en_name": "Jelly updated", "translations_en_summary": "Summary EN", "translations_en_content": "Content EN", + "translations_en_qa-count": 1, + "translations_en_qa-0-deleted": "", + "translations_en_qa-0-order": 0, + "translations_en_qa-0-type": "QaBlock", + "translations_en_qa-0-id": "bbc8985f-f249-4ce2-9cab-cee966ffb4aa", + "translations_en_qa-0-value-text": "Can you bring a Jelly in a plane ? Updated", + # ES + "translations_es_qa-count": 0, } resp = self.client.post(edit_url, data) self.assertEqual(resp.status_code, 302) self.assertEqual(resp.url, list_url) jelly = Food.objects.get(pk=1) self.assertEqual(jelly.get_translation("en").name, "Jelly updated") + self.assertEqual( + jelly.get_translation("en").qa.raw_data[0]["value"]["text"], + "Can you bring a Jelly in a plane ? Updated", + ) self.assertNotIn("es", jelly.get_available_languages()) def test_delete_translations(self: TestCase) -> None: @@ -359,6 +402,9 @@ def test_delete_translations(self: TestCase) -> None: "translations_fr_name": "Gelée", "translations_fr_summary": "Summary FR", "translations_fr_content": "Content FR", + "translations_fr_qa-count": 0, + "translations_en_qa-count": 0, + "translations_es_qa-count": 0, } resp = self.client.post(edit_url, data) self.assertEqual(resp.status_code, 302)