diff --git a/label_studio/projects/functions/next_task.py b/label_studio/projects/functions/next_task.py index eecb9f693950..fd7a998237c2 100644 --- a/label_studio/projects/functions/next_task.py +++ b/label_studio/projects/functions/next_task.py @@ -4,6 +4,7 @@ from core.feature_flags import flag_set from core.utils.common import conditional_atomic, db_is_not_sqlite, load_func +from core.utils.db import fast_first from django.conf import settings from django.db.models import BooleanField, Case, Count, Exists, F, Max, OuterRef, Q, QuerySet, Value, When from django.db.models.fields import DecimalField @@ -254,27 +255,43 @@ def get_next_task_without_dm_queue( return next_task, use_task_lock, queue_info -def skipped_queue(next_task, prepared_tasks, project, user, queue_info): +def skipped_queue(next_task, prepared_tasks, project, user, assigned_flag, queue_info): if not next_task and project.skip_queue == project.SkipQueue.REQUEUE_FOR_ME: q = Q(project=project, task__isnull=False, was_cancelled=True, task__is_labeled=False) skipped_tasks = user.annotations.filter(q).order_by('updated_at').values_list('task__pk', flat=True) if skipped_tasks.exists(): preserved_order = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(skipped_tasks)]) skipped_tasks = prepared_tasks.filter(pk__in=skipped_tasks).order_by(preserved_order) - next_task = _get_first_unlocked(skipped_tasks, user) + + # for assigned annotators locks don't make sense, moreover, + # _get_first_unlocked breaks label stream for manual mode because + # it evaluates locks based on auto-mode logic and returns None + # when there are no more tasks to label in auto-mode + if assigned_flag: + next_task = fast_first(skipped_tasks) + else: + next_task = _get_first_unlocked(skipped_tasks, user) queue_info = 'Skipped queue' return next_task, queue_info -def postponed_queue(next_task, prepared_tasks, project, user, queue_info): +def postponed_queue(next_task, prepared_tasks, project, user, assigned_flag, queue_info): if not next_task: q = Q(task__project=project, task__isnull=False, was_postponed=True, task__is_labeled=False) postponed_tasks = user.drafts.filter(q).order_by('updated_at').values_list('task__pk', flat=True) if postponed_tasks.exists(): preserved_order = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(postponed_tasks)]) postponed_tasks = prepared_tasks.filter(pk__in=postponed_tasks).order_by(preserved_order) - next_task = _get_first_unlocked(postponed_tasks, user) + + # for assigned annotators locks don't make sense, moreover, + # _get_first_unlocked breaks label stream for manual mode because + # it evaluates locks based on auto-mode logic and returns None + # when there are no more tasks to label in auto-mode + if assigned_flag: + next_task = fast_first(postponed_tasks) + else: + next_task = _get_first_unlocked(postponed_tasks, user) if next_task is not None: next_task.allow_postpone = False queue_info = 'Postponed draft queue' @@ -357,9 +374,9 @@ def get_next_task( not_solved_tasks, user_solved_tasks_array, prepared_tasks, user, project, queue_info ) - next_task, queue_info = postponed_queue(next_task, prepared_tasks, project, user, queue_info) + next_task, queue_info = postponed_queue(next_task, prepared_tasks, project, user, assigned_flag, queue_info) - next_task, queue_info = skipped_queue(next_task, prepared_tasks, project, user, queue_info) + next_task, queue_info = skipped_queue(next_task, prepared_tasks, project, user, assigned_flag, queue_info) if next_task and use_task_lock: # set lock for the task with TTL 3x time more then current average lead time (or 1 hour by default) diff --git a/poetry.lock b/poetry.lock index 5e9f227f24c0..99e679693dd2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2136,7 +2136,7 @@ optional = false python-versions = ">=3.9,<4" groups = ["main"] files = [ - {file = "739f3231a40c0903abb52936ed833c7d4a053595.zip", hash = "sha256:7d278a659312d8b299fa964572e5f1713d68aa38d41d3ba5d1539a1c4e118ccd"}, + {file = "8cd4269e3f84d02cfa5c85a0070bf10048f62a93.zip", hash = "sha256:0e8f20a29f1daef50cc9226bd70fd49c50b9ca448a2b912ce72ed6111a0ca94b"}, ] [package.dependencies] @@ -2164,7 +2164,7 @@ xmljson = "0.2.1" [package.source] type = "url" -url = "https://github.com/HumanSignal/label-studio-sdk/archive/739f3231a40c0903abb52936ed833c7d4a053595.zip" +url = "https://github.com/HumanSignal/label-studio-sdk/archive/8cd4269e3f84d02cfa5c85a0070bf10048f62a93.zip" [[package]] name = "launchdarkly-server-sdk" @@ -5037,4 +5037,4 @@ uwsgi = ["pyuwsgi", "uwsgitop"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4" -content-hash = "2cec409e00c0b54891e5299f4e71a4bbcd6955cb843ab8eb95960ff98c33ce0e" +content-hash = "80cf2624871135aeb8d4336485cb84cf6f0de526ac32c6b2a5f9f0f167d543aa" diff --git a/pyproject.toml b/pyproject.toml index c0a48ed204de..9ddeb9d4890f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ dependencies = [ "djangorestframework-simplejwt[crypto] (>=5.4.0,<6.0.0)", "tldextract (>=5.1.3)", ## HumanSignal repo dependencies :start - "label-studio-sdk @ https://github.com/HumanSignal/label-studio-sdk/archive/739f3231a40c0903abb52936ed833c7d4a053595.zip", + "label-studio-sdk @ https://github.com/HumanSignal/label-studio-sdk/archive/8cd4269e3f84d02cfa5c85a0070bf10048f62a93.zip", ## HumanSignal repo dependencies :end ]