"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
"""
|
import logging
|
from datetime import datetime
|
|
from core.feature_flags import flag_set
|
from core.permissions import AllPermissions
|
from core.redis import start_job_async_or_sync
|
from core.utils.common import load_func
|
from data_manager.actions import DataManagerAction
|
from data_manager.functions import evaluate_predictions
|
from django.conf import settings
|
from projects.models import Project
|
from tasks.functions import update_tasks_counters
|
from tasks.models import Annotation, AnnotationDraft, Prediction, Task
|
from users.models import User
|
from webhooks.models import WebhookAction
|
from webhooks.utils import emit_webhooks_for_instance
|
|
all_permissions = AllPermissions()
|
logger = logging.getLogger(__name__)
|
|
|
def retrieve_tasks_predictions(project, queryset, **kwargs):
|
"""Retrieve predictions by tasks ids
|
|
:param project: project instance
|
:param queryset: filtered tasks db queryset
|
"""
|
evaluate_predictions(queryset)
|
return {'processed_items': queryset.count(), 'detail': 'Retrieved ' + str(queryset.count()) + ' predictions'}
|
|
|
def delete_tasks(project, queryset, **kwargs):
|
"""Delete tasks by ids
|
|
:param project: project instance
|
:param queryset: filtered tasks db queryset
|
"""
|
tasks_ids = list(queryset.values('id'))
|
count = len(tasks_ids)
|
tasks_ids_list = [task['id'] for task in tasks_ids]
|
project_count = project.tasks.count()
|
# unlink tasks from project
|
queryset = Task.objects.filter(id__in=tasks_ids_list)
|
queryset.update(project=None)
|
# delete all project tasks
|
if count == project_count:
|
start_job_async_or_sync(Task.delete_tasks_without_signals_from_task_ids, tasks_ids_list)
|
logger.info(f'calling reset project_id={project.id} delete_tasks()')
|
project.summary.reset()
|
|
# delete only specific tasks
|
else:
|
# update project summary and delete tasks
|
start_job_async_or_sync(async_project_summary_recalculation, tasks_ids_list, project.id)
|
|
project.update_tasks_states(
|
maximum_annotations_changed=False, overlap_cohort_percentage_changed=False, tasks_number_changed=True
|
)
|
# emit webhooks for project
|
emit_webhooks_for_instance(project.organization, project, WebhookAction.TASKS_DELETED, tasks_ids)
|
|
# remove all tabs if there are no tasks in project
|
reload = False
|
if not project.tasks.exists():
|
project.views.all().delete()
|
reload = True
|
|
# Execute actions after delete tasks
|
Task.after_bulk_delete_actions(tasks_ids_list, project)
|
|
return {'processed_items': count, 'reload': reload, 'detail': 'Deleted ' + str(count) + ' tasks'}
|
|
|
def delete_tasks_annotations(project, queryset, **kwargs):
|
"""Delete all annotations and drafts by tasks ids
|
|
:param project: project instance
|
:param queryset: filtered tasks db queryset
|
"""
|
request = kwargs['request']
|
annotator_id = request.data.get('annotator')
|
|
task_ids = queryset.values_list('id', flat=True)
|
annotations = Annotation.objects.filter(task__id__in=task_ids)
|
if annotator_id:
|
annotations = annotations.filter(completed_by=int(annotator_id))
|
|
# take only tasks where annotations are going to be deleted
|
real_task_ids = set(list(annotations.values_list('task__id', flat=True)))
|
annotations_ids = list(annotations.values('id'))
|
# remove deleted annotations from project.summary
|
project.summary.remove_created_annotations_and_labels(annotations)
|
# also remove drafts for the task. This includes task and annotation level
|
# drafts by design.
|
drafts = AnnotationDraft.objects.filter(task__id__in=task_ids)
|
if annotator_id:
|
drafts = drafts.filter(user=int(annotator_id))
|
project.summary.remove_created_drafts_and_labels(drafts)
|
|
# count before delete to return the number of deleted items, not including cascade deletions
|
count = annotations.count()
|
annotations.delete()
|
drafts.delete() # since task-level annotation drafts will not have been deleted by CASCADE
|
emit_webhooks_for_instance(project.organization, project, WebhookAction.ANNOTATIONS_DELETED, annotations_ids)
|
request = kwargs['request']
|
|
tasks = Task.objects.filter(id__in=real_task_ids)
|
tasks.update(updated_at=datetime.now(), updated_by=request.user)
|
# Update tasks counter and is_labeled. It should be a single operation as counters affect bulk is_labeled update
|
project.update_tasks_counters_and_is_labeled(tasks_queryset=real_task_ids)
|
|
# LSE postprocess
|
postprocess = load_func(settings.DELETE_TASKS_ANNOTATIONS_POSTPROCESS)
|
if postprocess is not None:
|
tasks = Task.objects.filter(id__in=task_ids)
|
postprocess(project, tasks, **kwargs)
|
|
return {'processed_items': count, 'detail': 'Deleted ' + str(count) + ' annotations'}
|
|
|
def delete_tasks_annotations_form(user, project):
|
annotator_ids = list(Annotation.objects.filter(project=project).values_list('completed_by', flat=True))
|
draft_annotator_ids = list(AnnotationDraft.objects.filter(task__project=project).values_list('user', flat=True))
|
users = User.objects.filter(id__in=annotator_ids + draft_annotator_ids)
|
return [
|
{
|
'columnCount': 1,
|
'fields': [
|
{
|
'type': 'select',
|
'name': 'annotator',
|
'label': 'Annotator',
|
'options': [
|
{'value': str(user.id), 'label': user.get_full_name() or user.username or user.email}
|
for user in users
|
],
|
'placeholder': 'All',
|
'searchable': True,
|
}
|
],
|
}
|
]
|
|
|
def delete_tasks_predictions(project, queryset, **kwargs):
|
"""Delete all predictions by tasks ids
|
|
:param project: project instance
|
:param queryset: filtered tasks db queryset
|
"""
|
task_ids = queryset.values_list('id', flat=True)
|
predictions = Prediction.objects.filter(task__id__in=task_ids)
|
if flag_set('fflag_root_223_optimize_delete_predictions', organization=project.organization):
|
real_task_ids = predictions.order_by().values_list('task_id', flat=True).distinct()
|
else:
|
real_task_ids = set(list(predictions.values_list('task_id', flat=True)))
|
|
count = predictions.count()
|
predictions.delete()
|
start_job_async_or_sync(update_tasks_counters, Task.objects.filter(id__in=real_task_ids))
|
return {'processed_items': count, 'detail': 'Deleted ' + str(count) + ' predictions'}
|
|
|
def async_project_summary_recalculation(tasks_ids_list, project_id):
|
queryset = Task.objects.filter(id__in=tasks_ids_list)
|
project = Project.objects.get(id=project_id)
|
project.summary.remove_created_annotations_and_labels(Annotation.objects.filter(task__in=queryset))
|
project.summary.remove_data_columns(queryset)
|
Task.delete_tasks_without_signals(queryset)
|
|
|
actions: list[DataManagerAction] = [
|
{
|
'entry_point': retrieve_tasks_predictions,
|
'permission': all_permissions.predictions_any,
|
'title': 'Retrieve Predictions',
|
'order': 90,
|
'dialog': {
|
'title': 'Retrieve Predictions',
|
'text': 'Send the selected tasks to all ML backends connected to the project.'
|
'This operation might be abruptly interrupted due to a timeout. '
|
'The recommended way to get predictions is to update tasks using the Label Studio API.'
|
'Please confirm your action.',
|
'type': 'confirm',
|
},
|
},
|
{
|
'entry_point': delete_tasks,
|
'permission': all_permissions.tasks_delete,
|
'title': 'Delete Tasks',
|
'order': 100,
|
'reload': True,
|
'dialog': {
|
'text': 'You are going to delete the selected tasks. Please confirm your action.',
|
'type': 'confirm',
|
},
|
},
|
{
|
'entry_point': delete_tasks_annotations,
|
'permission': [all_permissions.tasks_change, all_permissions.annotations_delete],
|
'title': 'Delete Annotations',
|
'order': 101,
|
'dialog': {
|
'text': 'You are going to delete annotations from the selected tasks.\n'
|
'You can select specific annotators to delete annotations for.\n'
|
'Please confirm your action.',
|
'type': 'confirm',
|
'form': delete_tasks_annotations_form,
|
},
|
},
|
{
|
'entry_point': delete_tasks_predictions,
|
'permission': all_permissions.predictions_any,
|
'title': 'Delete Predictions',
|
'order': 102,
|
'dialog': {
|
'text': 'You are going to delete all predictions from the selected tasks. Please confirm your action.',
|
'type': 'confirm',
|
},
|
},
|
]
|