🔥(backend) remove masking document code

The mask feature has never been implemented by the frontend application
and it is not what we want to do. We want to implement a feature to
leave a document.
This commit is contained in:
Manuel Raynaud
2026-06-01 16:25:50 +02:00
parent 2ad24384bd
commit 7773a5b9bf
12 changed files with 28 additions and 532 deletions
-22
View File
@@ -64,9 +64,6 @@ class ListDocumentFilter(DocumentFilter):
is_creator_me = django_filters.BooleanFilter(
method="filter_is_creator_me", label=_("Creator is me")
)
is_masked = django_filters.BooleanFilter(
method="filter_is_masked", label=_("Masked")
)
is_favorite = django_filters.BooleanFilter(
method="filter_is_favorite", label=_("Favorite")
)
@@ -114,25 +111,6 @@ class ListDocumentFilter(DocumentFilter):
return queryset.filter(is_favorite=bool(value))
# pylint: disable=unused-argument
def filter_is_masked(self, queryset, name, value):
"""
Filter documents based on whether they are masked by the current user.
Example:
- /api/v1.0/documents/?is_masked=true
→ Filters documents marked as masked by the logged-in user
- /api/v1.0/documents/?is_masked=false
→ Filters documents not marked as masked by the logged-in user
"""
user = self.request.user
if not user.is_authenticated:
return queryset
queryset_method = queryset.filter if bool(value) else queryset.exclude
return queryset_method(link_traces__user=user, link_traces__is_masked=True)
class UserSearchFilter(django_filters.FilterSet):
"""
+6 -42
View File
@@ -687,8 +687,9 @@ class DocumentViewSet(
# Annotate favorite status and filter if applicable as late as possible
queryset = queryset.annotate_is_favorite(user)
for field in ["is_favorite", "is_masked"]:
queryset = filterset.filters[field].filter(queryset, filter_data[field])
queryset = filterset.filters["is_favorite"].filter(
queryset, filter_data["is_favorite"]
)
# Apply ordering only now that everything is filtered and annotated
queryset = filters.OrderingFilter().filter_queryset(
@@ -1177,8 +1178,9 @@ class DocumentViewSet(
# Annotate favorite status and filter if applicable as late as possible
queryset = queryset.annotate_is_favorite(user)
for field in ["is_favorite", "is_masked"]:
queryset = filterset.filters[field].filter(queryset, filter_data[field])
queryset = filterset.filters["is_favorite"].filter(
queryset, filter_data["is_favorite"]
)
# Apply ordering only now that everything is filtered and annotated
queryset = filters.OrderingFilter().filter_queryset(
@@ -1770,44 +1772,6 @@ class DocumentViewSet(
status=drf.status.HTTP_200_OK,
)
@drf.decorators.action(detail=True, methods=["post", "delete"], url_path="mask")
def mask(self, request, *args, **kwargs):
"""Mask or unmask the document for the logged-in user based on the HTTP method."""
# Check permissions first
document = self.get_object()
user = request.user
try:
link_trace = models.LinkTrace.objects.get(document=document, user=user)
except models.LinkTrace.DoesNotExist:
return drf.response.Response(
{"detail": "User never accessed this document before."},
status=status.HTTP_400_BAD_REQUEST,
)
if request.method == "POST":
if link_trace.is_masked:
return drf.response.Response(
{"detail": "Document was already masked"},
status=drf.status.HTTP_200_OK,
)
link_trace.is_masked = True
link_trace.save(update_fields=["is_masked"])
return drf.response.Response(
{"detail": "Document was masked"},
status=drf.status.HTTP_201_CREATED,
)
# Handle DELETE method to unmask document
if not link_trace.is_masked:
return drf.response.Response(
{"detail": "Document was already not masked"},
status=drf.status.HTTP_200_OK,
)
link_trace.is_masked = False
link_trace.save(update_fields=["is_masked"])
return drf.response.Response(status=drf.status.HTTP_204_NO_CONTENT)
@drf.decorators.action(detail=True, methods=["post"], url_path="attachment-upload")
def attachment_upload(self, request, *args, **kwargs):
"""Upload a file related to a given document"""
-9
View File
@@ -150,15 +150,6 @@ class DocumentFactory(factory.django.DjangoModelFactory):
for item in extracted:
models.DocumentFavorite.objects.create(document=self, user=item)
@factory.post_generation
def masked_by(self, create, extracted, **kwargs):
"""Mark document as masked by a list of users."""
if create and extracted:
for item in extracted:
models.LinkTrace.objects.update_or_create(
document=self, user=item, defaults={"is_masked": True}
)
class UserDocumentAccessFactory(factory.django.DjangoModelFactory):
"""Create fake document user accesses for testing."""
@@ -0,0 +1,16 @@
# Generated by Django 5.2.14 on 2026-05-28 14:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0031_clean_onboarding_accesses"),
]
operations = [
migrations.RemoveField(
model_name="linktrace",
name="is_masked",
),
]
-2
View File
@@ -1312,7 +1312,6 @@ class Document(MP_Node, BaseModel):
"favorite": can_get and user.is_authenticated,
"link_configuration": is_owner_or_admin,
"invite_owner": is_owner and not is_deleted,
"mask": can_get and user.is_authenticated,
"move": is_owner_or_admin and not is_deleted,
"partial_update": can_update,
"restore": is_owner,
@@ -1481,7 +1480,6 @@ class LinkTrace(BaseModel):
related_name="link_traces",
)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="link_traces")
is_masked = models.BooleanField(default=False)
class Meta:
db_table = "impress_link_trace"
@@ -321,84 +321,6 @@ def test_api_documents_list_filter_is_favorite_invalid():
assert len(results) == 5
# Filters: is_masked
def test_api_documents_list_filter_is_masked_true():
"""
Authenticated users should be able to filter documents they marked as masked.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.DocumentFactory.create_batch(2, users=[user])
masked_documents = factories.DocumentFactory.create_batch(
3, users=[user], masked_by=[user]
)
unmasked_documents = factories.DocumentFactory.create_batch(2, users=[user])
for document in unmasked_documents:
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
response = client.get("/api/v1.0/documents/?is_masked=true")
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 3
# Ensure all results are marked as masked by the current user
masked_documents_ids = [str(doc.id) for doc in masked_documents]
for result in results:
assert result["id"] in masked_documents_ids
def test_api_documents_list_filter_is_masked_false():
"""
Authenticated users should be able to filter documents they didn't mark as masked.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.DocumentFactory.create_batch(2, users=[user])
masked_documents = factories.DocumentFactory.create_batch(
3, users=[user], masked_by=[user]
)
unmasked_documents = factories.DocumentFactory.create_batch(2, users=[user])
for document in unmasked_documents:
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
response = client.get("/api/v1.0/documents/?is_masked=false")
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 4
# Ensure all results are not marked as masked by the current user
masked_documents_ids = [str(doc.id) for doc in masked_documents]
for result in results:
assert result["id"] not in masked_documents_ids
def test_api_documents_list_filter_is_masked_invalid():
"""Filtering with an invalid `is_masked` value should do nothing."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.DocumentFactory.create_batch(2, users=[user])
factories.DocumentFactory.create_batch(3, users=[user], masked_by=[user])
unmasked_documents = factories.DocumentFactory.create_batch(2, users=[user])
for document in unmasked_documents:
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
response = client.get("/api/v1.0/documents/?is_masked=invalid")
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 7
# Filters: title
@@ -1,353 +0,0 @@
"""Test mask document API endpoint for users in impress's core app."""
import pytest
from rest_framework.test import APIClient
from core import factories, models
pytestmark = pytest.mark.django_db
@pytest.mark.parametrize(
"reach",
[
"restricted",
"authenticated",
"public",
],
)
@pytest.mark.parametrize("method", ["post", "delete"])
def test_api_document_mask_anonymous_user(method, reach):
"""Anonymous users should not be able to mask/unmask documents."""
document = factories.DocumentFactory(link_reach=reach)
response = getattr(APIClient(), method)(
f"/api/v1.0/documents/{document.id!s}/mask/"
)
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
# Verify in database
assert models.LinkTrace.objects.exists() is False
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_post_allowed(reach, has_role):
"""Authenticated users should be able to mask a document to which they have access."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach)
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
# Try masking the document without a link trace
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 400
assert response.json() == {"detail": "User never accessed this document before."}
assert not models.LinkTrace.objects.filter(document=document, user=user).exists()
models.LinkTrace.objects.create(document=document, user=user)
# Mask document
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 201
assert response.json() == {"detail": "Document was masked"}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=True
).exists()
def test_api_document_mask_authenticated_post_forbidden():
"""
Authenticated users should no be allowed to mask a document
to which they don't have access.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted")
# Try masking
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
# Verify in database
assert (
models.LinkTrace.objects.filter(document=document, user=user).exists() is False
)
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_post_already_masked_allowed(reach, has_role):
"""POST should not create duplicate link trace if already marked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, masked_by=[user])
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
# Try masking again
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 200
assert response.json() == {"detail": "Document was already masked"}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=True
).exists()
def test_api_document_mask_authenticated_post_already_masked_forbidden():
"""POST should not create duplicate masks if already marked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted", masked_by=[user])
# Try masking again
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
assert models.LinkTrace.objects.filter(document=document, user=user).exists()
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_post_unmasked_allowed(reach, has_role):
"""POST should not create duplicate link trace if unmasked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach)
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
# Try masking again
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 201
assert response.json() == {"detail": "Document was masked"}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=True
).exists()
def test_api_document_mask_authenticated_post_unmasked_forbidden():
"""POST should not create duplicate masks if unmasked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted")
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
# Try masking again
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=False
).exists()
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_delete_allowed(reach, has_role):
"""Authenticated users should be able to unmask a document using DELETE."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, masked_by=[user])
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
# Unmask document
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 204
assert response.content == b"" # No body
assert response.text == "" # Empty decoded text
assert "Content-Type" not in response.headers # No Content-Type for 204
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=False
).exists()
def test_api_document_mask_authenticated_delete_forbidden():
"""
Authenticated users should not be allowed to unmask a document if
they don't have access to it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted", masked_by=[user])
# Unmask document
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=True
).exists()
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_delete_not_masked_allowed(reach, has_role):
"""DELETE should be idempotent if the document is not masked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach)
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
# Try unmasking the document without a link trace
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 400
assert response.json() == {"detail": "User never accessed this document before."}
assert not models.LinkTrace.objects.filter(document=document, user=user).exists()
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
# Unmask document
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 200
assert response.json() == {"detail": "Document was already not masked"}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=False
).exists()
def test_api_document_mask_authenticated_delete_not_masked_forbidden():
"""DELETE should be idempotent if the document is not masked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted")
# Try to unmask when no entry exists
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
assert (
models.LinkTrace.objects.filter(document=document, user=user).exists() is False
)
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_post_unmark_then_mark_again_allowed(
reach, has_role
):
"""A user should be able to mask, unmask, and mask a document again."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach)
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
url = f"/api/v1.0/documents/{document.id!s}/mask/"
# Mask document
response = client.post(url)
assert response.status_code == 201
# Unmask document
response = client.delete(url)
assert response.status_code == 204
assert response.content == b"" # No body
assert response.text == "" # Empty decoded text
assert "Content-Type" not in response.headers # No Content-Type for 204
# Mask document again
response = client.post(url)
assert response.status_code == 201
assert response.json() == {"detail": "Document was masked"}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=True
).exists()
@@ -52,7 +52,6 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": False,
"content_patch": document.link_role == "editor",
"content_retrieve": True,
"media_auth": True,
@@ -131,7 +130,6 @@ def test_api_documents_retrieve_anonymous_public_parent():
"link_select_options": models.LinkReachChoices.get_select_options(
**links_definition
),
"mask": False,
"content_patch": grand_parent.link_role == "editor",
"content_retrieve": True,
"media_auth": True,
@@ -243,7 +241,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": True,
"content_patch": document.link_role == "editor",
"content_retrieve": True,
"media_auth": True,
@@ -329,7 +326,6 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"link_select_options": models.LinkReachChoices.get_select_options(
**links_definition
),
"mask": True,
"move": False,
"content_patch": grand_parent.link_role == "editor",
"content_retrieve": True,
@@ -529,7 +525,6 @@ def test_api_documents_retrieve_authenticated_related_parent():
"link_select_options": models.LinkReachChoices.get_select_options(
**link_definition
),
"mask": True,
"content_patch": access.role not in ["reader", "commenter"],
"content_retrieve": True,
"media_auth": True,
@@ -94,7 +94,6 @@ def test_api_documents_trashbin_format():
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": False,
"content_patch": False,
"content_retrieve": True,
"media_auth": False,
@@ -171,7 +171,6 @@ def test_models_documents_get_abilities_forbidden(
"favorite": False,
"comment": False,
"invite_owner": False,
"mask": False,
"content_patch": False,
"content_retrieve": False,
"media_auth": False,
@@ -246,7 +245,6 @@ def test_models_documents_get_abilities_reader(
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": is_authenticated,
"content_patch": False,
"content_retrieve": True,
"media_auth": True,
@@ -320,7 +318,6 @@ def test_models_documents_get_abilities_commenter(
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": is_authenticated,
"content_patch": False,
"content_retrieve": True,
"media_auth": True,
@@ -391,7 +388,6 @@ def test_models_documents_get_abilities_editor(
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": is_authenticated,
"content_patch": True,
"content_retrieve": True,
"media_auth": True,
@@ -451,7 +447,6 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": True,
"content_patch": True,
"content_retrieve": True,
"media_auth": True,
@@ -497,7 +492,6 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": False,
"content_patch": False,
"content_retrieve": True,
"media_auth": False,
@@ -547,7 +541,6 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": True,
"content_patch": True,
"content_retrieve": True,
"media_auth": True,
@@ -607,7 +600,6 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": True,
"content_patch": True,
"content_retrieve": True,
"media_auth": True,
@@ -675,7 +667,6 @@ def test_models_documents_get_abilities_reader_user(
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": True,
"content_patch": access_from_link,
"content_retrieve": True,
"media_auth": True,
@@ -744,7 +735,6 @@ def test_models_documents_get_abilities_commenter_user(
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": True,
"content_patch": access_from_link,
"content_retrieve": True,
"media_auth": True,
@@ -809,7 +799,6 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"public": ["reader", "commenter", "editor"],
"restricted": None,
},
"mask": True,
"content_patch": False,
"content_retrieve": True,
"media_auth": True,
@@ -472,9 +472,7 @@ def test_process_reconciliation_updates_linktraces(
models.LinkTrace.objects.create(document=doc_both, user=user_2)
doc_inactive_only = userdocs_u2[4].document
models.LinkTrace.objects.create(
document=doc_inactive_only, user=user_2, is_masked=True
)
models.LinkTrace.objects.create(document=doc_inactive_only, user=user_2)
doc_active_only = userdocs_u1[4].document
models.LinkTrace.objects.create(document=doc_active_only, user=user_1)
@@ -515,12 +513,11 @@ def test_process_reconciliation_updates_linktraces(
is False
)
# doc_inactive_only should now be linked to active user and preserve is_masked
# doc_inactive_only should now be linked to active user
lt = models.LinkTrace.objects.filter(
user=user_1, document=doc_inactive_only
).first()
assert lt is not None
assert lt.is_masked is True
# doc_active_only should still belong to active user
assert models.LinkTrace.objects.filter(
@@ -481,7 +481,7 @@ def test_get_visited_document_ids_of():
doc1, doc2, _ = factories.DocumentFactory.create_batch(3)
create_link = partial(models.LinkTrace.objects.create, user=user, is_masked=False)
create_link = partial(models.LinkTrace.objects.create, user=user)
create_link(document=doc1)
create_link(document=doc2)
@@ -515,7 +515,7 @@ def test_get_visited_document_ids_of_deleted():
doc_deleted = factories.DocumentFactory()
doc_ancestor_deleted = factories.DocumentFactory(parent=doc_deleted)
create_link = partial(models.LinkTrace.objects.create, user=user, is_masked=False)
create_link = partial(models.LinkTrace.objects.create, user=user)
create_link(document=doc)
create_link(document=doc_deleted)
@@ -566,7 +566,7 @@ def test_services_search_indexers_search(mock_post, indexer_settings):
doc1, doc2, _ = factories.DocumentFactory.create_batch(3)
create_link = partial(models.LinkTrace.objects.create, user=user, is_masked=False)
create_link = partial(models.LinkTrace.objects.create, user=user)
create_link(document=doc1)
create_link(document=doc2)
@@ -607,7 +607,7 @@ def test_services_search_indexers_search_nb_results(mock_post, indexer_settings)
doc1, doc2, _ = factories.DocumentFactory.create_batch(3)
create_link = partial(models.LinkTrace.objects.create, user=user, is_masked=False)
create_link = partial(models.LinkTrace.objects.create, user=user)
create_link(document=doc1)
create_link(document=doc2)