mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
feat(links): log CONTENT_EDIT on text changes (no content persisted)
Previously, editing a custom link's text content created a spurious
URL_CHANGE row (fixed last commit) but no audit entry at all for the
actual content change. The product requirement: a change-log row SHOULD
be created so auditors see content was edited, but should NOT persist
the content itself.
LinkUpdateView now:
- Captures self.original_text in get_initial() alongside
self.original_url.
- After super().form_valid() writes the new text, compares old vs new
text. If different, creates a LinkChangeLog with
change_type=CONTENT_EDIT and metadata={'changed': True} — no content
payload. Applies to both LINK and CUSTOM link types.
- URL-change logging behaviour is unchanged.
Tests in TestCustomLinkEditUrlChangeLog now cover:
- custom link text edit creates CONTENT_EDIT, no URL_CHANGE
- custom link text unchanged creates no log
- regular link text change creates CONTENT_EDIT
- regular link URL change still logs URL_CHANGE
This commit is contained in:
@@ -171,6 +171,10 @@ class LinkUpdateView(UpdateView):
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
self.original_url = self.object.original_url
|
||||
# Capture the existing content so form_valid can detect text edits
|
||||
# and log them as a CONTENT_EDIT change (without persisting the
|
||||
# content itself).
|
||||
self.original_text = self.object.text
|
||||
return initial
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -216,6 +220,21 @@ class LinkUpdateView(UpdateView):
|
||||
else:
|
||||
logger.info("URL did not change")
|
||||
|
||||
# Log a CONTENT_EDIT entry when the link's text changed, regardless of
|
||||
# link type. We deliberately don't persist the actual content — only a
|
||||
# marker that it changed (per product requirement).
|
||||
new_text = form.cleaned_data.get('text') or ''
|
||||
if (self.original_text or '') != new_text:
|
||||
try:
|
||||
LinkChangeLog.objects.create(
|
||||
link=self.object,
|
||||
change_type=LinkChangeLog.ChangeType.CONTENT_EDIT,
|
||||
metadata={'changed': True},
|
||||
)
|
||||
logger.info(f"Created CONTENT_EDIT LinkChangeLog for link {self.object.pk}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating CONTENT_EDIT LinkChangeLog for link {self.object.pk}: {str(e)}")
|
||||
|
||||
return response
|
||||
|
||||
def form_invalid(self, form):
|
||||
|
||||
+66
-3
@@ -242,7 +242,11 @@ class TestCustomLinkEditUrlChangeLog:
|
||||
"URL changed" entry, because custom-link URLs are derived from the
|
||||
alias and the original_url form field is hidden for custom links.
|
||||
Regression test for the bug where every edit of a custom link
|
||||
produced a LinkChangeLog row."""
|
||||
produced a LinkChangeLog row.
|
||||
|
||||
A CONTENT_EDIT entry, however, SHOULD be created whenever the text
|
||||
changes (for either custom or regular links). The actual content is
|
||||
NOT persisted in the log — only a marker that it changed."""
|
||||
|
||||
def _update(self, client, link, **payload):
|
||||
data = {
|
||||
@@ -264,7 +268,12 @@ class TestCustomLinkEditUrlChangeLog:
|
||||
original_url="/custom/mynote",
|
||||
text="old body text",
|
||||
)
|
||||
before = LinkChangeLog.objects.filter(link=link).count()
|
||||
before_url_logs = LinkChangeLog.objects.filter(
|
||||
link=link, change_type=LinkChangeLog.ChangeType.URL_CHANGE
|
||||
).count()
|
||||
before_content_logs = LinkChangeLog.objects.filter(
|
||||
link=link, change_type=LinkChangeLog.ChangeType.CONTENT_EDIT
|
||||
).count()
|
||||
|
||||
response = self._update(client, link, text="new body text")
|
||||
assert response.status_code == 200
|
||||
@@ -272,7 +281,18 @@ class TestCustomLinkEditUrlChangeLog:
|
||||
link.refresh_from_db()
|
||||
assert link.text == "new body text"
|
||||
assert link.original_url == "/custom/mynote"
|
||||
assert LinkChangeLog.objects.filter(link=link).count() == before
|
||||
# No spurious URL-change log
|
||||
assert LinkChangeLog.objects.filter(
|
||||
link=link, change_type=LinkChangeLog.ChangeType.URL_CHANGE
|
||||
).count() == before_url_logs
|
||||
# A content-edit log IS created, without persisting the content
|
||||
content_logs = LinkChangeLog.objects.filter(
|
||||
link=link, change_type=LinkChangeLog.ChangeType.CONTENT_EDIT
|
||||
).order_by("-changed_at")
|
||||
assert content_logs.count() == before_content_logs + 1
|
||||
assert content_logs.first().metadata == {"changed": True}
|
||||
assert "new body text" not in str(content_logs.first().metadata)
|
||||
assert "old body text" not in str(content_logs.first().metadata)
|
||||
|
||||
def test_regular_link_text_unchanged_creates_no_url_change_log(self, client):
|
||||
link = Link.objects.create(
|
||||
@@ -296,6 +316,49 @@ class TestCustomLinkEditUrlChangeLog:
|
||||
assert response.status_code == 200
|
||||
assert LinkChangeLog.objects.filter(link=link).count() == before
|
||||
|
||||
def test_custom_link_text_unchanged_creates_no_content_log(self, client):
|
||||
link = Link.objects.create(
|
||||
alias="mynote",
|
||||
link_type=Link.LinkType.CUSTOM,
|
||||
original_url="/custom/mynote",
|
||||
text="same body text",
|
||||
)
|
||||
before = LinkChangeLog.objects.filter(link=link).count()
|
||||
|
||||
response = self._update(client, link, text="same body text")
|
||||
assert response.status_code == 200
|
||||
assert LinkChangeLog.objects.filter(link=link).count() == before
|
||||
|
||||
def test_regular_link_text_change_creates_content_log(self, client):
|
||||
"""Regular (LINK-type) links can also carry text content; editing that
|
||||
text should produce a CONTENT_EDIT entry too."""
|
||||
link = Link.objects.create(
|
||||
alias="gh",
|
||||
link_type=Link.LinkType.LINK,
|
||||
original_url="https://github.com",
|
||||
text="original notes",
|
||||
)
|
||||
before = LinkChangeLog.objects.filter(link=link).count()
|
||||
|
||||
response = client.post(
|
||||
f"/link/{link.pk}/edit/",
|
||||
{
|
||||
"alias": "gh",
|
||||
"original_url": "https://github.com",
|
||||
"link_type": Link.LinkType.LINK,
|
||||
"description": "",
|
||||
"text": "edited notes",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
content_logs = LinkChangeLog.objects.filter(
|
||||
link=link, change_type=LinkChangeLog.ChangeType.CONTENT_EDIT
|
||||
).order_by("-changed_at")
|
||||
assert content_logs.count() == before + 1
|
||||
assert content_logs.first().metadata == {"changed": True}
|
||||
assert "edited notes" not in str(content_logs.first().metadata)
|
||||
|
||||
def test_regular_link_url_change_logs_change(self, client):
|
||||
link = Link.objects.create(
|
||||
alias="gh",
|
||||
|
||||
Reference in New Issue
Block a user