未验证 提交 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
- 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>)
- 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>)
### 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>)
- 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
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):
db_task = self.db_job.segment.task
db_task.updated_date = timezone.now()
......@@ -302,17 +289,14 @@ class JobAnnotation:
def create(self, data):
self._create(data)
self._commit()
def put(self, data):
self._delete()
self._create(data)
self._commit()
def update(self, data):
self._delete(data)
self._create(data)
self._commit()
def _delete(self, data=None):
deleted_shapes = 0
......@@ -347,7 +331,6 @@ class JobAnnotation:
def delete(self, data=None):
self._delete(data)
self._commit()
@staticmethod
def _extend_attributes(attributeval_set, default_attribute_values):
......@@ -513,8 +496,7 @@ class JobAnnotation:
self.ir_data.tracks = serializer.data
def _init_version_from_db(self):
db_commit = self.db_job.commits.last()
self.ir_data.version = db_commit.version if db_commit else 0
self.ir_data.version = 0 # FIXME: should be removed in the future
def init_from_db(self):
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
from django.core.files.storage import FileSystemStorage
from django.db import models
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.organizations.models import Organization
......@@ -395,6 +396,15 @@ class Job(models.Model):
project = task.project
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:
default_permissions = ()
......@@ -491,11 +501,20 @@ class Annotation(models.Model):
default_permissions = ()
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)
scope = models.CharField(max_length=32, default="")
owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
version = models.PositiveIntegerField(default=0)
timestamp = models.DateTimeField(auto_now=True)
message = models.CharField(max_length=4096, default="")
data = models.JSONField(default=dict, encoder=JSONEncoder)
class Meta:
abstract = True
......
......@@ -138,7 +138,8 @@ class LabelSerializer(serializers.ModelSerializer):
class JobCommitSerializer(serializers.ModelSerializer):
class Meta:
model = models.JobCommit
fields = ('id', 'version', 'owner', 'message', 'timestamp')
fields = ('id', 'owner', 'data', 'timestamp', 'scope')
class JobReadSerializer(serializers.ModelSerializer):
task_id = serializers.ReadOnlyField(source="segment.task.id")
......@@ -167,6 +168,14 @@ class JobWriteSerializer(serializers.ModelSerializer):
serializer = JobReadSerializer(instance, context=self.context)
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):
state = validated_data.get('state')
stage = validated_data.get('stage')
......@@ -186,7 +195,13 @@ class JobWriteSerializer(serializers.ModelSerializer):
if assignee is not None:
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:
model = models.Job
......
......@@ -106,10 +106,10 @@ def _save_task_to_db(db_task):
db_segment.stop_frame = stop_frame
db_segment.save()
db_job = models.Job()
db_job.segment = db_segment
db_job = models.Job(segment=db_segment)
db_job.save()
db_task.data.save()
db_task.save()
......
......@@ -4118,7 +4118,7 @@ class JobAnnotationAPITestCase(APITestCase):
def _check_response(self, response, data):
if not response.status_code in [
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):
task, jobs = self._create_task(owner, assignee)
......@@ -4603,7 +4603,7 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
if not response.status_code in [
status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]:
try:
compare_objects(self, data, response.data, ignore_keys=["id"])
compare_objects(self, data, response.data, ignore_keys=["id", "version"])
except AssertionError as e:
print("Objects are not equal: ", data, response.data)
print(e)
......
......@@ -59,7 +59,7 @@ from cvat.apps.engine.serializers import (
LogEventSerializer, ProjectSerializer, ProjectSearchSerializer,
RqStatusSerializer, TaskSerializer, UserSerializer, PluginsSerializer, IssueReadSerializer,
IssueWriteSerializer, CommentReadSerializer, CommentWriteSerializer, CloudStorageWriteSerializer,
CloudStorageReadSerializer, DatasetFileSerializer)
CloudStorageReadSerializer, DatasetFileSerializer, JobCommitSerializer)
from utils.dataset_manifest import ImageManifestManager
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,
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(
summary='Method returns details of an issue',
responses={
......
......@@ -1226,12 +1226,11 @@ ALTER SEQUENCE public.engine_job_id_seq OWNED BY public.engine_job.id;
CREATE TABLE public.engine_jobcommit (
id bigint NOT NULL,
version integer NOT NULL,
"timestamp" timestamp with time zone NOT NULL,
message character varying(4096) NOT NULL,
owner_id integer,
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
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
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
--
COPY public.engine_jobcommit (id, version, "timestamp", message, owner_id, job_id) FROM stdin;
1 1 2021-12-22 07:14:15.237479+00 Changes: tags - 0; shapes - 5; tracks - 0 \N 2
2 2 2021-12-22 07:14:15.268804+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 2
3 3 2021-12-22 07:14:15.298016+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 2
4 1 2021-12-22 07:15:22.945367+00 Changes: tags - 0; shapes - 9; tracks - 0 \N 1
5 2 2021-12-22 07:15:22.985309+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 1
6 3 2021-12-22 07:15:23.019102+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 1
7 1 2021-12-22 07:17:34.839155+00 Changes: tags - 0; shapes - 7; tracks - 0 \N 6
8 2 2021-12-22 07:17:34.878804+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 6
9 3 2021-12-22 07:17:34.909805+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 6
10 1 2021-12-22 07:19:33.859315+00 Changes: tags - 0; shapes - 5; tracks - 0 \N 4
11 2 2021-12-22 07:19:33.907033+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 4
12 3 2021-12-22 07:19:33.934873+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 4
13 4 2021-12-22 07:22:30.331021+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 4
14 5 2021-12-22 07:22:30.362857+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 4
15 6 2021-12-22 07:22:30.388715+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 4
16 1 2022-02-21 10:32:04.068136+00 Changes: tags - 0; shapes - 1; tracks - 0 \N 9
17 2 2022-02-21 10:32:04.169838+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 9
18 3 2022-02-21 10:32:04.256121+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 9
19 1 2022-02-21 10:37:22.961448+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 3
20 2 2022-02-21 10:37:23.075321+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 3
21 3 2022-02-21 10:37:23.187161+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 3
22 4 2022-02-21 10:37:27.7082+00 Changes: tags - 0; shapes - 1; tracks - 0 \N 3
23 5 2022-02-21 10:37:27.834371+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 3
24 6 2022-02-21 10:37:27.95231+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 3
25 1 2022-02-21 10:40:21.267763+00 Changes: tags - 0; shapes - 1; tracks - 0 \N 7
26 2 2022-02-21 10:40:21.354689+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 7
27 3 2022-02-21 10:40:21.435822+00 Changes: tags - 0; shapes - 0; tracks - 0 \N 7
COPY public.engine_jobcommit (id, "timestamp", owner_id, job_id, data, scope) FROM stdin;
1 2021-12-22 07:14:15.237479+00 \N 2 {}
2 2021-12-22 07:14:15.268804+00 \N 2 {}
3 2021-12-22 07:14:15.298016+00 \N 2 {}
4 2021-12-22 07:15:22.945367+00 \N 1 {}
5 2021-12-22 07:15:22.985309+00 \N 1 {}
6 2021-12-22 07:15:23.019102+00 \N 1 {}
7 2021-12-22 07:17:34.839155+00 \N 6 {}
8 2021-12-22 07:17:34.878804+00 \N 6 {}
9 2021-12-22 07:17:34.909805+00 \N 6 {}
10 2021-12-22 07:19:33.859315+00 \N 4 {}
11 2021-12-22 07:19:33.907033+00 \N 4 {}
12 2021-12-22 07:19:33.934873+00 \N 4 {}
13 2021-12-22 07:22:30.331021+00 \N 4 {}
14 2021-12-22 07:22:30.362857+00 \N 4 {}
15 2021-12-22 07:22:30.388715+00 \N 4 {}
16 2022-02-21 10:32:04.068136+00 \N 9 {}
17 2022-02-21 10:32:04.169838+00 \N 9 {}
18 2022-02-21 10:32:04.256121+00 \N 9 {}
19 2022-02-21 10:37:22.961448+00 \N 3 {}
20 2022-02-21 10:37:23.075321+00 \N 3 {}
21 2022-02-21 10:37:23.187161+00 \N 3 {}
22 2022-02-21 10:37:27.7082+00 \N 3 {}
23 2022-02-21 10:37:27.834371+00 \N 3 {}
24 2022-02-21 10:37:27.95231+00 \N 3 {}
25 2022-02-21 10:40:21.267763+00 \N 7 {}
26 2022-02-21 10:40:21.354689+00 \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
--
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)
for jid, annotations in objects['job'].items():
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:
response = config.get_method('admin1', endpoint, page_size='all')
json_objs = json.load(f)
......
......@@ -115,7 +115,8 @@ class TestGetAnnotations:
response = get_method(user, f'jobs/{jid}/annotations', **kwargs)
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):
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):
if is_allow:
assert response.status_code == HTTPStatus.OK
assert DeepDiff(data, response.json()) == {}
assert DeepDiff(data, response.json(),
exclude_paths="root['version']") == {}
else:
assert response.status_code == HTTPStatus.FORBIDDEN
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册