kopia lustrzana https://github.com/wagtail/wagtail
Add missing TZ conversions and date formatting
* Usages of strftime("%d %b %Y %H:%M") have been replaced by usages of django's timezone handling and date formatting, so the output is similar to other dates rendered through the templating. * Dates stored in the action logs are now parsed and converted accordingly for the frontend. * Improved tests to check those conversions * Fixes #9581 * Add handling for wagtail.schedule.cancel with go_live_at=None * Migrate log timestamps to ISO 8601 in UTC * adapted new code from #9628 * replaced usages of test utility rendered_timestamp() with new core utility * Replaced usages of django.utils.timezone.utc * re-added migration on top of newest migrationspull/10620/head
rodzic
f866cd1608
commit
7962118dc0
docs/releases
wagtail
actions
admin
views
generic
pages
migrations
models
snippets/tests
test/utils
utils
|
@ -44,6 +44,8 @@ Changelog
|
|||
* Fix: Improve accessibility for header search, remove autofocus on page load, advise screen readers that content has changed when results update (LB (Ben) Johnston)
|
||||
* Fix: Fix incorrect override of `PagePermissionHelper.user_can_unpublish_obj()` in ModelAdmin (Sébastien Corbin)
|
||||
* Fix: Prevent memory exhaustion when updating a large number of image renditions (Jake Howard)
|
||||
* Fix: Add missing Time Zone conversions and date formatting throughout the admin (Stefan Hammer)
|
||||
* Fix: Ensure that audit logs and revisions are consistently use UTC and add migration for existing entries (Stefan Hammer)
|
||||
* Docs: Document how to add non-ModelAdmin views to a `ModelAdminGroup` (Onno Timmerman)
|
||||
* Docs: Document how to add StructBlock data to a StreamField (Ramon Wenger)
|
||||
* Docs: Update ReadTheDocs settings to v2 to resolve urllib3 issue in linkcheck extension (Thibaud Colas)
|
||||
|
|
|
@ -78,6 +78,8 @@ The `wagtail start` command now supports an optional `--template` argument that
|
|||
* Improve accessibility for header search, remove autofocus on page load, advise screen readers that content has changed when results update (LB (Ben) Johnston)
|
||||
* Fix incorrect override of `PagePermissionHelper.user_can_unpublish_obj()` in ModelAdmin (Sébastien Corbin)
|
||||
* Prevent memory exhaustion when updating a large number of image renditions (Jake Howard)
|
||||
* Add missing Time Zone conversions and date formatting throughout the admin (Stefan Hammer)
|
||||
* Ensure that audit logs and revisions are consistently use UTC and add migration for existing entries (Stefan Hammer)
|
||||
|
||||
### Documentation
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ from django.utils import timezone
|
|||
from wagtail.log_actions import log
|
||||
from wagtail.permission_policies.base import ModelPermissionPolicy
|
||||
from wagtail.signals import published
|
||||
from wagtail.utils.timestamps import ensure_utc
|
||||
|
||||
logger = logging.getLogger("wagtail")
|
||||
|
||||
|
@ -63,8 +64,8 @@ class PublishRevisionAction:
|
|||
data={
|
||||
"revision": {
|
||||
"id": self.revision.id,
|
||||
"created": self.revision.created_at.strftime("%d %b %Y %H:%M"),
|
||||
"go_live_at": self.object.go_live_at.strftime("%d %b %Y %H:%M"),
|
||||
"created": ensure_utc(self.revision.created_at),
|
||||
"go_live_at": ensure_utc(self.object.go_live_at),
|
||||
"has_live_version": self.object.live,
|
||||
}
|
||||
},
|
||||
|
@ -162,9 +163,7 @@ class PublishRevisionAction:
|
|||
data = {
|
||||
"revision": {
|
||||
"id": previous_revision.id,
|
||||
"created": previous_revision.created_at.strftime(
|
||||
"%d %b %Y %H:%M"
|
||||
),
|
||||
"created": ensure_utc(previous_revision.created_at),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,8 +43,9 @@ from wagtail.test.testapp.models import (
|
|||
)
|
||||
from wagtail.test.utils import WagtailTestUtils
|
||||
from wagtail.test.utils.form_data import inline_formset, nested_form_data
|
||||
from wagtail.test.utils.timestamps import rendered_timestamp, submittable_timestamp
|
||||
from wagtail.test.utils.timestamps import submittable_timestamp
|
||||
from wagtail.users.models import UserProfile
|
||||
from wagtail.utils.timestamps import render_timestamp
|
||||
|
||||
|
||||
class TestPageEdit(WagtailTestUtils, TestCase):
|
||||
|
@ -411,13 +412,13 @@ class TestPageEdit(WagtailTestUtils, TestCase):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -765,13 +766,13 @@ class TestPageEdit(WagtailTestUtils, TestCase):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -929,13 +930,13 @@ class TestPageEdit(WagtailTestUtils, TestCase):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -1099,7 +1100,7 @@ class TestPageEdit(WagtailTestUtils, TestCase):
|
|||
# Should still show the active expire_at in the live object
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -1113,13 +1114,13 @@ class TestPageEdit(WagtailTestUtils, TestCase):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(new_expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(new_expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -1210,7 +1211,7 @@ class TestPageEdit(WagtailTestUtils, TestCase):
|
|||
# override the existing expire_at when it goes live
|
||||
self.assertNotContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
)
|
||||
|
||||
|
@ -1222,13 +1223,13 @@ class TestPageEdit(WagtailTestUtils, TestCase):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(new_expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(new_expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -1313,7 +1314,7 @@ class TestPageEdit(WagtailTestUtils, TestCase):
|
|||
# unpublished (expired) -> published (scheduled) -> unpublished (expired again)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -1326,13 +1327,13 @@ class TestPageEdit(WagtailTestUtils, TestCase):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(new_expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(new_expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
|
|
@ -148,7 +148,16 @@ class TestRevisions(WagtailTestUtils, TestCase):
|
|||
reverse("wagtailadmin_pages:history", args=(self.christmas_event.id,))
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Page scheduled for publishing at 26 Dec 2014")
|
||||
|
||||
if settings.USE_TZ:
|
||||
# the default timezone is "Asia/Tokyo", so we expect UTC +9
|
||||
expected_date_string = "Dec. 26, 2014, 9 p.m."
|
||||
else:
|
||||
expected_date_string = "Dec. 26, 2014, noon"
|
||||
|
||||
self.assertContains(
|
||||
response, f"Page scheduled for publishing at {expected_date_string}"
|
||||
)
|
||||
self.assertContains(response, this_christmas_unschedule_url)
|
||||
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ from freezegun import freeze_time
|
|||
from wagtail.models import GroupPagePermission, Page, PageLogEntry, PageViewRestriction
|
||||
from wagtail.test.testapp.models import SimplePage
|
||||
from wagtail.test.utils import WagtailTestUtils
|
||||
from wagtail.utils.timestamps import render_timestamp
|
||||
|
||||
|
||||
class TestAuditLogAdmin(WagtailTestUtils, TestCase):
|
||||
|
@ -286,7 +287,7 @@ class TestAuditLogAdmin(WagtailTestUtils, TestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(
|
||||
response,
|
||||
f"Page unscheduled for publishing at {go_live_at.strftime('%d %b %Y %H:%M')}",
|
||||
f"Page unscheduled for publishing at {render_timestamp(go_live_at)}",
|
||||
)
|
||||
|
||||
def test_page_history_after_unscheduled_revision(self):
|
||||
|
@ -325,5 +326,5 @@ class TestAuditLogAdmin(WagtailTestUtils, TestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(
|
||||
response,
|
||||
f"Revision {revision.id} from {revision.created_at.strftime('%d %b %Y %H:%M')} unscheduled from publishing at {go_live_at.strftime('%d %b %Y %H:%M')}.",
|
||||
f"Revision {revision.id} from {render_timestamp(revision.created_at)} unscheduled from publishing at {render_timestamp(go_live_at)}.",
|
||||
)
|
||||
|
|
|
@ -31,6 +31,7 @@ from wagtail.models import (
|
|||
WorkflowMixin,
|
||||
WorkflowState,
|
||||
)
|
||||
from wagtail.utils.timestamps import render_timestamp
|
||||
|
||||
|
||||
class HookResponseMixin:
|
||||
|
@ -702,7 +703,7 @@ class RevisionsRevertMixin:
|
|||
)
|
||||
message_data = {
|
||||
"model_name": capfirst(self.model._meta.verbose_name),
|
||||
"created_at": self.revision.created_at.strftime("%d %b %Y %H:%M"),
|
||||
"created_at": render_timestamp(self.revision.created_at),
|
||||
"user": user_avatar,
|
||||
}
|
||||
message = mark_safe(message_string % message_data)
|
||||
|
@ -747,7 +748,7 @@ class RevisionsRevertMixin:
|
|||
return message % {
|
||||
"model_name": capfirst(self.model._meta.verbose_name),
|
||||
"object": self.object,
|
||||
"timestamp": self.revision.created_at.strftime("%d %b %Y %H:%M"),
|
||||
"timestamp": render_timestamp(self.revision.created_at),
|
||||
}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
|
|
@ -30,6 +30,7 @@ from wagtail.models import (
|
|||
PageSubscription,
|
||||
WorkflowState,
|
||||
)
|
||||
from wagtail.utils.timestamps import render_timestamp
|
||||
|
||||
|
||||
class EditView(TemplateResponseMixin, ContextMixin, HookResponseMixin, View):
|
||||
|
@ -61,8 +62,8 @@ class EditView(TemplateResponseMixin, ContextMixin, HookResponseMixin, View):
|
|||
"with version from %(previous_revision_datetime)s."
|
||||
) % {
|
||||
"page_title": self.page.get_admin_display_title(),
|
||||
"previous_revision_datetime": self.previous_revision.created_at.strftime(
|
||||
"%d %b %Y %H:%M"
|
||||
"previous_revision_datetime": render_timestamp(
|
||||
self.previous_revision.created_at
|
||||
),
|
||||
}
|
||||
else:
|
||||
|
@ -588,8 +589,8 @@ class EditView(TemplateResponseMixin, ContextMixin, HookResponseMixin, View):
|
|||
"Version from %(previous_revision_datetime)s "
|
||||
"of page '%(page_title)s' has been scheduled for publishing."
|
||||
) % {
|
||||
"previous_revision_datetime": self.previous_revision.created_at.strftime(
|
||||
"%d %b %Y %H:%M"
|
||||
"previous_revision_datetime": render_timestamp(
|
||||
self.previous_revision.created_at
|
||||
),
|
||||
"page_title": self.page.get_admin_display_title(),
|
||||
}
|
||||
|
@ -615,9 +616,7 @@ class EditView(TemplateResponseMixin, ContextMixin, HookResponseMixin, View):
|
|||
message = _(
|
||||
"Version from %(datetime)s of page '%(page_title)s' has been published."
|
||||
) % {
|
||||
"datetime": self.previous_revision.created_at.strftime(
|
||||
"%d %b %Y %H:%M"
|
||||
),
|
||||
"datetime": render_timestamp(self.previous_revision.created_at),
|
||||
"page_title": self.page.get_admin_display_title(),
|
||||
}
|
||||
else:
|
||||
|
|
|
@ -18,6 +18,7 @@ from wagtail.admin.views.generic.models import (
|
|||
)
|
||||
from wagtail.admin.views.generic.preview import PreviewRevision
|
||||
from wagtail.models import Page
|
||||
from wagtail.utils.timestamps import render_timestamp
|
||||
|
||||
|
||||
def revisions_index(request, page_id):
|
||||
|
@ -72,7 +73,7 @@ def revisions_revert(request, page_id, revision_id):
|
|||
"You are viewing a previous version of this page from <b>%(created_at)s</b> by %(user)s"
|
||||
)
|
||||
% {
|
||||
"created_at": revision.created_at.strftime("%d %b %Y %H:%M"),
|
||||
"created_at": render_timestamp(revision.created_at),
|
||||
"user": user_avatar,
|
||||
}
|
||||
),
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.utils.translation import gettext as _
|
|||
|
||||
from wagtail.admin.utils import get_latest_str, get_user_display_name
|
||||
from wagtail.utils.deprecation import RemovedInWagtail60Warning
|
||||
from wagtail.utils.timestamps import render_timestamp
|
||||
|
||||
|
||||
class BaseLock:
|
||||
|
@ -104,7 +105,7 @@ class BasicLock(BaseLock):
|
|||
"<b>'{title}' was locked</b> by <b>you</b> on <b>{datetime}</b>."
|
||||
),
|
||||
title=title,
|
||||
datetime=self.object.locked_at.strftime("%d %b %Y %H:%M"),
|
||||
datetime=render_timestamp(self.object.locked_at),
|
||||
)
|
||||
|
||||
else:
|
||||
|
@ -122,7 +123,7 @@ class BasicLock(BaseLock):
|
|||
),
|
||||
title=title,
|
||||
user=get_user_display_name(self.object.locked_by),
|
||||
datetime=self.object.locked_at.strftime("%d %b %Y %H:%M"),
|
||||
datetime=render_timestamp(self.object.locked_at),
|
||||
)
|
||||
else:
|
||||
# Object was probably locked with an old version of Wagtail, or a script
|
||||
|
@ -264,7 +265,7 @@ class ScheduledForPublishLock(BaseLock):
|
|||
),
|
||||
model_name=self.model_name,
|
||||
title=scheduled_revision.object_str,
|
||||
datetime=scheduled_revision.approved_go_live_at.strftime("%d %b %Y %H:%M"),
|
||||
datetime=render_timestamp(scheduled_revision.approved_go_live_at),
|
||||
)
|
||||
return mark_safe(capfirst(message))
|
||||
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
# Generated by Django 3.2.6 on 2022-11-09 07:50
|
||||
|
||||
import datetime
|
||||
|
||||
import django.core.serializers.json
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.utils import timezone, dateparse
|
||||
|
||||
|
||||
def legacy_to_iso_format(date_string, tz=None):
|
||||
dt = datetime.datetime.strptime(date_string, "%d %b %Y %H:%M")
|
||||
if settings.USE_TZ:
|
||||
dt = timezone.make_aware(dt, datetime.timezone.utc if tz is None else tz)
|
||||
dt = timezone.localtime(dt, datetime.timezone.utc)
|
||||
# We return the datetime object, so DjangoJSONEncoder will serialize it accordingly.
|
||||
return dt
|
||||
|
||||
|
||||
def iso_to_legacy_format(date_string, tz=None):
|
||||
dt = dateparse.parse_datetime(date_string)
|
||||
if dt is None:
|
||||
raise ValueError("date isn't well formatted")
|
||||
if settings.USE_TZ:
|
||||
dt = timezone.localtime(dt, datetime.timezone.utc if tz is None else tz)
|
||||
return dt.strftime("%d %b %Y %H:%M")
|
||||
|
||||
|
||||
def migrate_logs_with_created_only(model, converter):
|
||||
for item in (
|
||||
model.objects.filter(
|
||||
action__in=["wagtail.revert", "wagtail.rename", "wagtail.publish"]
|
||||
)
|
||||
.only("data")
|
||||
.iterator()
|
||||
):
|
||||
try:
|
||||
# If a previous_revision was available, the data contains "revision" with
|
||||
# its created date.
|
||||
# Also, there are "wagtail.publish" logs, which don't set data at all.
|
||||
created = item.data["revision"]["created"]
|
||||
# "created" is set to the previous revision's created_at, which is set
|
||||
# to UTC by django.
|
||||
item.data["revision"]["created"] = converter(created)
|
||||
except ValueError:
|
||||
# TODO TBD: log error?
|
||||
continue
|
||||
except KeyError:
|
||||
continue
|
||||
else:
|
||||
item.save(update_fields=["data"])
|
||||
|
||||
|
||||
def migrate_schedule_logs(model, converter):
|
||||
for item in (
|
||||
model.objects.filter(
|
||||
action__in=["wagtail.publish.schedule", "wagtail.schedule.cancel"]
|
||||
)
|
||||
.only("data")
|
||||
.iterator()
|
||||
):
|
||||
created = item.data["revision"]["created"]
|
||||
# May be unset for "wagtail.schedule.cancel"-logs.
|
||||
go_live_at = item.data["revision"].get("go_live_at")
|
||||
try:
|
||||
# "created" is set to timezone.now() for new revisions ("wagtail.publish.schedule")
|
||||
# and to self.created_at for "wagtail.schedule.cancel", which is set to UTC
|
||||
# by django.
|
||||
item.data["revision"]["created"] = converter(created)
|
||||
|
||||
if go_live_at:
|
||||
# The go_live_at date is set to the revision object's "go_live_at".
|
||||
# The revision's object is created by deserializing the json data (see wagtail.models.Revision.as_object()),
|
||||
# and this process converts all datetime objects to the local timestamp (see https://github.com/wagtail/django-modelcluster/blob/8666f16eaf23ca98afc160b0a4729864411c0563/modelcluster/models.py#L109-L115).
|
||||
# That's the reason, why this date is the only date, which is not stored in the log's JSON as UTC, but in the default timezone.
|
||||
item.data["revision"]["go_live_at"] = converter(
|
||||
go_live_at, tz=timezone.get_default_timezone()
|
||||
)
|
||||
except ValueError:
|
||||
# TODO TBD: log error?
|
||||
continue
|
||||
else:
|
||||
item.save(update_fields=["data"])
|
||||
|
||||
|
||||
def migrate_custom_to_iso_format(apps, schema_editor):
|
||||
ModelLogEntry = apps.get_model("wagtailcore.ModelLogEntry")
|
||||
PageLogEntry = apps.get_model("wagtailcore.PageLogEntry")
|
||||
|
||||
migrate_logs_with_created_only(ModelLogEntry, legacy_to_iso_format)
|
||||
migrate_logs_with_created_only(PageLogEntry, legacy_to_iso_format)
|
||||
|
||||
migrate_schedule_logs(ModelLogEntry, legacy_to_iso_format)
|
||||
migrate_schedule_logs(PageLogEntry, legacy_to_iso_format)
|
||||
|
||||
|
||||
def migrate_iso_to_custom_format(apps, schema_editor):
|
||||
ModelLogEntry = apps.get_model("wagtailcore.ModelLogEntry")
|
||||
PageLogEntry = apps.get_model("wagtailcore.PageLogEntry")
|
||||
|
||||
migrate_logs_with_created_only(ModelLogEntry, iso_to_legacy_format)
|
||||
migrate_logs_with_created_only(PageLogEntry, iso_to_legacy_format)
|
||||
|
||||
migrate_schedule_logs(ModelLogEntry, iso_to_legacy_format)
|
||||
migrate_schedule_logs(PageLogEntry, iso_to_legacy_format)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("wagtailcore", "0087_alter_grouppagepermission_unique_together_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="modellogentry",
|
||||
name="data",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
encoder=django.core.serializers.json.DjangoJSONEncoder,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="pagelogentry",
|
||||
name="data",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
encoder=django.core.serializers.json.DjangoJSONEncoder,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
migrate_custom_to_iso_format,
|
||||
migrate_iso_to_custom_format,
|
||||
),
|
||||
]
|
|
@ -92,6 +92,7 @@ from wagtail.signals import (
|
|||
)
|
||||
from wagtail.url_routing import RouteResult
|
||||
from wagtail.utils.deprecation import RemovedInWagtail60Warning
|
||||
from wagtail.utils.timestamps import ensure_utc
|
||||
|
||||
from .audit_log import ( # noqa: F401
|
||||
BaseLogEntry,
|
||||
|
@ -426,9 +427,7 @@ class RevisionMixin(models.Model):
|
|||
data={
|
||||
"revision": {
|
||||
"id": previous_revision.id,
|
||||
"created": previous_revision.created_at.strftime(
|
||||
"%d %b %Y %H:%M"
|
||||
),
|
||||
"created": ensure_utc(previous_revision.created_at),
|
||||
}
|
||||
},
|
||||
revision=revision,
|
||||
|
@ -1702,9 +1701,7 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
|
|||
data={
|
||||
"revision": {
|
||||
"id": previous_revision.id,
|
||||
"created": previous_revision.created_at.strftime(
|
||||
"%d %b %Y %H:%M"
|
||||
),
|
||||
"created": ensure_utc(previous_revision.created_at),
|
||||
}
|
||||
},
|
||||
revision=revision,
|
||||
|
@ -2775,8 +2772,8 @@ class Revision(models.Model):
|
|||
data={
|
||||
"revision": {
|
||||
"id": self.id,
|
||||
"created": self.created_at.strftime("%d %b %Y %H:%M"),
|
||||
"go_live_at": object.go_live_at.strftime("%d %b %Y %H:%M")
|
||||
"created": ensure_utc(self.created_at),
|
||||
"go_live_at": ensure_utc(object.go_live_at)
|
||||
if object.go_live_at
|
||||
else None,
|
||||
"has_live_version": object.live,
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.conf import settings
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
|
@ -162,7 +163,7 @@ class BaseLogEntry(models.Model):
|
|||
label = models.TextField()
|
||||
|
||||
action = models.CharField(max_length=255, blank=True, db_index=True)
|
||||
data = models.JSONField(blank=True, default=dict)
|
||||
data = models.JSONField(blank=True, default=dict, encoder=DjangoJSONEncoder)
|
||||
timestamp = models.DateTimeField(verbose_name=_("timestamp (UTC)"), db_index=True)
|
||||
uuid = models.UUIDField(
|
||||
blank=True,
|
||||
|
|
|
@ -3,6 +3,7 @@ import json
|
|||
from io import StringIO
|
||||
from unittest import mock
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.admin.utils import quote
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, Permission
|
||||
|
@ -66,7 +67,8 @@ from wagtail.test.testapp.models import (
|
|||
VariousOnDeleteModel,
|
||||
)
|
||||
from wagtail.test.utils import WagtailTestUtils
|
||||
from wagtail.test.utils.timestamps import rendered_timestamp, submittable_timestamp
|
||||
from wagtail.test.utils.timestamps import submittable_timestamp
|
||||
from wagtail.utils.timestamps import render_timestamp
|
||||
|
||||
|
||||
class TestSnippetIndexView(WagtailTestUtils, TestCase):
|
||||
|
@ -2093,13 +2095,13 @@ class TestEditDraftStateSnippet(BaseTestSnippetEditView):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -2237,13 +2239,13 @@ class TestEditDraftStateSnippet(BaseTestSnippetEditView):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -2412,13 +2414,13 @@ class TestEditDraftStateSnippet(BaseTestSnippetEditView):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -2589,7 +2591,7 @@ class TestEditDraftStateSnippet(BaseTestSnippetEditView):
|
|||
# Should still show the active expire_at in the live object
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -2603,13 +2605,13 @@ class TestEditDraftStateSnippet(BaseTestSnippetEditView):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(new_expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(new_expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -2703,7 +2705,7 @@ class TestEditDraftStateSnippet(BaseTestSnippetEditView):
|
|||
# override the existing expire_at when it goes live
|
||||
self.assertNotContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
)
|
||||
|
||||
|
@ -2715,13 +2717,13 @@ class TestEditDraftStateSnippet(BaseTestSnippetEditView):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(new_expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(new_expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -2817,7 +2819,7 @@ class TestEditDraftStateSnippet(BaseTestSnippetEditView):
|
|||
# unpublished (expired) -> published (scheduled) -> unpublished (expired again)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -2830,13 +2832,13 @@ class TestEditDraftStateSnippet(BaseTestSnippetEditView):
|
|||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {rendered_timestamp(new_expire_at)}',
|
||||
f'<span class="w-text-grey-600">Expiry:</span> {render_timestamp(new_expire_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -2903,7 +2905,7 @@ class TestScheduledForPublishLock(BaseTestSnippetEditView):
|
|||
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(self.go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(self.go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -2970,7 +2972,7 @@ class TestScheduledForPublishLock(BaseTestSnippetEditView):
|
|||
|
||||
self.assertContains(
|
||||
response,
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {rendered_timestamp(self.go_live_at)}',
|
||||
f'<span class="w-text-grey-600">Go-live:</span> {render_timestamp(self.go_live_at)}',
|
||||
html=True,
|
||||
count=1,
|
||||
)
|
||||
|
@ -3919,10 +3921,16 @@ class TestSnippetRevisions(WagtailTestUtils, TestCase):
|
|||
response = self.get()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
if settings.USE_TZ:
|
||||
# the default timezone is "Asia/Tokyo", so we expect UTC +9
|
||||
expected_date_string = "May 10, 2022, 8 p.m."
|
||||
else:
|
||||
expected_date_string = "May 10, 2022, 11 a.m."
|
||||
|
||||
# Message should be shown
|
||||
self.assertContains(
|
||||
response,
|
||||
"You are viewing a previous version of this Revisable model from <b>10 May 2022 11:00</b> by",
|
||||
f"You are viewing a previous version of this Revisable model from <b>{expected_date_string}</b> by",
|
||||
count=1,
|
||||
)
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.formats import date_format
|
||||
|
||||
|
||||
def submittable_timestamp(timestamp):
|
||||
|
@ -19,16 +17,6 @@ def submittable_timestamp(timestamp):
|
|||
return timestamp.strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
def rendered_timestamp(timestamp):
|
||||
"""
|
||||
Helper function to format a possibly-timezone-aware datetime into the format
|
||||
used by Django (e.g. in templates).
|
||||
"""
|
||||
if timezone.is_aware(timestamp):
|
||||
timestamp = timezone.localtime(timestamp)
|
||||
return date_format(timestamp, settings.DATETIME_FORMAT)
|
||||
|
||||
|
||||
def local_datetime(*args):
|
||||
dt = datetime.datetime(*args)
|
||||
return timezone.make_aware(dt)
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from datetime import datetime, timedelta
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from freezegun import freeze_time
|
||||
|
@ -155,9 +157,12 @@ class TestAuditLog(TestCase):
|
|||
)
|
||||
|
||||
def test_revision_schedule_publish(self):
|
||||
go_live_at = datetime.now() + timedelta(days=1)
|
||||
go_live_at = datetime.datetime.now() + datetime.timedelta(days=1)
|
||||
if settings.USE_TZ:
|
||||
go_live_at = timezone.make_aware(go_live_at)
|
||||
expected_go_live_at = timezone.localtime(go_live_at, datetime.timezone.utc)
|
||||
else:
|
||||
expected_go_live_at = go_live_at
|
||||
self.home_page.go_live_at = go_live_at
|
||||
|
||||
# with no live revision
|
||||
|
@ -169,7 +174,8 @@ class TestAuditLog(TestCase):
|
|||
self.assertEqual(log_entries[0].data["revision"]["id"], revision.id)
|
||||
self.assertEqual(
|
||||
log_entries[0].data["revision"]["go_live_at"],
|
||||
go_live_at.strftime("%d %b %Y %H:%M"),
|
||||
# skip double quotes
|
||||
json.dumps(expected_go_live_at, cls=DjangoJSONEncoder)[1:-1],
|
||||
)
|
||||
|
||||
def test_revision_schedule_revert(self):
|
||||
|
@ -178,10 +184,12 @@ class TestAuditLog(TestCase):
|
|||
|
||||
if settings.USE_TZ:
|
||||
self.home_page.go_live_at = timezone.make_aware(
|
||||
datetime.now() + timedelta(days=1)
|
||||
datetime.datetime.now() + datetime.timedelta(days=1)
|
||||
)
|
||||
else:
|
||||
self.home_page.go_live_at = datetime.now() + timedelta(days=1)
|
||||
self.home_page.go_live_at = datetime.datetime.now() + datetime.timedelta(
|
||||
days=1
|
||||
)
|
||||
|
||||
schedule_revision = self.home_page.save_revision(
|
||||
log_action=True, previous_revision=revision2
|
||||
|
@ -197,9 +205,12 @@ class TestAuditLog(TestCase):
|
|||
)
|
||||
|
||||
def test_revision_cancel_schedule(self):
|
||||
go_live_at = datetime.now() + timedelta(days=1)
|
||||
go_live_at = datetime.datetime.now() + datetime.timedelta(days=1)
|
||||
if settings.USE_TZ:
|
||||
go_live_at = timezone.make_aware(go_live_at)
|
||||
expected_go_live_at = timezone.localtime(go_live_at, datetime.timezone.utc)
|
||||
else:
|
||||
expected_go_live_at = go_live_at
|
||||
self.home_page.go_live_at = go_live_at
|
||||
revision = self.home_page.save_revision()
|
||||
revision.publish()
|
||||
|
@ -212,7 +223,8 @@ class TestAuditLog(TestCase):
|
|||
self.assertEqual(log_entries[0].data["revision"]["id"], revision.id)
|
||||
self.assertEqual(
|
||||
log_entries[0].data["revision"]["go_live_at"],
|
||||
go_live_at.strftime("%d %b %Y %H:%M"),
|
||||
# skip double quotes
|
||||
json.dumps(expected_go_live_at, cls=DjangoJSONEncoder)[1:-1],
|
||||
)
|
||||
# The home_page was live already and we've only cancelled the publication of the above revision.
|
||||
self.assertTrue(log_entries[0].data["revision"]["has_live_version"])
|
||||
|
|
|
@ -3611,19 +3611,31 @@ class TestGetLock(TestCase):
|
|||
christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
|
||||
christmas_event.locked = True
|
||||
christmas_event.locked_by = moderator
|
||||
christmas_event.locked_at = datetime.datetime(2022, 7, 29, 12, 19, 0)
|
||||
if settings.USE_TZ:
|
||||
christmas_event.locked_at = datetime.datetime(
|
||||
2022, 7, 29, 12, 19, 0, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
else:
|
||||
christmas_event.locked_at = datetime.datetime(2022, 7, 29, 12, 19, 0)
|
||||
|
||||
lock = christmas_event.get_lock()
|
||||
self.assertIsInstance(lock, BasicLock)
|
||||
self.assertTrue(lock.for_user(christmas_event.owner))
|
||||
self.assertFalse(lock.for_user(moderator))
|
||||
|
||||
if settings.USE_TZ:
|
||||
# the default timezone is "Asia/Tokyo", so we expect UTC +9
|
||||
expected_date_string = "July 29, 2022, 9:19 p.m."
|
||||
else:
|
||||
expected_date_string = "July 29, 2022, 12:19 p.m."
|
||||
|
||||
self.assertEqual(
|
||||
lock.get_message(christmas_event.owner),
|
||||
f"<b>'Christmas' was locked</b> by <b>{str(moderator)}</b> on <b>29 Jul 2022 12:19</b>.",
|
||||
f"<b>'Christmas' was locked</b> by <b>{str(moderator)}</b> on <b>{expected_date_string}</b>.",
|
||||
)
|
||||
self.assertEqual(
|
||||
lock.get_message(moderator),
|
||||
"<b>'Christmas' was locked</b> by <b>you</b> on <b>29 Jul 2022 12:19</b>.",
|
||||
f"<b>'Christmas' was locked</b> by <b>you</b> on <b>{expected_date_string}</b>.",
|
||||
)
|
||||
|
||||
def test_when_locked_without_locked_at(self):
|
||||
|
@ -3650,19 +3662,31 @@ class TestGetLock(TestCase):
|
|||
christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
|
||||
christmas_event.locked = True
|
||||
christmas_event.locked_by = moderator
|
||||
christmas_event.locked_at = datetime.datetime(2022, 7, 29, 12, 19, 0)
|
||||
if settings.USE_TZ:
|
||||
christmas_event.locked_at = datetime.datetime(
|
||||
2022, 7, 29, 12, 19, 0, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
else:
|
||||
christmas_event.locked_at = datetime.datetime(2022, 7, 29, 12, 19, 0)
|
||||
|
||||
lock = christmas_event.get_lock()
|
||||
self.assertIsInstance(lock, BasicLock)
|
||||
self.assertTrue(lock.for_user(christmas_event.owner))
|
||||
self.assertTrue(lock.for_user(moderator))
|
||||
|
||||
if settings.USE_TZ:
|
||||
# the default timezone is "Asia/Tokyo", so we expect UTC +9
|
||||
expected_date_string = "July 29, 2022, 9:19 p.m."
|
||||
else:
|
||||
expected_date_string = "July 29, 2022, 12:19 p.m."
|
||||
|
||||
self.assertEqual(
|
||||
lock.get_message(christmas_event.owner),
|
||||
f"<b>'Christmas' was locked</b> by <b>{str(moderator)}</b> on <b>29 Jul 2022 12:19</b>.",
|
||||
f"<b>'Christmas' was locked</b> by <b>{str(moderator)}</b> on <b>{expected_date_string}</b>.",
|
||||
)
|
||||
self.assertEqual(
|
||||
lock.get_message(moderator),
|
||||
"<b>'Christmas' was locked</b> by <b>you</b> on <b>29 Jul 2022 12:19</b>.",
|
||||
f"<b>'Christmas' was locked</b> by <b>you</b> on <b>{expected_date_string}</b>.",
|
||||
)
|
||||
|
||||
@override_settings(WAGTAILADMIN_GLOBAL_PAGE_EDIT_LOCK=True)
|
||||
|
@ -3672,7 +3696,12 @@ class TestGetLock(TestCase):
|
|||
christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
|
||||
christmas_event.locked = True
|
||||
christmas_event.locked_by = moderator
|
||||
christmas_event.locked_at = datetime.datetime(2022, 7, 29, 12, 19, 0)
|
||||
if settings.USE_TZ:
|
||||
christmas_event.locked_at = datetime.datetime(
|
||||
2022, 7, 29, 12, 19, 0, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
else:
|
||||
christmas_event.locked_at = datetime.datetime(2022, 7, 29, 12, 19, 0)
|
||||
|
||||
lock = christmas_event.get_lock()
|
||||
self.assertIsInstance(lock, BasicLock)
|
||||
|
@ -3685,13 +3714,19 @@ class TestGetLock(TestCase):
|
|||
self.assertTrue(lock.for_user(christmas_event.owner))
|
||||
self.assertTrue(lock.for_user(moderator))
|
||||
|
||||
if settings.USE_TZ:
|
||||
# the default timezone is "Asia/Tokyo", so we expect UTC +9
|
||||
expected_date_string = "July 29, 2022, 9:19 p.m."
|
||||
else:
|
||||
expected_date_string = "July 29, 2022, 12:19 p.m."
|
||||
|
||||
self.assertEqual(
|
||||
lock.get_message(christmas_event.owner),
|
||||
f"<b>'Christmas' was locked</b> by <b>{str(moderator)}</b> on <b>29 Jul 2022 12:19</b>.",
|
||||
f"<b>'Christmas' was locked</b> by <b>{str(moderator)}</b> on <b>{expected_date_string}</b>.",
|
||||
)
|
||||
self.assertEqual(
|
||||
lock.get_message(moderator),
|
||||
"<b>'Christmas' was locked</b> by <b>you</b> on <b>29 Jul 2022 12:19</b>.",
|
||||
f"<b>'Christmas' was locked</b> by <b>you</b> on <b>{expected_date_string}</b>.",
|
||||
)
|
||||
|
||||
def test_when_locked_by_workflow(self):
|
||||
|
@ -3730,23 +3765,29 @@ class TestGetLock(TestCase):
|
|||
|
||||
def test_when_scheduled_for_publish(self):
|
||||
christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
|
||||
christmas_event.go_live_at = datetime.datetime(2030, 7, 29, 16, 32, 0)
|
||||
if settings.USE_TZ:
|
||||
christmas_event.go_live_at = datetime.datetime(
|
||||
2030, 7, 29, 16, 32, 0, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
else:
|
||||
christmas_event.go_live_at = datetime.datetime(2030, 7, 29, 16, 32, 0)
|
||||
rvn = christmas_event.save_revision()
|
||||
rvn.publish()
|
||||
|
||||
lock = christmas_event.get_lock()
|
||||
self.assertIsInstance(lock, ScheduledForPublishLock)
|
||||
self.assertTrue(lock.for_user(christmas_event.owner))
|
||||
|
||||
if settings.USE_TZ:
|
||||
self.assertEqual(
|
||||
lock.get_message(christmas_event.owner),
|
||||
"Page 'Christmas' is locked and has been scheduled to go live at 29 Jul 2030 07:32",
|
||||
)
|
||||
# the default timezone is "Asia/Tokyo", so we expect UTC +9
|
||||
expected_date_string = "July 30, 2030, 1:32 a.m."
|
||||
else:
|
||||
self.assertEqual(
|
||||
lock.get_message(christmas_event.owner),
|
||||
"Page 'Christmas' is locked and has been scheduled to go live at 29 Jul 2030 16:32",
|
||||
)
|
||||
expected_date_string = "July 29, 2030, 4:32 p.m."
|
||||
|
||||
self.assertEqual(
|
||||
lock.get_message(christmas_event.owner),
|
||||
f"Page 'Christmas' is locked and has been scheduled to go live at {expected_date_string}",
|
||||
)
|
||||
|
||||
# Not even superusers can break this lock
|
||||
# This is because it shouldn't be possible to create a separate draft from what is scheduled to be published
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import formats, timezone
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
|
||||
def ensure_utc(value):
|
||||
"""
|
||||
Similar to how django-modelcluster stores the revision's data and similar to how
|
||||
django stores dates in the database, this converts the date to UTC if required.
|
||||
"""
|
||||
# https://github.com/wagtail/django-modelcluster/blob/8666f16eaf23ca98afc160b0a4729864411c0563/modelcluster/models.py#L21-L28
|
||||
if settings.USE_TZ:
|
||||
if timezone.is_naive(value):
|
||||
default_timezone = timezone.get_default_timezone()
|
||||
value = timezone.make_aware(value, default_timezone).astimezone(
|
||||
datetime.timezone.utc
|
||||
)
|
||||
else:
|
||||
# convert to UTC
|
||||
value = timezone.localtime(value, datetime.timezone.utc)
|
||||
return value
|
||||
|
||||
|
||||
def parse_datetime_localized(date_string):
|
||||
"""
|
||||
Uses Django's parse_datetime(), but ensures to return an aware datetime.
|
||||
"""
|
||||
dt = parse_datetime(date_string)
|
||||
if settings.USE_TZ and timezone.is_naive(dt):
|
||||
dt = timezone.make_aware(dt, timezone=timezone.get_default_timezone())
|
||||
return dt
|
||||
|
||||
|
||||
def render_timestamp(timestamp):
|
||||
"""
|
||||
Helper function to format a possibly-timezone-aware datetime into the format
|
||||
used by Django (e.g. in templates).
|
||||
"""
|
||||
if timezone.is_aware(timestamp):
|
||||
timestamp = timezone.localtime(timestamp)
|
||||
return formats.date_format(timestamp, "DATETIME_FORMAT")
|
|
@ -12,6 +12,7 @@ from wagtail.coreutils import get_content_languages
|
|||
from wagtail.log_actions import LogFormatter
|
||||
from wagtail.models import ModelLogEntry, Page, PageLogEntry, PageViewRestriction
|
||||
from wagtail.rich_text.pages import PageLinkHandler
|
||||
from wagtail.utils.timestamps import parse_datetime_localized, render_timestamp
|
||||
|
||||
|
||||
def require_wagtail_login(next):
|
||||
|
@ -152,7 +153,11 @@ def register_core_log_actions(actions):
|
|||
"Reverted to previous revision with id %(revision_id)s from %(created_at)s"
|
||||
) % {
|
||||
"revision_id": log_entry.data["revision"]["id"],
|
||||
"created_at": log_entry.data["revision"]["created"],
|
||||
"created_at": render_timestamp(
|
||||
parse_datetime_localized(
|
||||
log_entry.data["revision"]["created"],
|
||||
)
|
||||
),
|
||||
}
|
||||
except KeyError:
|
||||
return _("Reverted to previous revision")
|
||||
|
@ -245,12 +250,24 @@ def register_core_log_actions(actions):
|
|||
"Revision %(revision_id)s from %(created_at)s scheduled for publishing at %(go_live_at)s."
|
||||
) % {
|
||||
"revision_id": log_entry.data["revision"]["id"],
|
||||
"created_at": log_entry.data["revision"]["created"],
|
||||
"go_live_at": log_entry.data["revision"]["go_live_at"],
|
||||
"created_at": render_timestamp(
|
||||
parse_datetime_localized(
|
||||
log_entry.data["revision"]["created"],
|
||||
)
|
||||
),
|
||||
"go_live_at": render_timestamp(
|
||||
parse_datetime_localized(
|
||||
log_entry.data["revision"]["go_live_at"],
|
||||
)
|
||||
),
|
||||
}
|
||||
else:
|
||||
return _("Page scheduled for publishing at %(go_live_at)s") % {
|
||||
"go_live_at": log_entry.data["revision"]["go_live_at"],
|
||||
"go_live_at": render_timestamp(
|
||||
parse_datetime_localized(
|
||||
log_entry.data["revision"]["go_live_at"],
|
||||
)
|
||||
),
|
||||
}
|
||||
except KeyError:
|
||||
return _("Page scheduled for publishing")
|
||||
|
@ -266,12 +283,28 @@ def register_core_log_actions(actions):
|
|||
"Revision %(revision_id)s from %(created_at)s unscheduled from publishing at %(go_live_at)s."
|
||||
) % {
|
||||
"revision_id": log_entry.data["revision"]["id"],
|
||||
"created_at": log_entry.data["revision"]["created"],
|
||||
"go_live_at": log_entry.data["revision"]["go_live_at"],
|
||||
"created_at": render_timestamp(
|
||||
parse_datetime_localized(
|
||||
log_entry.data["revision"]["created"],
|
||||
)
|
||||
),
|
||||
"go_live_at": render_timestamp(
|
||||
parse_datetime_localized(
|
||||
log_entry.data["revision"]["go_live_at"],
|
||||
)
|
||||
)
|
||||
if log_entry.data["revision"]["go_live_at"]
|
||||
else None,
|
||||
}
|
||||
else:
|
||||
return _("Page unscheduled for publishing at %(go_live_at)s") % {
|
||||
"go_live_at": log_entry.data["revision"]["go_live_at"],
|
||||
"go_live_at": render_timestamp(
|
||||
parse_datetime_localized(
|
||||
log_entry.data["revision"]["go_live_at"],
|
||||
)
|
||||
)
|
||||
if log_entry.data["revision"]["go_live_at"]
|
||||
else None,
|
||||
}
|
||||
except KeyError:
|
||||
return _("Page unscheduled from publishing")
|
||||
|
|
Ładowanie…
Reference in New Issue