Project tasks pagination (#3910)

* Added project tasks search and pagination

* Increased npm versions

* Added CHANGELOG

* Fixed issues

* Fixed styles

* Fixed core tests

* Fixed core tests

* Fixed core tests

* Fixed core tests

* Fixed parameter

* Fixed project update action

* Fixed updating project

* Fixed comments
main
Dmitry Kalinin 4 years ago committed by GitHub
parent 958206c929
commit b5ed09ea94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add Open Images V6 format (<https://github.com/openvinotoolkit/cvat/pull/3679>)
- Rotated bounding boxes (<https://github.com/openvinotoolkit/cvat/pull/3832>)
- Player option: Smooth image when zoom-in, enabled by default (<https://github.com/openvinotoolkit/cvat/pull/3933>)
- Add project tasks paginations (<https://github.com/openvinotoolkit/cvat/pull/3910>)
### Changed
- TDB

@ -1,12 +1,12 @@
{
"name": "cvat-core",
"version": "3.19.0",
"version": "3.20.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-core",
"version": "3.19.0",
"version": "3.20.0",
"license": "MIT",
"dependencies": {
"axios": "^0.21.4",

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.19.0",
"version": "3.20.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {

@ -188,6 +188,7 @@
owner: isString,
assignee: isString,
search: isString,
ordering: isString,
status: isEnum.bind(TaskStatus),
mode: isEnum.bind(TaskMode),
dimension: isEnum.bind(DimensionType),
@ -196,11 +197,13 @@
checkExclusiveFields(filter, ['id', 'search', 'projectId'], ['page']);
const searchParams = new URLSearchParams();
for (const field of [
'name',
'owner',
'assignee',
'search',
'ordering',
'status',
'mode',
'id',
@ -209,7 +212,7 @@
'dimension',
]) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
searchParams.set(camelToSnake(field), filter[field]);
}
}
@ -230,35 +233,20 @@
owner: isString,
search: isString,
status: isEnum.bind(TaskStatus),
withoutTasks: isBoolean,
});
checkExclusiveFields(filter, ['id', 'search'], ['page', 'withoutTasks']);
if (typeof filter.withoutTasks === 'undefined') {
if (typeof filter.id === 'undefined') {
filter.withoutTasks = true;
} else {
filter.withoutTasks = false;
}
}
checkExclusiveFields(filter, ['id', 'search'], ['page']);
const searchParams = new URLSearchParams();
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page', 'withoutTasks']) {
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(camelToSnake(field), filter[field]);
}
}
const projectsData = await serverProxy.projects.get(searchParams.toString());
// prettier-ignore
const projects = projectsData.map((project) => {
if (filter.withoutTasks) {
project.task_ids = project.tasks;
project.tasks = [];
} else {
project.task_ids = project.tasks.map((task) => task.id);
}
project.task_ids = project.tasks;
return project;
}).map((project) => new Project(project));

@ -5,7 +5,6 @@
(() => {
const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const { Label } = require('./labels');
const User = require('./user');
@ -44,7 +43,6 @@
}
data.labels = [];
data.tasks = [];
if (Array.isArray(initialData.labels)) {
for (const label of initialData.labels) {
@ -53,19 +51,6 @@
}
}
if (Array.isArray(initialData.tasks)) {
for (const task of initialData.tasks) {
const taskInstance = new Task(task);
data.tasks.push(taskInstance);
}
}
if (!data.task_subsets) {
const subsetsSet = new Set();
for (const task of data.tasks) {
if (task.subset) subsetsSet.add(task.subset);
}
data.task_subsets = Array.from(subsetsSet);
}
if (typeof initialData.training_project === 'object') {
data.training_project = { ...initialData.training_project };
}
@ -212,17 +197,6 @@
data.labels = [...deletedLabels, ...labels];
},
},
/**
* Tasks related with the project
* @name tasks
* @type {module:API.cvat.classes.Task[]}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
tasks: {
get: () => [...data.tasks],
},
/**
* Subsets array for related tasks
* @name subsets

@ -11,12 +11,11 @@ jest.mock('../../src/server-proxy', () => {
// Initialize api
window.cvat = require('../../src/api');
const { Task } = require('../../src/session');
const { Project } = require('../../src/project');
describe('Feature: get projects', () => {
test('get all projects', async () => {
const result = await window.cvat.projects.get({ withoutTasks: false });
const result = await window.cvat.projects.get();
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(2);
for (const el of result) {
@ -33,8 +32,8 @@ describe('Feature: get projects', () => {
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Project);
expect(result[0].id).toBe(2);
expect(result[0].tasks).toHaveLength(1);
expect(result[0].tasks[0]).toBeInstanceOf(Task);
// eslint-disable-next-line no-underscore-dangle
expect(result[0]._internalData.task_ids).toHaveLength(1);
});
test('get a project by an unknown id', async () => {

@ -5,8 +5,9 @@
import { Dispatch, ActionCreator } from 'redux';
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { ProjectsQuery } from 'reducers/interfaces';
import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions';
import { ProjectsQuery, TasksQuery, CombinedState } from 'reducers/interfaces';
import { getTasksAsync } from 'actions/tasks-actions';
import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core-wrapper';
const cvat = getCore();
@ -34,8 +35,8 @@ const projectActions = {
createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, previews, count })
),
getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }),
updateProjectsGettingQuery: (query: Partial<ProjectsQuery>) => (
createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query })
updateProjectsGettingQuery: (query: Partial<ProjectsQuery>, tasksQuery: Partial<TasksQuery> = {}) => (
createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query, tasksQuery })
),
createProject: () => createAction(ProjectsActionTypes.CREATE_PROJECT),
createProjectSuccess: (projectId: number) => (
@ -58,10 +59,27 @@ const projectActions = {
export type ProjectActions = ActionUnion<typeof projectActions>;
export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
export function getProjectTasksAsync(tasksQuery: Partial<TasksQuery> = {}): ThunkAction<void> {
return (dispatch: ActionCreator<Dispatch>): void => {
const store = getCVATStore();
const state: CombinedState = store.getState();
dispatch(projectActions.updateProjectsGettingQuery({}, tasksQuery));
const query: Partial<TasksQuery> = {
...state.projects.tasksGettingQuery,
page: 1,
...tasksQuery,
};
dispatch(getTasksAsync(query));
};
}
export function getProjectsAsync(
query: Partial<ProjectsQuery>, tasksQuery: Partial<TasksQuery> = {},
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(projectActions.getProjects());
dispatch(projectActions.updateProjectsGettingQuery(query));
dispatch(projectActions.updateProjectsGettingQuery(query, tasksQuery));
// Clear query object from null fields
const filteredQuery: Partial<ProjectsQuery> = {
@ -85,38 +103,15 @@ export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
const array = Array.from(result);
// Appropriate tasks fetching proccess needs with retrieving only a single project
if (Object.keys(filteredQuery).includes('id')) {
const tasks: any[] = [];
const [project] = array;
const taskPreviewPromises: Promise<string>[] = (project as any).tasks.map((task: any): string => {
tasks.push(task);
return (task as any).frames.preview().catch(() => '');
});
const previewPromises = array.map((project): string => (project as any).preview().catch(() => ''));
dispatch(projectActions.getProjectsSuccess(array, await Promise.all(previewPromises), result.count));
const taskPreviews = await Promise.all(taskPreviewPromises);
const state = getState();
dispatch(projectActions.getProjectsSuccess(array, taskPreviews, result.count));
if (!state.tasks.fetching) {
dispatch(
getTasksSuccess(tasks, taskPreviews, tasks.length, {
page: 1,
assignee: null,
id: null,
mode: null,
name: null,
owner: null,
search: null,
status: null,
}),
);
}
} else {
const previewPromises = array.map((project): string => (project as any).preview().catch(() => ''));
dispatch(projectActions.getProjectsSuccess(array, await Promise.all(previewPromises), result.count));
// Appropriate tasks fetching proccess needs with retrieving only a single project
if (Object.keys(filteredQuery).includes('id') && typeof filteredQuery.id === 'number') {
dispatch(getProjectTasksAsync({
...tasksQuery,
projectId: filteredQuery.id,
}));
}
};
}
@ -136,17 +131,14 @@ export function createProjectAsync(data: any): ThunkAction {
}
export function updateProjectAsync(projectInstance: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
return async (dispatch, getState): Promise<void> => {
try {
const state = getState();
dispatch(projectActions.updateProject());
await projectInstance.save();
const [project] = await cvat.projects.get({ id: projectInstance.id });
// TODO: Check case when a project is not available anymore after update
// (assignee changes assignee and project is not public)
dispatch(projectActions.updateProjectSuccess(project));
project.tasks.forEach((task: any) => {
dispatch(updateTaskSuccess(task, task.id));
});
dispatch(getProjectTasksAsync(state.projects.tasksGettingQuery));
} catch (error) {
let project = null;
try {

@ -47,7 +47,9 @@ function getTasks(): AnyAction {
return action;
}
export function getTasksSuccess(array: any[], previews: string[], count: number, query: TasksQuery): AnyAction {
export function getTasksSuccess(
array: any[], previews: string[], count: number, query: Partial<TasksQuery>,
): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_SUCCESS,
payload: {
@ -61,7 +63,7 @@ export function getTasksSuccess(array: any[], previews: string[], count: number,
return action;
}
function getTasksFailed(error: any, query: TasksQuery): AnyAction {
function getTasksFailed(error: any, query: Partial<TasksQuery>): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_FAILED,
payload: {
@ -73,7 +75,7 @@ function getTasksFailed(error: any, query: TasksQuery): AnyAction {
return action;
}
export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function getTasksAsync(query: Partial<TasksQuery>): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(getTasks());
@ -248,7 +250,7 @@ export function exportTaskAsync(taskInstance: any): ThunkAction<Promise<void>, {
downloadAnchor.click();
dispatch(exportTaskSuccess(taskInstance.id));
} catch (error) {
dispatch(exportTaskFailed(taskInstance.id, error));
dispatch(exportTaskFailed(taskInstance.id, error as Error));
}
};
}

@ -32,7 +32,7 @@ export default function ProjectSubsetField(props: Props): JSX.Element {
useEffect(() => {
if (!projectSubsets?.length && projectId) {
core.projects.get({ id: projectId, withoutTasks: true }).then((response: ProjectPartialWithSubsets[]) => {
core.projects.get({ id: projectId }).then((response: ProjectPartialWithSubsets[]) => {
if (response.length) {
const [project] = response;
setInternalSubsets(

@ -10,7 +10,6 @@ import Title from 'antd/lib/typography/Title';
import Text from 'antd/lib/typography/Text';
import getCore from 'cvat-core-wrapper';
import { Project } from 'reducers/interfaces';
import { updateProjectAsync } from 'actions/projects-actions';
import LabelsEditor from 'components/labels-editor/labels-editor';
import BugTrackerEditor from 'components/task-page/bug-tracker-editor';
@ -19,7 +18,7 @@ import UserSelector from 'components/task-page/user-selector';
const core = getCore();
interface DetailsComponentProps {
project: Project;
project: any;
}
export default function DetailsComponent(props: DetailsComponentProps): JSX.Element {

@ -3,22 +3,25 @@
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect } from 'react';
import React, { useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useHistory, useParams } from 'react-router';
import { useHistory, useParams, useLocation } from 'react-router';
import Spin from 'antd/lib/spin';
import { Row, Col } from 'antd/lib/grid';
import Result from 'antd/lib/result';
import Button from 'antd/lib/button';
import Title from 'antd/lib/typography/Title';
import Pagination from 'antd/lib/pagination';
import { PlusOutlined } from '@ant-design/icons';
import { CombinedState, Task } from 'reducers/interfaces';
import { getProjectsAsync } from 'actions/projects-actions';
import { CombinedState, Task, TasksQuery } from 'reducers/interfaces';
import { getProjectsAsync, getProjectTasksAsync } from 'actions/projects-actions';
import { cancelInferenceAsync } from 'actions/models-actions';
import TaskItem from 'components/tasks-page/task-item';
import SearchField from 'components/search-field/search-field';
import MoveTaskModal from 'components/move-task-modal/move-task-modal';
import ModelRunnerDialog from 'components/model-runner-modal/model-runner-dialog';
import { useDidUpdateEffect } from 'utils/hooks';
import DetailsComponent from './details';
import ProjectTopBar from './top-bar';
@ -30,25 +33,54 @@ export default function ProjectPageComponent(): JSX.Element {
const id = +useParams<ParamType>().id;
const dispatch = useDispatch();
const history = useHistory();
const { search } = useLocation();
const projects = useSelector((state: CombinedState) => state.projects.current).map((project) => project.instance);
const projectsFetching = useSelector((state: CombinedState) => state.projects.fetching);
const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes);
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 tasksCount = useSelector((state: CombinedState) => state.tasks.count);
const tasksGettingQuery = useSelector((state: CombinedState) => state.projects.tasksGettingQuery);
const [project] = projects.filter((_project) => _project.id === id);
const projectSubsets = [''];
if (project) projectSubsets.push(...project.subsets);
const projectSubsets: Array<string> = [];
for (const task of tasks) {
if (!projectSubsets.includes(task.instance.subset)) projectSubsets.push(task.instance.subset);
}
const deleteActivity = project && id in deletes ? deletes[id] : null;
const onPageChange = useCallback(
(p: number) => {
dispatch(getProjectTasksAsync({
projectId: id,
page: p,
}));
},
[],
);
useEffect(() => {
dispatch(
getProjectsAsync({
id,
}),
);
}, [id, dispatch]);
const searchParams: Partial<TasksQuery> = {};
for (const [param, value] of new URLSearchParams(search)) {
searchParams[param] = ['page'].includes(param) ? Number.parseInt(value, 10) : value;
}
dispatch(getProjectsAsync({ id }, searchParams));
}, []);
useDidUpdateEffect(() => {
const searchParams = new URLSearchParams();
for (const [name, value] of Object.entries(tasksGettingQuery)) {
if (value !== null && typeof value !== 'undefined' && !['projectId', 'ordering'].includes(name)) {
searchParams.append(name, value.toString());
}
}
history.push({
pathname: `/projects/${id}`,
search: `?${searchParams.toString()}`,
});
}, [tasksGettingQuery, id]);
if (deleteActivity) {
history.push('/projects');
@ -69,14 +101,27 @@ export default function ProjectPageComponent(): JSX.Element {
);
}
const paginationDimensions = {
md: 22,
lg: 18,
xl: 16,
xxl: 16,
};
return (
<Row justify='center' align='top' className='cvat-project-page'>
<Col md={22} lg={18} xl={16} xxl={14}>
<ProjectTopBar projectInstance={project} />
<DetailsComponent project={project} />
<Row justify='space-between' align='middle' className='cvat-project-page-tasks-bar'>
<Col>
<Col className='cvat-project-tasks-title-search'>
<Title level={3}>Tasks</Title>
<SearchField
query={tasksGettingQuery}
instance='task'
skipFields={['ordering', 'projectId']}
onSearch={(query: TasksQuery) => dispatch(getProjectTasksAsync(query))}
/>
</Col>
<Col>
<Button
@ -110,6 +155,19 @@ export default function ProjectPageComponent(): JSX.Element {
))}
</React.Fragment>
))}
<Row justify='center'>
<Col {...paginationDimensions}>
<Pagination
className='cvat-project-tasks-pagination'
onChange={onPageChange}
showSizeChanger={false}
total={tasksCount}
pageSize={10}
current={tasksGettingQuery.page}
showQuickJumper
/>
</Col>
</Row>
</Col>
<MoveTaskModal />
<ModelRunnerDialog />

@ -59,3 +59,16 @@
display: flex;
align-items: center;
}
.cvat-project-tasks-pagination {
display: flex;
justify-content: center;
}
.cvat-project-tasks-title-search {
display: flex;
> * {
margin-right: $grid-unit-size * 2;
}
}

@ -38,12 +38,18 @@ export default function ProjectActionsMenuComponent(props: Props): JSX.Element {
return (
<Menu className='cvat-project-actions-menu'>
<Menu.Item
key='project-export'
onClick={() => dispatch(exportActions.openExportModal(projectInstance))}
>
Export project dataset
</Menu.Item>
<hr />
<Menu.Item onClick={onDeleteProject}>Delete</Menu.Item>
<Menu.Item
key='project-delete'
onClick={onDeleteProject}
>
Delete
</Menu.Item>
</Menu>
);
}

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Row, Col } from 'antd/lib/grid';
import Pagination from 'antd/lib/pagination';
@ -18,14 +18,14 @@ export default function ProjectListComponent(): JSX.Element {
const projects = useSelector((state: CombinedState) => state.projects.current);
const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
function changePage(p: number): void {
const changePage = useCallback((p: number) => {
dispatch(
getProjectsAsync({
...gettingQuery,
page: p,
}),
);
}
}, [dispatch, getProjectsAsync, gettingQuery]);
const dimensions = {
md: 22,

@ -14,16 +14,27 @@ interface Query {
interface Props {
query: Query;
instance: 'task' | 'project' | 'cloudstorage';
skipFields?: string[];
onSearch(query: object): void;
}
export default function SearchField(props: Props): JSX.Element {
const { onSearch, query, instance } = props;
function parse(_query: Query): string {
const {
onSearch,
query,
instance,
skipFields,
} = props;
const skip = ['page'];
if (typeof skipFields !== 'undefined') {
skip.push(...skipFields);
}
function parse(_query: Query, _skip: string[]): string {
let searchString = '';
for (const field of Object.keys(_query)) {
const value = _query[field];
if (value !== null && typeof value !== 'undefined' && field !== 'page') {
if (value !== null && typeof value !== 'undefined' && !_skip.includes(field)) {
if (field === 'search') {
return _query[field] as string;
}
@ -47,7 +58,7 @@ export default function SearchField(props: Props): JSX.Element {
.replace(/\s*:+\s*/g, ':')
.trim();
const fields = Object.keys(query).filter((key) => key !== 'page');
const fields = Object.keys(query).filter((key) => !skip.includes(key));
for (const field of fields) {
currentQuery[field] = null;
}
@ -82,7 +93,7 @@ export default function SearchField(props: Props): JSX.Element {
<SearchTooltip instance={instance}>
<Search
className='cvat-search-field'
defaultValue={parse(query)}
defaultValue={parse(query, skip)}
onSearch={handleSearch}
size='large'
placeholder='Search'

@ -44,6 +44,7 @@ export interface ProjectsState {
count: number;
current: Project[];
gettingQuery: ProjectsQuery;
tasksGettingQuery: TasksQuery;
activities: {
creates: {
id: null | number;
@ -64,6 +65,7 @@ export interface TasksQuery {
name: string | null;
status: string | null;
mode: string | null;
projectId: number | null;
[key: string]: string | number | null;
}

@ -23,6 +23,18 @@ const defaultState: ProjectsState = {
name: null,
status: null,
},
tasksGettingQuery: {
page: 1,
id: null,
search: null,
owner: null,
assignee: null,
name: null,
status: null,
mode: null,
projectId: null,
ordering: 'subset',
},
activities: {
deletes: {},
creates: {
@ -41,6 +53,10 @@ export default (state: ProjectsState = defaultState, action: AnyAction): Project
...defaultState.gettingQuery,
...action.payload.query,
},
tasksGettingQuery: {
...defaultState.tasksGettingQuery,
...action.payload.tasksQuery,
},
};
case ProjectsActionTypes.GET_PROJECTS:
return {

@ -30,6 +30,7 @@ const defaultState: TasksState = {
name: null,
status: null,
mode: null,
projectId: null,
},
activities: {
loads: {},
@ -74,7 +75,10 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
fetching: false,
count: action.payload.count,
current: combinedWithPreviews,
gettingQuery: { ...action.payload.query },
gettingQuery: {
...state.gettingQuery,
...action.payload.query,
},
};
}
case TasksActionTypes.GET_TASKS_FAILED:

@ -1,7 +1,7 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { useRef, useEffect, useState } from 'react';
import React, { useRef, useEffect, useState } from 'react';
// eslint-disable-next-line import/prefer-default-export
export function usePrevious<T>(value: T): T | undefined {
@ -12,6 +12,17 @@ export function usePrevious<T>(value: T): T | undefined {
return ref.current;
}
export function useDidUpdateEffect(effect: React.EffectCallback, deps?: React.DependencyList): void {
const didMountRef = useRef(false);
useEffect(() => {
if (didMountRef.current) {
effect();
} else {
didMountRef.current = true;
}
}, deps);
}
export interface ICardHeightHOC {
numberOfRows: number;
paddings: number;

@ -499,7 +499,7 @@ class TrainingProjectSerializer(serializers.ModelSerializer):
write_once_fields = ('host', 'username', 'password', 'project_class')
class ProjectWithoutTaskSerializer(serializers.ModelSerializer):
class ProjectSerializer(serializers.ModelSerializer):
labels = LabelSerializer(many=True, source='label_set', partial=True, default=[])
owner = BasicUserSerializer(required=False)
owner_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
@ -513,7 +513,7 @@ class ProjectWithoutTaskSerializer(serializers.ModelSerializer):
model = models.Project
fields = ('url', 'id', 'name', 'labels', 'tasks', 'owner', 'assignee', 'owner_id', 'assignee_id',
'bug_tracker', 'task_subsets', 'created_date', 'updated_date', 'status', 'training_project', 'dimension')
read_only_fields = ('created_date', 'updated_date', 'status', 'owner', 'asignee', 'task_subsets', 'dimension')
read_only_fields = ('created_date', 'updated_date', 'tasks', 'status', 'owner', 'asignee', 'task_subsets', 'dimension')
ordering = ['-id']
@ -525,12 +525,6 @@ class ProjectWithoutTaskSerializer(serializers.ModelSerializer):
response['dimension'] = instance.tasks.first().dimension if instance.tasks.count() else None
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')
@ -583,11 +577,6 @@ class ProjectSerializer(ProjectWithoutTaskSerializer):
raise serializers.ValidationError('All label names must be unique for the project')
return value
def to_representation(self, instance):
response = serializers.ModelSerializer.to_representation(self, instance) # ignoring subsets here
response['dimension'] = instance.tasks.first().dimension if instance.tasks.count() else None
return response
class ExceptionSerializer(serializers.Serializer):
system = serializers.CharField(max_length=255)
client = serializers.CharField(max_length=255)

@ -57,7 +57,7 @@ from cvat.apps.engine.serializers import (
AboutSerializer, AnnotationFileSerializer, BasicUserSerializer,
DataMetaSerializer, DataSerializer, ExceptionSerializer,
FileInfoSerializer, JobSerializer, LabeledDataSerializer,
LogEventSerializer, ProjectSerializer, ProjectSearchSerializer, ProjectWithoutTaskSerializer,
LogEventSerializer, ProjectSerializer, ProjectSearchSerializer,
RqStatusSerializer, TaskSerializer, UserSerializer, PluginsSerializer, ReviewSerializer,
CombinedReviewSerializer, IssueSerializer, CombinedIssueSerializer, CommentSerializer,
CloudStorageSerializer, BaseCloudStorageSerializer, TaskFileSerializer,)
@ -228,9 +228,7 @@ class ProjectFilter(filters.FilterSet):
openapi.Parameter('status', openapi.IN_QUERY, description="Find all projects with a specific status",
type=openapi.TYPE_STRING, enum=[str(i) for i in StatusChoice]),
openapi.Parameter('names_only', openapi.IN_QUERY, description="Returns only names and id's of projects.",
type=openapi.TYPE_BOOLEAN),
openapi.Parameter('without_tasks', openapi.IN_QUERY, description="Returns only projects entities without related tasks",
type=openapi.TYPE_BOOLEAN)],))
type=openapi.TYPE_BOOLEAN)]))
@method_decorator(name='create', decorator=swagger_auto_schema(operation_summary='Method creates a new project'))
@method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a specific project'))
@method_decorator(name='destroy', decorator=swagger_auto_schema(operation_summary='Method deletes a specific project'))
@ -247,8 +245,6 @@ class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
return TaskSerializer
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
@ -424,7 +420,7 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
serializer_class = TaskSerializer
search_fields = ("name", "owner__username", "mode", "status")
filterset_class = TaskFilter
ordering_fields = ("id", "name", "owner", "status", "assignee")
ordering_fields = ("id", "name", "owner", "status", "assignee", "subset")
def get_permissions(self):
http_method = self.request.method

Loading…
Cancel
Save