From 72df20703946a4496b77c55912df8f69b964cfc7 Mon Sep 17 00:00:00 2001 From: Erwan Rouchet <rouchet@teklia.com> Date: Tue, 11 Feb 2025 08:57:03 +0000 Subject: [PATCH] Budget models --- arkindex/budget/__init__.py | 0 arkindex/budget/apps.py | 5 +++ arkindex/budget/migrations/0001_initial.py | 44 +++++++++++++++++++ arkindex/budget/migrations/__init__.py | 0 arkindex/budget/models.py | 44 +++++++++++++++++++ .../migrations/0014_corpus_budget.py | 20 +++++++++ arkindex/documents/models.py | 8 ++++ arkindex/documents/serializers/elements.py | 4 ++ arkindex/documents/tests/test_corpus.py | 10 +++++ .../process/tests/process/test_destroy.py | 6 +-- arkindex/project/settings.py | 1 + arkindex/sql_validation/corpus_delete.sql | 3 +- .../corpus_delete_top_level_type.sql | 3 +- .../sql_validation/corpus_rights_filter.sql | 1 + .../corpus_rights_filter_public.sql | 2 + arkindex/sql_validation/list_elements.sql | 3 +- .../process_elements_filter_ml_class.sql | 3 +- .../process_elements_filter_type.sql | 3 +- .../process_elements_top_level.sql | 3 +- .../process_elements_with_image.sql | 3 +- 20 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 arkindex/budget/__init__.py create mode 100644 arkindex/budget/apps.py create mode 100644 arkindex/budget/migrations/0001_initial.py create mode 100644 arkindex/budget/migrations/__init__.py create mode 100644 arkindex/budget/models.py create mode 100644 arkindex/documents/migrations/0014_corpus_budget.py diff --git a/arkindex/budget/__init__.py b/arkindex/budget/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/arkindex/budget/apps.py b/arkindex/budget/apps.py new file mode 100644 index 0000000000..d24215cc34 --- /dev/null +++ b/arkindex/budget/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BudgetConfig(AppConfig): + name = "arkindex.budget" diff --git a/arkindex/budget/migrations/0001_initial.py b/arkindex/budget/migrations/0001_initial.py new file mode 100644 index 0000000000..67ebfb8b71 --- /dev/null +++ b/arkindex/budget/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 5.0.8 on 2025-02-10 09:37 + +import uuid + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("process", "0048_worker_cost_fields"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Budget", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("name", models.CharField(max_length=100, validators=[django.core.validators.MinLengthValidator(1)])), + ("total", models.DecimalField(decimal_places=3, default=0, max_digits=12)), + ], + ), + migrations.CreateModel( + name="BudgetEntry", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("created", models.DateTimeField(auto_now_add=True)), + ("description", models.CharField(max_length=255, validators=[django.core.validators.MinLengthValidator(1)])), + ("value", models.DecimalField(decimal_places=3, max_digits=12)), + ("budget", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="entries", to="budget.budget")), + ("process", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="budget_entries", to="process.process")), + ("user", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="budget_entries", to=settings.AUTH_USER_MODEL)), + ], + options={ + "verbose_name_plural": "budget entries", + }, + ), + ] diff --git a/arkindex/budget/migrations/__init__.py b/arkindex/budget/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/arkindex/budget/models.py b/arkindex/budget/models.py new file mode 100644 index 0000000000..c9567cb6ad --- /dev/null +++ b/arkindex/budget/models.py @@ -0,0 +1,44 @@ +import uuid + +from django.conf import settings +from django.core.validators import MinLengthValidator +from django.db import models + + +class Budget(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField(max_length=100, validators=[MinLengthValidator(1)]) + total = models.DecimalField(max_digits=12, decimal_places=3, default=0) + + def __str__(self) -> str: + return self.name + + +class BudgetEntry(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + created = models.DateTimeField(auto_now_add=True) + + budget = models.ForeignKey(Budget, on_delete=models.CASCADE, related_name="entries") + description = models.CharField(max_length=255, validators=[MinLengthValidator(1)]) + value = models.DecimalField(max_digits=12, decimal_places=3) + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + blank=True, + null=True, + on_delete=models.SET_NULL, + related_name="budget_entries", + ) + process = models.ForeignKey( + "process.Process", + blank=True, + null=True, + on_delete=models.SET_NULL, + related_name="budget_entries", + ) + + class Meta: + verbose_name_plural = "budget entries" + + def __str__(self) -> str: + return self.description diff --git a/arkindex/documents/migrations/0014_corpus_budget.py b/arkindex/documents/migrations/0014_corpus_budget.py new file mode 100644 index 0000000000..026c25cfb4 --- /dev/null +++ b/arkindex/documents/migrations/0014_corpus_budget.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.8 on 2025-02-10 09:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("budget", "0001_initial"), + ("documents", "0013_corpus_maximum_task_ttl"), + ] + + operations = [ + migrations.AddField( + model_name="corpus", + name="budget", + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="corpora", to="budget.budget"), + ), + ] diff --git a/arkindex/documents/models.py b/arkindex/documents/models.py index 6ea6ee7745..e21ef63b10 100644 --- a/arkindex/documents/models.py +++ b/arkindex/documents/models.py @@ -59,6 +59,14 @@ class Corpus(IndexableModel): "When not set, the instance-wide maximum time-to-live will be used instead.", ) + budget = models.ForeignKey( + "budget.Budget", + on_delete=models.SET_NULL, + related_name="corpora", + blank=True, + null=True, + ) + # Specific manager for ACL objects = CorpusManager() diff --git a/arkindex/documents/serializers/elements.py b/arkindex/documents/serializers/elements.py index ee9ec29171..528cd7b0d2 100644 --- a/arkindex/documents/serializers/elements.py +++ b/arkindex/documents/serializers/elements.py @@ -285,6 +285,7 @@ class CorpusSerializer(serializers.ModelSerializer): "authorized_users", "indexable", "maximum_task_ttl", + "budget_id", ) extra_kwargs = { "public": { @@ -293,6 +294,9 @@ class CorpusSerializer(serializers.ModelSerializer): }, "indexable": { "help_text": "Whether this corpus should be indexed in Solr and searchable using the `SearchCorpus` endpoint." + }, + "budget_id": { + "read_only": True, } } diff --git a/arkindex/documents/tests/test_corpus.py b/arkindex/documents/tests/test_corpus.py index 805447a33f..3055b5e8e6 100644 --- a/arkindex/documents/tests/test_corpus.py +++ b/arkindex/documents/tests/test_corpus.py @@ -104,6 +104,7 @@ class TestCorpus(FixtureAPITestCase): "authorized_users": 1, "top_level_type": None, "maximum_task_ttl": 3600, + "budget_id": None, } ] ) @@ -138,6 +139,7 @@ class TestCorpus(FixtureAPITestCase): "authorized_users": 2, "top_level_type": None, "maximum_task_ttl": 3600, + "budget_id": None, }, { "id": str(self.corpus_public.id), @@ -150,6 +152,7 @@ class TestCorpus(FixtureAPITestCase): "authorized_users": 1, "top_level_type": None, "maximum_task_ttl": 3600, + "budget_id": None, } ] ) @@ -184,6 +187,7 @@ class TestCorpus(FixtureAPITestCase): "authorized_users": 2, "top_level_type": None, "maximum_task_ttl": 3600, + "budget_id": None, }, { "id": str(self.corpus_hidden.id), @@ -196,6 +200,7 @@ class TestCorpus(FixtureAPITestCase): "authorized_users": 0, "top_level_type": None, "maximum_task_ttl": 3600, + "budget_id": None, }, { "id": str(self.corpus_public.id), @@ -208,6 +213,7 @@ class TestCorpus(FixtureAPITestCase): "authorized_users": 1, "top_level_type": None, "maximum_task_ttl": 3600, + "budget_id": None, } ] ) @@ -356,6 +362,7 @@ class TestCorpus(FixtureAPITestCase): "authorized_users": 1, "top_level_type": None, "maximum_task_ttl": 3600, + "budget_id": None, }) def test_retrieve(self): @@ -375,6 +382,7 @@ class TestCorpus(FixtureAPITestCase): "authorized_users": 2, "top_level_type": None, "maximum_task_ttl": 3600, + "budget_id": None, }) def test_retrieve_not_found(self): @@ -405,6 +413,7 @@ class TestCorpus(FixtureAPITestCase): "authorized_users": 1, "top_level_type": None, "maximum_task_ttl": 3600, + "budget_id": None, }) @expectedFailure @@ -434,6 +443,7 @@ class TestCorpus(FixtureAPITestCase): "authorized_users": 2, "top_level_type": None, "maximum_task_ttl": 3600, + "budget_id": None, }) def test_retrieve_maximum_task_ttl(self): diff --git a/arkindex/process/tests/process/test_destroy.py b/arkindex/process/tests/process/test_destroy.py index b87856e50c..ffa37334ae 100644 --- a/arkindex/process/tests/process/test_destroy.py +++ b/arkindex/process/tests/process/test_destroy.py @@ -148,7 +148,7 @@ class TestProcessDestroy(FixtureAPITestCase): """ self.client.force_login(self.user) - with self.assertNumQueries(19): + with self.assertNumQueries(20): response = self.client.delete(reverse("api:process-details", kwargs={"pk": self.process.id})) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -161,7 +161,7 @@ class TestProcessDestroy(FixtureAPITestCase): """ self.client.force_login(self.superuser) - with self.assertNumQueries(19): + with self.assertNumQueries(20): response = self.client.delete(reverse("api:process-details", kwargs={"pk": self.process.id})) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -211,7 +211,7 @@ class TestProcessDestroy(FixtureAPITestCase): state=WorkerActivityState.Processed, ) - with self.assertNumQueries(20): + with self.assertNumQueries(21): response = self.client.delete(reverse("api:process-details", kwargs={"pk": self.process.id})) self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) diff --git a/arkindex/project/settings.py b/arkindex/project/settings.py index b9fbcad8c7..fee1ef5ac2 100644 --- a/arkindex/project/settings.py +++ b/arkindex/project/settings.py @@ -127,6 +127,7 @@ INSTALLED_APPS = [ "arkindex.users", "arkindex.process", "arkindex.training", + "arkindex.budget", ] MIDDLEWARE = [ diff --git a/arkindex/sql_validation/corpus_delete.sql b/arkindex/sql_validation/corpus_delete.sql index bf5f227130..f789e6f8c7 100644 --- a/arkindex/sql_validation/corpus_delete.sql +++ b/arkindex/sql_validation/corpus_delete.sql @@ -6,7 +6,8 @@ SELECT "documents_corpus"."created", "documents_corpus"."top_level_type_id", "documents_corpus"."public", "documents_corpus"."indexable", - "documents_corpus"."maximum_task_ttl" + "documents_corpus"."maximum_task_ttl", + "documents_corpus"."budget_id" FROM "documents_corpus" WHERE "documents_corpus"."id" = '{corpus_id}'::uuid LIMIT 21; diff --git a/arkindex/sql_validation/corpus_delete_top_level_type.sql b/arkindex/sql_validation/corpus_delete_top_level_type.sql index dbbe418e73..d3ccdb0d2d 100644 --- a/arkindex/sql_validation/corpus_delete_top_level_type.sql +++ b/arkindex/sql_validation/corpus_delete_top_level_type.sql @@ -6,7 +6,8 @@ SELECT "documents_corpus"."created", "documents_corpus"."top_level_type_id", "documents_corpus"."public", "documents_corpus"."indexable", - "documents_corpus"."maximum_task_ttl" + "documents_corpus"."maximum_task_ttl", + "documents_corpus"."budget_id" FROM "documents_corpus" WHERE "documents_corpus"."id" = '{corpus_id}'::uuid LIMIT 21; diff --git a/arkindex/sql_validation/corpus_rights_filter.sql b/arkindex/sql_validation/corpus_rights_filter.sql index 6456c7da35..f914e4f909 100644 --- a/arkindex/sql_validation/corpus_rights_filter.sql +++ b/arkindex/sql_validation/corpus_rights_filter.sql @@ -23,6 +23,7 @@ SELECT "documents_corpus"."created", "documents_corpus"."public", "documents_corpus"."indexable", "documents_corpus"."maximum_task_ttl", + "documents_corpus"."budget_id", LEAST("users_right"."level", T5."level") AS "max_level" FROM "documents_corpus" INNER JOIN "users_right" ON ("documents_corpus"."id" = "users_right"."content_id" diff --git a/arkindex/sql_validation/corpus_rights_filter_public.sql b/arkindex/sql_validation/corpus_rights_filter_public.sql index 6042778029..d46840b4d4 100644 --- a/arkindex/sql_validation/corpus_rights_filter_public.sql +++ b/arkindex/sql_validation/corpus_rights_filter_public.sql @@ -24,6 +24,7 @@ LIMIT 21; "documents_corpus"."public", "documents_corpus"."indexable", "documents_corpus"."maximum_task_ttl", + "documents_corpus"."budget_id", LEAST("users_right"."level", T5."level") AS "max_level" FROM "documents_corpus" INNER JOIN "users_right" ON ("documents_corpus"."id" = "users_right"."content_id" @@ -44,6 +45,7 @@ UNION "documents_corpus"."public", "documents_corpus"."indexable", "documents_corpus"."maximum_task_ttl", + "documents_corpus"."budget_id", 10 AS "max_level" FROM "documents_corpus" WHERE "documents_corpus"."public") diff --git a/arkindex/sql_validation/list_elements.sql b/arkindex/sql_validation/list_elements.sql index 98e3284cfa..2cca682e6a 100644 --- a/arkindex/sql_validation/list_elements.sql +++ b/arkindex/sql_validation/list_elements.sql @@ -6,7 +6,8 @@ SELECT "documents_corpus"."created", "documents_corpus"."top_level_type_id", "documents_corpus"."public", "documents_corpus"."indexable", - "documents_corpus"."maximum_task_ttl" + "documents_corpus"."maximum_task_ttl", + "documents_corpus"."budget_id" FROM "documents_corpus" WHERE "documents_corpus"."id" = '{corpus_id}'::uuid LIMIT 21; diff --git a/arkindex/sql_validation/process_elements_filter_ml_class.sql b/arkindex/sql_validation/process_elements_filter_ml_class.sql index fb1e9d4389..bc15a46955 100644 --- a/arkindex/sql_validation/process_elements_filter_ml_class.sql +++ b/arkindex/sql_validation/process_elements_filter_ml_class.sql @@ -52,7 +52,8 @@ SELECT "documents_corpus"."created", "documents_corpus"."top_level_type_id", "documents_corpus"."public", "documents_corpus"."indexable", - "documents_corpus"."maximum_task_ttl" + "documents_corpus"."maximum_task_ttl", + "documents_corpus"."budget_id" FROM "documents_corpus" WHERE "documents_corpus"."id" = '{corpus_id}'::uuid LIMIT 21; diff --git a/arkindex/sql_validation/process_elements_filter_type.sql b/arkindex/sql_validation/process_elements_filter_type.sql index a566bc4a78..931808c1ed 100644 --- a/arkindex/sql_validation/process_elements_filter_type.sql +++ b/arkindex/sql_validation/process_elements_filter_type.sql @@ -52,7 +52,8 @@ SELECT "documents_corpus"."created", "documents_corpus"."top_level_type_id", "documents_corpus"."public", "documents_corpus"."indexable", - "documents_corpus"."maximum_task_ttl" + "documents_corpus"."maximum_task_ttl", + "documents_corpus"."budget_id" FROM "documents_corpus" WHERE "documents_corpus"."id" = '{corpus_id}'::uuid LIMIT 21; diff --git a/arkindex/sql_validation/process_elements_top_level.sql b/arkindex/sql_validation/process_elements_top_level.sql index 77423e4582..d10220c4fe 100644 --- a/arkindex/sql_validation/process_elements_top_level.sql +++ b/arkindex/sql_validation/process_elements_top_level.sql @@ -52,7 +52,8 @@ SELECT "documents_corpus"."created", "documents_corpus"."top_level_type_id", "documents_corpus"."public", "documents_corpus"."indexable", - "documents_corpus"."maximum_task_ttl" + "documents_corpus"."maximum_task_ttl", + "documents_corpus"."budget_id" FROM "documents_corpus" WHERE "documents_corpus"."id" = '{corpus_id}'::uuid LIMIT 21; diff --git a/arkindex/sql_validation/process_elements_with_image.sql b/arkindex/sql_validation/process_elements_with_image.sql index 2b6d311781..c2197f8318 100644 --- a/arkindex/sql_validation/process_elements_with_image.sql +++ b/arkindex/sql_validation/process_elements_with_image.sql @@ -52,7 +52,8 @@ SELECT "documents_corpus"."created", "documents_corpus"."top_level_type_id", "documents_corpus"."public", "documents_corpus"."indexable", - "documents_corpus"."maximum_task_ttl" + "documents_corpus"."maximum_task_ttl", + "documents_corpus"."budget_id" FROM "documents_corpus" WHERE "documents_corpus"."id" = '{corpus_id}'::uuid LIMIT 21; -- GitLab