Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions label_studio/core/argparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ def project_name(raw_name):
default=default_params
)

annotation_fill_updated_by = subparsers.add_parser(
'annotations_fill_updated_by', help='Fill the updated_by field for Annotations', parents=[root_parser]
)

args = parser.parse_args(input_args)

if not hasattr(args, 'label_config'):
Expand Down
3 changes: 3 additions & 0 deletions label_studio/tasks/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ def update(self, request, *args, **kwargs):
# save user history with annotator_id, time & annotation result
annotation_id = self.kwargs['pk']
annotation = get_object_with_check_and_log(request, Annotation, pk=annotation_id)
annotation.updated_by = request.user
annotation.save(update_fields=['updated_by'])

task = annotation.task
if self.request.data.get('ground_truth'):
Expand Down Expand Up @@ -384,6 +386,7 @@ def perform_create(self, ser):
# serialize annotation
extra_args.update({
'prediction': prediction_ser,
'updated_by': user
})

if 'was_cancelled' in self.request.GET:
Expand Down
1 change: 1 addition & 0 deletions label_studio/tasks/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,4 @@ def fill_annotations_project():
start_job_async_or_sync(_fill_annotations_project, project.id)

logger.info('Finished filling project field for Annotation model')

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import importlib
from django.core.management.base import BaseCommand


class Command(BaseCommand):
help = 'Fill updated_by field for Annotations'

def add_arguments(self, parser):
pass

def handle(self, *args, **options):
migration = importlib.import_module('tasks.migrations.0032_annotation_updated_by_fill')
migration.forward(None, None) # Unused position args
21 changes: 21 additions & 0 deletions label_studio/tasks/migrations/0032_annotation_updated_by.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 3.2.16 on 2022-11-18 23:38

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('tasks', '0031_alter_task_options'),
]

operations = [
migrations.AddField(
model_name='annotation',
name='updated_by',
field=models.ForeignKey(help_text='Last user who updated this annotation', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_annotations', to=settings.AUTH_USER_MODEL, verbose_name='updated by'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Generated by Django 3.2.16 on 2022-11-18 23:38

from core.models import AsyncMigrationStatus
from core.redis import start_job_async_or_sync
from django.conf import settings
from django.db import migrations, models
from django.db.models import F

from projects.models import Project
from tasks.models import Annotation


def _fill_annotations_updated_by():
projects = Project.objects.all()
for project in projects.iterator():
migration = AsyncMigrationStatus.objects.filter(project=project, name='0033_annotation_updated_by_fill').first()
if migration and migration.status == AsyncMigrationStatus.STATUS_FINISHED:
# Migration for this project already done
continue

migration = AsyncMigrationStatus.objects.create(
project=project,
name='0033_annotation_updated_by_fill',
status=AsyncMigrationStatus.STATUS_STARTED,
)


Annotation.objects.filter(project=project).update(updated_by=F('completed_by'))
migration.status = AsyncMigrationStatus.STATUS_FINISHED
migration.save()

def forward(apps, _):
annotations = Annotation.objects.all()

if settings.VERSION_EDITION == 'Community':
if annotations.count() > 100000:
command = 'label-studio annotations_fill_updated_by'
logger = logging.getLogger(__name__)
logger.error(
"There are over 100,000 annotations in this label studio instance, please run this "
f"migration manually using {command}"
)
return

start_job_async_or_sync(_fill_annotations_updated_by)

def backward(apps, _):
pass

class Migration(migrations.Migration):

dependencies = [
('tasks', '0032_annotation_updated_by')
]

operations = [
migrations.RunPython(forward, backward)
]
6 changes: 6 additions & 0 deletions label_studio/tasks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,9 @@ class Annotation(AnnotationMixin, models.Model):
help_text='Project ID for this annotation')
completed_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="annotations", on_delete=models.SET_NULL,
null=True, help_text='User ID of the person who created this annotation')
updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='updated_annotations',
on_delete=models.SET_NULL, null=True, verbose_name=_('updated by'),
help_text='Last user who updated this annotation')
was_cancelled = models.BooleanField(_('was cancelled'), default=False, help_text='User skipped the task', db_index=True)
ground_truth = models.BooleanField(_('ground_truth'), default=False, help_text='This annotation is a Ground Truth (ground_truth)', db_index=True)
created_at = models.DateTimeField(_('created at'), auto_now_add=True, help_text='Creation time')
Expand Down Expand Up @@ -436,6 +439,9 @@ def update_task(self):
self.task.save(update_fields=update_fields)

def save(self, *args, **kwargs):
request = get_current_request()
if request:
self.updated_by = request.user
result = super().save(*args, **kwargs)
self.update_task()
return result
Expand Down
5 changes: 5 additions & 0 deletions label_studio/tests/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ def test_create_annotation(caplog, any_client, configured_project_min_annotation
# annotator client
if hasattr(any_client, 'annotator') and any_client.annotator is not None:
assert annotation.completed_by.id == any_client.user.id
assert annotation.updated_by.id == any_client.user.id
# business client
else:
assert annotation.completed_by.id == any_client.business.admin.id
assert annotation.updated_by.id == any_client.business.admin.id

if apps.is_installed('businesses'):
assert annotation.task.accuracy == 1.0
Expand Down Expand Up @@ -93,6 +95,9 @@ def test_create_annotation_with_ground_truth(caplog, any_client, configured_proj
assert m.called
task = Task.objects.get(id=task.id)
assert task.annotations.count() == 2
annotations = Annotation.objects.filter(task=task)
for a in annotations:
assert a.updated_by.id == any_client.user.id


@pytest.mark.django_db
Expand Down