diff --git a/cvat/apps/engine/migrations/0012_auto_20181024_1817.py b/cvat/apps/engine/migrations/0012_auto_20181024_1817.py new file mode 100644 index 000000000000..b4ea6627adc3 --- /dev/null +++ b/cvat/apps/engine/migrations/0012_auto_20181024_1817.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.9 on 2018-10-24 15:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0011_add_task_source_and_safecharfield'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='status', + field=models.CharField(default='annotation', max_length=32), + ), + migrations.AlterField( + model_name='task', + name='status', + field=models.CharField(default='annotation', max_length=32), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 3e16f8fa3969..dafe1da6a5d1 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -8,12 +8,26 @@ 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) + + def __str__(self): + return self.value + class SafeCharField(models.CharField): def get_prep_value(self, value): value = super().get_prep_value(value) @@ -30,11 +44,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 = models.CharField(max_length=32, default="annotate") 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) # Extend default permission model class Meta: @@ -81,6 +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) # 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..6183b7710cd4 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -512,12 +512,24 @@ 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).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..84381ced2851 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 @@ -164,7 +166,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 +187,27 @@ 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 + 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.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: StatusChoice(x.status) == StatusChoice.ANNOTATION, db_jobs))) > 0: + db_task.status = StatusChoice.ANNOTATION + elif len(list(filter(lambda x: StatusChoice(x.status) == StatusChoice.VALIDATION, db_jobs))) > 0: + db_task.status = StatusChoice.VALIDATION + else: + db_task.status = StatusChoice.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 +228,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..a72bd4db4f71 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..0b76de50d98a 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': [str(i) for i in StatusChoice] }) else: return redirect('/dashboard/') @@ -273,6 +275,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}