Replace `content_json` `TextField` with `content` `JSONField` in `PageRevision`

pull/8046/head
Sage Abdullah 2022-02-22 19:38:51 +07:00 zatwierdzone przez Jacob Topp-Mugglestone
rodzic 861a509b32
commit bae76a2af0
11 zmienionych plików z 90 dodań i 53 usunięć

Wyświetl plik

@ -490,7 +490,7 @@ The ``locale`` and ``translation_key`` fields have a unique key constraint to pr
Every time a page is edited a new ``PageRevision`` is created and saved to the database. It can be used to find the full history of all changes that have been made to a page and it also provides a place for new changes to be kept before going live. Every time a page is edited a new ``PageRevision`` is created and saved to the database. It can be used to find the full history of all changes that have been made to a page and it also provides a place for new changes to be kept before going live.
- Revisions can be created from any :class:`~wagtail.core.models.Page` object by calling its :meth:`~Page.save_revision` method - Revisions can be created from any :class:`~wagtail.core.models.Page` object by calling its :meth:`~Page.save_revision` method
- The content of the page is JSON-serialised and stored in the :attr:`~PageRevision.content_json` field - The content of the page is JSON-serialisable and stored in the :attr:`~PageRevision.content` field
- You can retrieve a ``PageRevision`` as a :class:`~wagtail.core.models.Page` object by calling the :meth:`~PageRevision.as_page_object` method - You can retrieve a ``PageRevision`` as a :class:`~wagtail.core.models.Page` object by calling the :meth:`~PageRevision.as_page_object` method
Database fields Database fields
@ -520,12 +520,17 @@ Database fields
This links to the user that created the revision This links to the user that created the revision
.. attribute:: content_json .. attribute:: content
(text) (dict)
This field contains the JSON content for the page at the time the revision was created This field contains the JSON content for the page at the time the revision was created
.. versionchanged:: 2.17
The field has been renamed from ``content_json`` to ``content`` and it now uses :class:`~django.db.models.JSONField` instead of
:class:`~django.db.models.TextField`.
Managers Managers
~~~~~~~~ ~~~~~~~~

Wyświetl plik

@ -24,6 +24,7 @@ Here are other changes related to the redesign:
* Remove UI code for legacy browser support: polyfills, IE11 workarounds, Modernizr (Thibaud Colas) * Remove UI code for legacy browser support: polyfills, IE11 workarounds, Modernizr (Thibaud Colas)
* Remove redirect auto-creation recipe from documentation as this feature is now supported in Wagtail core (Andy Babic) * Remove redirect auto-creation recipe from documentation as this feature is now supported in Wagtail core (Andy Babic)
* Remove IE11 warnings (Gianluca De Cola) * Remove IE11 warnings (Gianluca De Cola)
* Replace `content_json` `TextField` with `content` `JSONField` in `PageRevision` (Sage Abdullah)
### Bug fixes ### Bug fixes
@ -40,3 +41,9 @@ Here are other changes related to the redesign:
* IE11 support was officially dropped in Wagtail 2.15, as of this release there will no longer be a warning shown to users of this browser. * IE11 support was officially dropped in Wagtail 2.15, as of this release there will no longer be a warning shown to users of this browser.
* Wagtail is fully compatible with Microsoft Edge, Microsofts replacement for Internet Explorer. You may consider using its `IE mode <https://docs.microsoft.com/en-us/deployedge/edge-ie-mode>`_ to keep access to IE11-only sites, while other sites and apps like Wagtail can leverage modern browser capabilities. * Wagtail is fully compatible with Microsoft Edge, Microsofts replacement for Internet Explorer. You may consider using its `IE mode <https://docs.microsoft.com/en-us/deployedge/edge-ie-mode>`_ to keep access to IE11-only sites, while other sites and apps like Wagtail can leverage modern browser capabilities.
## Replaced `content_json` `TextField` with `content` `JSONField` in `PageRevision`
* The `content_json` field in the `PageRevision` model has been renamed to `content`.
* The field now internally uses `JSONField` instead of `TextField`.
* If you have a large number of `PageRevision` objects, running the migrations might take a while.

Wyświetl plik

@ -677,10 +677,10 @@ Note that the above migration will work on published Page objects only. If you a
page.save() page.save()
for revision in page.revisions.all(): for revision in page.revisions.all():
revision_data = json.loads(revision.content_json) revision_data = revision.content
revision_data, changed = pagerevision_converter(revision_data) revision_data, changed = pagerevision_converter(revision_data)
if changed: if changed:
revision.content_json = json.dumps(revision_data, cls=DjangoJSONEncoder) revision.content = revision_data
revision.save() revision.save()

Wyświetl plik

