From 23f90156bfc317ffa93085a4bf05f20444f712b7 Mon Sep 17 00:00:00 2001 From: Samuel Paccoud - DINUM Date: Tue, 12 Nov 2024 16:28:34 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20add=20creator=20field=20on?= =?UTF-8?q?=20document=20and=20allow=20filtering=20on=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We want to be able to limit the documents displayed on a logged-in user's list view by the documents they created or by the documents that other users created. This is different from having the "owner" role on a document because this can be acquired and even lost. What we want here is to be able to identify documents by the user who created them so we add a new field. --- src/backend/core/api/filters.py | 63 ++++ src/backend/core/api/serializers.py | 4 + src/backend/core/api/viewsets.py | 30 +- src/backend/core/factories.py | 1 + .../0010_add_field_creator_to_document.py | 31 ++ ...late_creator_field_and_make_it_required.py | 52 ++++ src/backend/core/models.py | 3 + .../documents/test_api_documents_list.py | 277 ++++++++++++++---- .../documents/test_api_documents_retrieve.py | 14 +- .../documents/test_api_documents_update.py | 27 +- .../core/tests/test_models_documents.py | 14 +- .../demo/management/commands/create_demo.py | 4 +- src/backend/pyproject.toml | 1 + 13 files changed, 453 insertions(+), 68 deletions(-) create mode 100644 src/backend/core/api/filters.py create mode 100644 src/backend/core/migrations/0010_add_field_creator_to_document.py create mode 100644 src/backend/core/migrations/0011_populate_creator_field_and_make_it_required.py diff --git a/src/backend/core/api/filters.py b/src/backend/core/api/filters.py new file mode 100644 index 00000000..713f48da --- /dev/null +++ b/src/backend/core/api/filters.py @@ -0,0 +1,63 @@ +"""API filters for Impress' core application.""" + +from django.utils.translation import gettext_lazy as _ + +import django_filters + +from core import models + + +class DocumentFilter(django_filters.FilterSet): + """ + Custom filter for filtering documents. + """ + + is_creator_me = django_filters.BooleanFilter( + method="filter_is_creator_me", label=_("Creator is me") + ) + + class Meta: + model = models.Document + fields = ["is_creator_me"] + + # pylint: disable=unused-argument + def filter_is_creator_me(self, queryset, name, value): + """ + Filter documents based on the `creator` being the current user. + + Example: + - /api/v1.0/documents/?is_creator_me=true + → Filters documents created by the logged-in user + - /api/v1.0/documents/?is_creator_me=false + → Filters documents created by other users + """ + user = self.request.user + + if not user.is_authenticated: + return queryset + + if value: + return queryset.filter(creator=user) + + return queryset.exclude(creator=user) + + # pylint: disable=unused-argument + def filter_is_favorite(self, queryset, name, value): + """ + Filter documents based on whether they are marked as favorite by the current user. + + Example: + - /api/v1.0/documents/?favorite=true + → Filters documents marked as favorite by the logged-in user + - /api/v1.0/documents/?favorite=false + → Filters documents not marked as favorite by the logged-in user + """ + user = self.request.user + + if not user.is_authenticated: + return queryset + + clause = "filter" if value else "exclude" + return getattr(queryset, clause)( + favorited_by_users__user=user, favorited_by_users__is_favorite=True + ) diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 589346b7..9a81fc47 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -150,6 +150,7 @@ class ListDocumentSerializer(BaseResourceSerializer): "abilities", "content", "created_at", + "creator", "is_favorite", "link_role", "link_reach", @@ -161,6 +162,7 @@ class ListDocumentSerializer(BaseResourceSerializer): "id", "abilities", "created_at", + "creator", "is_favorite", "link_role", "link_reach", @@ -181,6 +183,7 @@ class DocumentSerializer(ListDocumentSerializer): "abilities", "content", "created_at", + "creator", "is_favorite", "link_role", "link_reach", @@ -192,6 +195,7 @@ class DocumentSerializer(ListDocumentSerializer): "id", "abilities", "created_at", + "creator", "is_avorite", "link_role", "link_reach", diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 607acfea..1f2a9e10 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -22,10 +22,10 @@ from django.db.models import ( from django.http import Http404 from botocore.exceptions import ClientError +from django_filters import rest_framework as filters from rest_framework import ( decorators, exceptions, - filters, metadata, mixins, pagination, @@ -33,6 +33,9 @@ from rest_framework import ( views, viewsets, ) +from rest_framework import ( + filters as drf_filters, +) from rest_framework import ( response as drf_response, ) @@ -42,6 +45,7 @@ from core import enums, models from core.services.ai_services import AIService from . import permissions, serializers, utils +from .filters import DocumentFilter ATTACHMENTS_FOLDER = "attachments" UUID_REGEX = ( @@ -311,9 +315,23 @@ class DocumentViewSet( mixins.UpdateModelMixin, viewsets.GenericViewSet, ): - """Document ViewSet""" + """ + Document ViewSet for managing documents. - filter_backends = [filters.OrderingFilter] + Provides endpoints for creating, updating, and deleting documents, + along with filtering options. + + Filtering: + - `is_creator_me=true`: Returns documents created by the current user. + - `is_creator_me=false`: Returns documents created by other users. + + Example Usage: + - GET /api/v1.0/documents/?creator=me + - GET /api/v1.0/documents/?creator=other + """ + + filter_backends = [filters.DjangoFilterBackend, drf_filters.OrderingFilter] + filterset_class = DocumentFilter metadata_class = DocumentMetadata ordering = ["-updated_at"] ordering_fields = ["created_at", "is_favorite", "updated_at", "title"] @@ -410,8 +428,8 @@ class DocumentViewSet( return drf_response.Response(serializer.data) def perform_create(self, serializer): - """Set the current user as owner of the newly created object.""" - obj = serializer.save() + """Set the current user as creator and owner of the newly created object.""" + obj = serializer.save(creator=self.request.user) models.DocumentAccess.objects.create( document=obj, user=self.request.user, @@ -739,7 +757,7 @@ class TemplateViewSet( ): """Template ViewSet""" - filter_backends = [filters.OrderingFilter] + filter_backends = [drf_filters.OrderingFilter] permission_classes = [ permissions.IsAuthenticatedOrSafe, permissions.AccessPermission, diff --git a/src/backend/core/factories.py b/src/backend/core/factories.py index a825e598..f1ce8590 100644 --- a/src/backend/core/factories.py +++ b/src/backend/core/factories.py @@ -56,6 +56,7 @@ class DocumentFactory(factory.django.DjangoModelFactory): title = factory.Sequence(lambda n: f"document{n}") content = factory.Sequence(lambda n: f"content{n}") + creator = factory.SubFactory(UserFactory) link_reach = factory.fuzzy.FuzzyChoice( [a[0] for a in models.LinkReachChoices.choices] ) diff --git a/src/backend/core/migrations/0010_add_field_creator_to_document.py b/src/backend/core/migrations/0010_add_field_creator_to_document.py new file mode 100644 index 00000000..0fa619e2 --- /dev/null +++ b/src/backend/core/migrations/0010_add_field_creator_to_document.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.2 on 2024-11-09 11:36 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_add_document_favorite'), + ] + + operations = [ + migrations.AddField( + model_name='document', + name='creator', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='documents_created', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='user', + name='language', + field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'), + ), + migrations.AlterField( + model_name='user', + name='sub', + field=models.CharField(blank=True, help_text='Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters.', regex='^[\\w.@+-:]+\\Z')], verbose_name='sub'), + ), + ] diff --git a/src/backend/core/migrations/0011_populate_creator_field_and_make_it_required.py b/src/backend/core/migrations/0011_populate_creator_field_and_make_it_required.py new file mode 100644 index 00000000..1895993d --- /dev/null +++ b/src/backend/core/migrations/0011_populate_creator_field_and_make_it_required.py @@ -0,0 +1,52 @@ +# Generated by Django 5.1.2 on 2024-11-09 11:48 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations +from django.db.models import F, ForeignKey, Subquery, OuterRef, Q + + +def set_creator_from_document_access(apps, schema_editor): + """ + Populate the `creator` field for existing Document records. + + This function assigns the `creator` field using the existing + DocumentAccess entries. We can be sure that all documents have at + least one user with "owner" role. If the document has several roles, + it should take the entry with the oldest date of creation. + + The update is performed using efficient bulk queries with Django's + Subquery and OuterRef to minimize database hits and ensure performance. + + Note: After running this migration, we quickly modify the schema to make + the `creator` field required. + """ + Document = apps.get_model("core", "Document") + DocumentAccess = apps.get_model("core", "DocumentAccess") + + # Update `creator` using the "owner" role + owner_subquery = DocumentAccess.objects.filter( + document=OuterRef('pk'), + user__isnull=False, + role='owner', + ).order_by('created_at').values('user_id')[:1] + + Document.objects.filter( + creator__isnull=True + ).update(creator=Subquery(owner_subquery)) + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_add_field_creator_to_document'), + ] + + operations = [ + migrations.RunPython(set_creator_from_document_access, reverse_code=migrations.RunPython.noop), + migrations.AlterField( + model_name='document', + name='creator', + field=ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='documents_created', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 70fedb7f..ec2f7588 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -341,6 +341,9 @@ class Document(BaseModel): link_role = models.CharField( max_length=20, choices=LinkRoleChoices.choices, default=LinkRoleChoices.READER ) + creator = models.ForeignKey( + User, on_delete=models.RESTRICT, related_name="documents_created" + ) _content = None diff --git a/src/backend/core/tests/documents/test_api_documents_list.py b/src/backend/core/tests/documents/test_api_documents_list.py index d185f4e7..5ddbe9e0 100644 --- a/src/backend/core/tests/documents/test_api_documents_list.py +++ b/src/backend/core/tests/documents/test_api_documents_list.py @@ -62,6 +62,7 @@ def test_api_documents_list_format(): "abilities": document.get_abilities(user), "content": document.content, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(document.creator.id), "is_favorite": True, "link_reach": document.link_reach, "link_role": document.link_role, @@ -271,58 +272,6 @@ def test_api_documents_list_authenticated_distinct(): assert content["results"][0]["id"] == str(document.id) -def test_api_documents_list_ordering_default(): - """Documents should be ordered by descending "updated_at" by default""" - user = factories.UserFactory() - client = APIClient() - client.force_login(user) - - factories.DocumentFactory.create_batch(5, users=[user]) - - response = client.get("/api/v1.0/documents/") - - assert response.status_code == 200 - results = response.json()["results"] - assert len(results) == 5 - - # Check that results are sorted by descending "updated_at" as expected - for i in range(4): - assert operator.ge(results[i]["updated_at"], results[i + 1]["updated_at"]) - - -def test_api_documents_list_ordering_by_fields(): - """It should be possible to order by several fields""" - user = factories.UserFactory() - client = APIClient() - client.force_login(user) - - factories.DocumentFactory.create_batch(5, users=[user]) - - for parameter in [ - "created_at", - "-created_at", - "is_favorite", - "-is_favorite", - "updated_at", - "-updated_at", - "title", - "-title", - ]: - is_descending = parameter.startswith("-") - field = parameter.lstrip("-") - querystring = f"?ordering={parameter}" - - response = client.get(f"/api/v1.0/documents/{querystring:s}") - assert response.status_code == 200 - results = response.json()["results"] - assert len(results) == 5 - - # Check that results are sorted by the field in querystring as expected - compare = operator.ge if is_descending else operator.le - for i in range(4): - assert compare(results[i][field], results[i + 1][field]) - - def test_api_documents_list_favorites_no_extra_queries(django_assert_num_queries): """ Ensure that marking documents as favorite does not generate additional queries @@ -363,3 +312,227 @@ def test_api_documents_list_favorites_no_extra_queries(django_assert_num_queries assert result["is_favorite"] is True else: assert result["is_favorite"] is False + + +# Filters: ordering + + +def test_api_documents_list_ordering_default(): + """Documents should be ordered by descending "updated_at" by default""" + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + factories.DocumentFactory.create_batch(5, users=[user]) + + response = client.get("/api/v1.0/documents/") + + assert response.status_code == 200 + results = response.json()["results"] + assert len(results) == 5 + + # Check that results are sorted by descending "updated_at" as expected + for i in range(4): + assert operator.ge(results[i]["updated_at"], results[i + 1]["updated_at"]) + + +def test_api_documents_list_ordering_by_fields(): + """It should be possible to order by several fields""" + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + factories.DocumentFactory.create_batch(5, users=[user]) + + for parameter in [ + "created_at", + "-created_at", + "is_favorite", + "-is_favorite", + "nb_accesses", + "-nb_accesses", + "title", + "-title", + "updated_at", + "-updated_at", + ]: + is_descending = parameter.startswith("-") + field = parameter.lstrip("-") + querystring = f"?ordering={parameter}" + + response = client.get(f"/api/v1.0/documents/{querystring:s}") + assert response.status_code == 200 + results = response.json()["results"] + assert len(results) == 5 + + # Check that results are sorted by the field in querystring as expected + compare = operator.ge if is_descending else operator.le + for i in range(4): + assert compare(results[i][field], results[i + 1][field]) + + +# Filters: is_creator_me + + +def test_api_documents_list_filter_is_creator_me_true(): + """ + Authenticated users should be able to filter documents they created. + """ + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + factories.DocumentFactory.create_batch(3, users=[user], creator=user) + factories.DocumentFactory.create_batch(2, users=[user]) + + response = client.get("/api/v1.0/documents/?is_creator_me=true") + + assert response.status_code == 200 + results = response.json()["results"] + assert len(results) == 3 + + # Ensure all results are created by the current user + for result in results: + assert result["creator"] == str(user.id) + + +def test_api_documents_list_filter_is_creator_me_false(): + """ + Authenticated users should be able to filter documents created by others. + """ + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + factories.DocumentFactory.create_batch(3, users=[user], creator=user) + factories.DocumentFactory.create_batch(2, users=[user]) + + response = client.get("/api/v1.0/documents/?is_creator_me=false") + + assert response.status_code == 200 + results = response.json()["results"] + assert len(results) == 2 + + # Ensure all results are created by other users + for result in results: + assert result["creator"] != str(user.id) + + +def test_api_documents_list_filter_is_creator_me_invalid(): + """Filtering with an invalid `is_creator_me` value should do nothing.""" + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + factories.DocumentFactory.create_batch(3, users=[user], creator=user) + factories.DocumentFactory.create_batch(2, users=[user]) + + response = client.get("/api/v1.0/documents/?is_creator_me=invalid") + + assert response.status_code == 200 + results = response.json()["results"] + assert len(results) == 5 + + +# Filters: is_favorite + + +def test_api_documents_list_filter_is_favorite_true(): + """ + Authenticated users should be able to filter documents they marked as favorite. + """ + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + factories.DocumentFactory.create_batch(3, users=[user], favorited_by=[user]) + factories.DocumentFactory.create_batch(2, users=[user]) + + response = client.get("/api/v1.0/documents/?is_favorite=true") + + assert response.status_code == 200 + results = response.json()["results"] + assert len(results) == 3 + + # Ensure all results are marked as favorite by the current user + for result in results: + assert result["is_favorite"] is True + + +def test_api_documents_list_filter_is_favorite_false(): + """ + Authenticated users should be able to filter documents they didn't mark as favorite. + """ + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + factories.DocumentFactory.create_batch(3, users=[user], favorited_by=[user]) + factories.DocumentFactory.create_batch(2, users=[user]) + + response = client.get("/api/v1.0/documents/?is_favorite=false") + + assert response.status_code == 200 + results = response.json()["results"] + assert len(results) == 2 + + # Ensure all results are not marked as favorite by the current user + for result in results: + assert result["is_favorite"] is False + + +def test_api_documents_list_filter_is_favorite_invalid(): + """Filtering with an invalid `is_favorite` value should do nothing.""" + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + factories.DocumentFactory.create_batch(3, users=[user], favorited_by=[user]) + factories.DocumentFactory.create_batch(2, users=[user]) + + response = client.get("/api/v1.0/documents/?is_favorite=invalid") + + assert response.status_code == 200 + results = response.json()["results"] + assert len(results) == 5 + + +# Filters: link_reach + + +@pytest.mark.parametrize("reach", models.LinkReachChoices.values) +def test_api_documents_list_filter_link_reach(reach): + """Authenticated users should be able to filter documents by link reach.""" + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + factories.DocumentFactory.create_batch(5, users=[user]) + + response = client.get(f"/api/v1.0/documents/?link_reach={reach:s}") + + assert response.status_code == 200 + results = response.json()["results"] + + # Ensure all results have the chosen link reach + for result in results: + assert result["link_reach"] == reach + + +def test_api_documents_list_filter_link_reach_invalid(): + """Filtering with an invalid `link_reach` value should raise an error.""" + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + factories.DocumentFactory.create_batch(3, users=[user]) + + response = client.get("/api/v1.0/documents/?link_reach=invalid") + + assert response.status_code == 400 + assert response.json() == { + "link_reach": [ + "Select a valid choice. invalid is not one of the available choices." + ] + } + diff --git a/src/backend/core/tests/documents/test_api_documents_retrieve.py b/src/backend/core/tests/documents/test_api_documents_retrieve.py index 2fd253ac..4ff18918 100644 --- a/src/backend/core/tests/documents/test_api_documents_retrieve.py +++ b/src/backend/core/tests/documents/test_api_documents_retrieve.py @@ -38,13 +38,14 @@ def test_api_documents_retrieve_anonymous_public(): "versions_list": False, "versions_retrieve": False, }, - "nb_accesses": 0, "content": document.content, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(document.creator.id), "is_favorite": False, "link_reach": "public", "link_role": document.link_role, "title": document.title, + "nb_accesses": 0, "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), } @@ -100,6 +101,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated( }, "content": document.content, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(document.creator.id), "is_favorite": False, "link_reach": reach, "link_role": document.link_role, @@ -186,6 +188,7 @@ def test_api_documents_retrieve_authenticated_related_direct(): "id": str(document.id), "abilities": document.get_abilities(user), "content": document.content, + "creator": str(document.creator.id), "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "is_favorite": False, "link_reach": document.link_reach, @@ -275,12 +278,13 @@ def test_api_documents_retrieve_authenticated_related_team_members( assert response.json() == { "id": str(document.id), "abilities": document.get_abilities(user), - "nb_accesses": 5, "content": document.content, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(document.creator.id), "is_favorite": False, "link_reach": "restricted", "link_role": document.link_role, + "nb_accesses": 5, "title": document.title, "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), } @@ -330,12 +334,13 @@ def test_api_documents_retrieve_authenticated_related_team_administrators( assert response.json() == { "id": str(document.id), "abilities": document.get_abilities(user), - "nb_accesses": 5, "content": document.content, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(document.creator.id), "is_favorite": False, "link_reach": "restricted", "link_role": document.link_role, + "nb_accesses": 5, "title": document.title, "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), } @@ -386,12 +391,13 @@ def test_api_documents_retrieve_authenticated_related_team_owners( assert response.json() == { "id": str(document.id), "abilities": document.get_abilities(user), - "nb_accesses": 5, "content": document.content, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), + "creator": str(document.creator.id), "is_favorite": False, "link_reach": "restricted", "link_role": document.link_role, + "nb_accesses": 5, "title": document.title, "updated_at": document.updated_at.isoformat().replace("+00:00", "Z"), } diff --git a/src/backend/core/tests/documents/test_api_documents_update.py b/src/backend/core/tests/documents/test_api_documents_update.py index f37907be..3724af38 100644 --- a/src/backend/core/tests/documents/test_api_documents_update.py +++ b/src/backend/core/tests/documents/test_api_documents_update.py @@ -132,7 +132,14 @@ def test_api_documents_update_anonymous_or_authenticated_unrelated( document = models.Document.objects.get(pk=document.pk) document_values = serializers.DocumentSerializer(instance=document).data for key, value in document_values.items(): - if key in ["id", "accesses", "created_at", "link_reach", "link_role"]: + if key in [ + "id", + "accesses", + "created_at", + "creator", + "link_reach", + "link_role", + ]: assert value == old_document_values[key] elif key == "updated_at": assert value > old_document_values[key] @@ -216,7 +223,14 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner( document = models.Document.objects.get(pk=document.pk) document_values = serializers.DocumentSerializer(instance=document).data for key, value in document_values.items(): - if key in ["id", "created_at", "link_reach", "link_role", "nb_accesses"]: + if key in [ + "id", + "created_at", + "creator", + "link_reach", + "link_role", + "nb_accesses", + ]: assert value == old_document_values[key] elif key == "updated_at": assert value > old_document_values[key] @@ -255,7 +269,14 @@ def test_api_documents_update_authenticated_owners(via, mock_user_teams): document = models.Document.objects.get(pk=document.pk) document_values = serializers.DocumentSerializer(instance=document).data for key, value in document_values.items(): - if key in ["id", "created_at", "link_reach", "link_role", "nb_accesses"]: + if key in [ + "id", + "created_at", + "creator", + "link_reach", + "link_role", + "nb_accesses", + ]: assert value == old_document_values[key] elif key == "updated_at": assert value > old_document_values[key] diff --git a/src/backend/core/tests/test_models_documents.py b/src/backend/core/tests/test_models_documents.py index a06c6cc5..cc195911 100644 --- a/src/backend/core/tests/test_models_documents.py +++ b/src/backend/core/tests/test_models_documents.py @@ -32,15 +32,25 @@ def test_models_documents_id_unique(): factories.DocumentFactory(id=document.id) +def test_models_documents_creator_required(): + """The "creator" field should be required.""" + with pytest.raises(ValidationError) as excinfo: + models.Document.objects.create() + + assert excinfo.value.message_dict["creator"] == ["This field cannot be null."] + + def test_models_documents_title_null(): """The "title" field can be null.""" - document = models.Document.objects.create(title=None) + document = models.Document.objects.create( + title=None, creator=factories.UserFactory() + ) assert document.title is None def test_models_documents_title_empty(): """The "title" field can be empty.""" - document = models.Document.objects.create(title="") + document = models.Document.objects.create(title="", creator=factories.UserFactory()) assert document.title == "" diff --git a/src/backend/demo/management/commands/create_demo.py b/src/backend/demo/management/commands/create_demo.py index 49cde524..4ac9efc7 100644 --- a/src/backend/demo/management/commands/create_demo.py +++ b/src/backend/demo/management/commands/create_demo.py @@ -132,10 +132,13 @@ def create_demo(stdout): ) queue.flush() + users_ids = list(models.User.objects.values_list("id", flat=True)) + with Timeit(stdout, "Creating documents"): for _ in range(defaults.NB_OBJECTS["docs"]): queue.push( models.Document( + creator_id=random.choice(users_ids), title=fake.sentence(nb_words=4), link_reach=models.LinkReachChoices.AUTHENTICATED if random_true_with_probability(0.5) @@ -147,7 +150,6 @@ def create_demo(stdout): with Timeit(stdout, "Creating docs accesses"): docs_ids = list(models.Document.objects.values_list("id", flat=True)) - users_ids = list(models.User.objects.values_list("id", flat=True)) for doc_id in docs_ids: for user_id in random.sample( users_ids, diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 8210c28c..61863eb9 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "django-configurations==2.5.1", "django-cors-headers==4.5.0", "django-countries==7.6.1", + "django-filter==24.3", "django-parler==2.3", "redis==5.1.1", "django-redis==5.4.0",