Skip to content

Commit

Permalink
fix: OPTIC-514: Dashboards page crashes when opened on project with n…
Browse files Browse the repository at this point in the history
…o imported tasks (#5674)

### PR fulfills these requirements
- [X] Commit message(s) and PR title follows the format
`[fix|feat|ci|chore|doc]: TICKET-ID: Short description of change made`
ex. `fix: DEV-XXXX: Removed inconsistent code usage causing intermittent
errors`
- [ ] Tests for the changes have been added/updated (for bug
fixes/features)
- [ ] Docs have been added/updated (for bug fixes/features)
- [X] Best efforts were made to ensure docs/code are concise and
coherent (checked for spelling/grammatical errors, commented out code,
debug logs etc.)
- [X] Self-reviewed and ran all changes on a local instance (for bug
fixes/features)



#### Change has impacts in these area(s)
_(check all that apply)_
- [ ] Product design
- [ ] Backend (Database)
- [X] Backend (API)
- [ ] Frontend



### Describe the reason for change
When no tasks were imported for a project, the absence of the
completed_at annotation used in DataManager filters would cause errors
to be thrown. This properly annotates the None result to ensure there is
always a completed_at annotated field so that the filters do not break.
  • Loading branch information
bmartel committed Apr 4, 2024
1 parent 5f17174 commit 78b362c
Showing 1 changed file with 41 additions and 25 deletions.
66 changes: 41 additions & 25 deletions label_studio/data_manager/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import ujson as json
from core.feature_flags import flag_set
from core.utils.db import fast_first
from data_manager.prepare_params import ConjunctionEnum
from django.conf import settings
from django.contrib.postgres.aggregates import ArrayAgg
Expand Down Expand Up @@ -509,46 +510,61 @@ def __init__(self, expression, distinct=False, output_field=None, **extra):
super().__init__(expression, distinct='DISTINCT ' if distinct else '', output_field=output_field, **extra)


def annotate_completed_at(queryset):
if flag_set('fflag_feat_optic_161_project_settings_for_low_agreement_threshold_score_short', user='auto'):
return annotated_completed_at_considering_agreement_threshold(queryset)

def newest_annotation_subquery() -> Subquery:
from tasks.models import Annotation

newest = Annotation.objects.filter(task=OuterRef('pk')).order_by('-id')[:1]
return queryset.annotate(completed_at=Case(When(is_labeled=True, then=Subquery(newest.values('created_at')))))
newest_annotations = Annotation.objects.filter(task=OuterRef('pk')).order_by('-id')[:1]
return Subquery(newest_annotations.values('created_at'))


def annotated_completed_at_considering_agreement_threshold(queryset):
from tasks.models import Annotation
def base_annotate_completed_at(queryset: TaskQuerySet) -> TaskQuerySet:
return queryset.annotate(completed_at=Case(When(is_labeled=True, then=newest_annotation_subquery())))


def annotate_completed_at(queryset: TaskQuerySet) -> TaskQuerySet:
LseProject = load_func(settings.LSE_PROJECT)
get_tasks_agreement_queryset = load_func(settings.GET_TASKS_AGREEMENT_QUERYSET)

if not queryset.exists():
# No tasks to annotate. Quit early
return queryset
is_lse_project = bool(LseProject)
has_custom_agreement_queryset = bool(get_tasks_agreement_queryset)

project_id = queryset[0].project_id
if (
is_lse_project
and has_custom_agreement_queryset
and flag_set('fflag_feat_optic_161_project_settings_for_low_agreement_threshold_score_short', user='auto')
):
return annotated_completed_at_considering_agreement_threshold(queryset)

newest_annotation = Annotation.objects.filter(task=OuterRef('pk')).order_by('-id')[:1]
return base_annotate_completed_at(queryset)

if not LseProject:
# Not LSE so there will not be agreement_threshold-based task completeness
return queryset.annotate(
completed_at=Case(When(is_labeled=True, then=Subquery(newest_annotation.values('created_at'))))

def annotated_completed_at_considering_agreement_threshold(queryset):
LseProject = load_func(settings.LSE_PROJECT)
get_tasks_agreement_queryset = load_func(settings.GET_TASKS_AGREEMENT_QUERYSET)

is_lse_project = bool(LseProject)
has_custom_agreement_queryset = bool(get_tasks_agreement_queryset)

project_exists = is_lse_project and hasattr(queryset, 'project') and queryset.project is not None

project_id = queryset.project.id if project_exists else None

if project_id is None or not is_lse_project or not has_custom_agreement_queryset:
return base_annotate_completed_at(queryset)

lse_project = fast_first(
LseProject.objects.filter(project_id=project_id).values(
'agreement_threshold', 'max_additional_annotators_assignable'
)
)

lse_project = LseProject.objects.filter(project_id=project_id).first()
agreement_threshold = lse_project.agreement_threshold if lse_project else None
if not (get_tasks_agreement_queryset and agreement_threshold):
agreement_threshold = lse_project['agreement_threshold'] if lse_project else None
if not lse_project or not agreement_threshold:
# This project doesn't use task_agreement so don't consider it when determining completed_at
return queryset.annotate(
completed_at=Case(When(is_labeled=True, then=Subquery(newest_annotation.values('created_at'))))
)
return base_annotate_completed_at(queryset)

queryset = get_tasks_agreement_queryset(queryset)
max_additional_annotators_assignable = lse_project.max_additional_annotators_assignable
max_additional_annotators_assignable = lse_project['max_additional_annotators_assignable']

completed_at_case = Case(
When(
Expand All @@ -558,7 +574,7 @@ def annotated_completed_at_considering_agreement_threshold(queryset):
Q(_agreement__gte=agreement_threshold)
| Q(annotation_count__gte=(F('overlap') + max_additional_annotators_assignable))
),
then=Subquery(newest_annotation.values('created_at')),
then=newest_annotation_subquery(),
),
default=Value(None),
output_field=DateTimeField(),
Expand Down

0 comments on commit 78b362c

Please sign in to comment.