Explorar el Código

Add WAGTAIL_EDITING_SESSION_PING_INTERVAL setting

Sage Abdullah hace 8 meses
padre
commit
f8564055b1

+ 22 - 0
client/src/controllers/SessionController.test.js

@@ -67,6 +67,17 @@ describe('SessionController', () => {
       handlePing.mockClear();
       jest.advanceTimersByTime(123456);
       expect(handlePing).toHaveBeenCalledTimes(6);
+
+      // Setting it to 0 should stop the interval
+      handlePing.mockClear();
+      element.setAttribute('data-w-session-interval-value', '0');
+      await Promise.resolve();
+
+      jest.advanceTimersByTime(20000);
+      expect(handlePing).toHaveBeenCalledTimes(0);
+      jest.advanceTimersByTime(20000);
+      expect(handlePing).toHaveBeenCalledTimes(0);
+      handlePing.mockClear();
     });
 
     it('should allow setting a custom interval value on init and changing it afterwards', async () => {
@@ -100,6 +111,17 @@ describe('SessionController', () => {
       handlePing.mockClear();
       jest.advanceTimersByTime(123456);
       expect(handlePing).toHaveBeenCalledTimes(8);
+
+      // Setting it to >= 2**31 should stop the interval
+      handlePing.mockClear();
+      element.setAttribute('data-w-session-interval-value', `${2 ** 31}`);
+      await Promise.resolve();
+
+      jest.advanceTimersByTime(20000);
+      expect(handlePing).toHaveBeenCalledTimes(0);
+      jest.advanceTimersByTime(20000);
+      expect(handlePing).toHaveBeenCalledTimes(0);
+      handlePing.mockClear();
     });
   });
 

+ 4 - 0
client/src/controllers/SessionController.ts

