Skip to content

Commit 13f1150

Browse files
committed
Merge branch 'release/2.024.36'
# Conflicts: # kobo/apps/trash_bin/tasks.py
2 parents 0852a2f + 6fd1326 commit 13f1150

File tree

7 files changed

+123
-76
lines changed

7 files changed

+123
-76
lines changed

jsapp/js/components/support/helpBubble.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ class HelpBubble extends React.Component<{}, HelpBubbleState> {
7474
$targetEl.parents('.help-bubble__popup').length === 0 &&
7575
$targetEl.parents('.help-bubble__popup-content').length === 0 &&
7676
$targetEl.parents('.help-bubble__row').length === 0 &&
77-
$targetEl.parents('.help-bubble__row-wrapper').length === 0
77+
$targetEl.parents('.help-bubble__row-wrapper').length === 0 &&
78+
$targetEl.parents('.help-bubble').length === 0
7879
) {
7980
this.close();
8081
}

kobo/apps/hook/models/service_definition_interface.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# coding: utf-8
21
import json
32
import os
43
import re
@@ -9,15 +8,16 @@
98
from ssrf_protect.ssrf_protect import SSRFProtect, SSRFProtectException
109

1110
from kpi.utils.log import logging
11+
from kpi.utils.strings import split_lines_to_list
12+
from .hook import Hook
13+
from .hook_log import HookLog
1214
from ..constants import (
1315
HOOK_LOG_FAILED,
1416
HOOK_LOG_SUCCESS,
1517
KOBO_INTERNAL_ERROR_STATUS_CODE,
1618
RETRIABLE_STATUS_CODES,
1719
)
1820
from ..exceptions import HookRemoteServerDownError
19-
from .hook import Hook
20-
from .hook_log import HookLog
2121

2222

2323
class ServiceDefinitionInterface(metaclass=ABCMeta):
@@ -130,12 +130,16 @@ def send(self) -> bool:
130130
ssrf_protect_options = {}
131131
if constance.config.SSRF_ALLOWED_IP_ADDRESS.strip():
132132
ssrf_protect_options['allowed_ip_addresses'] = (
133-
constance.config.SSRF_ALLOWED_IP_ADDRESS.strip().split('\r\n')
133+
split_lines_to_list(
134+
constance.config.SSRF_ALLOWED_IP_ADDRESS
135+
)
134136
)
135137

136138
if constance.config.SSRF_DENIED_IP_ADDRESS.strip():
137139
ssrf_protect_options['denied_ip_addresses'] = (
138-
constance.config.SSRF_DENIED_IP_ADDRESS.strip().split('\r\n')
140+
split_lines_to_list(
141+
constance.config.SSRF_DENIED_IP_ADDRESS
142+
)
139143
)
140144

141145
SSRFProtect.validate(self._hook.endpoint, options=ssrf_protect_options)

kobo/apps/hook/tests/test_ssrf.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,20 @@ class SSRFHookTestCase(HookTestCase):
2020
@override_config(SSRF_DENIED_IP_ADDRESS='1.2.3.4')
2121
@responses.activate
2222
def test_send_with_ssrf_options(self):
23-
# Create first hook
2423

24+
# Create first hook
2525
hook = self._create_hook()
2626

2727
ServiceDefinition = hook.get_service_definition()
2828
submissions = self.asset.deployment.get_submissions(self.asset.owner)
2929
submission_id = submissions[0]['_id']
3030
service_definition = ServiceDefinition(hook, submission_id)
31-
first_mock_response = {'error': 'not found'}
32-
33-
responses.add(responses.POST, hook.endpoint,
34-
status=status.HTTP_200_OK,
35-
content_type='application/json')
31+
responses.add(
32+
responses.POST,
33+
hook.endpoint,
34+
status=status.HTTP_200_OK,
35+
content_type='application/json',
36+
)
3637

3738
# Try to send data to external endpoint
3839
# Note: it should failed because we explicitly deny 1.2.3.4 and

kobo/apps/trash_bin/tasks.py

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
from django.db import transaction
77
from django.db.models.signals import post_delete
88
from django.utils.timezone import now
9-
from django_celery_beat.models import ClockedSchedule, PeriodicTask, PeriodicTasks
9+
from django_celery_beat.models import (
10+
ClockedSchedule,
11+
PeriodicTask,
12+
)
1013
from requests.exceptions import HTTPError
1114

1215
from kobo.apps.audit_log.audit_actions import AuditAction
@@ -22,7 +25,11 @@
2225
from .models import TrashStatus
2326
from .models.account import AccountTrash
2427
from .models.project import ProjectTrash
25-
from .utils import delete_asset, replace_user_with_placeholder
28+
from .utils import (
29+
delete_asset,
30+
replace_user_with_placeholder,
31+
temporarily_disconnect_signals,
32+
)
2633

2734

