未验证 提交 e43707d7 编写于 作者: D Dmitry Kalinin 提交者: GitHub

Project task subsets (#2774)

* Added task project subsets

* Added components list key

* Added subset field resetting and subset header

* Added CHANGELOG and increased npm package version

* Added replacing camelcase to snake
上级 98388f5a
......@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
function for interative segmentation
- Pre-built [cvat_server](https://hub.docker.com/r/openvino/cvat_server) and
[cvat_ui](https://hub.docker.com/r/openvino/cvat_ui) images were published on DockerHub (<https://github.com/openvinotoolkit/cvat/pull/2766>)
- Project task subsets (<https://github.com/openvinotoolkit/cvat/pull/2774>)
### Changed
......
{
"name": "cvat-core",
"version": "3.10.0",
"version": "3.11.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......
{
"name": "cvat-core",
"version": "3.10.0",
"version": "3.11.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
......
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -7,7 +7,13 @@
const serverProxy = require('./server-proxy');
const lambdaManager = require('./lambda-manager');
const {
isBoolean, isInteger, isEnum, isString, checkFilter,
isBoolean,
isInteger,
isEnum,
isString,
checkFilter,
checkExclusiveFields,
camelToSnake,
} = require('./common');
const { TaskStatus, TaskMode, DimensionType } = require('./enums');
......@@ -179,27 +185,21 @@
dimension: isEnum.bind(DimensionType),
});
if ('search' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "search" with others');
}
}
if ('id' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "id" with others');
}
}
if (
'projectId' in filter
&& (('page' in filter && Object.keys(filter).length > 2) || Object.keys(filter).length > 2)
) {
throw new ArgumentError('Do not use the filter field "projectId" with other');
}
checkExclusiveFields(filter, ['id', 'search', 'projectId'], ['page']);
const searchParams = new URLSearchParams();
for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page', 'projectId', 'dimension']) {
for (const field of [
'name',
'owner',
'assignee',
'search',
'status',
'mode',
'id',
'page',
'projectId',
'dimension',
]) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
}
......@@ -222,30 +222,26 @@
owner: isString,
search: isString,
status: isEnum.bind(TaskStatus),
withoutTasks: isBoolean,
});
if ('search' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "search" with others');
}
}
if ('id' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "id" with others');
}
}
checkExclusiveFields(filter, ['id', 'search'], ['page', 'withoutTasks']);
const searchParams = new URLSearchParams();
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page']) {
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page', 'withoutTasks']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
searchParams.set(camelToSnake(field), filter[field]);
}
}
const projectsData = await serverProxy.projects.get(searchParams.toString());
// prettier-ignore
const projects = projectsData.map((project) => new Project(project));
const projects = projectsData.map((project) => {
if (filter.withoutTasks) {
project.tasks = [];
}
return project;
}).map((project) => new Project(project));
projects.count = projectsData.count;
......
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -42,6 +42,25 @@
}
}
function checkExclusiveFields(obj, exclusive, ignore) {
const fields = {
exclusive: [],
other: [],
};
for (const field in Object.keys(obj)) {
if (!(field in ignore)) {
if (field in exclusive) {
if (fields.other.length) {
throw new ArgumentError(`Do not use the filter field "${field}" with others`);
}
fields.exclusive.push(field);
} else {
fields.other.push(field);
}
}
}
}
function checkObjectType(name, value, type, instance) {
if (type) {
if (typeof value !== type) {
......@@ -68,6 +87,16 @@
return true;
}
function camelToSnake(str) {
if (typeof str !== 'string') {
throw new ArgumentError('str is expected to be string');
}
return (
str[0].toLowerCase() + str.slice(1, str.length).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
);
}
function negativeIDGenerator() {
const value = negativeIDGenerator.start;
negativeIDGenerator.start -= 1;
......@@ -83,5 +112,7 @@
checkFilter,
checkObjectType,
negativeIDGenerator,
checkExclusiveFields,
camelToSnake,
};
})();
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -32,6 +32,7 @@
bug_tracker: undefined,
created_date: undefined,
updated_date: undefined,
task_subsets: undefined,
};
for (const property in data) {
......@@ -56,6 +57,13 @@
data.tasks.push(taskInstance);
}
}
if (!data.task_subsets && data.tasks.length) {
const subsetsSet = new Set();
for (const task in data.tasks) {
if (task.subset) subsetsSet.add(task.subset);
}
data.task_subsets = Array.from(subsetsSet);
}
Object.defineProperties(
this,
......@@ -192,6 +200,17 @@
tasks: {
get: () => [...data.tasks],
},
/**
* Subsets array for linked tasks
* @name subsets
* @type {string[]}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
subsets: {
get: () => [...data.task_subsets],
},
}),
);
}
......
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -972,6 +972,7 @@
created_date: undefined,
updated_date: undefined,
bug_tracker: undefined,
subset: undefined,
overlap: undefined,
segment_size: undefined,
image_quality: undefined,
......@@ -991,6 +992,7 @@
name: false,
assignee: false,
bug_tracker: false,
subset: false,
labels: false,
};
......@@ -1167,10 +1169,36 @@
bugTracker: {
get: () => data.bug_tracker,
set: (tracker) => {
if (typeof tracker !== 'string') {
throw new ArgumentError(
`Subset value must be a string. But ${typeof tracker} has been got.`,
);
}
updatedFields.bug_tracker = true;
data.bug_tracker = tracker;
},
},
/**
* @name subset
* @type {string}
* @memberof module:API.cvat.classes.Task
* @instance
* @throws {module:API.cvat.exception.ArgumentError}
*/
subset: {
get: () => data.subset,
set: (subset) => {
if (typeof subset !== 'string') {
throw new ArgumentError(
`Subset value must be a string. But ${typeof subset} has been got.`,
);
}
updatedFields.subset = true;
data.subset = subset;
},
},
/**
* @name overlap
* @type {integer}
......@@ -1888,6 +1916,9 @@
case 'bug_tracker':
taskData.bug_tracker = this.bugTracker;
break;
case 'subset':
taskData.subset = this.subset;
break;
case 'labels':
taskData.labels = [...this.labels.map((el) => el.toJSON())];
break;
......@@ -1903,6 +1934,7 @@
assignee: false,
name: false,
bugTracker: false,
subset: false,
labels: false,
};
......@@ -1926,6 +1958,9 @@
if (typeof this.projectId !== 'undefined') {
taskSpec.project_id = this.projectId;
}
if (typeof this.subset !== 'undefined') {
taskSpec.subset = this.subset;
}
const taskDataSpec = {
client_files: this.clientFiles,
......
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -397,6 +397,9 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
if (data.advanced.copyData) {
description.copy_data = data.advanced.copyData;
}
if (data.subset) {
description.subset = data.subset;
}
const taskInstance = new cvat.classes.Task(description);
taskInstance.clientFiles = data.files.local;
......
......@@ -17,11 +17,13 @@ import LabelsEditor from 'components/labels-editor/labels-editor';
import { Files } from 'components/file-manager/file-manager';
import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form';
import ProjectSearchField from './project-search-field';
import ProjectSubsetField from './project-subset-field';
import AdvancedConfigurationForm, { AdvancedConfiguration } from './advanced-configuration-form';
export interface CreateTaskData {
projectId: number | null;
basic: BaseConfiguration;
subset: string;
advanced: AdvancedConfiguration;
labels: any[];
files: Files;
......@@ -43,6 +45,7 @@ const defaultState = {
basic: {
name: '',
},
subset: '',
advanced: {
lfs: false,
useZipChunks: true,
......@@ -120,8 +123,11 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
};
private handleProjectIdChange = (value: null | number): void => {
const { projectId, subset } = this.state;
this.setState({
projectId: value,
subset: value && value === projectId ? subset : '',
});
};
......@@ -137,6 +143,12 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
});
};
private handleTaskSubsetChange = (value: string): void => {
this.setState({
subset: value,
});
};
private changeFileManagerTab = (key: string): void => {
const values = this.state;
this.setState({
......@@ -165,16 +177,18 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
}
if (this.basicConfigurationComponent.current) {
this.basicConfigurationComponent.current.submit()
this.basicConfigurationComponent.current
.submit()
.then(() => {
if (this.advancedConfigurationComponent.current) {
return this.advancedConfigurationComponent.current.submit();
}
return new Promise((resolve): void => {
return new Promise<void>((resolve): void => {
resolve();
});
}).then((): void => {
})
.then((): void => {
const { onCreate } = this.props;
onCreate(this.state);
})
......@@ -214,6 +228,29 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
);
}
private renderSubsetBlock(): JSX.Element | null {
const { projectId, subset } = this.state;
if (projectId !== null) {
return (
<>
<Col span={24}>
<Text className='cvat-text-color'>Subset:</Text>
</Col>
<Col span={24}>
<ProjectSubsetField
value={subset}
onChange={this.handleTaskSubsetChange}
projectId={projectId}
/>
</Col>
</>
);
}
return null;
}
private renderLabelsBlock(): JSX.Element {
const { projectId, labels } = this.state;
......@@ -293,6 +330,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
{this.renderBasicBlock()}
{this.renderProjectBlock()}
{this.renderSubsetBlock()}
{this.renderLabelsBlock()}
{this.renderFilesBlock()}
{this.renderAdvancedBlock()}
......
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useEffect, useState } from 'react';
import Autocomplete from 'antd/lib/auto-complete';
import { SelectValue } from 'antd/lib/select';
import getCore from 'cvat-core-wrapper';
import { SelectValue } from 'antd/lib/select';
const core = getCore();
......
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useEffect, useState } from 'react';
import Autocomplete from 'antd/lib/auto-complete';
import consts from 'consts';
import getCore from 'cvat-core-wrapper';
const core = getCore();
interface Props {
projectId: number;
projectSubsets?: Array<string>;
value: string;
onChange: (value: string) => void;
}
interface ProjectPartialWithSubsets {
id: number;
subsets: Array<string>;
}
export default function ProjectSubsetField(props: Props): JSX.Element {
const {
projectId, projectSubsets, value, onChange,
} = props;
const [internalValue, setInternalValue] = useState('');
const [internalSubsets, setInternalSubsets] = useState<Set<string>>(new Set());
useEffect(() => {
if (!projectSubsets?.length && projectId) {
core.projects.get({ id: projectId, withoutTasks: true }).then((response: ProjectPartialWithSubsets[]) => {
if (response.length) {
const [project] = response;
setInternalSubsets(
new Set([
...(internalValue ? [internalValue] : []),
...consts.DEFAULT_PROJECT_SUBSETS,
...project.subsets,
]),
);
}
});
} else {
setInternalSubsets(
new Set([
...(internalValue ? [internalValue] : []),
...consts.DEFAULT_PROJECT_SUBSETS,
...(projectSubsets || []),
]),
);
}
}, [projectId, projectSubsets]);
useEffect(() => {
setInternalValue(value);
}, [value]);
return (
<Autocomplete
value={internalValue}
placeholder='Input subset'
className='cvat-project-search-field'
onSearch={(_value) => setInternalValue(_value)}
onSelect={(_value) => {
if (_value !== internalValue) {
onChange(_value);
}
setInternalValue(_value);
}}
onBlur={() => onChange(internalValue)}
options={Array.from(new Set([...(internalValue ? [internalValue] : []), ...internalSubsets])).map(
(subset) => ({
value: subset,
label: subset,
}),
)}
/>
);
}
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -34,9 +34,12 @@ export default function ProjectPageComponent(): JSX.Element {
const taskDeletes = useSelector((state: CombinedState) => state.tasks.activities.deletes);
const tasksActiveInferences = useSelector((state: CombinedState) => state.models.inferences);
const tasks = useSelector((state: CombinedState) => state.tasks.current);
const projectSubsets = useSelector((state: CombinedState) => {
const [project] = state.projects.current.filter((_project) => _project.id === id);
return project ? ([...new Set(project.tasks.map((task: any) => task.subset))] as string[]) : [];
});
const filteredProjects = projects.filter((project) => project.id === id);
const project = filteredProjects[0];
const [project] = projects.filter((_project) => _project.id === id);
const deleteActivity = project && id in deletes ? deletes[id] : null;
useEffect(() => {
......@@ -73,7 +76,7 @@ export default function ProjectPageComponent(): JSX.Element {
<DetailsComponent project={project} />
<Row justify='space-between' align='middle' className='cvat-project-page-tasks-bar'>
<Col>
<Title level={4}>Tasks</Title>
<Title level={3}>Tasks</Title>
</Col>
<Col>
<Button
......@@ -87,21 +90,26 @@ export default function ProjectPageComponent(): JSX.Element {
</Button>
</Col>
</Row>
{tasks
.filter((task) => task.instance.projectId === project.id)
.map((task: Task) => (
<TaskItem
key={task.instance.id}
deleted={task.instance.id in taskDeletes ? taskDeletes[task.instance.id] : false}
hidden={false}
activeInference={tasksActiveInferences[task.instance.id] || null}
cancelAutoAnnotation={() => {
dispatch(cancelInferenceAsync(task.instance.id));
}}
previewImage={task.preview}
taskInstance={task.instance}
/>
))}
{projectSubsets.map((subset) => (
<React.Fragment key={subset}>
{subset && <Title level={4}>{subset}</Title>}
{tasks
.filter((task) => task.instance.projectId === project.id && task.instance.subset === subset)
.map((task: Task) => (
<TaskItem
key={task.instance.id}
deleted={task.instance.id in taskDeletes ? taskDeletes[task.instance.id] : false}
hidden={false}
activeInference={tasksActiveInferences[task.instance.id] || null}
cancelAutoAnnotation={() => {
dispatch(cancelInferenceAsync(task.instance.id));
}}
previewImage={task.preview}
taskInstance={task.instance}
/>
))}
</React.Fragment>
))}
</Col>
</Row>
);
......
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -20,6 +20,7 @@ import Descriptions from 'antd/lib/descriptions';
import UserSelector, { User } from './user-selector';
import BugTrackerEditor from './bug-tracker-editor';
import LabelsEditorComponent from '../labels-editor/labels-editor';
import ProjectSubsetField from '../create-task-page/project-subset-field';
const core = getCore();
......@@ -28,12 +29,14 @@ interface Props {
taskInstance: any;
installedGit: boolean; // change to git repos url
activeInference: ActiveInference | null;
projectSubsets: string[];
cancelAutoAnnotation(): void;
onTaskUpdate: (taskInstance: any) => void;
}
interface State {
name: string;
subset: string;
repository: string;
repositoryStatus: string;
}
......@@ -55,6 +58,7 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
this.previewWrapperRef = React.createRef<HTMLDivElement>();
this.state = {
name: taskInstance.name,
subset: taskInstance.subset,
repository: '',
repositoryStatus: '',
};
......@@ -289,6 +293,36 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
);
}
private renderSubsetField(): JSX.Element {
const { subset } = this.state;
const { taskInstance, projectSubsets, onTaskUpdate } = this.props;
return (
<Row>
<Col span={24}>
<Text className='cvat-text-color'>Subset:</Text>
</Col>
<Col span={24}>
<ProjectSubsetField
value={subset}
projectId={taskInstance.projectId}
projectSubsets={projectSubsets}
onChange={(value) => {
this.setState({
subset: value,
});
if (taskInstance.subset !== value) {
taskInstance.subset = value;
onTaskUpdate(taskInstance);
}
}}
/>
</Col>
</Row>
);
}
public render(): JSX.Element {
const {
activeInference, cancelAutoAnnotation, taskInstance, onTaskUpdate,
......@@ -329,6 +363,7 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
</Row>
{this.renderDatasetRepository()}
{!taskInstance.projectId && this.renderLabelsEditor()}
{taskInstance.projectId && this.renderSubsetField()}
</Col>
</Row>
</div>
......
......@@ -32,6 +32,10 @@
}
}
}
.cvat-project-search-field {
width: $grid-unit-size * 20;
}
}
> div > div > div > button {
......
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -21,6 +21,7 @@ const NEW_LABEL_COLOR = '#b3b3b3';
const LATEST_COMMENTS_SHOWN_QUICK_ISSUE = 3;
const QUICK_ISSUE_INCORRECT_POSITION_TEXT = 'Wrong position';
const QUICK_ISSUE_INCORRECT_ATTRIBUTE_TEXT = 'Wrong attribute';
const DEFAULT_PROJECT_SUBSETS = ['Train', 'Test', 'Validation'];
export default {
UNDEFINED_ATTRIBUTE_VALUE,
......@@ -39,4 +40,5 @@ export default {
LATEST_COMMENTS_SHOWN_QUICK_ISSUE,
QUICK_ISSUE_INCORRECT_POSITION_TEXT,
QUICK_ISSUE_INCORRECT_ATTRIBUTE_TEXT,
DEFAULT_PROJECT_SUBSETS,
};
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -17,6 +17,7 @@ interface OwnProps {
interface StateToProps {
activeInference: ActiveInference | null;
installedGit: boolean;
projectSubsets: string[];
}
interface DispatchToProps {
......@@ -26,10 +27,16 @@ interface DispatchToProps {
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const { list } = state.plugins;
const [taskProject] = state.projects.current.filter((project) => project.id === own.task.instance.projectId);
return {
installedGit: list.GIT_INTEGRATION,
activeInference: state.models.inferences[own.task.instance.id] || null,
projectSubsets: taskProject ?
([
...new Set(taskProject.tasks.map((task: any) => task.subset).filter((subset: string) => subset)),
] as string[]) :
[],
};
}
......@@ -46,7 +53,7 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const {
task, installedGit, activeInference, cancelAutoAnnotation, onTaskUpdate,
task, installedGit, activeInference, projectSubsets, cancelAutoAnnotation, onTaskUpdate,
} = props;
return (
......@@ -55,6 +62,7 @@ function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JS
taskInstance={task.instance}
installedGit={installedGit}
activeInference={activeInference}
projectSubsets={projectSubsets}
onTaskUpdate={onTaskUpdate}
cancelAutoAnnotation={cancelAutoAnnotation}
/>
......
# Generated by Django 3.1.1 on 2021-01-29 11:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('engine', '0036_auto_20201216_0943'),
]
operations = [
migrations.AddField(
model_name='task',
name='subset',
field=models.CharField(blank=True, default='', max_length=64),
),
]
......@@ -214,6 +214,7 @@ class Task(models.Model):
default=StatusChoice.ANNOTATION)
data = models.ForeignKey(Data, on_delete=models.CASCADE, null=True, related_name="tasks")
dimension = models.CharField(max_length=2, choices=DimensionType.choices(), default=DimensionType.DIM_2D)
subset = models.CharField(max_length=64, blank=True, default="")
# Extend default permission model
class Meta:
......
......@@ -337,7 +337,7 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
'bug_tracker', 'created_date', 'updated_date', 'overlap',
'segment_size', 'status', 'labels', 'segments',
'data_chunk_size', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality',
'data', 'dimension')
'data', 'dimension', 'subset')
read_only_fields = ('mode', 'created_date', 'updated_date', 'status', 'data_chunk_size', 'owner', 'assignee',
'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data')
write_once_fields = ('overlap', 'segment_size', 'project_id')
......@@ -385,6 +385,7 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
instance.assignee_id = validated_data.get('assignee_id', instance.assignee_id)
instance.bug_tracker = validated_data.get('bug_tracker',
instance.bug_tracker)
instance.subset = validated_data.get('subset', instance.subset)
labels = validated_data.get('label_set', [])
for label in labels:
LabelSerializer.update_instance(label, instance)
......@@ -406,20 +407,36 @@ class ProjectSearchSerializer(serializers.ModelSerializer):
ordering = ['-id']
class ProjectSerializer(serializers.ModelSerializer):
class ProjectWithoutTaskSerializer(serializers.ModelSerializer):
labels = LabelSerializer(many=True, source='label_set', partial=True, default=[])
tasks = TaskSerializer(many=True, read_only=True)
owner = BasicUserSerializer(required=False)
owner_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
assignee = BasicUserSerializer(allow_null=True, required=False)
assignee_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
class Meta:
model = models.Project
fields = ('url', 'id', 'name', 'labels', 'tasks', 'owner', 'assignee', 'owner_id', 'assignee_id',
fields = ('url', 'id', 'name', 'labels', 'owner', 'assignee', 'owner_id', 'assignee_id',
'bug_tracker', 'created_date', 'updated_date', 'status')
read_only_fields = ('created_date', 'updated_date', 'status', 'owner', 'asignee')
ordering = ['-id']
def to_representation(self, instance):
response = super().to_representation(instance)
subsets = set()
for task in instance.tasks.all():
if task.subset:
subsets.add(task.subset)
response['task_subsets'] = list(subsets)
return response
class ProjectSerializer(ProjectWithoutTaskSerializer):
tasks = TaskSerializer(many=True, read_only=True)
class Meta(ProjectWithoutTaskSerializer.Meta):
fields = ProjectWithoutTaskSerializer.Meta.fields + ('tasks',)
# pylint: disable=no-self-use
def create(self, validated_data):
labels = validated_data.pop('label_set')
......@@ -463,6 +480,9 @@ class ProjectSerializer(serializers.ModelSerializer):
raise serializers.ValidationError('All label names must be unique for the project')
return value
def to_representation(self, instance):
return serializers.ModelSerializer.to_representation(self, instance) # ignoring subsets here
class ExceptionSerializer(serializers.Serializer):
system = serializers.CharField(max_length=255)
client = serializers.CharField(max_length=255)
......
......@@ -47,8 +47,8 @@ from cvat.apps.engine.serializers import (
AboutSerializer, AnnotationFileSerializer, BasicUserSerializer,
DataMetaSerializer, DataSerializer, ExceptionSerializer,
FileInfoSerializer, JobSerializer, LabeledDataSerializer,
LogEventSerializer, ProjectSerializer, ProjectSearchSerializer, RqStatusSerializer,
TaskSerializer, UserSerializer, PluginsSerializer, ReviewSerializer,
LogEventSerializer, ProjectSerializer, ProjectSearchSerializer, ProjectWithoutTaskSerializer,
RqStatusSerializer, TaskSerializer, UserSerializer, PluginsSerializer, ReviewSerializer,
CombinedReviewSerializer, IssueSerializer, CombinedIssueSerializer, CommentSerializer
)
from cvat.apps.engine.utils import av_scan_paths
......@@ -229,6 +229,8 @@ class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
def get_serializer_class(self):
if self.request.query_params and self.request.query_params.get("names_only") == "true":
return ProjectSearchSerializer
if self.request.query_params and self.request.query_params.get("without_tasks") == "true":
return ProjectWithoutTaskSerializer
else:
return ProjectSerializer
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册