From d06959473fddd1e93cfe5a5914a43924cc3777bb Mon Sep 17 00:00:00 2001 From: Christopher Maddalena Date: Tue, 21 Jul 2026 12:24:28 -0700 Subject: [PATCH 01/18] Added shortcuts for typing current date and time --- .../operation-logs/create-a-new-entry.mdx | 5 + .../collaborative-editor/editor-features.mdx | 7 + .../user_extra_fields/admin_change_form.html | 3 +- ghostwriter/context_processors.py | 4 + ghostwriter/home/editor_shortcuts.py | 26 +++ ghostwriter/home/tests/test_views.py | 54 +++++- ghostwriter/home/urls.py | 5 + ghostwriter/home/views.py | 11 ++ .../oplog/templates/oplog/oplog_detail.html | 1 + ghostwriter/oplog/tests/test_views.py | 21 +++ ghostwriter/static/js/editor_shortcuts.js | 120 +++++++++++++ ghostwriter/static/js/tinymce/config.js | 118 ++++++++++++ ghostwriter/templates/base_generic.html | 1 + ghostwriter/templates/base_generic_empty.html | 1 + .../snippets/editor_shortcuts_config.html | 4 + javascript/src/tiptap_gw/index.ts | 2 + javascript/src/tiptap_gw/now_shortcut.ts | 99 ++++++++++ javascript/tests/e2e/now_shortcut.spec.ts | 170 ++++++++++++++++++ 18 files changed, 650 insertions(+), 2 deletions(-) create mode 100644 ghostwriter/home/editor_shortcuts.py create mode 100644 ghostwriter/static/js/editor_shortcuts.js create mode 100644 ghostwriter/templates/snippets/editor_shortcuts_config.html create mode 100644 javascript/src/tiptap_gw/now_shortcut.ts create mode 100644 javascript/tests/e2e/now_shortcut.spec.ts diff --git a/DOCS/features/operation-logs/create-a-new-entry.mdx b/DOCS/features/operation-logs/create-a-new-entry.mdx index 0cf0a76fc..c454b9bdf 100644 --- a/DOCS/features/operation-logs/create-a-new-entry.mdx +++ b/DOCS/features/operation-logs/create-a-new-entry.mdx @@ -20,6 +20,11 @@ To manually create an entry, click on the "Create a new entry" button in the act You see a new row appear pre-populated with the current UTC timestamps and your username in the _Operator_ field. + +When writing in a rich text field, type `@today` or `@date` followed by a space or punctuation to insert the current date using +Ghostwriter's configured date format. Type `@now` or `@time` the same way to insert the current time in UTC, such as `20:14:13 UTC`. + + ### Modifying an Entry You can modify fields by double-clicking the table row you want to edit or pressing _Enter_ while it is selected. A modal form will open: diff --git a/DOCS/features/reporting/collaborative-editor/editor-features.mdx b/DOCS/features/reporting/collaborative-editor/editor-features.mdx index 42283bb1b..c2b6b61bc 100644 --- a/DOCS/features/reporting/collaborative-editor/editor-features.mdx +++ b/DOCS/features/reporting/collaborative-editor/editor-features.mdx @@ -5,6 +5,13 @@ description: "Features of the collaborative editor" Collaborative editing is the star of the show, but the editor contains other features worth exploring. +## Inserting the Current Date and Time + +Type `@today` or its alias `@date` followed by a space or punctuation to insert the current date using Ghostwriter's configured date +format. For example, with the default format, `@today,` becomes `21 Jul 2026,`. Type `@now` or its alias `@time` the same way to insert +the current time in UTC, such as `20:14:13 UTC`. The inserted values are ordinary text and do not change afterward. The shortcuts are +disabled inside inline code and code blocks. + ## Inserting Objects The editor supports inserting objects that make it easier to visualize certain elements of your writing, such as captions and evidence files. diff --git a/ghostwriter/commandcenter/templates/user_extra_fields/admin_change_form.html b/ghostwriter/commandcenter/templates/user_extra_fields/admin_change_form.html index 3b2b311bb..e162ed314 100644 --- a/ghostwriter/commandcenter/templates/user_extra_fields/admin_change_form.html +++ b/ghostwriter/commandcenter/templates/user_extra_fields/admin_change_form.html @@ -3,6 +3,7 @@ {% block admin_change_form_document_ready %} {{ block.super }} +{% include "snippets/editor_shortcuts_config.html" %} {% if original.is_collab_editable %} @@ -101,4 +102,4 @@ }); }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/ghostwriter/context_processors.py b/ghostwriter/context_processors.py index 953685456..038a83eb5 100644 --- a/ghostwriter/context_processors.py +++ b/ghostwriter/context_processors.py @@ -1,9 +1,13 @@ # Django Imports from django.conf import settings +# Ghostwriter Libraries +from ghostwriter.home.editor_shortcuts import get_editor_shortcuts_date_config + def selected_settings(request): return { "VERSION": settings.VERSION, "RELEASE_DATE": settings.RELEASE_DATE, + "EDITOR_SHORTCUTS_DATE_CONFIG": get_editor_shortcuts_date_config(), } diff --git a/ghostwriter/home/editor_shortcuts.py b/ghostwriter/home/editor_shortcuts.py new file mode 100644 index 000000000..9c25640c1 --- /dev/null +++ b/ghostwriter/home/editor_shortcuts.py @@ -0,0 +1,26 @@ +"""Server-provided configuration for rich-text editor date shortcuts.""" + +# Standard Libraries +from datetime import datetime, time, timedelta + +# Django Imports +from django.conf import settings +from django.urls import reverse +from django.utils import dateformat, timezone + + +def get_editor_shortcuts_date_config(): + """Return the formatted current date and its next server-local rollover.""" + current_time = timezone.localtime() + current_date = current_time.date() + next_midnight = timezone.make_aware( + datetime.combine(current_date + timedelta(days=1), time.min), + timezone.get_current_timezone(), + ) + + return { + "date": dateformat.format(current_date, settings.DATE_FORMAT), + "expiresAt": round(next_midnight.timestamp() * 1000), + "serverTime": round(current_time.timestamp() * 1000), + "refreshUrl": reverse("home:ajax_editor_shortcuts_date"), + } diff --git a/ghostwriter/home/tests/test_views.py b/ghostwriter/home/tests/test_views.py index 3da594de5..055f8487a 100644 --- a/ghostwriter/home/tests/test_views.py +++ b/ghostwriter/home/tests/test_views.py @@ -2,14 +2,17 @@ import logging from datetime import date, datetime, timedelta from io import StringIO +from unittest.mock import patch +from zoneinfo import ZoneInfo # Django Imports from django.conf import settings from django.core.management import call_command from django.core.management.base import CommandError from django.db.models import Q -from django.test import Client, TestCase +from django.test import Client, TestCase, override_settings from django.urls import reverse +from django.utils import timezone # 3rd Party Libraries from allauth.mfa.totp.internal.auth import generate_totp_secret, TOTP @@ -35,6 +38,55 @@ PASSWORD = "SuperNaturalReporting!" +class EditorShortcutsDateTests(TestCase): + """Tests for refreshing server-formatted editor shortcut dates.""" + + @classmethod + def setUpTestData(cls): + cls.user = UserFactory(password=PASSWORD) + cls.uri = reverse("home:ajax_editor_shortcuts_date") + + def setUp(self): + self.client = Client() + self.client_auth = Client() + self.assertTrue( + self.client_auth.login(username=self.user.username, password=PASSWORD) + ) + + def test_view_requires_login(self): + response = self.client.get(self.uri) + + self.assertEqual(response.status_code, 302) + + @override_settings(DATE_FORMAT="Y/m/d") + @patch("ghostwriter.home.editor_shortcuts.timezone.localtime") + def test_view_returns_date_and_next_local_midnight(self, mock_localtime): + local_timezone = ZoneInfo("America/Los_Angeles") + current_time = datetime(2026, 7, 21, 23, 59, 30, tzinfo=local_timezone) + mock_localtime.return_value = current_time + + with timezone.override(local_timezone): + response = self.client_auth.get(self.uri) + + next_midnight = datetime(2026, 7, 22, tzinfo=local_timezone) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "date": "2026/07/21", + "expiresAt": round(next_midnight.timestamp() * 1000), + "serverTime": round(current_time.timestamp() * 1000), + "refreshUrl": self.uri, + }, + ) + self.assertIn("no-store", response.headers["Cache-Control"]) + + def test_view_rejects_post(self): + response = self.client_auth.post(self.uri) + + self.assertEqual(response.status_code, 405) + + # Tests related to custom management commands diff --git a/ghostwriter/home/urls.py b/ghostwriter/home/urls.py index 8f68f6ce9..772bce038 100644 --- a/ghostwriter/home/urls.py +++ b/ghostwriter/home/urls.py @@ -17,6 +17,11 @@ # URLs for AJAX test functions urlpatterns += [ + path( + "ajax/editor-shortcuts/date", + views.editor_shortcuts_date, + name="ajax_editor_shortcuts_date", + ), path( "ajax/management/test/aws", views.TestAWSConnection.as_view(), diff --git a/ghostwriter/home/views.py b/ghostwriter/home/views.py index de56582e0..b75850239 100644 --- a/ghostwriter/home/views.py +++ b/ghostwriter/home/views.py @@ -12,6 +12,8 @@ from django.db.models import Prefetch, Q from django.http import HttpResponseNotAllowed, JsonResponse from django.shortcuts import redirect, render +from django.views.decorators.cache import never_cache +from django.views.decorators.http import require_GET from django.views.generic.edit import View # 3rd Party Libraries @@ -20,6 +22,7 @@ # Ghostwriter Libraries from ghostwriter.api.utils import RoleBasedAccessControlMixin, get_project_list, verify_user_is_privileged +from ghostwriter.home.editor_shortcuts import get_editor_shortcuts_date_config from ghostwriter.modules.health_utils import DjangoHealthChecks from ghostwriter.reporting.models import ReportFindingLink, ReportObservationLink from ghostwriter.rolodex.models import ProjectAssignment @@ -30,6 +33,14 @@ logger = logging.getLogger(__name__) +@login_required +@require_GET +@never_cache +def editor_shortcuts_date(request): + """Return the current server-formatted date for long-lived editor pages.""" + return JsonResponse(get_editor_shortcuts_date_config()) + + def _format_assignment_operator(assignment): if not assignment.operator: return "Unassigned" diff --git a/ghostwriter/oplog/templates/oplog/oplog_detail.html b/ghostwriter/oplog/templates/oplog/oplog_detail.html index bc3b10469..8c49cc9f1 100644 --- a/ghostwriter/oplog/templates/oplog/oplog_detail.html +++ b/ghostwriter/oplog/templates/oplog/oplog_detail.html @@ -303,6 +303,7 @@