Merge remote-tracking branch 'upstream/develop' into dkru/cypress-test-case-17-lock-hide-features

main
Kruchinin 5 years ago
commit 243298526a

@ -5,10 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.2.0] - Unreleased
## [1.2.0-beta] - Unreleased
### Added
- Removed Z-Order flag from task creation process
-
### Changed
-
### Deprecated
-
### Removed
-
### Fixed
-
### Security
-
## [1.2.0-alpha] - 2020-11-09
### Added
- Ability to login into CVAT-UI with token from api/v1/auth/login (<https://github.com/openvinotoolkit/cvat/pull/2234>)
- Added layout grids toggling ('ctrl + alt + Enter')
- Added password reset functionality (<https://github.com/opencv/cvat/pull/2058>)
@ -29,6 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Optional chaining plugin for cvat-canvas and cvat-ui (<https://github.com/openvinotoolkit/cvat/pull/2249>)
- MOTS png mask format support (<https://github.com/openvinotoolkit/cvat/pull/2198>)
- Ability to correct upload video with a rotation record in the metadata (<https://github.com/openvinotoolkit/cvat/pull/2218>)
- User search field for assignee fields (<https://github.com/openvinotoolkit/cvat/pull/2370>)
### Changed
@ -42,13 +69,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Track frames in skips between annotation is presented in MOT and MOTS formats are marked `outside` (<https://github.com/openvinotoolkit/cvat/pull/2198>)
- UI packages installation with `npm ci` instead of `npm install` (<https://github.com/openvinotoolkit/cvat/pull/2350>)
### Deprecated
-
### Removed
-
- Removed Z-Order flag from task creation process
### Fixed
@ -72,10 +95,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 'List of tasks' Kibana visualization (<https://github.com/openvinotoolkit/cvat/pull/2361>)
- An error on exporting not `jpg` or `png` images in TF Detection API format (<https://github.com/openvinotoolkit/datumaro/issues/35>)
### Security
-
## [1.1.0] - 2020-08-31
### Added

@ -124,4 +124,5 @@ Other ways to ask questions and get our support:
- [VentureBeat: Intel open-sources CVAT, a toolkit for data labeling](https://venturebeat.com/2019/03/05/intel-open-sources-cvat-a-toolkit-for-data-labeling/)
## Projects using CVAT
- [Onepanel](https://github.com/onepanelio/core) - Onepanel is an open source vision AI platform that fully integrates CVAT with scalable data processing and parallelized training pipelines.

@ -4,6 +4,7 @@
module.exports = {
env: {
amd: true,
node: false,
browser: true,
es6: true,

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.8.1",
"version": "3.9.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

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

@ -17,26 +17,6 @@
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
function attachUsers(task, users) {
if (task.assignee !== null) {
[task.assignee] = users.filter((user) => user.id === task.assignee);
}
for (const segment of task.segments) {
for (const job of segment.jobs) {
if (job.assignee !== null) {
[job.assignee] = users.filter((user) => user.id === job.assignee);
}
}
}
if (task.owner !== null) {
[task.owner] = users.filter((user) => user.id === task.owner);
}
return task;
}
function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list;
cvat.plugins.register.implementation = PluginRegistry.register.bind(cvat);
@ -122,7 +102,10 @@
cvat.users.get.implementation = async (filter) => {
checkFilter(filter, {
id: isInteger,
self: isBoolean,
search: isString,
limit: isInteger,
});
let users = null;
@ -130,7 +113,13 @@
users = await serverProxy.users.getSelf();
users = [users];
} else {
users = await serverProxy.users.getUsers();
const searchParams = {};
for (const key in filter) {
if (filter[key] && key !== 'self') {
searchParams[key] = filter[key];
}
}
users = await serverProxy.users.getUsers(new URLSearchParams(searchParams).toString());
}
users = users.map((user) => new User(user));
@ -163,8 +152,7 @@
// If task was found by its id, then create task instance and get Job instance from it
if (tasks !== null && tasks.length) {
const users = (await serverProxy.users.getUsers()).map((userData) => new User(userData));
const task = new Task(attachUsers(tasks[0], users));
const task = new Task(tasks[0]);
return filter.jobID ? task.jobs.filter((job) => job.id === filter.jobID) : task.jobs;
}
@ -203,9 +191,8 @@
}
}
const users = (await serverProxy.users.getUsers()).map((userData) => new User(userData));
const tasksData = await serverProxy.tasks.getTasks(searchParams.toString());
const tasks = tasksData.map((task) => attachUsers(task, users)).map((task) => new Task(task));
const tasks = tasksData.map((task) => new Task(task));
tasks.count = tasksData.count;

@ -500,20 +500,14 @@
}
}
async function getUsers(id = null) {
async function getUsers(filter = 'page_size=all') {
const { backendAPI } = config;
let response = null;
try {
if (id === null) {
response = await Axios.get(`${backendAPI}/users?page_size=all`, {
proxy: config.proxy,
});
} else {
response = await Axios.get(`${backendAPI}/users/${id}`, {
proxy: config.proxy,
});
}
response = await Axios.get(`${backendAPI}/users?${filter}`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}

@ -686,6 +686,8 @@
}
}
if (data.assignee) data.assignee = new User(data.assignee);
Object.defineProperties(
this,
Object.freeze({
@ -883,6 +885,9 @@
}
}
if (data.assignee) data.assignee = new User(data.assignee);
if (data.owner) data.owner = new User(data.owner);
data.labels = [];
data.jobs = [];
data.files = Object.freeze({
@ -1440,7 +1445,7 @@
if (this.id) {
const jobData = {
status: this.status,
assignee: this.assignee ? this.assignee.id : null,
assignee_id: this.assignee ? this.assignee.id : null,
};
await serverProxy.jobs.saveJob(this.id, jobData);
@ -1649,7 +1654,7 @@
if (typeof this.id !== 'undefined') {
// If the task has been already created, we update it
const taskData = {
assignee: this.assignee ? this.assignee.id : null,
assignee_id: this.assignee ? this.assignee.id : null,
name: this.name,
bug_tracker: this.bugTracker,
labels: [...this.labels.map((el) => el.toJSON())],

@ -154,7 +154,11 @@ const tasksDummyData = {
name: 'Test',
size: 1,
mode: 'annotation',
owner: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: '',
created_date: '2019-09-05T11:59:22.987942Z',
@ -194,7 +198,11 @@ const tasksDummyData = {
name: 'Image Task',
size: 9,
mode: 'annotation',
owner: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: '',
created_date: '2019-06-18T13:05:08.941304+03:00',
@ -239,7 +247,11 @@ const tasksDummyData = {
name: 'Video Task',
size: 5002,
mode: 'interpolation',
owner: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: '',
created_date: '2019-06-21T16:34:49.199691+03:00',
@ -558,7 +570,11 @@ const tasksDummyData = {
name: 'Test Task',
size: 5002,
mode: 'interpolation',
owner: 2,
owner: {
url: 'http://localhost:7000/api/v1/users/2',
id: 2,
username: 'bsekache',
},
assignee: null,
bug_tracker: '',
created_date: '2019-05-16T13:08:00.621747+03:00',
@ -767,7 +783,11 @@ const tasksDummyData = {
name: 'Video',
size: 75,
mode: 'interpolation',
owner: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: '',
created_date: '2019-05-15T11:40:19.487999+03:00',
@ -964,7 +984,11 @@ const tasksDummyData = {
name: 'Labels Set',
size: 9,
mode: 'annotation',
owner: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: 'http://bugtracker.com/issue12345',
created_date: '2019-05-13T15:35:29.871003+03:00',

@ -94,8 +94,8 @@ class ServerProxy {
const object = tasksDummyData.results.filter((task) => task.id === id)[0];
for (const prop in taskData) {
if (
Object.prototype.hasOwnProperty.call(taskData, prop) &&
Object.prototype.hasOwnProperty.call(object, prop)
Object.prototype.hasOwnProperty.call(taskData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)
) {
object[prop] = taskData[prop];
}
@ -110,7 +110,10 @@ class ServerProxy {
name: taskData.name,
size: 5000,
mode: 'interpolation',
owner: 2,
owner: {
id: 2,
username: 'bsekache',
},
assignee: null,
bug_tracker: taskData.bug_tracker,
created_date: '2019-05-16T13:08:00.621747+03:00',
@ -175,8 +178,8 @@ class ServerProxy {
for (const prop in jobData) {
if (
Object.prototype.hasOwnProperty.call(jobData, prop) &&
Object.prototype.hasOwnProperty.call(object, prop)
Object.prototype.hasOwnProperty.call(jobData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)
) {
object[prop] = jobData[prop];
}

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.9.14",
"version": "1.10.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1179,6 +1179,11 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
"@types/lodash": {
"version": "4.14.165",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz",
"integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg=="
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -17047,7 +17052,7 @@
},
"fs-minipass": {
"version": "1.2.7",
"resolved": false,
"resolved": "",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"optional": true,
"requires": {
@ -17062,7 +17067,7 @@
},
"gauge": {
"version": "2.7.4",
"resolved": false,
"resolved": "",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"optional": true,
"requires": {
@ -17078,7 +17083,7 @@
},
"glob": {
"version": "7.1.6",
"resolved": false,
"resolved": "",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"optional": true,
"requires": {
@ -17116,7 +17121,7 @@
},
"inflight": {
"version": "1.0.6",
"resolved": false,
"resolved": "",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"optional": true,
"requires": {
@ -17168,7 +17173,7 @@
},
"minipass": {
"version": "2.9.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"optional": true,
"requires": {
@ -17178,7 +17183,7 @@
},
"minizlib": {
"version": "1.3.3",
"resolved": false,
"resolved": "",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"optional": true,
"requires": {
@ -17213,7 +17218,7 @@
},
"node-pre-gyp": {
"version": "0.14.0",
"resolved": false,
"resolved": "",
"integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==",
"optional": true,
"requires": {
@ -17267,7 +17272,7 @@
},
"npmlog": {
"version": "4.1.2",
"resolved": false,
"resolved": "",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"optional": true,
"requires": {
@ -17291,7 +17296,7 @@
},
"once": {
"version": "1.4.0",
"resolved": false,
"resolved": "",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"optional": true,
"requires": {
@ -17361,7 +17366,7 @@
},
"rimraf": {
"version": "2.7.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"optional": true,
"requires": {
@ -17441,7 +17446,7 @@
},
"tar": {
"version": "4.4.13",
"resolved": false,
"resolved": "",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"optional": true,
"requires": {
@ -17471,13 +17476,13 @@
},
"wrappy": {
"version": "1.0.2",
"resolved": false,
"resolved": "",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"optional": true
},
"yallist": {
"version": "3.1.1",
"resolved": false,
"resolved": "",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"optional": true
}
@ -25949,9 +25954,9 @@
}
},
"lodash": {
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"lodash._reinterpolate": {
"version": "3.0.0",

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.9.14",
"version": "1.10.0",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
@ -47,6 +47,7 @@
"worker-loader": "^2.0.0"
},
"dependencies": {
"@types/lodash": "^4.14.165",
"@types/platform": "^1.3.3",
"@types/react": "^16.9.53",
"@types/react-color": "^3.0.4",
@ -62,6 +63,7 @@
"cvat-core": "file:../cvat-core",
"dotenv-webpack": "^1.8.0",
"error-stack-parser": "^2.0.6",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"platform": "^1.3.6",
"prop-types": "^15.7.2",

@ -1,36 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper';
const core = getCore();
export enum UsersActionTypes {
GET_USERS = 'GET_USERS',
GET_USERS_SUCCESS = 'GET_USERS_SUCCESS',
GET_USERS_FAILED = 'GET_USERS_FAILED',
}
const usersActions = {
getUsers: () => createAction(UsersActionTypes.GET_USERS),
getUsersSuccess: (users: any[]) => createAction(UsersActionTypes.GET_USERS_SUCCESS, { users }),
getUsersFailed: (error: any) => createAction(UsersActionTypes.GET_USERS_FAILED, { error }),
};
export type UsersActions = ActionUnion<typeof usersActions>;
export function getUsersAsync(): ThunkAction {
return async (dispatch): Promise<void> => {
dispatch(usersActions.getUsers());
try {
const users = await core.users.get();
const wrappedUsers = users.map((userData: any): any => new core.classes.User(userData));
dispatch(usersActions.getUsersSuccess(wrappedUsers));
} catch (error) {
dispatch(usersActions.getUsersFailed(error));
}
};
}

@ -34,7 +34,6 @@ import '../styles.scss';
interface CVATAppProps {
loadFormats: () => void;
loadUsers: () => void;
loadAbout: () => void;
verifyAuthorized: () => void;
loadUserAgreements: () => void;
@ -54,8 +53,6 @@ interface CVATAppProps {
modelsFetching: boolean;
formatsInitialized: boolean;
formatsFetching: boolean;
usersInitialized: boolean;
usersFetching: boolean;
aboutInitialized: boolean;
aboutFetching: boolean;
userAgreementsFetching: boolean;
@ -92,7 +89,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
const {
verifyAuthorized,
loadFormats,
loadUsers,
loadAbout,
loadUserAgreements,
initPlugins,
@ -102,8 +98,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
userFetching,
formatsInitialized,
formatsFetching,
usersInitialized,
usersFetching,
aboutInitialized,
aboutFetching,
pluginsInitialized,
@ -143,10 +137,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
loadFormats();
}
if (!usersInitialized && !usersFetching) {
loadUsers();
}
if (!aboutInitialized && !aboutFetching) {
loadAbout();
}
@ -235,7 +225,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
public render(): JSX.Element {
const {
userInitialized,
usersInitialized,
aboutInitialized,
pluginsInitialized,
formatsInitialized,
@ -248,7 +237,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
const readyForRender =
(userInitialized && (user == null || !user.isVerified)) ||
(userInitialized && formatsInitialized && pluginsInitialized && usersInitialized && aboutInitialized);
(userInitialized && formatsInitialized && pluginsInitialized && aboutInitialized);
const subKeyMap = {
SWITCH_SHORTCUTS: keyMap.SWITCH_SHORTCUTS,
@ -270,7 +259,10 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
if (showPlatformNotification()) {
stopNotifications(false);
const info = platformInfo();
const {
name, version, engine, os,
} = platformInfo();
Modal.warning({
title: 'Unsupported platform detected',
content: (
@ -278,7 +270,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
<Row>
<Col>
<Text>
{`The browser you are using is ${info.name} ${info.version} based on ${info.engine} .` +
{`The browser you are using is ${name} ${version} based on ${engine}.` +
' CVAT was tested in the latest versions of Chrome and Firefox.' +
' We recommend to use Chrome (or another Chromium based browser)'}
</Text>
@ -286,7 +278,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
</Row>
<Row>
<Col>
<Text type='secondary'>{`The operating system is ${info.os}`}</Text>
<Text type='secondary'>{`The operating system is ${os}`}</Text>
</Col>
</Row>
</>

@ -18,7 +18,7 @@ import patterns from 'utils/validation-patterns';
import { getReposData, syncRepos } from 'utils/git-utils';
import { ActiveInference } from 'reducers/interfaces';
import AutomaticAnnotationProgress from 'components/tasks-page/automatic-annotation-progress';
import UserSelector from './user-selector';
import UserSelector, { User } from './user-selector';
import LabelsEditorComponent from '../labels-editor/labels-editor';
const core = getCore();
@ -27,7 +27,6 @@ interface Props {
previewImage: string;
taskInstance: any;
installedGit: boolean; // change to git repos url
registeredUsers: any[];
activeInference: ActiveInference | null;
cancelAutoAnnotation(): void;
onTaskUpdate: (taskInstance: any) => void;
@ -196,35 +195,26 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
}
private renderUsers(): JSX.Element {
const { taskInstance, registeredUsers, onTaskUpdate } = this.props;
const { taskInstance, onTaskUpdate } = this.props;
const owner = taskInstance.owner ? taskInstance.owner.username : null;
const assignee = taskInstance.assignee ? taskInstance.assignee.username : null;
const assignee = taskInstance.assignee ? taskInstance.assignee : null;
const created = moment(taskInstance.createdDate).format('MMMM Do YYYY');
const assigneeSelect = (
<UserSelector
users={registeredUsers}
value={assignee}
onChange={(value: string): void => {
let [userInstance] = registeredUsers.filter((user: any) => user.username === value);
if (userInstance === undefined) {
userInstance = null;
}
taskInstance.assignee = userInstance;
onSelect={(value: User | null): void => {
taskInstance.assignee = value;
onTaskUpdate(taskInstance);
}}
/>
);
return (
<Row type='flex' justify='space-between' align='middle'>
<Row className='cvat-task-details-user-block' type='flex' justify='space-between' align='middle'>
<Col span={12}>{owner && <Text type='secondary'>{`Created by ${owner} on ${created}`}</Text>}</Col>
<Col span={10}>
<Text type='secondary'>
Assigned to
{assigneeSelect}
</Text>
<Text type='secondary'>Assigned to</Text>
{assigneeSelect}
</Col>
</Row>
);

@ -15,7 +15,7 @@ import moment from 'moment';
import copy from 'copy-to-clipboard';
import getCore from 'cvat-core-wrapper';
import UserSelector from './user-selector';
import UserSelector, { User } from './user-selector';
const core = getCore();
@ -23,14 +23,12 @@ const baseURL = core.config.backendAPI.slice(0, -7);
interface Props {
taskInstance: any;
registeredUsers: any[];
onJobUpdate(jobInstance: any): void;
}
function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
const {
taskInstance,
registeredUsers,
onJobUpdate,
history: { push },
} = props;
@ -100,21 +98,14 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
dataIndex: 'assignee',
key: 'assignee',
render: (jobInstance: any): JSX.Element => {
const assignee = jobInstance.assignee ? jobInstance.assignee.username : null;
const assignee = jobInstance.assignee ? jobInstance.assignee : null;
return (
<UserSelector
users={registeredUsers}
value={assignee}
onChange={(value: string): void => {
let [userInstance] = [...registeredUsers].filter((user: any) => user.username === value);
if (userInstance === undefined) {
userInstance = null;
}
onSelect={(value: User | null): void => {
// eslint-disable-next-line
jobInstance.assignee = userInstance;
jobInstance.assignee = value;
onJobUpdate(jobInstance);
}}
/>

@ -17,6 +17,12 @@
padding: 20px;
background: $background-color-1;
.cvat-task-details-user-block {
> div:nth-child(2) > span {
margin-right: 8px;
}
}
> div:nth-child(2) {
> div:nth-child(2) {
padding-left: 20px;
@ -87,11 +93,6 @@
background-color: $background-color-2;
}
.cvat-user-selector {
margin-left: 10px;
width: 150px;
}
.cvat-open-bug-tracker-button {
margin-left: 15px;
}

@ -2,30 +2,114 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Select from 'antd/lib/select';
import React, { useState, useEffect, useRef } from 'react';
import Autocomplete from 'antd/lib/auto-complete';
import Input from 'antd/lib/input';
import getCore from 'cvat-core-wrapper';
import { SelectValue } from 'antd/lib/select';
import debounce from 'lodash/debounce';
const core = getCore();
export interface User {
id: number;
username: string;
}
interface Props {
value: string | null;
users: any[];
onChange: (user: string) => void;
value: User | null;
onSelect: (user: User | null) => void;
}
const searchUsers = debounce(
(searchValue: string, setUsers: (users: User[]) => void): void => {
core.users
.get({
search: searchValue,
limit: 10,
})
.then((result: User[]) => {
if (result) {
setUsers(result);
}
});
},
250,
{
maxWait: 750,
},
);
export default function UserSelector(props: Props): JSX.Element {
const { value, users, onChange } = props;
const { value, onSelect } = props;
const [searchPhrase, setSearchPhrase] = useState('');
const [users, setUsers] = useState<User[]>([]);
const autocompleteRef = useRef<Autocomplete | null>(null);
const handleSearch = (searchValue: string): void => {
if (searchValue) {
searchUsers(searchValue, setUsers);
} else {
setUsers([]);
}
setSearchPhrase(searchValue);
};
const handleFocus = (open: boolean): void => {
if (!users.length && open) {
core.users.get({ limit: 10 }).then((result: User[]) => {
if (result) {
setUsers(result);
}
});
}
if (!open && searchPhrase !== value?.username) {
setSearchPhrase('');
if (value) {
onSelect(null);
}
}
};
const handleSelect = (_value: SelectValue): void => {
setSearchPhrase(users.filter((user) => user.id === +_value)[0].username);
onSelect(_value ? users.filter((user) => user.id === +_value)[0] : null);
};
useEffect(() => {
if (value && !users.filter((user) => user.id === value.id).length) {
core.users.get({ id: value.id }).then((result: User[]) => {
const [user] = result;
setUsers([
...users,
{
id: user.id,
username: user.username,
},
]);
setSearchPhrase(user.username);
});
}
}, [value]);
return (
<Select defaultValue={value || '—'} size='small' showSearch className='cvat-user-selector' onChange={onChange}>
<Select.Option key='-1' value='—'>
</Select.Option>
{users.map(
(user): JSX.Element => (
<Select.Option key={user.id} value={user.username}>
{user.username}
</Select.Option>
),
)}
</Select>
<Autocomplete
ref={autocompleteRef}
value={searchPhrase}
placeholder='Select a user'
onSearch={handleSearch}
onSelect={handleSelect}
className='cvat-user-search-field'
onDropdownVisibleChange={handleFocus}
dataSource={users.map((user) => ({
value: user.id.toString(),
text: user.username,
}))}
>
<Input onPressEnter={() => autocompleteRef.current?.blur()} />
</Autocomplete>
);
}

@ -15,7 +15,6 @@ interface OwnProps {
}
interface StateToProps {
registeredUsers: any[];
activeInference: ActiveInference | null;
installedGit: boolean;
}
@ -29,7 +28,6 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const { list } = state.plugins;
return {
registeredUsers: state.users.users,
installedGit: list.GIT_INTEGRATION,
activeInference: state.models.inferences[own.task.instance.id] || null,
};
@ -47,14 +45,15 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
}
function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const { task, installedGit, activeInference, registeredUsers, cancelAutoAnnotation, onTaskUpdate } = props;
const {
task, installedGit, activeInference, cancelAutoAnnotation, onTaskUpdate,
} = props;
return (
<DetailsComponent
previewImage={task.preview}
taskInstance={task.instance}
installedGit={installedGit}
registeredUsers={registeredUsers}
activeInference={activeInference}
onTaskUpdate={onTaskUpdate}
cancelAutoAnnotation={cancelAutoAnnotation}

@ -7,38 +7,26 @@ import { connect } from 'react-redux';
import JobListComponent from 'components/task-page/job-list';
import { updateJobAsync } from 'actions/tasks-actions';
import { Task, CombinedState } from 'reducers/interfaces';
import { Task } from 'reducers/interfaces';
interface OwnProps {
task: Task;
}
interface StateToProps {
registeredUsers: any[];
}
interface DispatchToProps {
onJobUpdate(jobInstance: any): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
return {
registeredUsers: state.users.users,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onJobUpdate: (jobInstance: any): void => dispatch(updateJobAsync(jobInstance)),
};
}
function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const { task, registeredUsers, onJobUpdate } = props;
function TaskPageContainer(props: DispatchToProps & OwnProps): JSX.Element {
const { task, onJobUpdate } = props;
return (
<JobListComponent taskInstance={task.instance} registeredUsers={registeredUsers} onJobUpdate={onJobUpdate} />
);
return <JobListComponent taskInstance={task.instance} onJobUpdate={onJobUpdate} />;
}
export default connect(mapStateToProps, mapDispatchToProps)(TaskPageContainer);
export default connect(null, mapDispatchToProps)(TaskPageContainer);

@ -10,7 +10,6 @@ import { getPluginsAsync } from 'actions/plugins-actions';
import { switchSettingsDialog } from 'actions/settings-actions';
import { shortcutsActions } from 'actions/shortcuts-actions';
import { getUserAgreementsAsync } from 'actions/useragreements-actions';
import { getUsersAsync } from 'actions/users-actions';
import CVATApplication from 'components/cvat-app';
import LayoutGrid from 'components/layout-grid/layout-grid';
import logger, { LogType } from 'cvat-logger';
@ -34,8 +33,6 @@ interface StateToProps {
modelsFetching: boolean;
userInitialized: boolean;
userFetching: boolean;
usersInitialized: boolean;
usersFetching: boolean;
aboutInitialized: boolean;
aboutFetching: boolean;
formatsInitialized: boolean;
@ -55,7 +52,6 @@ interface StateToProps {
interface DispatchToProps {
loadFormats: () => void;
verifyAuthorized: () => void;
loadUsers: () => void;
loadAbout: () => void;
initModels: () => void;
initPlugins: () => void;
@ -71,7 +67,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { plugins } = state;
const { auth } = state;
const { formats } = state;
const { users } = state;
const { about } = state;
const { shortcuts } = state;
const { userAgreements } = state;
@ -84,8 +79,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
pluginsFetching: plugins.fetching,
modelsInitialized: models.initialized,
modelsFetching: models.fetching,
usersInitialized: users.initialized,
usersFetching: users.fetching,
aboutInitialized: about.initialized,
aboutFetching: about.fetching,
formatsInitialized: formats.initialized,
@ -110,7 +103,6 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
loadUserAgreements: (): void => dispatch(getUserAgreementsAsync()),
initPlugins: (): void => dispatch(getPluginsAsync()),
initModels: (): void => dispatch(getModelsAsync()),
loadUsers: (): void => dispatch(getUsersAsync()),
loadAbout: (): void => dispatch(getAboutAsync()),
resetErrors: (): void => dispatch(resetErrors()),
resetMessages: (): void => dispatch(resetMessages()),

@ -93,12 +93,6 @@ export interface PluginsState {
list: PluginsList;
}
export interface UsersState {
users: any[];
fetching: boolean;
initialized: boolean;
}
export interface AboutState {
server: any;
packageVersion: {
@ -494,7 +488,6 @@ export interface MetaState {
export interface CombinedState {
auth: AuthState;
tasks: TasksState;
users: UsersState;
about: AboutState;
share: ShareState;
formats: FormatsState;

@ -9,7 +9,6 @@ import { FormatsActionTypes } from 'actions/formats-actions';
import { ModelsActionTypes } from 'actions/models-actions';
import { ShareActionTypes } from 'actions/share-actions';
import { TasksActionTypes } from 'actions/tasks-actions';
import { UsersActionTypes } from 'actions/users-actions';
import { AboutActionTypes } from 'actions/about-actions';
import { AnnotationActionTypes } from 'actions/annotation-actions';
import { NotificationsActionType } from 'actions/notification-actions';
@ -357,8 +356,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
tasks: {
...state.errors.tasks,
updating: {
message:
'Could not update ' + `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
message: `Could not update <a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
},
},
@ -431,21 +429,6 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case UsersActionTypes.GET_USERS_FAILED: {
return {
...state,
errors: {
...state.errors,
users: {
...state.errors.users,
fetching: {
message: 'Could not get users from the server',
reason: action.payload.error.toString(),
},
},
},
};
}
case AboutActionTypes.GET_ABOUT_FAILED: {
return {
...state,

@ -5,7 +5,6 @@
import { combineReducers, Reducer } from 'redux';
import authReducer from './auth-reducer';
import tasksReducer from './tasks-reducer';
import usersReducer from './users-reducer';
import aboutReducer from './about-reducer';
import shareReducer from './share-reducer';
import formatsReducer from './formats-reducer';
@ -21,7 +20,6 @@ export default function createRootReducer(): Reducer {
return combineReducers({
auth: authReducer,
tasks: tasksReducer,
users: usersReducer,
about: aboutReducer,
share: shareReducer,
formats: formatsReducer,

@ -1,48 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { BoundariesActionTypes, BoundariesActions } from 'actions/boundaries-actions';
import { AuthActionTypes, AuthActions } from 'actions/auth-actions';
import { UsersActionTypes, UsersActions } from 'actions/users-actions';
import { UsersState } from './interfaces';
const defaultState: UsersState = {
users: [],
fetching: false,
initialized: false,
};
export default function (
state: UsersState = defaultState,
action: UsersActions | AuthActions | BoundariesActions,
): UsersState {
switch (action.type) {
case UsersActionTypes.GET_USERS: {
return {
...state,
fetching: true,
initialized: false,
};
}
case UsersActionTypes.GET_USERS_SUCCESS:
return {
...state,
fetching: false,
initialized: true,
users: action.payload.users,
};
case UsersActionTypes.GET_USERS_FAILED:
return {
...state,
fetching: false,
initialized: true,
};
case BoundariesActionTypes.RESET_AFTER_ERROR:
case AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState };
}
default:
return state;
}
}

@ -4,6 +4,6 @@
from cvat.utils.version import get_version
VERSION = (1, 2, 0, 'alpha', 0)
VERSION = (1, 2, 0, 'beta', 0)
__version__ = get_version(VERSION)

@ -218,8 +218,6 @@ class TaskExportTest(_DbTestBase):
def _generate_task(self, images):
task = {
"name": "my task #1",
"owner": '',
"assignee": '',
"overlap": 0,
"segment_size": 100,
"labels": [
@ -438,8 +436,6 @@ class FrameMatchingTest(_DbTestBase):
def _generate_task(self, images):
task = {
"name": "my task #1",
"owner": '',
"assignee": '',
"overlap": 0,
"segment_size": 100,
"labels": [

@ -13,6 +13,36 @@ from cvat.apps.engine import models
from cvat.apps.engine.log import slogger
from cvat.apps.dataset_manager.formats.utils import get_label_color
class BasicUserSerializer(serializers.ModelSerializer):
def validate(self, data):
if hasattr(self, 'initial_data'):
unknown_keys = set(self.initial_data.keys()) - set(self.fields.keys())
if unknown_keys:
if set(['is_staff', 'is_superuser', 'groups']) & unknown_keys:
message = 'You do not have permissions to access some of' + \
' these fields: {}'.format(unknown_keys)
else:
message = 'Got unknown fields: {}'.format(unknown_keys)
raise serializers.ValidationError(message)
return data
class Meta:
model = User
fields = ('url', 'id', 'username', 'first_name', 'last_name')
ordering = ['-id']
class UserSerializer(serializers.ModelSerializer):
groups = serializers.SlugRelatedField(many=True,
slug_field='name', queryset=Group.objects.all())
class Meta:
model = User
fields = ('url', 'id', 'username', 'first_name', 'last_name', 'email',
'groups', 'is_staff', 'is_superuser', 'is_active', 'last_login',
'date_joined')
read_only_fields = ('last_login', 'date_joined')
write_only_fields = ('password', )
ordering = ['-id']
class AttributeSerializer(serializers.ModelSerializer):
class Meta:
@ -53,16 +83,21 @@ class JobSerializer(serializers.ModelSerializer):
task_id = serializers.ReadOnlyField(source="segment.task.id")
start_frame = serializers.ReadOnlyField(source="segment.start_frame")
stop_frame = serializers.ReadOnlyField(source="segment.stop_frame")
assignee = BasicUserSerializer(allow_null=True, required=False)
assignee_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
class Meta:
model = models.Job
fields = ('url', 'id', 'assignee', 'status', 'start_frame',
fields = ('url', 'id', 'assignee', 'assignee_id', 'status', 'start_frame',
'stop_frame', 'task_id')
class SimpleJobSerializer(serializers.ModelSerializer):
assignee = BasicUserSerializer(allow_null=True)
assignee_id = serializers.IntegerField(write_only=True, allow_null=True)
class Meta:
model = models.Job
fields = ('url', 'id', 'assignee', 'status')
fields = ('url', 'id', 'assignee', 'assignee_id', 'status')
class SegmentSerializer(serializers.ModelSerializer):
jobs = SimpleJobSerializer(many=True, source='job_set')
@ -239,14 +274,18 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
size = serializers.ReadOnlyField(source='data.size')
image_quality = serializers.ReadOnlyField(source='data.image_quality')
data = serializers.ReadOnlyField(source='data.id')
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.Task
fields = ('url', 'id', 'name', 'mode', 'owner', 'assignee',
fields = ('url', 'id', 'name', 'mode', 'owner', 'assignee', 'owner_id', 'assignee_id',
'bug_tracker', 'created_date', 'updated_date', 'overlap',
'segment_size', 'status', 'labels', 'segments',
'project', 'data_chunk_size', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data')
read_only_fields = ('mode', 'created_date', 'updated_date', 'status', 'data_chunk_size',
read_only_fields = ('mode', 'created_date', 'updated_date', 'status', 'data_chunk_size', 'owner', 'asignee',
'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data')
write_once_fields = ('overlap', 'segment_size')
ordering = ['-id']
@ -278,8 +317,8 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
# pylint: disable=no-self-use
def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name)
instance.owner = validated_data.get('owner', instance.owner)
instance.assignee = validated_data.get('assignee', instance.assignee)
instance.owner_id = validated_data.get('owner_id', instance.owner_id)
instance.assignee_id = validated_data.get('assignee_id', instance.assignee_id)
instance.bug_tracker = validated_data.get('bug_tracker',
instance.bug_tracker)
instance.project = validated_data.get('project', instance.project)
@ -339,37 +378,6 @@ class ProjectSerializer(serializers.ModelSerializer):
read_only_fields = ('created_date', 'updated_date', 'status')
ordering = ['-id']
class BasicUserSerializer(serializers.ModelSerializer):
def validate(self, data):
if hasattr(self, 'initial_data'):
unknown_keys = set(self.initial_data.keys()) - set(self.fields.keys())
if unknown_keys:
if set(['is_staff', 'is_superuser', 'groups']) & unknown_keys:
message = 'You do not have permissions to access some of' + \
' these fields: {}'.format(unknown_keys)
else:
message = 'Got unknown fields: {}'.format(unknown_keys)
raise serializers.ValidationError(message)
return data
class Meta:
model = User
fields = ('url', 'id', 'username', 'first_name', 'last_name')
ordering = ['-id']
class UserSerializer(serializers.ModelSerializer):
groups = serializers.SlugRelatedField(many=True,
slug_field='name', queryset=Group.objects.all())
class Meta:
model = User
fields = ('url', 'id', 'username', 'first_name', 'last_name', 'email',
'groups', 'is_staff', 'is_superuser', 'is_active', 'last_login',
'date_joined')
read_only_fields = ('last_login', 'date_joined')
write_only_fields = ('password', )
ordering = ['-id']
class ExceptionSerializer(serializers.Serializer):
system = serializers.CharField(max_length=255)
client = serializers.CharField(max_length=255)

@ -297,47 +297,47 @@ class JobUpdateAPITestCase(APITestCase):
self.assertEqual(response.data["id"], self.job.id)
self.assertEqual(response.data["status"], data.get('status', self.job.status))
assignee = self.job.assignee.id if self.job.assignee else None
self.assertEqual(response.data["assignee"], data.get('assignee', assignee))
self.assertEqual(response.data["assignee"]["id"], data.get('assignee_id', assignee))
self.assertEqual(response.data["start_frame"], self.job.segment.start_frame)
self.assertEqual(response.data["stop_frame"], self.job.segment.stop_frame)
def test_api_v1_jobs_id_admin(self):
data = {"status": StatusChoice.COMPLETED, "assignee": self.owner.id}
data = {"status": StatusChoice.COMPLETED, "assignee_id": self.owner.id}
response = self._run_api_v1_jobs_id(self.job.id, self.admin, data)
self._check_request(response, data)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.admin, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_api_v1_jobs_id_owner(self):
data = {"status": StatusChoice.VALIDATION, "assignee": self.annotator.id}
data = {"status": StatusChoice.VALIDATION, "assignee_id": self.annotator.id}
response = self._run_api_v1_jobs_id(self.job.id, self.owner, data)
self._check_request(response, data)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.owner, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_api_v1_jobs_id_annotator(self):
data = {"status": StatusChoice.ANNOTATION, "assignee": self.user.id}
data = {"status": StatusChoice.ANNOTATION, "assignee_id": self.user.id}
response = self._run_api_v1_jobs_id(self.job.id, self.annotator, data)
self._check_request(response, data)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.annotator, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_api_v1_jobs_id_observer(self):
data = {"status": StatusChoice.ANNOTATION, "assignee": self.admin.id}
data = {"status": StatusChoice.ANNOTATION, "assignee_id": self.admin.id}
response = self._run_api_v1_jobs_id(self.job.id, self.observer, data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.observer, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_api_v1_jobs_id_user(self):
data = {"status": StatusChoice.ANNOTATION, "assignee": self.user.id}
data = {"status": StatusChoice.ANNOTATION, "assignee_id": self.user.id}
response = self._run_api_v1_jobs_id(self.job.id, self.user, data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.user, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_api_v1_jobs_id_no_auth(self):
data = {"status": StatusChoice.ANNOTATION, "assignee": self.user.id}
data = {"status": StatusChoice.ANNOTATION, "assignee_id": self.user.id}
response = self._run_api_v1_jobs_id(self.job.id, None, data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
response = self._run_api_v1_jobs_id(self.job.id + 10, None, data)
@ -356,7 +356,7 @@ class JobPartialUpdateAPITestCase(JobUpdateAPITestCase):
self._check_request(response, data)
def test_api_v1_jobs_id_admin_partial(self):
data = {"assignee": self.user.id}
data = {"assignee_id": self.user.id}
response = self._run_api_v1_jobs_id(self.job.id, self.owner, data)
self._check_request(response, data)
@ -1073,9 +1073,11 @@ class TaskGetAPITestCase(APITestCase):
self.assertEqual(response.data["size"], db_task.data.size)
self.assertEqual(response.data["mode"], db_task.mode)
owner = db_task.owner.id if db_task.owner else None
self.assertEqual(response.data["owner"], owner)
response_owner = response.data["owner"]["id"] if response.data["owner"] else None
self.assertEqual(response_owner, owner)
assignee = db_task.assignee.id if db_task.assignee else None
self.assertEqual(response.data["assignee"], assignee)
response_assignee = response.data["assignee"]["id"] if response.data["assignee"] else None
self.assertEqual(response_assignee, assignee)
self.assertEqual(response.data["overlap"], db_task.overlap)
self.assertEqual(response.data["segment_size"], db_task.segment_size)
self.assertEqual(response.data["image_quality"], db_task.data.image_quality)
@ -1179,11 +1181,13 @@ class TaskUpdateAPITestCase(APITestCase):
mode = data.get("mode", db_task.mode)
self.assertEqual(response.data["mode"], mode)
owner = db_task.owner.id if db_task.owner else None
owner = data.get("owner", owner)
self.assertEqual(response.data["owner"], owner)
owner = data.get("owner_id", owner)
response_owner = response.data["owner"]["id"] if response.data["owner"] else None
self.assertEqual(response_owner, owner)
assignee = db_task.assignee.id if db_task.assignee else None
assignee = data.get("assignee", assignee)
self.assertEqual(response.data["assignee"], assignee)
assignee = data.get("assignee_id", assignee)
response_assignee = response.data["assignee"]["id"] if response.data["assignee"] else None
self.assertEqual(response_assignee, assignee)
self.assertEqual(response.data["overlap"], db_task.overlap)
self.assertEqual(response.data["segment_size"], db_task.segment_size)
image_quality = data.get("image_quality", db_task.data.image_quality)
@ -1213,7 +1217,7 @@ class TaskUpdateAPITestCase(APITestCase):
def test_api_v1_tasks_id_admin(self):
data = {
"name": "new name for the task",
"owner": self.owner.id,
"owner_id": self.owner.id,
"labels": [{
"name": "non-vehicle",
"attributes": [{
@ -1229,7 +1233,7 @@ class TaskUpdateAPITestCase(APITestCase):
def test_api_v1_tasks_id_user(self):
data = {
"name": "new name for the task",
"owner": self.assignee.id,
"owner_id": self.assignee.id,
"labels": [{
"name": "car",
"attributes": [{
@ -1277,7 +1281,7 @@ class TaskPartialUpdateAPITestCase(TaskUpdateAPITestCase):
data = {
"name": "new name for the task",
"owner": self.owner.id
"owner_id": self.owner.id
}
self._check_api_v1_tasks_id(self.admin, data)
# Now owner is updated, but self.db_tasks are obsolete
@ -1300,8 +1304,8 @@ class TaskPartialUpdateAPITestCase(TaskUpdateAPITestCase):
self._check_api_v1_tasks_id(self.user, data)
data = {
"owner": self.observer.id,
"assignee": self.annotator.id
"owner_id": self.observer.id,
"assignee_id": self.annotator.id
}
self._check_api_v1_tasks_id(self.user, data)
@ -1339,8 +1343,9 @@ class TaskCreateAPITestCase(APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["name"], data["name"])
self.assertEqual(response.data["mode"], "")
self.assertEqual(response.data["owner"], data.get("owner", user.id))
self.assertEqual(response.data["assignee"], data.get("assignee"))
self.assertEqual(response.data["owner"]["id"], data.get("owner_id", user.id))
assignee = response.data["assignee"]["id"] if response.data["assignee"] else None
self.assertEqual(assignee, data.get("assignee_id", None))
self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", ""))
self.assertEqual(response.data["overlap"], data.get("overlap", None))
self.assertEqual(response.data["segment_size"], data.get("segment_size", 0))
@ -1377,7 +1382,7 @@ class TaskCreateAPITestCase(APITestCase):
def test_api_v1_tasks_user(self):
data = {
"name": "new name for the task",
"owner": self.assignee.id,
"owner_id": self.assignee.id,
"labels": [{
"name": "car",
"attributes": [{
@ -1663,8 +1668,8 @@ class TaskDataAPITestCase(APITestCase):
response = self._get_task(user, task_id)
expected_status_code = status.HTTP_200_OK
if user == self.user and "owner" in spec and spec["owner"] != user.id and \
"assignee" in spec and spec["assignee"] != user.id:
if user == self.user and "owner_id" in spec and spec["owner_id"] != user.id and \
"assignee_id" in spec and spec["assignee_id"] != user.id:
expected_status_code = status.HTTP_403_FORBIDDEN
self.assertEqual(response.status_code, expected_status_code)
@ -1746,8 +1751,8 @@ class TaskDataAPITestCase(APITestCase):
def _test_api_v1_tasks_id_data(self, user):
task_spec = {
"name": "my task #1",
"owner": self.owner.id,
"assignee": self.assignee.id,
"owner_id": self.owner.id,
"assignee_id": self.assignee.id,
"overlap": 0,
"segment_size": 100,
"labels": [
@ -2085,8 +2090,8 @@ class TaskDataAPITestCase(APITestCase):
def test_api_v1_tasks_id_data_no_auth(self):
data = {
"name": "my task #3",
"owner": self.owner.id,
"assignee": self.assignee.id,
"owner_id": self.owner.id,
"assignee_id": self.assignee.id,
"overlap": 0,
"segment_size": 100,
"labels": [
@ -2131,8 +2136,8 @@ class JobAnnotationAPITestCase(APITestCase):
def _create_task(self, owner, assignee):
data = {
"name": "my task #1",
"owner": owner.id,
"assignee": assignee.id,
"owner_id": owner.id,
"assignee_id": assignee.id,
"overlap": 0,
"segment_size": 100,
"labels": [

@ -704,7 +704,16 @@ class JobViewSet(viewsets.GenericViewSet,
return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(data)
class UserFilter(filters.FilterSet):
class Meta:
model = User
fields = ("id",)
@method_decorator(name='list', decorator=swagger_auto_schema(
manual_parameters=[
openapi.Parameter('id',openapi.IN_QUERY,description="A unique number value identifying this user",type=openapi.TYPE_NUMBER),
],
operation_summary='Method provides a paginated list of users registered on the server'))
@method_decorator(name='retrieve', decorator=swagger_auto_schema(
operation_summary='Method provides information of a specific user'))
@ -716,6 +725,8 @@ class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin):
queryset = User.objects.prefetch_related('groups').all().order_by('id')
http_method_names = ['get', 'post', 'head', 'patch', 'delete']
search_fields = ('username', 'first_name', 'last_name')
filterset_class = UserFilter
def get_serializer_class(self):
user = self.request.user

@ -45,16 +45,20 @@ context('Merge/split features', () => {
it('Create rectangle shape on first frame', () => {
goCheckFrameNumber(frameNum);
cy.createRectangle(createRectangleShape2Points);
cy.get('#cvat_canvas_shape_1').should('have.attr', 'x').then(xCoords => {
xCoordinatesObjectFirstFrame = Math.floor(xCoords);
});
cy.get('#cvat_canvas_shape_1')
.should('have.attr', 'x')
.then((xCoords) => {
xCoordinatesObjectFirstFrame = Math.floor(xCoords);
});
});
it('Create rectangle shape on third frame with another position', () => {
goCheckFrameNumber(frameNum + 2);
cy.createRectangle(createRectangleShape2PointsSecond);
cy.get('#cvat_canvas_shape_2').should('have.attr', 'x').then(xCoords => {
xCoordinatesObjectThirdFrame = Math.floor(xCoords);
});
cy.get('#cvat_canvas_shape_2')
.should('have.attr', 'x')
.then((xCoords) => {
xCoordinatesObjectThirdFrame = Math.floor(xCoords);
});
});
it('Merge the objects with "Merge button"', () => {
cy.get('.cvat-merge-control').click();
@ -65,14 +69,20 @@ context('Merge/split features', () => {
});
it('Get a track with keyframes on first and third frame', () => {
cy.get('#cvat_canvas_shape_3').should('exist').and('be.visible');
cy.get('#cvat-objects-sidebar-state-item-3').should('contain', '3').and('contain', 'RECTANGLE TRACK').within(() => {
cy.get('.cvat-object-item-button-keyframe-enabled').should('exist');
});
cy.get('#cvat-objects-sidebar-state-item-3')
.should('contain', '3')
.and('contain', 'RECTANGLE TRACK')
.within(() => {
cy.get('.cvat-object-item-button-keyframe-enabled').should('exist');
});
goCheckFrameNumber(frameNum + 2);
cy.get('#cvat_canvas_shape_3').should('exist').and('be.visible');
cy.get('#cvat-objects-sidebar-state-item-3').should('contain', '3').and('contain', 'RECTANGLE TRACK').within(() => {
cy.get('.cvat-object-item-button-keyframe-enabled').should('exist');
});
cy.get('#cvat-objects-sidebar-state-item-3')
.should('contain', '3')
.and('contain', 'RECTANGLE TRACK')
.within(() => {
cy.get('.cvat-object-item-button-keyframe-enabled').should('exist');
});
});
it('On the second frame and on the fourth frame the track is invisible', () => {
goCheckFrameNumber(frameNum + 1);
@ -82,29 +92,43 @@ context('Merge/split features', () => {
});
it('Go to the second frame and remove "outside" flag from the track. The track now visible.', () => {
goCheckFrameNumber(frameNum + 1);
cy.get('#cvat-objects-sidebar-state-item-3').should('contain', '3').and('contain', 'RECTANGLE TRACK').within(() => {
cy.get('.cvat-object-item-button-outside').click();
cy.get('.cvat-object-item-button-outside-enabled').should('not.exist');
});
cy.get('#cvat-objects-sidebar-state-item-3')
.should('contain', '3')
.and('contain', 'RECTANGLE TRACK')
.within(() => {
cy.get('.cvat-object-item-button-outside').click();
cy.get('.cvat-object-item-button-outside-enabled').should('not.exist');
});
cy.get('#cvat_canvas_shape_3').should('exist').and('be.visible');
});
it('Remove "keyframe" flag from the track. Track now interpolated between position on the first and the third frames.', () => {
cy.get('#cvat-objects-sidebar-state-item-3').should('contain', '3').and('contain', 'RECTANGLE TRACK').within(() => {
cy.get('.cvat-object-item-button-keyframe').click();
cy.get('.cvat-object-item-button-keyframe-enabled').should('not.exist');
});
cy.get('#cvat_canvas_shape_3').should('have.attr', 'x').then(xCoords => {
// expected 9785 to be within 9642..9928
expect(Math.floor(xCoords)).to.be.within(xCoordinatesObjectFirstFrame, xCoordinatesObjectThirdFrame);
});
cy.get('#cvat-objects-sidebar-state-item-3')
.should('contain', '3')
.and('contain', 'RECTANGLE TRACK')
.within(() => {
cy.get('.cvat-object-item-button-keyframe').click();
cy.get('.cvat-object-item-button-keyframe-enabled').should('not.exist');
});
cy.get('#cvat_canvas_shape_3')
.should('have.attr', 'x')
.then((xCoords) => {
// expected 9785 to be within 9642..9928
expect(Math.floor(xCoords)).to.be.within(
xCoordinatesObjectFirstFrame,
xCoordinatesObjectThirdFrame,
);
});
});
it('On the fourth frame remove "keyframe" flag from the track. The track now visible and "outside" flag is disabled.', () => {
goCheckFrameNumber(frameNum + 3);
cy.get('#cvat-objects-sidebar-state-item-3').should('contain', '3').and('contain', 'RECTANGLE TRACK').within(() => {
cy.get('.cvat-object-item-button-keyframe').click();
cy.get('.cvat-object-item-button-keyframe-enabled').should('not.exist');
cy.get('.cvat-object-item-button-outside-enabled').should('not.exist');
});
cy.get('#cvat-objects-sidebar-state-item-3')
.should('contain', '3')
.and('contain', 'RECTANGLE TRACK')
.within(() => {
cy.get('.cvat-object-item-button-keyframe').click();
cy.get('.cvat-object-item-button-keyframe-enabled').should('not.exist');
cy.get('.cvat-object-item-button-outside-enabled').should('not.exist');
});
cy.get('#cvat_canvas_shape_3').should('exist').and('be.visible');
});
it('Split a track with "split" button. Previous track became invisible (has "outside" flag). One more track and it is visible.', () => {
@ -112,14 +136,20 @@ context('Merge/split features', () => {
// A single click does not reproduce the split a track scenario in cypress test.
cy.get('#cvat_canvas_shape_3').click().click();
cy.get('#cvat_canvas_shape_4').should('exist').and('be.hidden');
cy.get('#cvat-objects-sidebar-state-item-4').should('contain', '4').and('contain', 'RECTANGLE TRACK').within(() => {
cy.get('.cvat-object-item-button-outside-enabled').should('exist');
});
cy.get('#cvat-objects-sidebar-state-item-4')
.should('contain', '4')
.and('contain', 'RECTANGLE TRACK')
.within(() => {
cy.get('.cvat-object-item-button-outside-enabled').should('exist');
});
cy.get('#cvat_canvas_shape_5').should('exist').and('be.visible');
cy.get('#cvat-objects-sidebar-state-item-5').should('contain', '5').and('contain', 'RECTANGLE TRACK').within(() => {
cy.get('.cvat-object-item-button-outside-enabled').should('not.exist');
cy.get('.cvat-object-item-button-keyframe-enabled').should('exist');
});
cy.get('#cvat-objects-sidebar-state-item-5')
.should('contain', '5')
.and('contain', 'RECTANGLE TRACK')
.within(() => {
cy.get('.cvat-object-item-button-outside-enabled').should('not.exist');
cy.get('.cvat-object-item-button-keyframe-enabled').should('exist');
});
});
});
});

@ -0,0 +1,126 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/// <reference types="cypress" />
import { taskName } from '../../support/const';
context('Group features', () => {
const caseId = '15';
const createRectangleShape2Points = {
points: 'By 2 Points',
type: 'Shape',
switchLabel: false,
firstX: 250,
firstY: 350,
secondX: 350,
secondY: 450,
};
const createRectangleShape2PointsSecond = {
points: 'By 2 Points',
type: 'Shape',
switchLabel: false,
firstX: createRectangleShape2Points.firstX + 300,
firstY: createRectangleShape2Points.firstY,
secondX: createRectangleShape2Points.secondX + 300,
secondY: createRectangleShape2Points.secondY,
};
const createRectangleTrack2Points = {
points: 'By 2 Points',
type: 'Track',
switchLabel: false,
firstX: 250,
firstY: 600,
secondX: 350,
secondY: 700,
};
const createRectangleTrack2PointsSecond = {
points: 'By 2 Points',
type: 'Track',
switchLabel: false,
firstX: createRectangleTrack2Points.firstX + 300,
firstY: createRectangleTrack2Points.firstY,
secondX: createRectangleTrack2Points.secondX + 300,
secondY: createRectangleTrack2Points.secondY,
};
let defaultGroupColor = '';
let shapesGroupColor = '';
let tracksGroupColor = '';
before(() => {
cy.openTaskJob(taskName);
});
describe(`Testing case "${caseId}"`, () => {
it('Create two shapes and two tracks.', () => {
cy.createRectangle(createRectangleShape2Points);
cy.createRectangle(createRectangleShape2PointsSecond);
cy.createRectangle(createRectangleTrack2Points);
cy.createRectangle(createRectangleTrack2PointsSecond);
});
it('Set option "Color by" to "Group".', () => {
cy.changeAppearance('Group');
cy.get('.cvat_canvas_shape').then($listCanvasShapes => {
for (let i=0; i<$listCanvasShapes.length; i++) {
cy.get($listCanvasShapes[i]).should('have.css', 'fill').then($fill => {
defaultGroupColor = $fill;
});
}
});
cy.get('.cvat-objects-sidebar-state-item').then($listObjectsSidebarStateItem => {
for (let i=0; i<$listObjectsSidebarStateItem.length; i++) {
cy.get($listObjectsSidebarStateItem[i]).should('have.css', 'background-color').then($bColorObjectsSidebarStateItem => {
// expected rgba(224, 224, 224, 0.533) to include [ 224, 224, 224, index: 4, input: 'rgb(224, 224, 224)', groups: undefined ]
expect($bColorObjectsSidebarStateItem).contain(defaultGroupColor.match(/\d+, \d+, \d+/));
});
}
});
});
it('With group button unite two shapes. They have corresponding colors.', () => {
cy.get('.cvat-group-control').click();
for (const shapeToGroup of ['#cvat_canvas_shape_1', '#cvat_canvas_shape_2']) {
cy.get(shapeToGroup).click();
}
cy.get('.cvat-group-control').click();
for (const groupedShape of ['#cvat_canvas_shape_1', '#cvat_canvas_shape_2']) {
cy.get(groupedShape).should('have.css', 'fill').then($shapesGroupColor => {
// expected rgb(250, 50, 83) to not equal rgb(224, 224, 224)
expect($shapesGroupColor).to.not.equal(defaultGroupColor);
shapesGroupColor = $shapesGroupColor;
});
}
for (const objectSideBarShape of ['#cvat-objects-sidebar-state-item-1', '#cvat-objects-sidebar-state-item-2']) {
cy.get(objectSideBarShape).should('have.css', 'background-color').then($bColorobjectSideBarShape => {
// expected rgba(250, 50, 83, 0.533) to not include [ 224, 224, 224, index: 4, input: 'rgb(224, 224, 224)', groups: undefined ]
expect($bColorobjectSideBarShape).to.not.contain(defaultGroupColor.match(/\d+, \d+, \d+/));
// expected rgba(250, 50, 83, 0.533) to include [ 250, 50, 83, index: 4, input: 'rgb(250, 50, 83)', groups: undefined ]
expect($bColorobjectSideBarShape).to.be.contain(shapesGroupColor.match(/\d+, \d+, \d+/));
});
}
});
it('With group button unite two track. They have corresponding colors.', () => {
cy.get('.cvat-group-control').click();
for (const trackToGroup of ['#cvat_canvas_shape_3', '#cvat_canvas_shape_4']) {
cy.get(trackToGroup).click();
}
cy.get('.cvat-group-control').click();
for (const groupedTrack of ['#cvat_canvas_shape_3', '#cvat_canvas_shape_4']) {
cy.get(groupedTrack).should('have.css', 'fill').then($tracksGroupColor => {
// expected rgb(250, 50, 83) to not equal rgb(224, 224, 224)
expect($tracksGroupColor).to.not.equal(defaultGroupColor);
tracksGroupColor = $tracksGroupColor;
});
}
for (const objectSideBarTrack of ['#cvat-objects-sidebar-state-item-3', '#cvat-objects-sidebar-state-item-4']) {
cy.get(objectSideBarTrack).should('have.css', 'background-color').then($bColorobjectSideBarTrack => {
// expected rgba(52, 209, 183, 0.533) to not include [ 224, 224, 224, index: 4, input: 'rgb(224, 224, 224)', groups: undefined ]
expect($bColorobjectSideBarTrack).to.not.contain(defaultGroupColor.match(/\d+, \d+, \d+/));
// expected rgba(52, 209, 183, 0.533) to include [ 52, 209, 183, index: 4, input: 'rgb(52, 209, 183)', groups: undefined ]
expect($bColorobjectSideBarTrack).to.be.contain(tracksGroupColor.match(/\d+, \d+, \d+/));
});
}
});
});
});

@ -86,7 +86,7 @@ context('Multiple users. Assign task, job.', () => {
it('Assign the task to the second user and logout', () => {
cy.openTask(taskName);
cy.get('.cvat-task-details').within(() => {
cy.get('.cvat-user-selector').click({ force: true });
cy.get('.cvat-user-search-field').click({ force: true });
});
cy.contains(secondUserName).click();
cy.logout();
@ -112,7 +112,7 @@ context('Multiple users. Assign task, job.', () => {
cy.get('[value="tasks"]').click();
cy.openTask(taskName);
cy.get('.cvat-task-job-list').within(() => {
cy.get('.cvat-user-selector').click({ force: true });
cy.get('.cvat-user-search-field').click({ force: true });
});
cy.contains(thirdUserName).click();
cy.logout();

@ -21,7 +21,7 @@ module.exports = (on, config) => {
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
launchOptions.args.push('--disable-gpu');
return launchOptions
return launchOptions;
}
});
return config;

@ -100,13 +100,14 @@ Cypress.Commands.add('createRectangle', (createRectangleParams) => {
cy.switchLabel(createRectangleParams.labelName, 'rectangle');
}
cy.contains('Draw new rectangle')
.parents('.cvat-draw-shape-popover-content').within(() => {
cy.get('.ant-select-selection-selected-value').then(($labelValue) => {
selectedValueGlobal = $labelValue.text();
.parents('.cvat-draw-shape-popover-content')
.within(() => {
cy.get('.ant-select-selection-selected-value').then(($labelValue) => {
selectedValueGlobal = $labelValue.text();
});
cy.get('.ant-radio-wrapper').contains(createRectangleParams.points).click();
cy.get('button').contains(createRectangleParams.type).click({ force: true });
});
cy.get('.ant-radio-wrapper').contains(createRectangleParams.points).click();
cy.get('button').contains(createRectangleParams.type).click({ force: true });
})
cy.get('.cvat-canvas-container').click(createRectangleParams.firstX, createRectangleParams.firstY);
cy.get('.cvat-canvas-container').click(createRectangleParams.secondX, createRectangleParams.secondY);
if (createRectangleParams.points === 'By 4 Points') {
@ -135,9 +136,11 @@ Cypress.Commands.add('checkObjectParameters', (objectParameters, objectType) =>
const maxId = Math.max(...listCanvasShapeId);
cy.get(`#cvat_canvas_shape_${maxId}`).should('exist').and('be.visible');
cy.get(`#cvat-objects-sidebar-state-item-${maxId}`)
.should('contain', maxId).and('contain', `${objectType} ${objectParameters.type.toUpperCase()}`).within(() => {
cy.get('.ant-select-selection-selected-value').should('have.text', selectedValueGlobal);
});
.should('contain', maxId)
.and('contain', `${objectType} ${objectParameters.type.toUpperCase()}`)
.within(() => {
cy.get('.ant-select-selection-selected-value').should('have.text', selectedValueGlobal);
});
});
});

Loading…
Cancel
Save