From 9e584b41911ad496d37a56855a1c20a9e6d93d33 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 28 Oct 2020 17:20:52 +0300 Subject: [PATCH 01/10] Added user search filed for assignee --- cvat-core/src/api-implementation.js | 37 ++----- cvat-core/src/server-proxy.js | 14 +-- cvat-core/src/session.js | 9 +- cvat-ui/src/actions/users-actions.ts | 36 ------ cvat-ui/src/components/cvat-app.tsx | 22 ++-- cvat-ui/src/components/task-page/details.tsx | 26 ++--- cvat-ui/src/components/task-page/job-list.tsx | 17 +-- cvat-ui/src/components/task-page/styles.scss | 6 + .../components/task-page/user-selector.tsx | 103 +++++++++++++++--- cvat-ui/src/containers/task-page/details.tsx | 7 +- cvat-ui/src/containers/task-page/job-list.tsx | 22 +--- cvat-ui/src/index.tsx | 8 -- cvat-ui/src/reducers/interfaces.ts | 7 -- cvat-ui/src/reducers/notifications-reducer.ts | 19 +--- cvat-ui/src/reducers/root-reducer.ts | 2 - cvat-ui/src/reducers/users-reducer.ts | 48 -------- cvat/apps/engine/serializers.py | 80 ++++++++------ cvat/apps/engine/views.py | 11 ++ 18 files changed, 197 insertions(+), 277 deletions(-) delete mode 100644 cvat-ui/src/actions/users-actions.ts delete mode 100644 cvat-ui/src/reducers/users-reducer.ts diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 0766fb79..f7f9a110 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 aaea4dce..524defe7 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 8c47fa43..26483cc1 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-ui/src/actions/users-actions.ts b/cvat-ui/src/actions/users-actions.ts deleted file mode 100644 index 154b4fee..00000000 --- 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 2ceea61a..8b600cd5 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 cfda2acb..ebe7dbde 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 ff9b6ec6..2b10e225 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 2674edac..cb205585 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; diff --git a/cvat-ui/src/components/task-page/user-selector.tsx b/cvat-ui/src/components/task-page/user-selector.tsx index d2778223..d1693a46 100644 --- a/cvat-ui/src/components/task-page/user-selector.tsx +++ b/cvat-ui/src/components/task-page/user-selector.tsx @@ -2,30 +2,97 @@ // // SPDX-License-Identifier: MIT -import React from 'react'; -import Select from 'antd/lib/select'; +import React, { useState, useEffect } from 'react'; +import Autocomplete from 'antd/lib/auto-complete'; + +import getCore from 'cvat-core-wrapper'; +import { SelectValue } from 'antd/lib/select'; + +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; } 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 handleSearch = (searchValue: string): void => { + if (searchValue) { + core.users + .get({ + search: searchValue, + limit: 10, + }) + .then((result: User[]) => { + if (result) { + setUsers(result); + } + }); + } 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 && 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, + }))} + /> ); } diff --git a/cvat-ui/src/containers/task-page/details.tsx b/cvat-ui/src/containers/task-page/details.tsx index c0e2ce86..c05c6f33 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 b11f9465..31188f77 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 93639108..8c283a7a 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 54e931db..862dd674 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 e726b94e..7d96841f 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 6fe50116..00000000 --- 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/engine/serializers.py b/cvat/apps/engine/serializers.py index 71843878..2fe598f5 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -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) + assignee_id = serializers.IntegerField(write_only=True, allow_null=True) 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,10 +274,14 @@ 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() + owner_id = serializers.IntegerField(write_only=True, allow_null=True) + assignee = BasicUserSerializer(allow_null=True) + assignee_id = serializers.IntegerField(write_only=True, allow_null=True) 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') @@ -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) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 50798c73..4dda9331 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -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 From 0a5cf55ed39a0c82bda79f894b1647c1450a24e3 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 28 Oct 2020 17:21:42 +0300 Subject: [PATCH 02/10] fixed eslint for cvat-core --- cvat-core/.eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cvat-core/.eslintrc.js b/cvat-core/.eslintrc.js index b67fbe97..1b300300 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, From ad03d80e0d4c9a00f9187d45d781573abe60630a Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 28 Oct 2020 17:25:30 +0300 Subject: [PATCH 03/10] Added npm versions --- cvat-core/package-lock.json | 2 +- cvat-core/package.json | 2 +- cvat-ui/package-lock.json | 2 +- cvat-ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 0b31ce41..6fc9b5a1 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 ece057ac..e7e86118 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-ui/package-lock.json b/cvat-ui/package-lock.json index 4ed13697..b854e4c3 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": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 432c439d..7304204e 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": { From ab299b4c27d75c1175cf9a36416f64eae973ae61 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 28 Oct 2020 17:33:41 +0300 Subject: [PATCH 04/10] Added CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d49d3b..c79ed77a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ability to upload prepared meta information along with a video when creating a task () - Optional chaining plugin for cvat-canvas and cvat-ui () - MOTS png mask format support () +- User search field for assignee fields () ### Changed From a3404df407d27e69e6a881309fede9caf62f3aea Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 30 Oct 2020 12:08:44 +0300 Subject: [PATCH 05/10] Fixed cvat-core tests --- cvat-core/tests/mocks/dummy-data.mock.js | 36 ++++++++++++++++++---- cvat-core/tests/mocks/server-proxy.mock.js | 13 +++++--- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/cvat-core/tests/mocks/dummy-data.mock.js b/cvat-core/tests/mocks/dummy-data.mock.js index 8b3b5d5a..efbc16e4 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 2db07977..983562e4 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]; } From 88d4c1798bb3355f1cea0693a8bf406a437c268b Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 30 Oct 2020 20:51:19 +0300 Subject: [PATCH 06/10] Fixed server tests --- .../dataset_manager/tests/test_formats.py | 4 -- cvat/apps/engine/serializers.py | 14 ++-- cvat/apps/engine/tests/test_rest_api.py | 65 ++++++++++--------- 3 files changed, 42 insertions(+), 41 deletions(-) diff --git a/cvat/apps/dataset_manager/tests/test_formats.py b/cvat/apps/dataset_manager/tests/test_formats.py index 1eb3e2b5..54f5d286 100644 --- a/cvat/apps/dataset_manager/tests/test_formats.py +++ b/cvat/apps/dataset_manager/tests/test_formats.py @@ -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": [ @@ -435,8 +433,6 @@ class FrameMatchingTest(_DbTestBase): 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 2fe598f5..a6b8dee7 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -83,8 +83,8 @@ 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) - assignee_id = serializers.IntegerField(write_only=True, allow_null=True) + assignee = BasicUserSerializer(allow_null=True, required=False) + assignee_id = serializers.IntegerField(write_only=True, allow_null=True, required=False) class Meta: model = models.Job @@ -274,10 +274,10 @@ 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() - owner_id = serializers.IntegerField(write_only=True, allow_null=True) - assignee = BasicUserSerializer(allow_null=True) - assignee_id = serializers.IntegerField(write_only=True, allow_null=True) + owner = BasicUserSerializer(required=False) + owner_id = serializers.IntegerField(write_only=True, allow_null=True, required=False) + assignee = BasicUserSerializer(allow_null=True, required=False) + assignee_id = serializers.IntegerField(write_only=True, allow_null=True, required=False) class Meta: model = models.Task @@ -285,7 +285,7 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer): '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'] diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index b7448fff..2b3d7378 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -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": [{ @@ -1653,8 +1658,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) @@ -1736,8 +1741,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": [ @@ -2034,8 +2039,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": [ @@ -2080,8 +2085,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": [ From ee7547aecb61a2c4319a4228a32c2add0e75a3ab Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 30 Oct 2020 22:33:56 +0300 Subject: [PATCH 07/10] Fixed empty user selector --- cvat-ui/src/components/task-page/user-selector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat-ui/src/components/task-page/user-selector.tsx b/cvat-ui/src/components/task-page/user-selector.tsx index d1693a46..b9b10c18 100644 --- a/cvat-ui/src/components/task-page/user-selector.tsx +++ b/cvat-ui/src/components/task-page/user-selector.tsx @@ -52,7 +52,7 @@ export default function UserSelector(props: Props): JSX.Element { } }); } - if (!open && searchPhrase && searchPhrase !== value?.username) { + if (!open && searchPhrase !== value?.username) { setSearchPhrase(''); if (value) { onSelect(null); From 9194029dd27eb73893cd2ae85bbb898c4c2de2a3 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 30 Oct 2020 22:57:51 +0300 Subject: [PATCH 08/10] Fixed cvat-ui tests --- .../integration/actions_users/case_4_assign_taks_job_users.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 8fca5567..1475a044 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(); From 17216826227b956d7e98e2517fa8fe70060376db Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 10 Nov 2020 16:09:44 +0300 Subject: [PATCH 09/10] Added debouncing for search field --- cvat-ui/package-lock.json | 37 +++++++++++-------- cvat-ui/package.json | 2 + .../components/task-page/user-selector.tsx | 32 +++++++++++----- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 37f82cf9..1d67a279 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -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", @@ -17048,7 +17053,7 @@ }, "fs-minipass": { "version": "1.2.7", - "resolved": false, + "resolved": "", "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", "optional": true, "requires": { @@ -17063,7 +17068,7 @@ }, "gauge": { "version": "2.7.4", - "resolved": false, + "resolved": "", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "optional": true, "requires": { @@ -17079,7 +17084,7 @@ }, "glob": { "version": "7.1.6", - "resolved": false, + "resolved": "", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "optional": true, "requires": { @@ -17117,7 +17122,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "optional": true, "requires": { @@ -17169,7 +17174,7 @@ }, "minipass": { "version": "2.9.0", - "resolved": false, + "resolved": "", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "optional": true, "requires": { @@ -17179,7 +17184,7 @@ }, "minizlib": { "version": "1.3.3", - "resolved": false, + "resolved": "", "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", "optional": true, "requires": { @@ -17214,7 +17219,7 @@ }, "node-pre-gyp": { "version": "0.14.0", - "resolved": false, + "resolved": "", "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", "optional": true, "requires": { @@ -17268,7 +17273,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "optional": true, "requires": { @@ -17292,7 +17297,7 @@ }, "once": { "version": "1.4.0", - "resolved": false, + "resolved": "", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "optional": true, "requires": { @@ -17362,7 +17367,7 @@ }, "rimraf": { "version": "2.7.1", - "resolved": false, + "resolved": "", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "optional": true, "requires": { @@ -17442,7 +17447,7 @@ }, "tar": { "version": "4.4.13", - "resolved": false, + "resolved": "", "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "optional": true, "requires": { @@ -17472,13 +17477,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 } @@ -25950,9 +25955,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 1fd71828..f07fbf3d 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -47,6 +47,7 @@ "worker-loader": "^2.0.0" }, "dependencies": { + "@types/lodash": "^4.14.165", "@types/platform": "^1.3.3", "@types/react": "^16.9.51", "@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/components/task-page/user-selector.tsx b/cvat-ui/src/components/task-page/user-selector.tsx index b9b10c18..382fffa0 100644 --- a/cvat-ui/src/components/task-page/user-selector.tsx +++ b/cvat-ui/src/components/task-page/user-selector.tsx @@ -8,6 +8,8 @@ import Autocomplete from 'antd/lib/auto-complete'; import getCore from 'cvat-core-wrapper'; import { SelectValue } from 'antd/lib/select'; +import debounce from 'lodash/debounce'; + const core = getCore(); export interface User { @@ -20,6 +22,25 @@ interface Props { 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, onSelect } = props; const [searchPhrase, setSearchPhrase] = useState(''); @@ -28,16 +49,7 @@ export default function UserSelector(props: Props): JSX.Element { const handleSearch = (searchValue: string): void => { if (searchValue) { - core.users - .get({ - search: searchValue, - limit: 10, - }) - .then((result: User[]) => { - if (result) { - setUsers(result); - } - }); + searchUsers(searchValue, setUsers); } else { setUsers([]); } From 237d192789697c39d0fe86b5247bfb3cfb03c7cb Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 10 Nov 2020 21:48:40 +0300 Subject: [PATCH 10/10] Fixed PR issues --- cvat-ui/src/components/task-page/styles.scss | 5 ----- cvat-ui/src/components/task-page/user-selector.tsx | 11 ++++++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cvat-ui/src/components/task-page/styles.scss b/cvat-ui/src/components/task-page/styles.scss index cb205585..dfdf229a 100644 --- a/cvat-ui/src/components/task-page/styles.scss +++ b/cvat-ui/src/components/task-page/styles.scss @@ -93,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 382fffa0..0cc42db9 100644 --- a/cvat-ui/src/components/task-page/user-selector.tsx +++ b/cvat-ui/src/components/task-page/user-selector.tsx @@ -2,8 +2,9 @@ // // SPDX-License-Identifier: MIT -import React, { useState, useEffect } from 'react'; +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'; @@ -46,6 +47,7 @@ export default function UserSelector(props: Props): JSX.Element { const [searchPhrase, setSearchPhrase] = useState(''); const [users, setUsers] = useState([]); + const autocompleteRef = useRef(null); const handleSearch = (searchValue: string): void => { if (searchValue) { @@ -95,8 +97,9 @@ export default function UserSelector(props: Props): JSX.Element { return ( + > + autocompleteRef.current?.blur()} /> + ); }