REST API /api/jobs/<id>/commits (#4368)

main
Nikita Manovich 4 years ago committed by GitHub
parent df8590e747
commit e8f294f673
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -35,10 +35,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Basic page with jobs list, basic filtration to this list (<https://github.com/openvinotoolkit/cvat/pull/4258>) - Basic page with jobs list, basic filtration to this list (<https://github.com/openvinotoolkit/cvat/pull/4258>)
- Added OpenCV.js TrackerMIL as tracking tool (<https://github.com/openvinotoolkit/cvat/pull/4200>) - Added OpenCV.js TrackerMIL as tracking tool (<https://github.com/openvinotoolkit/cvat/pull/4200>)
- Ability to continue working from the latest frame where an annotator was before (<https://github.com/openvinotoolkit/cvat/pull/4297>) - Ability to continue working from the latest frame where an annotator was before (<https://github.com/openvinotoolkit/cvat/pull/4297>)
- `GET /api/jobs/<id>/commits` was implemented (<https://github.com/openvinotoolkit/cvat/pull/4368>)
- Advanced filtration and sorting for a list of jobs (<https://github.com/openvinotoolkit/cvat/pull/4319>) - Advanced filtration and sorting for a list of jobs (<https://github.com/openvinotoolkit/cvat/pull/4319>)
### Changed ### Changed
- Users don't have access to a task object anymore if they are assigneed only on some jobs of the task (<https://github.com/openvinotoolkit/cvat/pull/3788>) - Users don't have access to a task object anymore if they are assigneed only on some jobs of the task (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- Different resources (tasks, projects) are not visible anymore for all CVAT instance users by default (<https://github.com/openvinotoolkit/cvat/pull/3788>) - Different resources (tasks, projects) are not visible anymore for all CVAT instance users by default (<https://github.com/openvinotoolkit/cvat/pull/3788>)

@ -269,19 +269,6 @@ class JobAnnotation:
self.ir_data.tags = tags self.ir_data.tags = tags
def _commit(self):
db_prev_commit = self.db_job.commits.last()
db_curr_commit = models.JobCommit()
if db_prev_commit:
db_curr_commit.version = db_prev_commit.version + 1
else:
db_curr_commit.version = 1
db_curr_commit.job = self.db_job
db_curr_commit.message = "Changes: tags - {}; shapes - {}; tracks - {}".format(
len(self.ir_data.tags), len(self.ir_data.shapes), len(self.ir_data.tracks))
db_curr_commit.save()
self.ir_data.version = db_curr_commit.version
def _set_updated_date(self): def _set_updated_date(self):
db_task = self.db_job.segment.task db_task = self.db_job.segment.task
db_task.updated_date = timezone.now() db_task.updated_date = timezone.now()
@ -302,17 +289,14 @@ class JobAnnotation:
def create(self, data): def create(self, data):
self._create(data) self._create(data)
self._commit()
def put(self, data): def put(self, data):
self._delete() self._delete()
self._create(data) self._create(data)
self._commit()
def update(self, data): def update(self, data):
self._delete(data) self._delete(data)
self._create(data) self._create(data)
self._commit()
def _delete(self, data=None): def _delete(self, data=None):
deleted_shapes = 0 deleted_shapes = 0
@ -347,7 +331,6 @@ class JobAnnotation:
def delete(self, data=None): def delete(self, data=None):
self._delete(data) self._delete(data)
self._commit()
@staticmethod @staticmethod
def _extend_attributes(attributeval_set, default_attribute_values): def _extend_attributes(attributeval_set, default_attribute_values):
@ -513,8 +496,7 @@ class JobAnnotation:
self.ir_data.tracks = serializer.data self.ir_data.tracks = serializer.data
def _init_version_from_db(self): def _init_version_from_db(self):
db_commit = self.db_job.commits.last() self.ir_data.version = 0 # FIXME: should be removed in the future
self.ir_data.version = db_commit.version if db_commit else 0
def init_from_db(self): def init_from_db(self):
self._init_tags_from_db() self._init_tags_from_db()

@ -0,0 +1,32 @@
# Generated by Django 3.2.12 on 2022-02-20 18:24
import cvat.apps.engine.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('engine', '0050_auto_20220211_1425'),
]
operations = [
migrations.RemoveField(
model_name='jobcommit',
name='message',
),
migrations.RemoveField(
model_name='jobcommit',
name='version',
),
migrations.AddField(
model_name='jobcommit',
name='data',
field=models.JSONField(default=dict, encoder=cvat.apps.engine.models.Commit.JSONEncoder),
),
migrations.AddField(
model_name='jobcommit',
name='scope',
field=models.CharField(default='', max_length=32),
),
]

@ -12,6 +12,7 @@ from django.contrib.auth.models import User
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.db import models from django.db import models
from django.db.models.fields import FloatField from django.db.models.fields import FloatField
from django.core.serializers.json import DjangoJSONEncoder
from cvat.apps.engine.utils import parse_specific_attributes from cvat.apps.engine.utils import parse_specific_attributes
from cvat.apps.organizations.models import Organization from cvat.apps.organizations.models import Organization
@ -395,6 +396,15 @@ class Job(models.Model):
project = task.project project = task.project
return project.label_set if project else task.label_set return project.label_set if project else task.label_set
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
db_commit = JobCommit(job=self, scope='create',
owner=self.segment.task.owner, data={
'stage': self.stage, 'state': self.state, 'assignee': self.assignee
})
db_commit.save()
class Meta: class Meta:
default_permissions = () default_permissions = ()
@ -491,11 +501,20 @@ class Annotation(models.Model):
default_permissions = () default_permissions = ()
class Commit(models.Model): class Commit(models.Model):
class JSONEncoder(DjangoJSONEncoder):
def default(self, o):
if isinstance(o, User):
data = {'user': {'id': o.id, 'username': o.username}}
return data
else:
return super().default(o)
id = models.BigAutoField(primary_key=True) id = models.BigAutoField(primary_key=True)
scope = models.CharField(max_length=32, default="")
owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
version = models.PositiveIntegerField(default=0)
timestamp = models.DateTimeField(auto_now=True) timestamp = models.DateTimeField(auto_now=True)
message = models.CharField(max_length=4096, default="") data = models.JSONField(default=dict, encoder=JSONEncoder)
class Meta: class Meta:
abstract = True abstract = True

@ -138,7 +138,8 @@ class LabelSerializer(serializers.ModelSerializer):
class JobCommitSerializer(serializers.ModelSerializer): class JobCommitSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.JobCommit model = models.JobCommit
fields = ('id', 'version', 'owner', 'message', 'timestamp') fields = ('id', 'owner', 'data', 'timestamp', 'scope')
class JobReadSerializer(serializers.ModelSerializer): class JobReadSerializer(serializers.ModelSerializer):
task_id = serializers.ReadOnlyField(source="segment.task.id") task_id = serializers.ReadOnlyField(source="segment.task.id")
@ -167,6 +168,14 @@ class JobWriteSerializer(serializers.ModelSerializer):
serializer = JobReadSerializer(instance, context=self.context) serializer = JobReadSerializer(instance, context=self.context)
return serializer.data return serializer.data
def create(self, validated_data):
instance = super().create(validated_data)
db_commit = models.JobCommit(job=instance, scope='create',
owner=self.context['request'].user, data=validated_data)
db_commit.save()
return instance
def update(self, instance, validated_data): def update(self, instance, validated_data):
state = validated_data.get('state') state = validated_data.get('state')
stage = validated_data.get('stage') stage = validated_data.get('stage')
@ -186,7 +195,13 @@ class JobWriteSerializer(serializers.ModelSerializer):
if assignee is not None: if assignee is not None:
validated_data['assignee'] = User.objects.get(id=assignee) validated_data['assignee'] = User.objects.get(id=assignee)
return super().update(instance, validated_data) instance = super().update(instance, validated_data)
db_commit = models.JobCommit(job=instance, scope='update',
owner=self.context['request'].user, data=validated_data)
db_commit.save()
return instance
class Meta: class Meta:
model = models.Job model = models.Job

@ -106,10 +106,10 @@ def _save_task_to_db(db_task):
db_segment.stop_frame = stop_frame db_segment.stop_frame = stop_frame
db_segment.save() db_segment.save()
db_job = models.Job() db_job = models.Job(segment=db_segment)
db_job.segment = db_segment
db_job.save() db_job.save()
db_task.data.save() db_task.data.save()
db_task.save() db_task.save()

@ -4118,7 +4118,7 @@ class JobAnnotationAPITestCase(APITestCase):
def _check_response(self, response, data): def _check_response(self, response, data):
if not response.status_code in [ if not response.status_code in [
status.HTTP_403_FORBIDDEN, status.HTTP_401_UNAUTHORIZED]: status.HTTP_403_FORBIDDEN, status.HTTP_401_UNAUTHORIZED]:
compare_objects(self, data, response.data, ignore_keys=["id"]) compare_objects(self, data, response.data, ignore_keys=["id", "version"])
def _run_api_v2_jobs_id_annotations(self, owner, assignee, annotator): def _run_api_v2_jobs_id_annotations(self, owner, assignee, annotator):
task, jobs = self._create_task(owner, assignee) task, jobs = self._create_task(owner, assignee)
@ -4603,7 +4603,7 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
if not response.status_code in [ if not response.status_code in [
status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]: status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]:
try: try:
compare_objects(self, data, response.data, ignore_keys=["id"]) compare_objects(self, data, response.data, ignore_keys=["id", "version"])
except AssertionError as e: except AssertionError as e:
print("Objects are not equal: ", data, response.data) print("Objects are not equal: ", data, response.data)
print(e) print(e)

@ -59,7 +59,7 @@ from cvat.apps.engine.serializers import (
LogEventSerializer, ProjectSerializer, ProjectSearchSerializer, LogEventSerializer, ProjectSerializer, ProjectSearchSerializer,
RqStatusSerializer, TaskSerializer, UserSerializer, PluginsSerializer, IssueReadSerializer, RqStatusSerializer, TaskSerializer, UserSerializer, PluginsSerializer, IssueReadSerializer,
IssueWriteSerializer, CommentReadSerializer, CommentWriteSerializer, CloudStorageWriteSerializer, IssueWriteSerializer, CommentReadSerializer, CommentWriteSerializer, CloudStorageWriteSerializer,
CloudStorageReadSerializer, DatasetFileSerializer) CloudStorageReadSerializer, DatasetFileSerializer, JobCommitSerializer)
from utils.dataset_manifest import ImageManifestManager from utils.dataset_manifest import ImageManifestManager
from cvat.apps.engine.utils import av_scan_paths from cvat.apps.engine.utils import av_scan_paths
@ -1065,6 +1065,20 @@ class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
return data_getter(request, db_job.segment.start_frame, return data_getter(request, db_job.segment.start_frame,
db_job.segment.stop_frame, db_job.segment.task.data) db_job.segment.stop_frame, db_job.segment.task.data)
@extend_schema(summary='The action returns the list of tracked '
'changes for the job', responses={
'200': JobCommitSerializer(many=True),
}, tags=['jobs'], versions=['2.0'])
@action(detail=True, methods=['GET'], serializer_class=JobCommitSerializer)
def commits(self, request, pk):
db_job = self.get_object()
queryset = db_job.commits
serializer = JobCommitSerializer(queryset,
context={'request': request}, many=True)
return Response(serializer.data)
@extend_schema_view(retrieve=extend_schema( @extend_schema_view(retrieve=extend_schema(
summary='Method returns details of an issue', summary='Method returns details of an issue',
responses={ responses={

@ -1226,12 +1226,11 @@ ALTER SEQUENCE public.engine_job_id_seq OWNED BY public.engine_job.id;
CREATE TABLE public.engine_jobcommit ( CREATE TABLE public.engine_jobcommit (
id bigint NOT NULL, id bigint NOT NULL,
version integer NOT NULL,
"timestamp" timestamp with time zone NOT NULL, "timestamp" timestamp with time zone NOT NULL,
message character varying(4096) NOT NULL,
owner_id integer, owner_id integer,
job_id integer NOT NULL, job_id integer NOT NULL,
CONSTRAINT engine_jobcommit_version_check CHECK ((version >= 0)) data jsonb NOT NULL,
scope character varying(32) NOT NULL
); );
@ -2912,6 +2911,7 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin;
88 dataset_repo 0002_auto_20190123_1305 2021-12-14 17:51:27.588845+00 88 dataset_repo 0002_auto_20190123_1305 2021-12-14 17:51:27.588845+00
89 engine 0049_auto_20220202_0710 2022-02-11 14:54:41.053611+00 89 engine 0049_auto_20220202_0710 2022-02-11 14:54:41.053611+00
90 engine 0050_auto_20220211_1425 2022-02-11 14:54:41.126041+00 90 engine 0050_auto_20220211_1425 2022-02-11 14:54:41.126041+00
91 engine 0051_auto_20220220_1824 2022-02-24 09:22:16.717995+00
\. \.
@ -3781,34 +3781,34 @@ COPY public.engine_job (id, segment_id, assignee_id, status, stage, state) FROM
-- Data for Name: engine_jobcommit; Type: TABLE DATA; Schema: public; Owner: root -- Data for Name: engine_jobcommit; Type: TABLE DATA; Schema: public; Owner: root
-- --
COPY public.engine_jobcommit (id, version, "timestamp", message, owner_id, job_id) FROM stdin; COPY public.engine_jobcommit (id, "timestamp", owner_id, job_id, data, scope) FROM stdin;
1 1 2021-12-22 07:14:15.237479+00 Changes: tags - 0; shapes - 5; tracks - 0 \N 2 1 2021-12-22 07:14:15.237479+00 \N 2 {}
2 2 2021-12-22 07:14:15.268804+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 2 2 2021-12-22 07:14:15.268804+00 \N 2 {}
3 3 2021-12-22 07:14:15.298016+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 2 3 2021-12-22 07:14:15.298016+00 \N 2 {}
4 1 2021-12-22 07:15:22.945367+00 Changes: tags - 0; shapes - 9; tracks - 0 \N 1 4 2021-12-22 07:15:22.945367+00 \N 1 {}
5 2 2021-12-22 07:15:22.985309+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 1 5 2021-12-22 07:15:22.985309+00 \N 1 {}
6 3 2021-12-22 07:15:23.019102+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 1 6 2021-12-22 07:15:23.019102+00 \N 1 {}
7 1 2021-12-22 07:17:34.839155+00 Changes: tags - 0; shapes - 7; tracks - 0 \N 6 7 2021-12-22 07:17:34.839155+00 \N 6 {}
8 2 2021-12-22 07:17:34.878804+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 6 8 2021-12-22 07:17:34.878804+00 \N 6 {}
9 3 2021-12-22 07:17:34.909805+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 6 9 2021-12-22 07:17:34.909805+00 \N 6 {}
10 1 2021-12-22 07:19:33.859315+00 Changes: tags - 0; shapes - 5; tracks - 0 \N 4 10 2021-12-22 07:19:33.859315+00 \N 4 {}
11 2 2021-12-22 07:19:33.907033+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 4 11 2021-12-22 07:19:33.907033+00 \N 4 {}
12 3 2021-12-22 07:19:33.934873+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 4 12 2021-12-22 07:19:33.934873+00 \N 4 {}
13 4 2021-12-22 07:22:30.331021+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 4 13 2021-12-22 07:22:30.331021+00 \N 4 {}
14 5 2021-12-22 07:22:30.362857+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 4 14 2021-12-22 07:22:30.362857+00 \N 4 {}
15 6 2021-12-22 07:22:30.388715+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 4 15 2021-12-22 07:22:30.388715+00 \N 4 {}
16 1 2022-02-21 10:32:04.068136+00 Changes: tags - 0; shapes - 1; tracks - 0 \N 9 16 2022-02-21 10:32:04.068136+00 \N 9 {}
17 2 2022-02-21 10:32:04.169838+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 9 17 2022-02-21 10:32:04.169838+00 \N 9 {}
18 3 2022-02-21 10:32:04.256121+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 9 18 2022-02-21 10:32:04.256121+00 \N 9 {}
19 1 2022-02-21 10:37:22.961448+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 3 19 2022-02-21 10:37:22.961448+00 \N 3 {}
20 2 2022-02-21 10:37:23.075321+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 3 20 2022-02-21 10:37:23.075321+00 \N 3 {}
21 3 2022-02-21 10:37:23.187161+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 3 21 2022-02-21 10:37:23.187161+00 \N 3 {}
22 4 2022-02-21 10:37:27.7082+00 Changes: tags - 0; shapes - 1; tracks - 0 \N 3 22 2022-02-21 10:37:27.7082+00 \N 3 {}
23 5 2022-02-21 10:37:27.834371+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 3 23 2022-02-21 10:37:27.834371+00 \N 3 {}
24 6 2022-02-21 10:37:27.95231+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 3 24 2022-02-21 10:37:27.95231+00 \N 3 {}
25 1 2022-02-21 10:40:21.267763+00 Changes: tags - 0; shapes - 1; tracks - 0 \N 7 25 2022-02-21 10:40:21.267763+00 \N 7 {}
26 2 2022-02-21 10:40:21.354689+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 7 26 2022-02-21 10:40:21.354689+00 \N 7 {}
27 3 2022-02-21 10:40:21.435822+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 7 27 2022-02-21 10:40:21.435822+00 \N 7 {}
\. \.
@ -4196,7 +4196,7 @@ SELECT pg_catalog.setval('public.django_content_type_id_seq', 48, true);
-- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: root -- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: root
-- --
SELECT pg_catalog.setval('public.django_migrations_id_seq', 90, true); SELECT pg_catalog.setval('public.django_migrations_id_seq', 91, true);
-- --

@ -17,7 +17,8 @@ def test_check_objects_integrity(path):
objects = json.load(f) objects = json.load(f)
for jid, annotations in objects['job'].items(): for jid, annotations in objects['job'].items():
response = config.get_method('admin1', f'jobs/{jid}/annotations').json() response = config.get_method('admin1', f'jobs/{jid}/annotations').json()
assert DeepDiff(annotations, response, ignore_order=True) == {} assert DeepDiff(annotations, response, ignore_order=True,
exclude_paths="root['version']") == {}
else: else:
response = config.get_method('admin1', endpoint, page_size='all') response = config.get_method('admin1', endpoint, page_size='all')
json_objs = json.load(f) json_objs = json.load(f)

@ -115,7 +115,8 @@ class TestGetAnnotations:
response = get_method(user, f'jobs/{jid}/annotations', **kwargs) response = get_method(user, f'jobs/{jid}/annotations', **kwargs)
assert response.status_code == HTTPStatus.OK assert response.status_code == HTTPStatus.OK
assert DeepDiff(data, response.json()) == {} assert DeepDiff(data, response.json(),
exclude_paths="root['version']") == {}
def _test_get_job_annotations_403(self, user, jid, **kwargs): def _test_get_job_annotations_403(self, user, jid, **kwargs):
response = get_method(user, f'jobs/{jid}/annotations', **kwargs) response = get_method(user, f'jobs/{jid}/annotations', **kwargs)
@ -182,7 +183,8 @@ class TestPatchJobAnnotations:
def _test_check_respone(self, is_allow, response, data=None): def _test_check_respone(self, is_allow, response, data=None):
if is_allow: if is_allow:
assert response.status_code == HTTPStatus.OK assert response.status_code == HTTPStatus.OK
assert DeepDiff(data, response.json()) == {} assert DeepDiff(data, response.json(),
exclude_paths="root['version']") == {}
else: else:
assert response.status_code == HTTPStatus.FORBIDDEN assert response.status_code == HTTPStatus.FORBIDDEN

Loading…
Cancel
Save