@ -326,7 +326,7 @@ class TestPageEdit(TestCase, WagtailTestUtils):
def test_edit_post_scheduled(self): def test_edit_post_scheduled(self):
# put go_live_at and expire_at several days away from the current date, to avoid # put go_live_at and expire_at several days away from the current date, to avoid
# false matches in content_json__contains tests # false matches in content__ tests
go_live_at = timezone.now() + datetime.timedelta(days=10) go_live_at = timezone.now() + datetime.timedelta(days=10)
expire_at = timezone.now() + datetime.timedelta(days=20) expire_at = timezone.now() + datetime.timedelta(days=20)
post_data = { post_data = {
@ -358,12 +358,12 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# But a revision with go_live_at and expire_at in their content json *should* exist # But a revision with go_live_at and expire_at in their content json *should* exist
self.assertTrue( self.assertTrue(
PageRevision.objects.filter( PageRevision.objects.filter(
page=child_page_new, content_json__contains=str(go_live_at.date()) page=child_page_new, content__go_live_at__startswith=str(go_live_at.date())
).exists() ).exists()
) )
self.assertTrue( self.assertTrue(
PageRevision.objects.filter( PageRevision.objects.filter(
page=child_page_new, content_json__contains=str(expire_at.date()) page=child_page_new, content__expire_at__startswith=str(expire_at.date())
).exists() ).exists()
) )

Wyświetl plik

@ -1,4 +1,3 @@
import json
import logging import logging
import uuid import uuid
@ -182,7 +181,7 @@ class CopyPageAction:
revision.page = page_copy revision.page = page_copy
# Update ID fields in content # Update ID fields in content
revision_content = json.loads(revision.content_json) revision_content = revision.content
revision_content["pk"] = page_copy.pk revision_content["pk"] = page_copy.pk
for child_relation in get_all_child_relations(specific_page): for child_relation in get_all_child_relations(specific_page):
@ -207,7 +206,7 @@ class CopyPageAction:
copied_child_object.pk if copied_child_object else None copied_child_object.pk if copied_child_object else None
) )
revision.content_json = json.dumps(revision_content) revision.content = revision_content
# Save # Save
revision.save() revision.save()

Wyświetl plik

@ -150,9 +150,7 @@ class PublishPageRevisionAction:
) )
# Update alias pages # Update alias pages
page.update_aliases( page.update_aliases(revision=revision, user=user, _content=revision.content)
revision=revision, user=user, _content_json=revision.content_json
)
if log_action: if log_action:
data = None data = None

Wyświetl plik

