From 9e584b41911ad496d37a56855a1c20a9e6d93d33 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 28 Oct 2020 17:20:52 +0300 Subject: [PATCH] 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