2835
@celery_app.task(
@@ -202,8 +209,6 @@ def empty_project(project_trash_id: int):
202209

203210
@task_failure.connect(sender=empty_account)
204211
def empty_account_failure(sender=None, **kwargs):
205-
# Force scheduler to refresh
206-
PeriodicTasks.update_changed()
207212

208213
exception = kwargs['exception']
209214
account_trash_id = kwargs['args'][0]
@@ -231,8 +236,6 @@ def empty_account_retry(sender=None, **kwargs):
231236

232237
@task_failure.connect(sender=empty_project)
233238
def empty_project_failure(sender=None, **kwargs):
234-
# Force scheduler to refresh
235-
PeriodicTasks.update_changed()
236239

237240
exception = kwargs['exception']
238241
project_trash_id = kwargs['args'][0]
@@ -260,27 +263,29 @@ def empty_project_retry(sender=None, **kwargs):
260263

261264
@celery_app.task
262265
def garbage_collector():
263-
with transaction.atomic():
264-
# Remove orphan periodic tasks
265-
PeriodicTask.objects.exclude(
266-
pk__in=AccountTrash.objects.values_list(
267-
'periodic_task_id', flat=True
268-
),
269-
).filter(
270-
name__startswith=DELETE_USER_STR_PREFIX, clocked__isnull=False
271-
).delete()
272-
273-
PeriodicTask.objects.exclude(
274-
pk__in=ProjectTrash.objects.values_list(
275-
'periodic_task_id', flat=True
276-
),
277-
).filter(
278-
name__startswith=DELETE_PROJECT_STR_PREFIX, clocked__isnull=False
279-
).delete()
280-
281-
# Then, remove clocked schedules
282-
ClockedSchedule.objects.exclude(
283-
pk__in=PeriodicTask.objects.filter(
284-
clocked__isnull=False
285-
).values_list('clocked_id', flat=True),
286-
).delete()
266+
267+
with temporarily_disconnect_signals(delete=True):
268+
with transaction.atomic():
269+
# Remove orphan periodic tasks
270+
PeriodicTask.objects.exclude(
271+
pk__in=AccountTrash.objects.values_list(
272+
'periodic_task_id', flat=True
273+
),
274+
).filter(
275+
name__startswith=DELETE_USER_STR_PREFIX, clocked__isnull=False
276+
).delete()
277+
278+
PeriodicTask.objects.exclude(
279+
pk__in=ProjectTrash.objects.values_list(
280+
'periodic_task_id', flat=True
281+
),
282+
).filter(
283+
name__startswith=DELETE_PROJECT_STR_PREFIX, clocked__isnull=False
284+
).delete()
285+
286+
# Then, remove clocked schedules
287+
ClockedSchedule.objects.exclude(
288+
pk__in=PeriodicTask.objects.filter(
289+
clocked__isnull=False
290+
).values_list('clocked_id', flat=True),
291+
).delete()

kobo/apps/trash_bin/utils.py

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
from __future__ import annotations
22

33
import json
4+
from contextlib import contextmanager
45
from copy import deepcopy
56
from datetime import timedelta
67

78
from django.conf import settings
89
from django.contrib.auth import get_user_model
910
from django.db import IntegrityError, models, transaction
1011
from django.db.models import F, Q
11-
from django.db.models.signals import pre_delete
12-
from django.utils.timezone import now
13-
from django_celery_beat.models import ClockedSchedule, PeriodicTask, PeriodicTasks
12+
from django.db.models.signals import post_delete, post_save, pre_delete, pre_save
13+
from django.utils import timezone
14+
from django_celery_beat.models import (
15+
ClockedSchedule,
16+
PeriodicTask,
17+
PeriodicTasks,
18+
)
1419

1520
from kobo.apps.audit_log.audit_actions import AuditAction
1621
from kobo.apps.audit_log.models import AuditLog, AuditType
@@ -98,9 +103,6 @@ def move_to_trash(
98103
username and primary key is retained after deleting all other data.
99104
"""
100105

101-
clocked_time = now() + timedelta(days=grace_period)
102-
clocked = ClockedSchedule.objects.create(clocked_time=clocked_time)
103-
104106
(
105107
trash_model,
106108
fk_field_name,
@@ -152,25 +154,27 @@ def move_to_trash(
152154
)
153155
)
154156

155-
try:
157+
with temporarily_disconnect_signals(save=True):
158+
clocked_time = timezone.now() + timedelta(days=grace_period)
159+
clocked = ClockedSchedule.objects.create(clocked_time=clocked_time)
156160
trash_model.objects.bulk_create(trash_objects)
161+
try:
162+
periodic_tasks = PeriodicTask.objects.bulk_create(
163+
[
164+
PeriodicTask(
165+
clocked=clocked,
166+
name=task_name_placeholder.format(**ato.metadata),
167+
task=f'kobo.apps.trash_bin.tasks.{task}',
168+
args=json.dumps([ato.id]),
169+
one_off=True,
170+
enabled=not empty_manually,
171+
)
172+
for ato in trash_objects
173+
],
174+
)
157175

158-
periodic_tasks = PeriodicTask.objects.bulk_create(
159-
[
160-
PeriodicTask(
161-
clocked=clocked,
162-
name=task_name_placeholder.format(**ato.metadata),
163-
task=f'kobo.apps.trash_bin.tasks.{task}',
164-
args=json.dumps([ato.id]),
165-
one_off=True,
166-
enabled=not empty_manually,
167-
)
168-
for ato in trash_objects
169-
],
170-
)
171-
172-
except IntegrityError as e:
173-
raise TrashIntegrityError
176+
except IntegrityError:
177+
raise TrashIntegrityError
174178

175179
# Update relationships between periodic task and trash objects
176180
updated_trash_objects = []
@@ -240,17 +244,9 @@ def put_back(
240244
for obj_dict in objects_list
241245
]
242246
)
243-
try:
244-
# Disconnect `PeriodicTasks` (plural) signal, until `PeriodicTask` (singular)
245-
# delete query finishes to avoid unnecessary DB queries.
246-
# see https://django-celery-beat.readthedocs.io/en/stable/reference/django-celery-beat.models.html#django_celery_beat.models.PeriodicTasks
247-
pre_delete.disconnect(PeriodicTasks.changed, sender=PeriodicTask)
248-
PeriodicTask.objects.only('pk').filter(pk__in=periodic_task_ids).delete()
249-
finally:
250-
pre_delete.connect(PeriodicTasks.changed, sender=PeriodicTask)
251247

252-
# Force celery beat scheduler to refresh
253-
PeriodicTasks.update_changed()
248+
with temporarily_disconnect_signals(delete=True):
249+
PeriodicTask.objects.only('pk').filter(pk__in=periodic_task_ids).delete()
254250

255251

256252
def replace_user_with_placeholder(
@@ -293,6 +289,35 @@ def replace_user_with_placeholder(
293289
return placeholder_user
294290

295291

292+
@contextmanager
293+
def temporarily_disconnect_signals(save=False, delete=False):
294+
"""
295+
Temporarily disconnects `PeriodicTasks` signals to prevent accumulating
296+
update queries for Celery Beat while bulk operations are in progress.
297+
298+
See https://django-celery-beat.readthedocs.io/en/stable/reference/django-celery-beat.models.html#django_celery_beat.models.PeriodicTasks
299+
"""
300+
301+
try:
302+
if delete:
303+
pre_delete.disconnect(PeriodicTasks.changed, sender=PeriodicTask)
304+
post_delete.disconnect(PeriodicTasks.update_changed, sender=ClockedSchedule)
305+
if save:
306+
pre_save.disconnect(PeriodicTasks.changed, sender=PeriodicTask)
307+
post_save.disconnect(PeriodicTasks.update_changed, sender=ClockedSchedule)
308+
yield
309+
finally:
310+
if delete:
311+
post_delete.connect(PeriodicTasks.update_changed, sender=ClockedSchedule)
312+
pre_delete.connect(PeriodicTasks.changed, sender=PeriodicTask)
313+
if save:
314+
pre_save.connect(PeriodicTasks.changed, sender=PeriodicTask)
315+
post_save.connect(PeriodicTasks.update_changed, sender=ClockedSchedule)
316+
317+
# Force celery beat scheduler to refresh
318+
PeriodicTasks.update_changed()
319+
320+
296321
def _delete_submissions(request_author: settings.AUTH_USER_MODEL, asset: 'kpi.Asset'):
297322

298323
while True:

kpi/tests/test_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from kpi.utils.pyxform_compatibility import allow_choice_duplicates
2121
from kpi.utils.query_parser import parse
2222
from kpi.utils.sluggify import sluggify, sluggify_label
23+
from kpi.utils.strings import split_lines_to_list
2324
from kpi.utils.xml import (
2425
edit_submission_xml,
2526
fromstring_preserve_root_xmlns,
@@ -437,6 +438,12 @@ def test_allow_choice_duplicates(self):
437438
== 'no'
438439
)
439440

441+
def test_split_lines_to_list(self):
442+
443+
value = '\r\nfoo\r\nbar\n\n'
444+
expected = ['foo', 'bar']
445+
assert split_lines_to_list(value) == expected
446+
440447

441448
class XmlUtilsTestCase(TestCase):
442449

kpi/utils/strings.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# coding: utf-8
21
import base64
32

43

@@ -10,3 +9,8 @@ def to_str(obj):
109
if isinstance(obj, bytes):
1110
return obj.decode()
1211
return obj
12+
13+
14+
def split_lines_to_list(value: str) -> list:
15+
values = value.strip().split('\n')
16+
return [ip.strip() for ip in values if ip.strip()]

0 commit comments

Comments
 (0)