diff --git a/CHANGELOG.md b/CHANGELOG.md index 244c85413307..9f8c3eda397b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,26 +8,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.2.0-beta] - Unreleased ### Added + - ### 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 () - Added layout grids toggling ('ctrl + alt + Enter') - Added password reset functionality () @@ -48,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 () - MOTS png mask format support () - Ability to correct upload video with a rotation record in the metadata () +- User search field for assignee fields () ### Changed @@ -65,7 +73,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed Z-Order flag from task creation process - ### Fixed - Fixed multiple errors which arises when polygon is of length 5 or less () diff --git a/README.md b/README.md index a3957fb6c67c..33901c512f84 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cvat-core/.eslintrc.js b/cvat-core/.eslintrc.js index b67fbe97bc01..1b30030032b7 100644 --- a/cvat-core/.eslintrc.js +++ b/cvat-core/.eslintrc.js @@ -4,6 +4,7 @@ module.exports = { env: { + amd: true, node: false, browser: true, es6: true, diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 6128b1d4aa18..dd36746135c6 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.8.1", + "version": "3.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-core/package.json b/cvat-core/package.json index 0227549ffdfd..84140ddef9a3 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -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": { diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 0766fb7903e8..f7f9a1108b4a 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -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; diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index aaea4dce49f0..524defe76303 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -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); } diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 8c47fa43a898..26483cc19af8 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -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())], diff --git a/cvat-core/tests/mocks/dummy-data.mock.js b/cvat-core/tests/mocks/dummy-data.mock.js index 8b3b5d5a452e..efbc16e4cf85 100644 --- a/cvat-core/tests/mocks/dummy-data.mock.js +++ b/cvat-core/tests/mocks/dummy-data.mock.js @@ -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', diff --git a/cvat-core/tests/mocks/server-proxy.mock.js b/cvat-core/tests/mocks/server-proxy.mock.js index 2db07977b094..983562e4098a 100644 --- a/cvat-core/tests/mocks/server-proxy.mock.js +++ b/cvat-core/tests/mocks/server-proxy.mock.js @@ -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]; } diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 2c4f592ba013..c8a305a6e15c 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -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", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 4a5aa1b7e01e..1afed2a47d4a 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -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", diff --git a/cvat-ui/src/actions/users-actions.ts b/cvat-ui/src/actions/users-actions.ts deleted file mode 100644 index 154b4fee6e4f..000000000000 --- a/cvat-ui/src/actions/users-actions.ts +++ /dev/null @@ -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; - -export function getUsersAsync(): ThunkAction { - return async (dispatch): Promise => { - 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)); - } - }; -} diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 2ceea61a49c0..8b600cd5808c 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -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 - {`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)'} @@ -286,7 +278,7 @@ class CVATApplication extends React.PureComponent - {`The operating system is ${info.os}`} + {`The operating system is ${os}`} diff --git a/cvat-ui/src/components/task-page/details.tsx b/cvat-ui/src/components/task-page/details.tsx index cfda2acbf282..ebe7dbde891f 100644 --- a/cvat-ui/src/components/task-page/details.tsx +++ b/cvat-ui/src/components/task-page/details.tsx @@ -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 } 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 = ( { - 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 ( - + {owner && {`Created by ${owner} on ${created}`}} - - Assigned to - {assigneeSelect} - + Assigned to + {assigneeSelect} ); diff --git a/cvat-ui/src/components/task-page/job-list.tsx b/cvat-ui/src/components/task-page/job-list.tsx index ff9b6ec66cd2..2b10e2256f63 100644 --- a/cvat-ui/src/components/task-page/job-list.tsx +++ b/cvat-ui/src/components/task-page/job-list.tsx @@ -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 ( { - 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); }} /> diff --git a/cvat-ui/src/components/task-page/styles.scss b/cvat-ui/src/components/task-page/styles.scss index 2674edaca0c3..dfdf229a4abf 100644 --- a/cvat-ui/src/components/task-page/styles.scss +++ b/cvat-ui/src/components/task-page/styles.scss @@ -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; } diff --git a/cvat-ui/src/components/task-page/user-selector.tsx b/cvat-ui/src/components/task-page/user-selector.tsx index d2778223dd6e..0cc42db9b2a8 100644 --- a/cvat-ui/src/components/task-page/user-selector.tsx +++ b/cvat-ui/src/components/task-page/user-selector.tsx @@ -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([]); + const autocompleteRef = useRef(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 ( - + ({ + value: user.id.toString(), + text: user.username, + }))} + > + autocompleteRef.current?.blur()} /> + ); } diff --git a/cvat-ui/src/containers/task-page/details.tsx b/cvat-ui/src/containers/task-page/details.tsx index c0e2ce868f02..c05c6f3310c7 100644 --- a/cvat-ui/src/containers/task-page/details.tsx +++ b/cvat-ui/src/containers/task-page/details.tsx @@ -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 ( 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 ( - - ); + return ; } -export default connect(mapStateToProps, mapDispatchToProps)(TaskPageContainer); +export default connect(null, mapDispatchToProps)(TaskPageContainer); diff --git a/cvat-ui/src/index.tsx b/cvat-ui/src/index.tsx index b11f9465655e..31188f77bb82 100644 --- a/cvat-ui/src/index.tsx +++ b/cvat-ui/src/index.tsx @@ -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()), diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 936391082a7c..8c283a7a36f2 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -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; diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index 54e931dbbf60..862dd674476b 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -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 ' + `task ${taskID}`, + message: `Could not update task ${taskID}`, 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, diff --git a/cvat-ui/src/reducers/root-reducer.ts b/cvat-ui/src/reducers/root-reducer.ts index e726b94e85ca..7d96841f0c16 100644 --- a/cvat-ui/src/reducers/root-reducer.ts +++ b/cvat-ui/src/reducers/root-reducer.ts @@ -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, diff --git a/cvat-ui/src/reducers/users-reducer.ts b/cvat-ui/src/reducers/users-reducer.ts deleted file mode 100644 index 6fe501166b7b..000000000000 --- a/cvat-ui/src/reducers/users-reducer.ts +++ /dev/null @@ -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; - } -} diff --git a/cvat/apps/dataset_manager/tests/test_formats.py b/cvat/apps/dataset_manager/tests/test_formats.py index 07640a24b3d7..d41e253c5821 100644 --- a/cvat/apps/dataset_manager/tests/test_formats.py +++ b/cvat/apps/dataset_manager/tests/test_formats.py @@ -218,8 +218,6 @@ def _generate_task_images(self, count): # pylint: disable=no-self-use def _generate_task(self, images): task = { "name": "my task #1", - "owner": '', - "assignee": '', "overlap": 0, "segment_size": 100, "labels": [ @@ -438,8 +436,6 @@ def _generate_task_images(self, paths): # pylint: disable=no-self-use def _generate_task(self, images): task = { "name": "my task #1", - "owner": '', - "assignee": '', "overlap": 0, "segment_size": 100, "labels": [ diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 71843878313f..a6b8dee74c6f 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -13,6 +13,36 @@ 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 @@ def create(self, validated_data): # 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 Meta: 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) diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index 446783e70372..02635a70c815 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -297,47 +297,47 @@ def _check_request(self, response, data): 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 @@ def test_api_v1_jobs_id_annotator_partial(self): 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 @@ def _check_response(self, response, db_task): 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 @@ def _check_response(self, response, db_task, data): 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 @@ def _check_api_v1_tasks_id(self, user, data): 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 @@ def test_api_v1_tasks_id_admin(self): 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 @@ def test_api_v1_tasks_id_admin_partial(self): 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 @@ def test_api_v1_tasks_id_user_partial(self): 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 @@ def _check_response(self, response, user, data): 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 @@ def test_api_v1_tasks_admin(self): 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 @@ def _test_api_v1_tasks_id_data_spec(self, user, spec, data, expected_compressed_ 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 @@ def _test_api_v1_tasks_id_data_spec(self, user, spec, data, expected_compressed_ 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 @@ def test_api_v1_tasks_id_data_user(self): 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 @@ def setUpTestData(cls): 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": [ diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 50798c732c5a..4dda933135f9 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -704,7 +704,16 @@ def annotations(self, request, pk): 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 diff --git a/tests/cypress/integration/actions_tasks_objects/case_13_merge_split_features.js b/tests/cypress/integration/actions_tasks_objects/case_13_merge_split_features.js index 3ef65781cd9a..70139bc85703 100644 --- a/tests/cypress/integration/actions_tasks_objects/case_13_merge_split_features.js +++ b/tests/cypress/integration/actions_tasks_objects/case_13_merge_split_features.js @@ -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'); + }); }); }); }); diff --git a/tests/cypress/integration/actions_users/case_4_assign_taks_job_users.js b/tests/cypress/integration/actions_users/case_4_assign_taks_job_users.js index 8fca556720ea..1475a044c988 100644 --- a/tests/cypress/integration/actions_users/case_4_assign_taks_job_users.js +++ b/tests/cypress/integration/actions_users/case_4_assign_taks_job_users.js @@ -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(); diff --git a/tests/cypress/plugins/index.js b/tests/cypress/plugins/index.js index f1af3f8c68c0..645405378335 100644 --- a/tests/cypress/plugins/index.js +++ b/tests/cypress/plugins/index.js @@ -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; diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index b81bfe72cc61..5ee311eb325d 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -100,13 +100,14 @@ Cypress.Commands.add('createRectangle', (createRectangleParams) => { cy.switchLabel(createRectangleParams.labelName); } 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') { @@ -131,9 +132,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); + }); }); });