Skip to content

Commit

Permalink
Merge branch 'trash-bin' into beta
Browse files Browse the repository at this point in the history
  • Loading branch information
noliveleger committed Apr 6, 2023
2 parents 3de4738 + df05530 commit fd5246d
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 75 deletions.
14 changes: 8 additions & 6 deletions hub/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def remove(self, request, queryset, **kwargs):
return

users = list(queryset.values('pk', 'username'))
self._delete_or_purge(
self._remove_or_delete(
request, users=users, grace_period=config.ACCOUNT_TRASH_GRACE_PERIOD
)

Expand All @@ -169,8 +169,8 @@ def delete(self, request, queryset, **kwargs):
return

users = list(queryset.values('pk', 'username'))
self._delete_or_purge(
request, users=users, grace_period=0, delete_all=True
self._remove_or_delete(
request, users=users, grace_period=0, retain_placeholder=False
)

def deployed_forms_count(self, obj):
Expand Down Expand Up @@ -266,15 +266,17 @@ def monthly_submission_count(self, obj):
)
return instances.get('counter')

def _delete_or_purge(
def _remove_or_delete(
self,
request,
grace_period: int,
users: list[dict],
delete_all: bool = False,
retain_placeholder: bool = True,
):
try:
move_to_trash(request.user, users, grace_period, 'user', delete_all)
move_to_trash(
request.user, users, grace_period, 'user', retain_placeholder
)
except TrashIntegrityError:
self.message_user(
request,
Expand Down
2 changes: 1 addition & 1 deletion kobo/apps/trash_bin/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class AccountTrashAdmin(TrashMixin, admin.ModelAdmin):
list_display = [
'user',
'request_author',
'delete_all',
'retain_placeholder',
'status',
'get_start_time',
'get_failure_error',
Expand Down
4 changes: 2 additions & 2 deletions kobo/apps/trash_bin/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Migration(migrations.Migration):
('date_modified', models.DateTimeField(default=django.utils.timezone.now)),
('metadata', models.JSONField(default=dict)),
('empty_manually', models.BooleanField(default=False)),
('delete_all', models.BooleanField(default=False)),
('retain_placeholder', models.BooleanField(default=True)),
('uid', kpi.fields.kpi_uid.KpiUidField(uid_prefix='pt')),
('asset', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='trash', to='kpi.asset')),
('periodic_task', models.OneToOneField(null=True, on_delete=django.db.models.deletion.RESTRICT, to='django_celery_beat.periodictask')),
Expand All @@ -47,7 +47,7 @@ class Migration(migrations.Migration):
('date_modified', models.DateTimeField(default=django.utils.timezone.now)),
('metadata', models.JSONField(default=dict)),
('empty_manually', models.BooleanField(default=False)),
('delete_all', models.BooleanField(default=False)),
('retain_placeholder', models.BooleanField(default=True)),
('uid', kpi.fields.kpi_uid.KpiUidField(uid_prefix='at')),
('periodic_task', models.OneToOneField(null=True, on_delete=django.db.models.deletion.RESTRICT, to='django_celery_beat.periodictask')),
('request_author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
Expand Down
2 changes: 1 addition & 1 deletion kobo/apps/trash_bin/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ class Meta:
# users' accounts.
# Projects are always deleted entirely and related Celery task ignore this
# field, but it could be implemented at a later time.
delete_all = models.BooleanField(default=False)
retain_placeholder = models.BooleanField(default=True)
8 changes: 4 additions & 4 deletions kobo/apps/trash_bin/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,7 @@ def empty_account(account_trash_id: int):
}
}

if account_trash.delete_all:
audit_log_params['action'] = AuditAction.DELETE
user.delete()
else:
if account_trash.retain_placeholder:
audit_log_params['action'] = AuditAction.REMOVE
placeholder_user = replace_user_with_placeholder(user)
# Retain removal date information
Expand All @@ -118,6 +115,9 @@ def empty_account(account_trash_id: int):
extra_details.save(
update_fields=['date_removal_requested', 'date_removed']
)
else:
audit_log_params['action'] = AuditAction.DELETE
user.delete()

AuditLog.objects.create(**audit_log_params)

Expand Down
13 changes: 9 additions & 4 deletions kobo/apps/trash_bin/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.test import TestCase
from django.utils.timezone import now
from django_celery_beat.models import PeriodicTask
from mock import patch

from kobo.apps.audit_log.models import AuditAction, AuditLog
from kpi.models import Asset
Expand Down Expand Up @@ -47,10 +48,12 @@ def test_delete_user(self):
],
grace_period=grace_period,
trash_type='user',
delete_all=True,
retain_placeholder=False,
)
account_trash = AccountTrash.objects.get(user=someuser)
empty_account.apply([account_trash.pk])
with patch('kobo.apps.trash_bin.tasks.delete_kc_user') as mock_delete_kc_user:
mock_delete_kc_user.return_value = True
empty_account.apply([account_trash.pk])

assert not get_user_model().objects.filter(pk=someuser_id).exists()
assert not Asset.objects.filter(owner_id=someuser_id).exists()
Expand Down Expand Up @@ -167,10 +170,12 @@ def test_remove_user(self):
],
grace_period=grace_period,
trash_type='user',
delete_all=False,
retain_placeholder=True,
)
account_trash = AccountTrash.objects.get(user=someuser)
empty_account.apply([account_trash.pk])
with patch('kobo.apps.trash_bin.tasks.delete_kc_user') as mock_delete_kc_user:
mock_delete_kc_user.return_value = True
empty_account.apply([account_trash.pk])
after = now() + timedelta(days=grace_period)

