From ff6a4b3e065420eddf240822dc23f9f91467ca47 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 24 Oct 2018 15:32:49 +0300 Subject: [PATCH 1/4] Job status was implemented --- .../migrations/0012_auto_20181024_1504.py | 24 +++++++++++++++++++ cvat/apps/engine/models.py | 3 ++- .../engine/static/engine/js/annotationUI.js | 18 ++++++++++++-- cvat/apps/engine/task.py | 24 +++++++++++++++++-- .../engine/templates/engine/annotation.html | 12 ++++++++-- cvat/apps/engine/urls.py | 3 ++- cvat/apps/engine/views.py | 17 +++++++++++++ 7 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 cvat/apps/engine/migrations/0012_auto_20181024_1504.py diff --git a/cvat/apps/engine/migrations/0012_auto_20181024_1504.py b/cvat/apps/engine/migrations/0012_auto_20181024_1504.py new file mode 100644 index 000000000000..034172d047df --- /dev/null +++ b/cvat/apps/engine/migrations/0012_auto_20181024_1504.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.9 on 2018-10-24 12:04 + +import cvat.apps.engine.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0011_add_task_source_and_safecharfield'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='status', + field=cvat.apps.engine.models.SafeCharField(default='annotation', max_length=32), + ), + migrations.AlterField( + model_name='task', + name='status', + field=cvat.apps.engine.models.SafeCharField(default='annotation', max_length=32), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 3e16f8fa3969..d329e709f62f 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -30,7 +30,7 @@ class Task(models.Model): bug_tracker = models.CharField(max_length=2000, default="") created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now_add=True) - status = models.CharField(max_length=32, default="annotate") + status = SafeCharField(max_length=32, default="annotation") overlap = models.PositiveIntegerField(default=0) z_order = models.BooleanField(default=False) flipped = models.BooleanField(default=False) @@ -81,6 +81,7 @@ class Segment(models.Model): class Job(models.Model): segment = models.ForeignKey(Segment, on_delete=models.CASCADE) annotator = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + status = SafeCharField(max_length=32, default="annotation") # TODO: add sub-issue number for the task class Label(models.Model): diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index 9a80c51cd3ca..8f3454d4bd69 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -512,12 +512,26 @@ function setupMenu(job, shapeCollectionModel, annotationParser, aamModel, player })(); $('#statTaskName').text(job.slug); - $('#statTaskStatus').text(job.status); $('#statFrames').text(`[${job.start}-${job.stop}]`); $('#statOverlap').text(job.overlap); $('#statZOrder').text(job.z_order); $('#statFlipped').text(job.flipped); - + $('#statTaskStatus').prop("value", job.status === "completed" ? "completed" : ( + job.status === "validation" ? "validation" : "annotation" + )).on('change', (e) => { + $.ajax({ + type: 'POST', + url: 'save/job/status', + data: JSON.stringify({ + jid: window.cvat.job.id, + status: e.target.value + }), + contentType: "application/json; charset=utf-8", + error: (data) => { + showMessage(`Can not change job status. Code: ${data.status}. Message: ${data.responeText || data.statusText}`); + } + }); + }); let shortkeys = window.cvat.config.shortkeys; $('#helpButton').on('click', () => { diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index c8e165b9165b..9308e882a2be 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -164,7 +164,7 @@ def get(tid): job_indexes = [segment.job_set.first().id for segment in db_segments] response = { - "status": db_task.status.capitalize(), + "status": db_task.status, "spec": { "labels": { db_label.id:db_label.name for db_label in db_labels }, "attributes": attributes @@ -185,6 +185,26 @@ def get(tid): return response +def save_job_status(jid, status, user): + db_job = models.Job.objects.select_related("segment__task").select_for_update().get(pk = jid) + db_task = db_job.segment.task + if status not in ["annotation", "validation", "completed"]: + raise Exception("Got unknown job status") + slogger.job[jid].info('changing job status from {} to {} by an user {}'.format(db_job.status, status, user)) + db_job.status = status + db_job.save() + db_segments = list(db_task.segment_set.prefetch_related('job_set').select_for_update().all()) + db_jobs = [db_segment.job_set.first() for db_segment in db_segments] + + if len(list(filter(lambda x: x.status == "annotation", db_jobs))) > 0: + db_task.status = "annotation" + elif len(list(filter(lambda x: x.status == "validation", db_jobs))) > 0: + db_task.status = "validation" + else: + db_task.status = "completed" + + db_task.save() + def get_job(jid): """Get the job as dictionary of attributes""" db_job = models.Job.objects.select_related("segment__task").get(id=jid) @@ -205,7 +225,7 @@ def get_job(jid): attributes[db_label.id][db_attrspec.id] = db_attrspec.text response = { - "status": db_task.status.capitalize(), + "status": db_job.status, "labels": { db_label.id:db_label.name for db_label in db_labels }, "stop": db_segment.stop_frame, "taskid": db_task.id, diff --git a/cvat/apps/engine/templates/engine/annotation.html b/cvat/apps/engine/templates/engine/annotation.html index fcdbc4260426..d30c707a81e9 100644 --- a/cvat/apps/engine/templates/engine/annotation.html +++ b/cvat/apps/engine/templates/engine/annotation.html @@ -306,7 +306,9 @@ -
+
+ +
@@ -325,7 +327,13 @@

