diff --git a/arkindex/documents/api/export.py b/arkindex/documents/api/export.py index 220a8efe98b362fe30e18d6d100c2bb957f03a04..f5145b8badd294f8ccd097f885531378595a3ed2 100644 --- a/arkindex/documents/api/export.py +++ b/arkindex/documents/api/export.py @@ -1,5 +1,8 @@ -from datetime import datetime, timedelta, timezone +from datetime import timedelta +from textwrap import dedent +from django.conf import settings +from django.utils import timezone from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import serializers, status from rest_framework.exceptions import ValidationError @@ -12,9 +15,6 @@ from arkindex.project.mixins import CorpusACLMixin from arkindex.project.permissions import IsVerified from arkindex.users.models import Role -# Delay to generate a new export from a specific user -EXPORT_DELAY_HOURS = 6 - @extend_schema(tags=['exports']) @extend_schema_view( @@ -28,10 +28,15 @@ EXPORT_DELAY_HOURS = 6 post=extend_schema( operation_id='StartExport', request=None, - description=( - 'Start a corpus export job.\n' - f'A user must wait {EXPORT_DELAY_HOURS} hours before being able to generate a new export of the same corpus.\n\n' - 'Contributor access is required.' + description=dedent( + f""" + Start a corpus export job. + + A user must wait for {settings.EXPORT_TTL_SECONDS} seconds after the last successful import + before being able to generate a new export of the same corpus. + + Contributor access is required. + """ ), ) ) @@ -55,10 +60,10 @@ class CorpusExportAPIView(CorpusACLMixin, ListCreateAPIView): available_exports = corpus.exports.filter( state=CorpusExportState.Done, - created__gte=datetime.now(timezone.utc) - timedelta(hours=EXPORT_DELAY_HOURS) + created__gte=timezone.now() - timedelta(seconds=settings.EXPORT_TTL_SECONDS) ) if available_exports.exists(): - raise ValidationError(f'An export has already been made for this corpus in the last {EXPORT_DELAY_HOURS} hours.') + raise ValidationError(f'An export has already been made for this corpus in the last {settings.EXPORT_TTL_SECONDS} seconds.') export = corpus.exports.create(user=self.request.user) export.start() diff --git a/arkindex/documents/tests/test_export.py b/arkindex/documents/tests/test_export.py index 539cea687be34b027e3f29f609b0fd5d1c5865ab..80611182a64189c77fdf2d0ebd33b03678d2fe5e 100644 --- a/arkindex/documents/tests/test_export.py +++ b/arkindex/documents/tests/test_export.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta, timezone from unittest.mock import call, patch +from django.test import override_settings from django.urls import reverse from rest_framework import status @@ -12,6 +13,7 @@ from arkindex.users.models import Role class TestExport(FixtureAPITestCase): @patch('arkindex.project.triggers.export.export_corpus.delay') + @override_settings(EXPORT_TTL_SECONDS=420) def test_start(self, delay_mock): self.client.force_login(self.superuser) response = self.client.post(reverse('api:corpus-export', kwargs={'pk': self.corpus.id})) @@ -81,11 +83,12 @@ class TestExport(FixtureAPITestCase): self.assertEqual(self.corpus.exports.count(), 1) self.assertFalse(delay_mock.called) + @override_settings(EXPORT_TTL_SECONDS=420) @patch('arkindex.project.triggers.export.export_corpus.delay') def test_start_recent_export(self, delay_mock): self.client.force_login(self.superuser) with patch('django.utils.timezone.now') as mock_now: - mock_now.return_value = datetime.now(timezone.utc) - timedelta(hours=2) + mock_now.return_value = datetime.now(timezone.utc) - timedelta(minutes=2) self.corpus.exports.create( user=self.user, state=CorpusExportState.Done, @@ -93,7 +96,7 @@ class TestExport(FixtureAPITestCase): response = self.client.post(reverse('api:corpus-export', kwargs={'pk': self.corpus.id})) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertListEqual(response.json(), ['An export has already been made for this corpus in the last 6 hours.']) + self.assertListEqual(response.json(), ['An export has already been made for this corpus in the last 420 seconds.']) self.assertEqual(self.corpus.exports.count(), 1) self.assertFalse(delay_mock.called) diff --git a/arkindex/project/config.py b/arkindex/project/config.py index b00c0ae3e02eaf21790cefeda2ef3c10d2bfe93e..c98d3211eb1f7e3fab44e16ea4f0e333857af098 100644 --- a/arkindex/project/config.py +++ b/arkindex/project/config.py @@ -114,6 +114,9 @@ def get_settings_parser(base_dir): email_parser.add_option('password', type=str) email_parser.add_option('error_report_recipients', type=str, many=True, default=[]) + export_parser = parser.add_subparser('export', default={}) + export_parser.add_option('ttl', type=int, default=21600) + static_parser = parser.add_subparser('static', default={}) static_parser.add_option('root_path', type=dir_path, default=None) static_parser.add_option('cdn_assets_url', type=str, default=None) diff --git a/arkindex/project/settings.py b/arkindex/project/settings.py index bae672ddc6852576e7582a6b8819d785802103fb..695ee75296c839ad7d525ab7ec5e2b5d3d3e6383 100644 --- a/arkindex/project/settings.py +++ b/arkindex/project/settings.py @@ -373,6 +373,9 @@ RQ = { # How many keys to delete at once inside a sorted set in Redis using a single ZREM command REDIS_ZREM_CHUNK_SIZE = 10000 +# How long before a corpus export can be run again after a successful one +EXPORT_TTL_SECONDS = conf['export']['ttl'] + LOGGING = { 'version': 1, 'filters': { diff --git a/arkindex/project/tests/config_samples/defaults.yaml b/arkindex/project/tests/config_samples/defaults.yaml index 2abdc4ffd7be6bb1e33cae444be4ab4b219e8300..1f2531b69c30f5a78012512c8d7a9fdc0aab4b2c 100644 --- a/arkindex/project/tests/config_samples/defaults.yaml +++ b/arkindex/project/tests/config_samples/defaults.yaml @@ -33,6 +33,8 @@ doorbell: appkey: null id: null email: null +export: + ttl: 21600 features: search: false selection: true diff --git a/arkindex/project/tests/config_samples/errors.yaml b/arkindex/project/tests/config_samples/errors.yaml index e9228fc431fa6e2896fe40bdf741f4f271c7c58e..bee83a7c02abfff1da8520b2667a96cec7334c18 100644 --- a/arkindex/project/tests/config_samples/errors.yaml +++ b/arkindex/project/tests/config_samples/errors.yaml @@ -22,6 +22,8 @@ docker: here: have a dict email: host: 123 +export: + ttl: forever features: sv_cheats: 1 gitlab: diff --git a/arkindex/project/tests/config_samples/expected_errors.yaml b/arkindex/project/tests/config_samples/expected_errors.yaml index 648227fc7fca1e3a9e2e21aec6fab09014e6f0c1..892ad86043f16d5073f6d34d86587e39b4376ed1 100644 --- a/arkindex/project/tests/config_samples/expected_errors.yaml +++ b/arkindex/project/tests/config_samples/expected_errors.yaml @@ -13,6 +13,8 @@ email: password: This option is required port: This option is required user: This option is required +export: + ttl: "invalid literal for int() with base 10: 'forever'" features: sv_cheats: This option does not exist ingest: diff --git a/arkindex/project/tests/config_samples/override.yaml b/arkindex/project/tests/config_samples/override.yaml index 3993e1e1f585b92f9a434f6842dfca86f38e45ef..11d8487bd739d84e03327d24d2ff2ef7a133c245 100644 --- a/arkindex/project/tests/config_samples/override.yaml +++ b/arkindex/project/tests/config_samples/override.yaml @@ -45,6 +45,8 @@ email: password: hunter2 port: 25 user: teklia@wanadoo.fr +export: + ttl: 123456 features: search: true selection: false