Skip to content

Commit c67f74b

Browse files
author
kobo-bot[bot]
committed
Merge branch 'release/2.025.43'
2 parents 7c49d57 + acbe702 commit c67f74b

File tree

3 files changed

+36
-32
lines changed

3 files changed

+36
-32
lines changed

kobo/apps/trash_bin/tests/test_utils.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
from constance import config
55
from ddt import data, ddt, unpack
6-
from django.db.models.signals import pre_delete
7-
from django.core.management import call_command
86
from django.conf import settings
97
from django.contrib.auth import get_user_model
8+
from django.core.management import call_command
9+
from django.db.models.signals import pre_delete
1010
from django.test import TestCase
1111
from django.utils import timezone
1212
from django_celery_beat.models import PeriodicTask
@@ -16,26 +16,20 @@
1616
AuditAction,
1717
AuditLog,
1818
AuditType,
19-
ProjectHistoryLog
19+
ProjectHistoryLog,
2020
)
2121
from kobo.apps.kobo_auth.shortcuts import User
2222
from kobo.apps.openrosa.apps.logger.models import Attachment, Instance, XForm
2323
from kobo.apps.openrosa.apps.logger.models.attachment import AttachmentDeleteStatus
2424
from kobo.apps.openrosa.apps.logger.signals import pre_delete_attachment
25-
2625
from kpi.models import Asset
2726
from kpi.tests.mixins.create_asset_and_submission_mixin import AssetSubmissionTestMixin
2827
from ..constants import DELETE_PROJECT_STR_PREFIX, DELETE_USER_STR_PREFIX
2928
from ..models import TrashStatus
3029
from ..models.account import AccountTrash
3130
from ..models.attachment import AttachmentTrash
3231
from ..models.project import ProjectTrash
33-
from ..tasks import (
34-
empty_account,
35-
empty_attachment,
36-
empty_project,
37-
task_restarter,
38-
)
32+
from ..tasks import empty_account, empty_attachment, empty_project, task_restarter
3933
from ..utils import move_to_trash, put_back, trash_bin_task_failure
4034

4135

@@ -174,6 +168,7 @@ def test_remove_user(self):
174168
everything from their account is deleted except their username
175169
"""
176170
someuser = get_user_model().objects.get(username='someuser')
171+
uid = someuser.extra_details.uid
177172
admin = get_user_model().objects.get(username='adminuser')
178173
someuser.extra_details.data['name'] = 'someuser'
179174
someuser.extra_details.save(update_fields=['data'])
@@ -207,6 +202,7 @@ def test_remove_user(self):
207202

208203
assert not AccountTrash.objects.filter(user=someuser).exists()
209204
assert before <= someuser.extra_details.date_removed <= after
205+
assert someuser.extra_details.uid == uid
210206

211207
# Ensure action is logged
212208
assert AuditLog.objects.filter(

kobo/apps/trash_bin/utils/account.py

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from django.conf import settings
44
from django.contrib.auth import get_user_model
5-
from django.db import models, transaction
5+
from django.db import transaction
66
from django.db.models.signals import post_delete
77
from django.utils import timezone
88

@@ -86,13 +86,14 @@ def delete_account(account_trash: AccountTrash):
8686

8787
if account_trash.retain_placeholder:
8888
audit_log_params['action'] = AuditAction.REMOVE
89-
placeholder_user = _replace_user_with_placeholder(user)
89+
placeholder_user, uid = _replace_user_with_placeholder(user)
9090
# Retain removal date information
9191
extra_details = placeholder_user.extra_details
9292
extra_details.date_removal_requested = date_removal_requested
9393
extra_details.date_removed = timezone.now()
94+
extra_details.uid = uid
9495
extra_details.save(
95-
update_fields=['date_removal_requested', 'date_removed']
96+
update_fields=['date_removal_requested', 'date_removed', 'uid']
9697
)
9798
else:
9899
audit_log_params['action'] = AuditAction.DELETE
@@ -114,7 +115,7 @@ def delete_account(account_trash: AccountTrash):
114115

115116
def _replace_user_with_placeholder(
116117
user: settings.AUTH_USER_MODEL, retain_audit_logs: bool = True
117-
) -> settings.AUTH_USER_MODEL:
118+
) -> tuple[settings.AUTH_USER_MODEL, str]:
118119
"""
119120
Replace a user with an inactive placeholder, which prevents others from
120121
registering a new account with the same username. The placeholder uses the
@@ -130,26 +131,12 @@ def _replace_user_with_placeholder(
130131
for field in FIELDS_TO_RETAIN:
131132
setattr(placeholder_user, field, getattr(user, field))
132133

133-
if not retain_audit_logs:
134+
uid = user.extra_details.uid
135+
with transaction.atomic():
134136
user.delete()
135137
placeholder_user.save()
136-
return placeholder_user
137138

138-
audit_log_user_field = AuditLog._meta.get_field('user').remote_field
139-
original_audit_log_delete_handler = audit_log_user_field.on_delete
140-
with transaction.atomic():
141-
try:
142-
# prevent the delete() call from touching the audit logs
143-
audit_log_user_field.on_delete = models.DO_NOTHING
144-
# …and cause a FK violation!
145-
user.delete()
146-
# then resolve the violation by creating the placeholder with the
147-
# same PK as the original user
148-
placeholder_user.save()
149-
finally:
150-
audit_log_user_field.on_delete = original_audit_log_delete_handler
151-
152-
return placeholder_user
139+
return placeholder_user, uid
153140

154141

155142
def validate_pre_deletion(account_trash: AccountTrash):

kpi/views/v2/user.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ def get_queryset(self, *args, **kwargs):
8888
self.queryset = (
8989
self.queryset.exclude(pk=settings.ANONYMOUS_USER_ID)
9090
.select_related('extra_details')
91-
.annotate(assets_count=Count('assets', distinct=True))
9291
.order_by('id')
9392
)
9493

@@ -106,6 +105,28 @@ def list(self, request, *args, **kwargs):
106105

107106
return super().list(request, *args, **kwargs)
108107

108+
def paginate_queryset(self, queryset):
109+
if not (page := super().paginate_queryset(queryset)):
110+
return None
111+
112+
user_ids = (user.pk for user in page)
113+
counts_map = {
114+
asset['owner_id']: asset['assets_count']
115+
for asset in (
116+
Asset.objects.filter(owner_id__in=user_ids)
117+
.values('owner_id')
118+
.annotate(assets_count=Count('id'))
119+
)
120+
}
121+
122+
def _page_generator():
123+
# Inject and yield on-the-fly assets count
124+
for user in page:
125+
user.assets_count = counts_map.get(user.pk, 0)
126+
yield user
127+
128+
return _page_generator()
129+
109130
@action(detail=True, methods=['GET'],
110131
url_path=r'migrate(?:/(?P<task_id>[\d\w\-]+))?')
111132
def migrate(self, request, task_id: str = None, **kwargs):

0 commit comments

Comments
 (0)