diff --git a/arkindex/budget/__init__.py b/arkindex/budget/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/arkindex/budget/apps.py b/arkindex/budget/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..d24215cc34d6bd983d8525543a655aecbb90edff --- /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 0000000000000000000000000000000000000000..67ebfb8b710210ec03d3f261aa2e0957c11d3459 --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/arkindex/budget/models.py b/arkindex/budget/models.py new file mode 100644 index 0000000000000000000000000000000000000000..c9567cb6ada63fe2881287abb46576215abc2d49 --- /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 0000000000000000000000000000000000000000..026c25cfb45183301b48072927efb9514456d19d --- /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 6ea6ee7745c9cba8008808ddf6b669ae2ea007b9..e21ef63b10a23bd1f029ac5c12f1d17919a37567 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 ee9ec29171e743ad7b17eaa618f0ea80d0c54ba1..528cd7b0d238b20c7a059be15a191073cf9001d9 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 805447a33f06bf23f7ac7a47f022b69bac8db688..3055b5e8e67b7fb6787c53db95bb3568a9af3a51 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 b87856e50cc290f43f37fc638862e8f1f1c2c4d8..ffa37334ae10eafb3cc4e5c2229990564a78c87f 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 b9fbcad8c7af6fb75c9cf77979ea5ca34f330e6c..fee1ef5ac2602f1ef00c67da2c85b36efe2bc350 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 bf5f22713085f427c02cea00062664ba4268beab..f789e6f8c7cd5ca1f78a0ef925f011695f106e65 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 dbbe418e7389f4b1d9b79c37984632511f432571..d3ccdb0d2d4ceec1105a22f3a6e0dfbd97be161f 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 6456c7da350966b8be2f82ef0215810931153463..f914e4f909827dbddd3783b6a5e44ac19f9bb342 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 60427780297b6d7fb4ab6a5d61865cdbc63bc03e..d46840b4d4d4ed8862c234a1e7a708f7d139b113 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 98e3284cfa465154415b194f505440a0ae88d3b5..2cca682e6af8bf240a703ea26bccaded0a327b61 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 fb1e9d4389cea1361a85639375aad08f83c50b18..bc15a46955079de959819a7ad85860be0e2e951f 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 a566bc4a782d58d2fe29839372458bf19dfb37d8..931808c1ed99f5d98d030add763a5103a9bbee20 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 77423e4582a0ca0620335a39ec096af91e4ee5b0..d10220c4fe27cfe33da7fe6b9eaf4aaf67543ff2 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 2b6d311781ba790d1e63af7d571895f2c1beba55..c2197f83183cabcbecd14237ad98baf87ed7fbfd 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;