未验证 提交 e8f294f6 编写于 作者: N Nikita Manovich 提交者: GitHub

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

上级 df8590e7
...@@ -35,10 +35,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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()
......
# 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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 ( ...@@ -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, ...@@ -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; ...@@ -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; ...@@ -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 ...@@ -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); ...@@ -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): ...@@ -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: ...@@ -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: ...@@ -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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册