- +
+ +
diff --git a/cvat/apps/engine/urls.py b/cvat/apps/engine/urls.py index 9f433fdc39df..b12e3017233d 100644 --- a/cvat/apps/engine/urls.py +++ b/cvat/apps/engine/urls.py @@ -22,5 +22,6 @@ path('save/annotation/task/', views.save_annotation_for_task), path('get/annotation/job/', views.get_annotation), path('get/username', views.get_username), - path('save/exception/', views.catch_client_exception) + path('save/exception/', views.catch_client_exception), + path('save/job/status', views.save_job_status), ] diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 61a046f7592d..1141ad4f6771 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -273,6 +273,23 @@ def save_annotation_for_task(request, tid): return HttpResponse() +@login_required +@permission_required(perm=['engine.view_task', 'engine.change_task'], raise_exception=True) +def save_job_status(request): + try: + data = json.loads(request.body.decode('utf-8')) + jid = data['jid'] + status = data['status'] + slogger.job[jid].info("changing job status request") + task.save_job_status(jid, status, request.user.username) + except Exception as e: + if jid: + slogger.job[jid].error("cannot change status", exc_info=True) + else: + slogger.glob.error("cannot change status", exc_info=True) + return HttpResponseBadRequest(str(e)) + return HttpResponse() + @login_required def get_username(request): response = {'username': request.user.username} From a0a181c4c2e0cbc0808a98a052497ad15eb27111 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 24 Oct 2018 16:29:17 +0300 Subject: [PATCH 2/4] Simplified condition --- cvat/apps/engine/static/engine/js/annotationUI.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index 8f3454d4bd69..6183b7710cd4 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -516,9 +516,7 @@ function setupMenu(job, shapeCollectionModel, annotationParser, aamModel, player $('#statOverlap').text(job.overlap); $('#statZOrder').text(job.z_order); $('#statFlipped').text(job.flipped); - $('#statTaskStatus').prop("value", job.status === "completed" ? "completed" : ( - job.status === "validation" ? "validation" : "annotation" - )).on('change', (e) => { + $('#statTaskStatus').prop("value", job.status).on('change', (e) => { $.ajax({ type: 'POST', url: 'save/job/status', From 75519a5253e61d6d97a17d0aad558bd1c4819db2 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 24 Oct 2018 18:26:31 +0300 Subject: [PATCH 3/4] Enum for status choice --- ...024_1504.py => 0012_auto_20181024_1817.py} | 9 ++++----- cvat/apps/engine/models.py | 17 ++++++++++++++--- cvat/apps/engine/task.py | 19 +++++++++++-------- .../engine/templates/engine/annotation.html | 6 +++--- cvat/apps/engine/views.py | 4 +++- 5 files changed, 35 insertions(+), 20 deletions(-) rename cvat/apps/engine/migrations/{0012_auto_20181024_1504.py => 0012_auto_20181024_1817.py} (54%) diff --git a/cvat/apps/engine/migrations/0012_auto_20181024_1504.py b/cvat/apps/engine/migrations/0012_auto_20181024_1817.py similarity index 54% rename from cvat/apps/engine/migrations/0012_auto_20181024_1504.py rename to cvat/apps/engine/migrations/0012_auto_20181024_1817.py index 034172d047df..b4ea6627adc3 100644 --- a/cvat/apps/engine/migrations/0012_auto_20181024_1504.py +++ b/cvat/apps/engine/migrations/0012_auto_20181024_1817.py @@ -1,7 +1,6 @@ -# Generated by Django 2.0.9 on 2018-10-24 12:04 +# Generated by Django 2.0.9 on 2018-10-24 15:17 -import cvat.apps.engine.models -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -14,11 +13,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='job', name='status', - field=cvat.apps.engine.models.SafeCharField(default='annotation', max_length=32), + field=models.CharField(default='annotation', max_length=32), ), migrations.AlterField( model_name='task', name='status', - field=cvat.apps.engine.models.SafeCharField(default='annotation', max_length=32), + field=models.CharField(default='annotation', max_length=32), ), ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index d329e709f62f..f4bc431b0a50 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -8,12 +8,23 @@ from django.contrib.auth.models import User +from io import StringIO +from enum import Enum + import shlex import csv -from io import StringIO import re import os +class StatusChoice(Enum): + ANNOTATION = 'annotation' + VALIDATION = 'validation' + COMPLETED = 'completed' + + @classmethod + def choices(self): + return tuple((x.name, x.value) for x in self) + class SafeCharField(models.CharField): def get_prep_value(self, value): value = super().get_prep_value(value) @@ -30,11 +41,11 @@ class Task(models.Model): bug_tracker = models.CharField(max_length=2000, default="") created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now_add=True) - status = SafeCharField(max_length=32, default="annotation") overlap = models.PositiveIntegerField(default=0) z_order = models.BooleanField(default=False) flipped = models.BooleanField(default=False) source = SafeCharField(max_length=256, default="unknown") + status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION.value) # Extend default permission model class Meta: @@ -81,7 +92,7 @@ class Segment(models.Model): class Job(models.Model): segment = models.ForeignKey(Segment, on_delete=models.CASCADE) annotator = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) - status = SafeCharField(max_length=32, default="annotation") + status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION.value) # TODO: add sub-issue number for the task class Label(models.Model): diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 9308e882a2be..aebcc05c651f 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -18,6 +18,8 @@ _MEDIA_MIMETYPES_FILE = os.path.join(_SCRIPT_DIR, "media.mimetypes") mimetypes.init(files=[_MEDIA_MIMETYPES_FILE]) +from cvat.apps.engine.models import StatusChoice + import django_rq from django.conf import settings from django.db import transaction @@ -188,20 +190,21 @@ def get(tid): def save_job_status(jid, status, user): db_job = models.Job.objects.select_related("segment__task").select_for_update().get(pk = jid) db_task = db_job.segment.task - if status not in ["annotation", "validation", "completed"]: - raise Exception("Got unknown job status") + status = StatusChoice(status) + slogger.job[jid].info('changing job status from {} to {} by an user {}'.format(db_job.status, status, user)) - db_job.status = status + + db_job.status = status.value db_job.save() db_segments = list(db_task.segment_set.prefetch_related('job_set').select_for_update().all()) db_jobs = [db_segment.job_set.first() for db_segment in db_segments] - if len(list(filter(lambda x: x.status == "annotation", db_jobs))) > 0: - db_task.status = "annotation" - elif len(list(filter(lambda x: x.status == "validation", db_jobs))) > 0: - db_task.status = "validation" + if len(list(filter(lambda x: StatusChoice(x.status) == StatusChoice.ANNOTATION, db_jobs))) > 0: + db_task.status = StatusChoice.ANNOTATION.value + elif len(list(filter(lambda x: StatusChoice(x.status) == StatusChoice.VALIDATION, db_jobs))) > 0: + db_task.status = StatusChoice.VALIDATION.value else: - db_task.status = "completed" + db_task.status = StatusChoice.COMPLETED.value db_task.save() diff --git a/cvat/apps/engine/templates/engine/annotation.html b/cvat/apps/engine/templates/engine/annotation.html index d30c707a81e9..a72bd4db4f71 100644 --- a/cvat/apps/engine/templates/engine/annotation.html +++ b/cvat/apps/engine/templates/engine/annotation.html @@ -329,9 +329,9 @@
diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 1141ad4f6771..aed178fcf47c 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -20,6 +20,7 @@ from requests.exceptions import RequestException import logging from .log import slogger, clogger +from cvat.apps.engine.models import StatusChoice ############################# High Level server API @login_required @@ -36,7 +37,8 @@ def dispatch_request(request): """An entry point to dispatch legacy requests""" if request.method == 'GET' and 'id' in request.GET: return render(request, 'engine/annotation.html', { - 'js_3rdparty': JS_3RDPARTY.get('engine', []) + 'js_3rdparty': JS_3RDPARTY.get('engine', []), + 'status_list': [i.value for i in StatusChoice] }) else: return redirect('/dashboard/') From 1eb463f0189d6869d39e20447a44c200a8ae2727 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 24 Oct 2018 18:44:11 +0300 Subject: [PATCH 4/4] Enum imroved --- cvat/apps/engine/models.py | 7 +++++-- cvat/apps/engine/task.py | 6 +++--- cvat/apps/engine/views.py | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index f4bc431b0a50..dafe1da6a5d1 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -25,6 +25,9 @@ class StatusChoice(Enum): def choices(self): return tuple((x.name, x.value) for x in self) + def __str__(self): + return self.value + class SafeCharField(models.CharField): def get_prep_value(self, value): value = super().get_prep_value(value) @@ -45,7 +48,7 @@ class Task(models.Model): z_order = models.BooleanField(default=False) flipped = models.BooleanField(default=False) source = SafeCharField(max_length=256, default="unknown") - status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION.value) + status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION) # Extend default permission model class Meta: @@ -92,7 +95,7 @@ class Segment(models.Model): class Job(models.Model): segment = models.ForeignKey(Segment, on_delete=models.CASCADE) annotator = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) - status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION.value) + status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION) # TODO: add sub-issue number for the task class Label(models.Model): diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index aebcc05c651f..84381ced2851 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -200,11 +200,11 @@ def save_job_status(jid, status, user): db_jobs = [db_segment.job_set.first() for db_segment in db_segments] if len(list(filter(lambda x: StatusChoice(x.status) == StatusChoice.ANNOTATION, db_jobs))) > 0: - db_task.status = StatusChoice.ANNOTATION.value + db_task.status = StatusChoice.ANNOTATION elif len(list(filter(lambda x: StatusChoice(x.status) == StatusChoice.VALIDATION, db_jobs))) > 0: - db_task.status = StatusChoice.VALIDATION.value + db_task.status = StatusChoice.VALIDATION else: - db_task.status = StatusChoice.COMPLETED.value + db_task.status = StatusChoice.COMPLETED db_task.save() diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index aed178fcf47c..0b76de50d98a 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -38,7 +38,7 @@ def dispatch_request(request): if request.method == 'GET' and 'id' in request.GET: return render(request, 'engine/annotation.html', { 'js_3rdparty': JS_3RDPARTY.get('engine', []), - 'status_list': [i.value for i in StatusChoice] + 'status_list': [str(i) for i in StatusChoice] }) else: return redirect('/dashboard/')