📈(backend) capture an event when a document is duplicated

We want to capture an event when a document is duplicated. The document
from where come from the duplication is added in the properties.
This commit is contained in:
Manuel Raynaud
2026-06-01 17:41:56 +02:00
parent f4139e3ab0
commit d1476024a7
3 changed files with 188 additions and 66 deletions
+9
View File
@@ -1343,6 +1343,15 @@ class DocumentViewSet(
user=user,
)
posthog_capture(
PosthogEventName.DOC_DUPLICATED,
user,
{
"duplicated_from": str(document_to_duplicate.id),
},
document=duplicated_document,
)
return drf_response.Response(
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
)
@@ -5,6 +5,7 @@ Test file uploads API endpoint for users in impress's core app.
import base64
import uuid
from io import BytesIO
from unittest import mock
from urllib.parse import urlparse
from django.conf import settings
@@ -46,16 +47,18 @@ def test_api_documents_duplicate_forbidden():
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(
link_reach="restricted",
users=[factories.UserFactory()],
title="my document",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
document = factories.DocumentFactory(
link_reach="restricted",
users=[factories.UserFactory()],
title="my document",
)
response = client.post(f"/api/v1.0/documents/{document.id!s}/duplicate/")
assert response.status_code == 403
assert models.Document.objects.count() == 1
mock_capture.assert_not_called()
def test_api_documents_duplicate_anonymous():
@@ -63,10 +66,12 @@ def test_api_documents_duplicate_anonymous():
document = factories.DocumentFactory(link_reach="public", link_role="reader")
response = APIClient().post(f"/api/v1.0/documents/{document.id!s}/duplicate/")
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = APIClient().post(f"/api/v1.0/documents/{document.id!s}/duplicate/")
assert response.status_code == 401
assert models.Document.objects.count() == 1
mock_capture.assert_not_called()
@pytest.mark.parametrize("index", range(3))
@@ -108,7 +113,8 @@ def test_api_documents_duplicate_success(index):
# Don't create document for third ID to check that it doesn't impact access to attachments
# Duplicate the document via the API endpoint
response = client.post(f"/api/v1.0/documents/{document.id}/duplicate/")
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(f"/api/v1.0/documents/{document.id}/duplicate/")
assert response.status_code == 201
@@ -125,6 +131,13 @@ def test_api_documents_duplicate_success(index):
assert duplicated_document.get_parent() == document.get_parent()
assert duplicated_document.path == document.get_last_sibling().path
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(document.id)},
document=duplicated_document,
)
# Check that accesses were not duplicated.
# The user who did the duplicate is forced as owner
assert duplicated_document.accesses.count() == 1
@@ -195,11 +208,12 @@ def test_api_documents_duplicate_with_accesses_admin(role):
paths = {document.pk: document.path for document in all_documents}
# Duplicate the document via the API endpoint requesting to duplicate accesses
response = client.post(
f"/api/v1.0/documents/{document.id!s}/duplicate/",
{"with_accesses": True},
format="json",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{document.id!s}/duplicate/",
{"with_accesses": True},
format="json",
)
assert response.status_code == 201
@@ -212,6 +226,13 @@ def test_api_documents_duplicate_with_accesses_admin(role):
assert duplicated_document.duplicated_from == document
assert duplicated_document.attachments == []
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(document.id)},
document=duplicated_document,
)
# Check that accesses were duplicated and the user who did the duplicate is forced as owner
duplicated_accesses = duplicated_document.accesses
assert duplicated_accesses.count() == 3
@@ -242,11 +263,12 @@ def test_api_documents_duplicate_with_accesses_non_admin(role):
factories.TeamDocumentAccessFactory(document=document)
# Duplicate the document via the API endpoint requesting to duplicate accesses
response = client.post(
f"/api/v1.0/documents/{document.id!s}/duplicate/",
{"with_accesses": True},
format="json",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{document.id!s}/duplicate/",
{"with_accesses": True},
format="json",
)
assert response.status_code == 201
@@ -259,6 +281,13 @@ def test_api_documents_duplicate_with_accesses_non_admin(role):
assert duplicated_document.duplicated_from == document
assert duplicated_document.attachments == []
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(document.id)},
document=duplicated_document,
)
# Check that accesses were duplicated and the user who did the duplicate is forced as owner
duplicated_accesses = duplicated_document.accesses
assert duplicated_accesses.count() == 1
@@ -282,11 +311,12 @@ def test_api_documents_duplicate_non_root_document(role):
assert child.accesses.count() == 1
# Duplicate the document via the API endpoint requesting to duplicate accesses
response = client.post(
f"/api/v1.0/documents/{child.id!s}/duplicate/",
{"with_accesses": True},
format="json",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{child.id!s}/duplicate/",
{"with_accesses": True},
format="json",
)
assert response.status_code == 201
@@ -299,6 +329,13 @@ def test_api_documents_duplicate_non_root_document(role):
assert duplicated_document.duplicated_from == child
assert duplicated_document.attachments == []
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(child.id)},
document=duplicated_document,
)
# No access should be created for non root documents
duplicated_accesses = duplicated_document.accesses
assert duplicated_accesses.count() == 0
@@ -320,9 +357,10 @@ def test_api_documents_duplicate_reader_non_root_document():
assert child.get_role(user) == "reader"
response = client.post(
f"/api/v1.0/documents/{child.id!s}/duplicate/", format="json"
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{child.id!s}/duplicate/", format="json"
)
assert response.status_code == 201
duplicated_document = models.Document.objects.get(id=response.json()["id"])
@@ -330,6 +368,13 @@ def test_api_documents_duplicate_reader_non_root_document():
assert duplicated_document.accesses.count() == 1
assert duplicated_document.accesses.get(user=user).role == "owner"
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(child.id)},
document=duplicated_document,
)
def test_api_documents_duplicate_with_descendants_simple():
"""
@@ -357,11 +402,12 @@ def test_api_documents_duplicate_with_descendants_simple():
assert initial_count == 3
# Duplicate with descendants
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
@@ -375,6 +421,13 @@ def test_api_documents_duplicate_with_descendants_simple():
assert duplicated_root.duplicated_from == root
assert duplicated_root.get_children().count() == 2
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(root.id)},
document=duplicated_root,
)
# Check children duplication
duplicated_children = duplicated_root.get_children().order_by("title")
assert duplicated_children.count() == 2
@@ -421,15 +474,23 @@ def test_api_documents_duplicate_with_descendants_multi_level():
assert initial_count == 4
# Duplicate with descendants
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(root.id)},
document=duplicated_root,
)
# Check that all documents were duplicated
assert models.Document.objects.count() == 8
@@ -455,6 +516,7 @@ def test_api_documents_duplicate_with_descendants_multi_level():
assert dup_great_grandchild.duplicated_from == great_grandchild
# pylint: disable=too-many-locals
def test_api_documents_duplicate_with_descendants_and_attachments():
"""
Duplicating with descendants should properly handle attachments in all children.
@@ -499,7 +561,8 @@ def test_api_documents_duplicate_with_descendants_and_attachments():
update_child = ydoc_child.get_update()
child_content = base64.b64encode(update_child).decode("utf-8")
child = factories.DocumentFactory(
# child
factories.DocumentFactory(
id=child_id,
parent=root,
title="Child with Image",
@@ -508,15 +571,23 @@ def test_api_documents_duplicate_with_descendants_and_attachments():
)
# Duplicate with descendants
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(root.id)},
document=duplicated_root,
)
# Check root attachments
assert duplicated_root.attachments == [image_key_root]
assert duplicated_root.content == root_content
@@ -550,15 +621,23 @@ def test_api_documents_duplicate_with_descendants_and_accesses():
factories.UserDocumentAccessFactory(document=child, user=other_user, role="reader")
# Duplicate with descendants and accesses
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True, "with_accesses": True},
format="json",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True, "with_accesses": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(root.id)},
document=duplicated_root,
)
# Check root accesses (should be duplicated)
root_accesses = duplicated_root.accesses.order_by("user_id")
assert root_accesses.count() == 2
@@ -597,15 +676,23 @@ def test_api_documents_duplicate_with_descendants_non_root_document_becomes_root
assert child.is_child_of(parent)
# Duplicate the child (non-root) with descendants
response = client.post(
f"/api/v1.0/documents/{child.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{child.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
assert response.status_code == 201
duplicated_child = models.Document.objects.get(id=response.json()["id"])
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(child.id)},
document=duplicated_child,
)
assert duplicated_child.title == "Copy of Sub Document"
dup_grandchildren = duplicated_child.get_children()
@@ -628,7 +715,8 @@ def test_api_documents_duplicate_without_descendants_should_not_duplicate_childr
users=[(user, "owner")],
title="Root",
)
child = factories.DocumentFactory(
# child
factories.DocumentFactory(
parent=root,
title="Child",
)
@@ -637,14 +725,22 @@ def test_api_documents_duplicate_without_descendants_should_not_duplicate_childr
assert initial_count == 2
# Duplicate without descendants (default behavior)
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
format="json",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(root.id)},
document=duplicated_root,
)
# Only root should be duplicated, not children
assert models.Document.objects.count() == 3
assert duplicated_root.get_children().count() == 0
@@ -674,15 +770,23 @@ def test_api_documents_duplicate_with_descendants_preserves_link_configuration()
)
# Duplicate with descendants and accesses
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True, "with_accesses": True},
format="json",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True, "with_accesses": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(root.id)},
document=duplicated_root,
)
# Check root link configuration
assert duplicated_root.link_reach == root.link_reach
assert duplicated_root.link_role == root.link_role
@@ -722,15 +826,23 @@ def test_api_documents_duplicate_with_descendants_complex_tree():
assert initial_count == 6
# Duplicate with descendants
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
mock_capture.assert_called_once_with(
"doc_duplicated",
user,
{"duplicated_from": str(root.id)},
document=duplicated_root,
)
# All documents should be duplicated
assert models.Document.objects.count() == 12
+1
View File
@@ -16,6 +16,7 @@ class PosthogEventName(StrEnum):
DOC_CREATED = "doc_created"
DOC_DELETED = "doc_deleted"
DOC_DUPLICATED = "doc_duplicated"
USER_LOGIN = "user_login"