Skip to content

Commit 6fd1326

Browse files
committed
Merge branch 'release/2.024.33' into release/2.024.36
# Conflicts: # kobo/apps/hook/models/service_definition_interface.py # kobo/apps/hook/tests/test_ssrf.py # kobo/apps/trash_bin/utils.py
2 parents 78b1d97 + bcae088 commit 6fd1326

File tree

8 files changed

+123
-79
lines changed

8 files changed

+123
-79
lines changed

jsapp/js/components/map.es6

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,9 +330,10 @@ export class FormMap extends React.Component {
330330
});
331331
}
332332

333-
this.setState({submissions: results});
334-
this.buildMarkers(map);
335-
this.buildHeatMap(map);
333+
this.setState({submissions: results}, () => {
334+
this.buildMarkers(map);
335+
this.buildHeatMap(map);
336+
});
336337
})
337338
.fail((error) => {
338339
if (error.responseText) {

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: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from django_celery_beat.models import (
1010
ClockedSchedule,
1111
PeriodicTask,
12-
PeriodicTasks,
1312
)
1413
from requests.exceptions import HTTPError
1514

@@ -25,7 +24,11 @@
2524
from .models import TrashStatus
2625
from .models.account import AccountTrash
2726
from .models.project import ProjectTrash
28-
from .utils import delete_asset, replace_user_with_placeholder
27+
from .utils import (
28+
delete_asset,
29+
replace_user_with_placeholder,
30+
temporarily_disconnect_signals,
31+
)
2932

3033

3134
@celery_app.task(
@@ -205,8 +208,6 @@ def empty_project(project_trash_id: int):
205208

206209
@task_failure.connect(sender=empty_account)
207210
def empty_account_failure(sender=None, **kwargs):
208-
# Force scheduler to refresh
209-
PeriodicTasks.update_changed()
210211

211212
exception = kwargs['exception']
212213
account_trash_id = kwargs['args'][0]
@@ -234,8 +235,6 @@ def empty_account_retry(sender=None, **kwargs):
234235

235236
@task_failure.connect(sender=empty_project)
236237
def empty_project_failure(sender=None, **kwargs):
237-
# Force scheduler to refresh
238-
PeriodicTasks.update_changed()
239238

240239
exception = kwargs['exception']
241240
project_trash_id = kwargs['args'][0]
@@ -263,27 +262,29 @@ def empty_project_retry(sender=None, **kwargs):
263262

264263
@celery_app.task
265264
def garbage_collector():
266-
with transaction.atomic():
267-
# Remove orphan periodic tasks
268-
PeriodicTask.objects.exclude(
269-
pk__in=AccountTrash.objects.values_list(
270-
'periodic_task_id', flat=True
271-
),
272-
).filter(
273-
name__startswith=DELETE_USER_STR_PREFIX, clocked__isnull=False
274-
).delete()
275-
276-
PeriodicTask.objects.exclude(
277-
pk__in=ProjectTrash.objects.values_list(
278-
'periodic_task_id', flat=True
279-
),
280-
).filter(
281-
name__startswith=DELETE_PROJECT_STR_PREFIX, clocked__isnull=False
282-
).delete()
283-
284-
# Then, remove clocked schedules
285-
ClockedSchedule.objects.exclude(
286-
pk__in=PeriodicTask.objects.filter(
287-
clocked__isnull=False
288-
).values_list('clocked_id', flat=True),
289-
).delete()
265+
266+
with temporarily_disconnect_signals(delete=True):
267+
with transaction.atomic():
268+
# Remove orphan periodic tasks
269+
PeriodicTask.objects.exclude(
270+
pk__in=AccountTrash.objects.values_list(
271+
'periodic_task_id', flat=True
272+
),
273+
).filter(
274+
name__startswith=DELETE_USER_STR_PREFIX, clocked__isnull=False
275+
).delete()
276+
277+
PeriodicTask.objects.exclude(
278+
pk__in=ProjectTrash.objects.values_list(
279+
'periodic_task_id', flat=True
280+
),
281+
).filter(
282+
name__startswith=DELETE_PROJECT_STR_PREFIX, clocked__isnull=False
283+
).delete()
284+
285+
# Then, remove clocked schedules
286+
ClockedSchedule.objects.exclude(
287+
pk__in=PeriodicTask.objects.filter(
288+
clocked__isnull=False
289+
).values_list('clocked_id', flat=True),
290+
).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.models import AuditAction, AuditLog, AuditType
1621
from kpi.exceptions import InvalidXFormException, MissingXFormException
@@ -97,9 +102,6 @@ def move_to_trash(
97102
username and primary key is retained after deleting all other data.
98103
"""
99104

100-
clocked_time = now() + timedelta(days=grace_period)
101-
clocked = ClockedSchedule.objects.create(clocked_time=clocked_time)
102-
103105
(
104106
trash_model,
105107
fk_field_name,
@@ -151,25 +153,27 @@ def move_to_trash(
151153
)
152154
)
153155

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

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

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

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

254250

255251
def replace_user_with_placeholder(
@@ -292,6 +288,35 @@ def replace_user_with_placeholder(
292288
return placeholder_user
293289

294290

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

297322
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)