@ -1,5 +1,3 @@
import json
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import dateparse, timezone from django.utils import dateparse, timezone
@ -7,7 +5,7 @@ from wagtail.core.models import Page, PageRevision
def revision_date_expired(r): def revision_date_expired(r):
expiry_str = json.loads(r.content_json).get("expire_at") expiry_str = r.content.get("expire_at")
if not expiry_str: if not expiry_str:
return False return False
expire_at = dateparse.parse_datetime(expiry_str) expire_at = dateparse.parse_datetime(expiry_str)
@ -72,7 +70,7 @@ class Command(BaseCommand):
self.stdout.write("Expiry datetime\t\tSlug\t\tName") self.stdout.write("Expiry datetime\t\tSlug\t\tName")
self.stdout.write("---------------\t\t----\t\t----") self.stdout.write("---------------\t\t----\t\t----")
for er in expired_revs: for er in expired_revs:
rev_data = json.loads(er.content_json) rev_data = er.content
self.stdout.write( self.stdout.write(
"{0}\t{1}\t{2}".format( "{0}\t{1}\t{2}".format(
dateparse.parse_datetime( dateparse.parse_datetime(
@ -100,7 +98,7 @@ class Command(BaseCommand):
self.stdout.write("Go live datetime\t\tSlug\t\tName") self.stdout.write("Go live datetime\t\tSlug\t\tName")
self.stdout.write("---------------\t\t\t----\t\t----") self.stdout.write("---------------\t\t\t----\t\t----")
for rp in revs_for_publishing: for rp in revs_for_publishing:
rev_data = json.loads(rp.content_json) rev_data = rp.content
self.stdout.write( self.stdout.write(
"{0}\t\t{1}\t{2}".format( "{0}\t\t{1}\t{2}".format(
rp.approved_go_live_at.strftime("%Y-%m-%d %H:%M"), rp.approved_go_live_at.strftime("%Y-%m-%d %H:%M"),

Wyświetl plik

@ -1,5 +1,8 @@
import json
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import models from django.db import models
from django.db.models.functions import Cast
from modelcluster.models import get_all_child_relations from modelcluster.models import get_all_child_relations
from wagtail.core.models import PageRevision, get_page_models from wagtail.core.models import PageRevision, get_page_models
@ -32,9 +35,12 @@ class Command(BaseCommand):
from_text = options["from_text"] from_text = options["from_text"]
to_text = options["to_text"] to_text = options["to_text"]
for revision in PageRevision.objects.filter(content_json__contains=from_text): for revision in PageRevision.objects.annotate(
revision.content_json = revision.content_json.replace(from_text, to_text) content_text=Cast("content", output_field=models.TextField())
revision.save(update_fields=["content_json"]) ).filter(content_text__contains=from_text):
replacement = revision.content_text.replace(from_text, to_text)
revision.content = json.loads(replacement)
revision.save(update_fields=["content"])
for page_class in get_page_models(): for page_class in get_page_models():
self.stdout.write("scanning %s" % page_class._meta.verbose_name) self.stdout.write("scanning %s" % page_class._meta.verbose_name)

Wyświetl plik

@ -0,0 +1,27 @@
# Generated by Django 4.0.2 on 2022-02-22 13:06
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("wagtailcore", "0066_collection_management_permissions"),
]
operations = [
migrations.AlterField(
model_name="pagerevision",
name="content_json",
field=models.JSONField(
encoder=django.core.serializers.json.DjangoJSONEncoder,
verbose_name="content JSON",
),
),
migrations.RenameField(
model_name="pagerevision",
old_name="content_json",
new_name="content",
),
]

Wyświetl plik

@ -10,7 +10,6 @@ as Page.
""" """
import functools import functools
import json
import logging import logging
import uuid import uuid
from io import StringIO from io import StringIO
@ -25,6 +24,7 @@ from django.core.cache import cache
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
from django.core.handlers.base import BaseHandler from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models, transaction from django.db import models, transaction
from django.db.models import DEFERRED, Q, Value from django.db.models import DEFERRED, Q, Value
from django.db.models.expressions import OuterRef, Subquery from django.db.models.expressions import OuterRef, Subquery
@ -900,7 +900,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
# Create revision # Create revision
revision = self.revisions.create( revision = self.revisions.create(
content_json=self.to_json(), content=self.serializable_data(),
user=user, user=user,
submitted_for_moderation=submitted_for_moderation, submitted_for_moderation=submitted_for_moderation,
approved_go_live_at=approved_go_live_at, approved_go_live_at=approved_go_live_at,
@ -990,7 +990,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
return self.specific return self.specific
def update_aliases( def update_aliases(
self, *, revision=None, user=None, _content_json=None, _updated_ids=None self, *, revision=None, user=None, _content=None, _updated_ids=None
): ):
""" """
Publishes all aliases that follow this page with the latest content from this page. Publishes all aliases that follow this page with the latest content from this page.
@ -1005,8 +1005,8 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
specific_self = self.specific specific_self = self.specific
# Only compute this if necessary since it's quite a heavy operation # Only compute this if necessary since it's quite a heavy operation
if _content_json is None: if _content is None:
_content_json = self.to_json() _content = self.serializable_data()
# A list of IDs that have already been updated. This is just in case someone has # A list of IDs that have already been updated. This is just in case someone has
# created an alias loop (which is impossible to do with the UI Wagtail provides) # created an alias loop (which is impossible to do with the UI Wagtail provides)
@ -1029,7 +1029,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
] ]
# Copy field content # Copy field content
alias_updated = alias.with_content_json(_content_json) alias_updated = alias.with_content_json(_content)
# Publish the alias if it's currently in draft # Publish the alias if it's currently in draft
alias_updated.live = True alias_updated.live = True
@ -1115,7 +1115,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
alias.update_aliases( alias.update_aliases(
revision=revision, revision=revision,
_content_json=_content_json, _content=_content,
_updated_ids=_updated_ids, _updated_ids=_updated_ids,
) )
@ -1936,10 +1936,10 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
context["action_url"] = action_url context["action_url"] = action_url
return TemplateResponse(request, self.password_required_template, context) return TemplateResponse(request, self.password_required_template, context)
def with_content_json(self, content_json): def with_content_json(self, content):
""" """
Returns a new version of the page with field values updated to reflect changes Returns a new version of the page with field values updated to reflect changes
in the provided ``content_json`` (which usually comes from a previously-saved in the provided ``content`` (which usually comes from a previously-saved
page revision). page revision).
Certain field values are preserved in order to prevent errors if the returned Certain field values are preserved in order to prevent errors if the returned
@ -1960,24 +1960,22 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
* ``wagtail_admin_comments`` (COMMENTS_RELATION_NAME) * ``wagtail_admin_comments`` (COMMENTS_RELATION_NAME)
""" """
data = json.loads(content_json)
# Old revisions (pre Wagtail 2.15) may have saved comment data under the name 'comments' # Old revisions (pre Wagtail 2.15) may have saved comment data under the name 'comments'
# rather than the current relation name as set by COMMENTS_RELATION_NAME; # rather than the current relation name as set by COMMENTS_RELATION_NAME;
# if a 'comments' field exists and looks like our comments model, alter the data to use # if a 'comments' field exists and looks like our comments model, alter the data to use
# COMMENTS_RELATION_NAME before restoring # COMMENTS_RELATION_NAME before restoring
if ( if (
COMMENTS_RELATION_NAME not in data COMMENTS_RELATION_NAME not in content
and "comments" in data and "comments" in content
and isinstance(data["comments"], list) and isinstance(content["comments"], list)
and len(data["comments"]) and len(content["comments"])
and isinstance(data["comments"][0], dict) and isinstance(content["comments"][0], dict)
and "contentpath" in data["comments"][0] and "contentpath" in content["comments"][0]
): ):
data[COMMENTS_RELATION_NAME] = data["comments"] content[COMMENTS_RELATION_NAME] = content["comments"]
del data["comments"] del content["comments"]
obj = self.specific_class.from_serializable_data(data) obj = self.specific_class.from_serializable_data(content)
# These should definitely never change between revisions # These should definitely never change between revisions
obj.id = self.id obj.id = self.id
@ -2153,7 +2151,9 @@ class PageRevision(models.Model):
blank=True, blank=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
content_json = models.TextField(verbose_name=_("content JSON")) content = models.JSONField(
verbose_name=_("content JSON"), encoder=DjangoJSONEncoder
)
approved_go_live_at = models.DateTimeField( approved_go_live_at = models.DateTimeField(
verbose_name=_("approved go live at"), null=True, blank=True, db_index=True verbose_name=_("approved go live at"), null=True, blank=True, db_index=True
) )
@ -2200,7 +2200,7 @@ class PageRevision(models.Model):
) )
def as_page_object(self): def as_page_object(self):
return self.page.specific.with_content_json(self.content_json) return self.page.specific.with_content_json(self.content)
def approve_moderation(self, user=None): def approve_moderation(self, user=None):
if self.submitted_for_moderation: if self.submitted_for_moderation:
@ -3467,7 +3467,7 @@ class WorkflowState(models.Model):
return PageRevision.objects.filter( return PageRevision.objects.filter(
page_id=self.page_id, page_id=self.page_id,
id__in=self.task_states.values_list("page_revision_id", flat=True), id__in=self.task_states.values_list("page_revision_id", flat=True),
).defer("content_json") ).defer("content")
def _get_applicable_task_states(self): def _get_applicable_task_states(self):
"""Returns the set of task states whose status applies to the current revision""" """Returns the set of task states whose status applies to the current revision"""

Wyświetl plik

@ -1,5 +1,4 @@
import datetime import datetime
import json
import unittest import unittest
from unittest.mock import Mock from unittest.mock import Mock
@ -1493,7 +1492,7 @@ class TestCopyPage(TestCase):
# Check that the ids within the revision were updated correctly # Check that the ids within the revision were updated correctly
new_revision = new_christmas_event.revisions.first() new_revision = new_christmas_event.revisions.first()
new_revision_content = json.loads(new_revision.content_json) new_revision_content = new_revision.content
self.assertEqual(new_revision_content["pk"], new_christmas_event.id) self.assertEqual(new_revision_content["pk"], new_christmas_event.id)
self.assertEqual( self.assertEqual(
new_revision_content["speakers"][0]["page"], new_christmas_event.id new_revision_content["speakers"][0]["page"], new_christmas_event.id
@ -3310,7 +3309,7 @@ class TestPageWithContentJSON(TestCase):
# Take a json representation of the page and update it # Take a json representation of the page and update it
# with some alternative values # with some alternative values
content = json.loads(original_page.to_json()) content = original_page.serializable_data()
content.update( content.update(
title="About them", title="About them",
draft_title="About them", draft_title="About them",
@ -3333,10 +3332,8 @@ class TestPageWithContentJSON(TestCase):
owner=1, owner=1,
) )
# Convert values back to json and pass them to with_content_json() # Pass the values to with_content_json() to get an updated version of the page
# to get an updated version of the page updated_page = original_page.with_content_json(content)
content_json = json.dumps(content)
updated_page = original_page.with_content_json(content_json)
# The following attributes values should have changed # The following attributes values should have changed
for attr_name in ("title", "slug", "content", "url_path", "show_in_menus"): for attr_name in ("title", "slug", "content", "url_path", "show_in_menus"):
@ -3345,7 +3342,7 @@ class TestPageWithContentJSON(TestCase):
) )
# The following attribute values should have been preserved, # The following attribute values should have been preserved,
# despite new values being provided in content_json # despite new values being provided in content
for attr_name in ( for attr_name in (
"pk", "pk",
"path", "path",