@@ -128,6 +128,10 @@ export class SessionController extends Controller<HTMLElement> {
    */
   addInterval(): void {
     this.clearInterval();
+    // Values outside this range will be ignored by window.setInterval,
+    // making it fire all the time.
+    if (this.intervalValue <= 0 || this.intervalValue >= 2 ** 31) return;
+
     this.interval = window.setInterval(this.ping, this.intervalValue);
   }
 

+ 10 - 0
docs/reference/settings.md

@@ -259,6 +259,16 @@ WAGTAIL_AUTO_UPDATE_PREVIEW_INTERVAL = 500
 
 The interval (in milliseconds) is to check for changes made in the page editor before updating the preview. The default value is `500`.
 
+(wagtail_editing_session_ping_interval)=
+
+### `WAGTAIL_EDITING_SESSION_PING_INTERVAL`
+
+```python
+WAGTAIL_EDITING_SESSION_PING_INTERVAL = 10000
+```
+
+The interval (in milliseconds) to ping the server during an editing session. This is used to indicate that the session is active, as well as to display the list of other sessions that are currently editing the same content. The default value is `10000` (10 seconds). In order to effectively display the sessions list, this value needs to be set to under 1 minute. If set to 0, the interval will be disabled.
+
 (wagtailadmin_global_edit_lock)=
 
 ### `WAGTAILADMIN_GLOBAL_EDIT_LOCK`

+ 1 - 0
wagtail/admin/templates/wagtailadmin/shared/editing_sessions/module.html

@@ -29,6 +29,7 @@
     data-w-swap-defer-value="true"
     data-w-action-continue-value="true"
     data-w-action-url-value="{{ release_url }}"
+    data-w-session-interval-value="{{ ping_interval }}"
     data-w-session-w-dialog-outlet="[data-edit-form] [data-controller='w-dialog']#w-overwrite-changes-dialog"
     data-action="w-session:ping->w-swap#submit visibilitychange@document->w-session#dispatchVisibilityState w-session:visible->w-session#ping w-session:visible->w-session#addInterval w-session:hidden->w-session#clearInterval w-session:hidden->w-action#sendBeacon w-unsaved:add@document->w-session#setUnsavedChanges w-unsaved:clear@document->w-session#setUnsavedChanges w-swap:json->w-session#updateSessionData"
 >

+ 156 - 2
wagtail/admin/tests/test_editing_sessions.py

@@ -1,16 +1,22 @@
 import datetime
 
 from django.conf import settings
+from django.contrib.admin.utils import quote
 from django.contrib.auth.models import Group, Permission
 from django.contrib.contenttypes.models import ContentType
-from django.test import TestCase
+from django.test import TestCase, override_settings
 from django.urls import reverse
 from django.utils import timezone
 from freezegun import freeze_time
 
 from wagtail.admin.models import EditingSession
 from wagtail.models import GroupPagePermission, Page
-from wagtail.test.testapp.models import Advert, SimplePage
+from wagtail.test.testapp.models import (
+    Advert,
+    AdvertWithCustomPrimaryKey,
+    FullFeaturedSnippet,
+    SimplePage,
+)
 from wagtail.test.utils import WagtailTestUtils
 
 if settings.USE_TZ:
@@ -1180,3 +1186,151 @@ class TestReleaseView(WagtailTestUtils, TestCase):
         self.assertTrue(
             EditingSession.objects.filter(id=self.other_session.id).exists()
         )
+
+
+class TestModuleInEditView(WagtailTestUtils, TestCase):
+    url_name = "wagtailadmin_pages:edit"
+    model = Page
+
+    def setUp(self):
+        self.user = self.create_superuser(
+            "bob", password="password", first_name="Bob", last_name="Testuser"
+        )
+        self.login(user=self.user)
+        self.content_type = ContentType.objects.get_for_model(self.model)
+
+        self.object = self.create_object()
+
+        self.session = EditingSession.objects.create(
+            user=self.user,
+            content_type=self.content_type,
+            object_id=self.object.pk,
+            last_seen_at=TIMESTAMP_1,
+        )
+        self.old_session = EditingSession.objects.create(
+            user=self.user,
+            content_type=self.content_type,
+            object_id=self.object.pk,
+            last_seen_at=TIMESTAMP_PAST,
+        )
+
+    def create_object(self):
+        root_page = Page.get_first_root_node()
+        page = SimplePage(title="Foo", slug="foo", content="bar")
+        root_page.add_child(instance=page)
+        page.save_revision()
+        return page
+
+    def get(self):
+        return self.client.get(reverse(self.url_name, args=(quote(self.object.pk),)))
+
+    def assertRevisionInput(self, soup):
+        revision_input = soup.select_one('input[name="revision_id"]')
+        self.assertIsNotNone(revision_input)
+        self.assertEqual(revision_input.get("type"), "hidden")
+        self.assertEqual(
+            revision_input.get("value"),
+            str(self.object.latest_revision.id),
+        )
+
+    @freeze_time(TIMESTAMP_NOW)
+    def test_edit_view_with_default_interval(self):
+        self.assertEqual(EditingSession.objects.all().count(), 2)
+        response = self.get()
+        self.assertEqual(response.status_code, 200)
+
+        # Should perform a cleanup of the EditingSessions
+        self.assertTrue(EditingSession.objects.filter(id=self.session.id).exists())
+        self.assertFalse(EditingSession.objects.filter(id=self.old_session.id).exists())
+
+        # Should create a new EditingSession for the current user
+        self.assertEqual(EditingSession.objects.all().count(), 2)
+        new_session = EditingSession.objects.exclude(id=self.session.id).get(
+            content_type=self.content_type,
+            object_id=self.object.pk,
+        )
+        self.assertEqual(new_session.user, self.user)
+
+        # Should load the EditingSessionsModule with the default interval (10s)
+        soup = self.get_soup(response.content)
+        module = soup.select_one('form[data-controller~="w-session"]')
+        self.assertIsNotNone(module)
+        self.assertEqual(module.get("data-w-session-interval-value"), "10000")
+
+        # Should show the revision_id input
+        self.assertRevisionInput(module)
+
+    @freeze_time(TIMESTAMP_NOW)
+    @override_settings(WAGTAIL_EDITING_SESSION_PING_INTERVAL=30000)
+    def test_edit_view_with_custom_interval(self):
+        self.assertEqual(EditingSession.objects.all().count(), 2)
+        response = self.get()
+        self.assertEqual(response.status_code, 200)
+
+        # Should perform a cleanup of the EditingSessions
+        self.assertTrue(EditingSession.objects.filter(id=self.session.id).exists())
+        self.assertFalse(EditingSession.objects.filter(id=self.old_session.id).exists())
+
+        # Should create a new EditingSession for the current user
+        self.assertEqual(EditingSession.objects.all().count(), 2)
+        new_session = EditingSession.objects.exclude(id=self.session.id).get(
+            content_type=self.content_type,
+            object_id=self.object.pk,
+        )
+        self.assertEqual(new_session.user, self.user)
+
+        # Should load the EditingSessionsModule
+        soup = self.get_soup(response.content)
+        module = soup.select_one('form[data-controller~="w-session"]')
+        self.assertIsNotNone(module)
+        self.assertEqual(
+            module.get("data-w-swap-src-value"),
+            reverse(
+                "wagtailadmin_editing_sessions:ping",
+                args=(
+                    self.content_type.app_label,
+                    self.content_type.model,
+                    quote(self.object.pk),
+                    new_session.id,
+                ),
+            ),
+        )
+        self.assertEqual(
+            module.get("data-w-action-url-value"),
+            reverse(
+                "wagtailadmin_editing_sessions:release",
+                args=(new_session.id,),
+            ),
+        )
+
+        # Should use the custom interval (30s)
+        self.assertEqual(module.get("data-w-session-interval-value"), "30000")
+        self.assertRevisionInput(module)
+
+
+class TestModuleInEditViewWithRevisableSnippet(TestModuleInEditView):
+    model = FullFeaturedSnippet
+
+    @property
+    def url_name(self):
+        return self.model.snippet_viewset.get_url_name("edit")
+
+    def create_object(self):
+        obj = self.model.objects.create(text="Shodan")
+        obj.save_revision()
+        return obj
+
+
+class TestModuleInEditViewWithNonRevisableSnippet(TestModuleInEditView):
+    model = AdvertWithCustomPrimaryKey
+
+    @property
+    def url_name(self):
+        return self.model.snippet_viewset.get_url_name("edit")
+
+    def create_object(self):
+        return self.model.objects.create(text="GLaDOS", advert_id="m0n5t3r!/#")
+
+    def assertRevisionInput(self, soup):
+        revision_input = soup.select_one('input[name="revision_id"]')
+        self.assertIsNone(revision_input)

+ 8 - 0
wagtail/admin/ui/editing_sessions.py

@@ -1,3 +1,5 @@
+from django.conf import settings
+
 from wagtail.admin.ui.components import Component
 
 
@@ -23,10 +25,16 @@ class EditingSessionsModule(Component):
         self.revision_id = revision_id
 
     def get_context_data(self, parent_context):
+        ping_interval = getattr(
+            settings,
+            "WAGTAIL_EDITING_SESSION_PING_INTERVAL",
+            10000,
+        )
         return {
             "current_session": self.current_session,
             "ping_url": self.ping_url,
             "release_url": self.release_url,
+            "ping_interval": str(ping_interval),  # avoid the need to | unlocalize
             "sessions_list": self.sessions_list,
             "content_type": self.content_type,
             "revision_id": self.revision_id,