someuser.refresh_from_db()
Expand Down
38 changes: 20 additions & 18 deletions kobo/apps/trash_bin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@


def delete_asset(request_author: 'auth.User', asset: 'kpi.Asset'):
deployment_backend_uuid = None

asset_id = asset.pk
asset_uid = asset.uid
owner_username = asset.owner.username
Expand Down Expand Up @@ -80,20 +80,22 @@ def move_to_trash(
objects_list: list[dict],
grace_period: int,
trash_type: str,
delete_all: bool = False,
retain_placeholder: bool = True,
):
"""
Create trash objects and their related scheduled celery tasks.
`objects_list` must a list of dictionaries which contain at a 'pk' key and
any other key that would be saved as attributes in AuditLog.metadata.
If `trash_type` is 'asset', dictionaries of `objects_list should contain
`objects_list` must be a list of dictionaries which contain at a 'pk' key
and any other key that would be saved as attributes in AuditLog.metadata.
If `trash_type` is 'asset', dictionaries of `objects_list` should contain
'pk', 'asset_uid' and 'asset_name'. Otherwise, if `trash_type` is 'user',
they should contain 'pk' and 'username'.
Projects and accounts get in trash for `grace_period` and they are hard-deleted
when their related schedule task run.
We keep only username if `delete_all` equals False.
Projects and accounts stay in trash for `grace_period` and then are
hard-deleted when their related scheduled task runs.
If `retain_placeholder` is True, in instance of `auth.User` with the same
username and primary key is retained after deleting all other data.
"""

clocked_time = now() + timedelta(days=grace_period)
Expand All @@ -105,16 +107,16 @@ def move_to_trash(
related_model,
task,
task_name_placeholder
) = _get_settings(trash_type, delete_all)
) = _get_settings(trash_type, retain_placeholder)

if delete_all:
# Delete any previous trash object if it belongs to this "delete all"
# list because "delete all" supersedes deactivated status.
if not retain_placeholder:
# Total deletion, without retaining any placeholder, supersedes
# existing requests to retain placeholders. Delete those requests
obj_ids = [obj_dict['pk'] for obj_dict in objects_list]
allowed_statuses = [TrashStatus.PENDING, TrashStatus.FAILED]
trash_model.objects.filter(
status__in=allowed_statuses,
delete_all=False,
retain_placeholder=True,
**{f'{fk_field_name:}__in': obj_ids}
).delete()

Expand All @@ -128,7 +130,7 @@ def move_to_trash(
request_author=request_author,
metadata=_remove_pk_from_dict(obj_dict),
empty_manually=empty_manually,
delete_all=delete_all,
retain_placeholder=retain_placeholder,
**{fk_field_name: obj_dict['pk']},
)
)
Expand Down Expand Up @@ -346,7 +348,7 @@ def _delete_submissions(request_author: 'auth.User', asset: 'kpi.Asset'):
AuditLog.objects.bulk_create(audit_logs)


def _get_settings(trash_type: str, delete_all: bool = False) -> tuple:
def _get_settings(trash_type: str, retain_placeholder: bool = True) -> tuple:
if trash_type == 'asset':
return (
ProjectTrash,
Expand All @@ -362,9 +364,9 @@ def _get_settings(trash_type: str, delete_all: bool = False) -> tuple:
'user_id',
get_user_model(),
'empty_account',
f'{DELETE_USER_STR_PREFIX} account ({{username}})'
if delete_all
else f'{DELETE_USER_STR_PREFIX} data ({{username}})'
f'{DELETE_USER_STR_PREFIX} data ({{username}})'
if retain_placeholder
else f'{DELETE_USER_STR_PREFIX} account ({{username}})'
)

raise TrashNotImplementedError
Expand Down
19 changes: 18 additions & 1 deletion kpi/serializers/v2/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import json
import re
from distutils import util


from constance import config
from django.conf import settings
Expand All @@ -29,6 +29,7 @@
ASSET_STATUS_PRIVATE,
ASSET_STATUS_PUBLIC,
ASSET_STATUS_SHARED,
ASSET_TYPE_SURVEY,
ASSET_TYPES,
ASSET_TYPE_COLLECTION,
PERM_CHANGE_ASSET,
Expand Down Expand Up @@ -114,6 +115,7 @@ def validate_payload(self, payload: dict) -> dict:
asset_uids = []

self._has_perms(payload, asset_uids)
self._validate_asset_types(payload, asset_uids)

return payload

Expand Down Expand Up @@ -256,6 +258,21 @@ def _validate_action(self, payload: dict):
):
raise exceptions.PermissionDenied()

def _validate_asset_types(self, payload: dict, asset_uids: list[str]):
delete_request, put_back_ = self._get_action_type_and_direction(payload)

if put_back_ or delete_request or not asset_uids:
return

if Asset.objects.filter(
asset_type=ASSET_TYPE_SURVEY,
uid__in=asset_uids,
_deployment_data={},
).exists():
raise serializers.ValidationError(
t('Draft projects cannot be archived')
)

def _validate_confirm(self, payload: dict):

if not payload.get('confirm'):
Expand Down

0 comments on commit fd5246d

Please sign in to comment.