diff --git a/CHANGELOG.md b/CHANGELOG.md index 61f26f2d..b4efddcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 () - Added OpenCV.js TrackerMIL as tracking tool () - Ability to continue working from the latest frame where an annotator was before () +- `GET /api/jobs//commits` was implemented () - Advanced filtration and sorting for a list of jobs () - - ### Changed - Users don't have access to a task object anymore if they are assigneed only on some jobs of the task () - Different resources (tasks, projects) are not visible anymore for all CVAT instance users by default () diff --git a/cvat/apps/dataset_manager/task.py b/cvat/apps/dataset_manager/task.py index a5ea8310..244a3e33 100644 --- a/cvat/apps/dataset_manager/task.py +++ b/cvat/apps/dataset_manager/task.py @@ -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() diff --git a/cvat/apps/engine/migrations/0051_auto_20220220_1824.py b/cvat/apps/engine/migrations/0051_auto_20220220_1824.py new file mode 100644 index 00000000..66974739 --- /dev/null +++ b/cvat/apps/engine/migrations/0051_auto_20220220_1824.py @@ -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), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index ede2bf71..3e8b0f2a 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -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 diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 3027cae3..471a1282 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -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 diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 82dda7ad..89079929 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -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() diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index 5dc5798f..a87cf5f0 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -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) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index c6eb411f..1e8e0004 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -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={ diff --git a/tests/rest_api/assets/cvat_db/cvat_db.sql b/tests/rest_api/assets/cvat_db/cvat_db.sql index 7ad93d19..dc67c651 100644 --- a/tests/rest_api/assets/cvat_db/cvat_db.sql +++ b/tests/rest_api/assets/cvat_db/cvat_db.sql @@ -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); -- diff --git a/tests/rest_api/test_check_objects_integrity.py b/tests/rest_api/test_check_objects_integrity.py index 508c6259..6e6988fb 100644 --- a/tests/rest_api/test_check_objects_integrity.py +++ b/tests/rest_api/test_check_objects_integrity.py @@ -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) diff --git a/tests/rest_api/test_jobs.py b/tests/rest_api/test_jobs.py index 3e88990c..f710122a 100644 --- a/tests/rest_api/test_jobs.py +++ b/tests/rest_api/test_jobs.py @@ -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