kopia lustrzana https://github.com/OpenDroneMap/WebODM
Started working on project permission management
rodzic
7e77b8521f
commit
2beaad3910
|
@ -1,4 +1,4 @@
|
||||||
from guardian.shortcuts import get_perms
|
from guardian.shortcuts import get_perms, get_users_with_perms
|
||||||
from rest_framework import serializers, viewsets
|
from rest_framework import serializers, viewsets
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -9,6 +9,9 @@ from .tasks import TaskIDsSerializer
|
||||||
from .common import get_and_check_project
|
from .common import get_and_check_project
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
def normalized_perm_names(perms):
|
||||||
|
return list(map(lambda p: p.replace("_project", ""),perms))
|
||||||
|
|
||||||
class ProjectSerializer(serializers.ModelSerializer):
|
class ProjectSerializer(serializers.ModelSerializer):
|
||||||
tasks = TaskIDsSerializer(many=True, read_only=True)
|
tasks = TaskIDsSerializer(many=True, read_only=True)
|
||||||
owner = serializers.HiddenField(
|
owner = serializers.HiddenField(
|
||||||
|
@ -19,7 +22,7 @@ class ProjectSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
def get_permissions(self, obj):
|
def get_permissions(self, obj):
|
||||||
if 'request' in self.context:
|
if 'request' in self.context:
|
||||||
return list(map(lambda p: p.replace("_project", ""), get_perms(self.context['request'].user, obj)))
|
return normalized_perm_names(get_perms(self.context['request'].user, obj))
|
||||||
else:
|
else:
|
||||||
# Cannot list permissions, no user is associated with request (happens when serializing ui test mocks)
|
# Cannot list permissions, no user is associated with request (happens when serializing ui test mocks)
|
||||||
return []
|
return []
|
||||||
|
@ -60,3 +63,17 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
||||||
return Response({'success': True, 'project': ProjectSerializer(new_project).data}, status=status.HTTP_200_OK)
|
return Response({'success': True, 'project': ProjectSerializer(new_project).data}, status=status.HTTP_200_OK)
|
||||||
else:
|
else:
|
||||||
return Response({'error': _("Cannot duplicate project")}, status=status.HTTP_200_OK)
|
return Response({'error': _("Cannot duplicate project")}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@detail_route(methods=['get'])
|
||||||
|
def permissions(self, request, pk=None):
|
||||||
|
project = get_and_check_project(request, pk, ('change_project', ))
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
perms = get_users_with_perms(project, attach_perms=True)
|
||||||
|
for user in perms:
|
||||||
|
result.append({'user': user.username,
|
||||||
|
'owner': project.owner == user,
|
||||||
|
'permissions': normalized_perm_names(perms[user])})
|
||||||
|
|
||||||
|
return Response(result, status=status.HTTP_200_OK)
|
|
@ -0,0 +1,157 @@
|
||||||
|
import '../css/EditPermissionsPanel.scss';
|
||||||
|
import React from 'react';
|
||||||
|
import ErrorMessage from './ErrorMessage';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import $ from 'jquery';
|
||||||
|
import { _, interpolate } from '../classes/gettext';
|
||||||
|
import update from 'immutability-helper';
|
||||||
|
import Css from '../classes/Css';
|
||||||
|
|
||||||
|
class EditPermissionsPanel extends React.Component {
|
||||||
|
static defaultProps = {
|
||||||
|
projectId: -1,
|
||||||
|
lazyLoad: true
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
projectId: PropTypes.number.isRequired,
|
||||||
|
lazyLoad: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
error: "",
|
||||||
|
loading: false,
|
||||||
|
permissions: [],
|
||||||
|
validUsernames: []
|
||||||
|
};
|
||||||
|
|
||||||
|
this.backgroundFailedColor = Css.getValue('btn-danger', 'backgroundColor');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPermissions = () => {
|
||||||
|
this.setState({loading: true, permissions: []});
|
||||||
|
|
||||||
|
this.permsRequest =
|
||||||
|
$.getJSON(`/api/projects/${this.props.projectId}/permissions/`, json => {
|
||||||
|
let validUsernames = json.map(p => p.user);
|
||||||
|
this.setState({validUsernames, permissions: json});
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
this.setState({error: _("Cannot load permissions.")});
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
this.setState({loading: false});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
if (!this.props.lazyLoad) this.loadPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
if (this.permsRequest) this.permsRequest.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangePermissionRole = e => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangePermissionUser = perm => {
|
||||||
|
return e => {
|
||||||
|
perm.user = e.target.value;
|
||||||
|
|
||||||
|
// Update
|
||||||
|
this.setState({permissions: this.state.permissions});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
simplifiedPermission = perms => {
|
||||||
|
// We simplify WebODM's internal permission model into
|
||||||
|
// a simpler read or read/write model.
|
||||||
|
if (perms.indexOf("change") !== -1) return "rw";
|
||||||
|
else if (perms.indexOf("view") !== -1) return "r";
|
||||||
|
else return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionLabel = simPerm => {
|
||||||
|
if (simPerm === "rw") return _("Read/Write");
|
||||||
|
else if (simPerm === "r") return _("Read");
|
||||||
|
else if (simPerm === "") return _("No Access");
|
||||||
|
}
|
||||||
|
|
||||||
|
allPermissions = () => {
|
||||||
|
return ["rw", "r"].map(p => { return {key: p, label: this.permissionLabel(p)}});
|
||||||
|
}
|
||||||
|
|
||||||
|
getColorFor = (username) => {
|
||||||
|
if (this.state.validUsernames.indexOf(username) !== -1) return "";
|
||||||
|
else return this.backgroundFailedColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNewPermission = () => {
|
||||||
|
this.setState(update(this.state, {
|
||||||
|
permissions: {$push: [{user: "", permissions: ["view"]}]}
|
||||||
|
}));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.lastTextbox) this.lastTextbox.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeletePermission = perm => {
|
||||||
|
return () => {
|
||||||
|
this.setState(update(this.state, {
|
||||||
|
permissions: arr => arr.filter(p => p !== perm)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const permissions = this.state.permissions.map((p, i) => <div key={i}>
|
||||||
|
<div className="permission">
|
||||||
|
<div className="username-container">
|
||||||
|
<i className="fa fa-user user-indicator"/>
|
||||||
|
<input
|
||||||
|
style={{color: this.getColorFor(p.user)}}
|
||||||
|
onChange={this.handleChangePermissionUser(p)}
|
||||||
|
type="text"
|
||||||
|
disabled={p.owner}
|
||||||
|
value={p.user}
|
||||||
|
className="form-control username"
|
||||||
|
placeholder={_("Username / e-mail")}
|
||||||
|
ref={(domNode) => this.lastTextbox = domNode} />
|
||||||
|
</div>
|
||||||
|
<div className="remove">
|
||||||
|
{!p.owner ? <a onClick={this.handleDeletePermission(p)}><i className="fa fa-times"></i></a> : ""}
|
||||||
|
</div>
|
||||||
|
<div className="role-container">
|
||||||
|
<select disabled={p.owner} className="form-control" value={this.simplifiedPermission(p.permissions)} onChange={this.handleChangePermissionRole}>
|
||||||
|
{this.allPermissions().map(p => <option key={p.key} value={p.key}>{p.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="edit-permissions-panel">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="col-sm-2 control-label">{_("Permissions")}</label>
|
||||||
|
<div className="col-sm-10">
|
||||||
|
<ErrorMessage bind={[this, 'error']} />
|
||||||
|
|
||||||
|
{this.state.loading ?
|
||||||
|
<i className="fa fa-circle-notch fa-spin fa-fw perms-loading"></i>
|
||||||
|
: [permissions, <div key="add-new">
|
||||||
|
<button onClick={this.addNewPermission} className="btn btn-default btn-sm add-new"><i className="fa fa-user-plus"></i></button>
|
||||||
|
</div>]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditPermissionsPanel;
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import FormDialog from './FormDialog';
|
import FormDialog from './FormDialog';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ErrorMessage from './ErrorMessage';
|
import ErrorMessage from './ErrorMessage';
|
||||||
|
import EditPermissionsPanel from './EditPermissionsPanel';
|
||||||
import { _ } from '../classes/gettext';
|
import { _ } from '../classes/gettext';
|
||||||
|
|
||||||
class EditProjectDialog extends React.Component {
|
class EditProjectDialog extends React.Component {
|
||||||
|
@ -16,6 +17,7 @@ class EditProjectDialog extends React.Component {
|
||||||
deleteWarning: _("All tasks, images and models associated with this project will be permanently deleted. Are you sure you want to continue?"),
|
deleteWarning: _("All tasks, images and models associated with this project will be permanently deleted. Are you sure you want to continue?"),
|
||||||
show: false,
|
show: false,
|
||||||
showDuplicate: false,
|
showDuplicate: false,
|
||||||
|
showPermissions: false,
|
||||||
onDuplicated: () => {}
|
onDuplicated: () => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,6 +35,7 @@ class EditProjectDialog extends React.Component {
|
||||||
deleteWarning: PropTypes.string,
|
deleteWarning: PropTypes.string,
|
||||||
show: PropTypes.bool,
|
show: PropTypes.bool,
|
||||||
showDuplicate: PropTypes.bool,
|
showDuplicate: PropTypes.bool,
|
||||||
|
showPermissions: PropTypes.bool,
|
||||||
onDuplicated: PropTypes.func
|
onDuplicated: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,6 +72,7 @@ class EditProjectDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow(){
|
onShow(){
|
||||||
|
this.editPermissionsPanel.loadPermissions();
|
||||||
this.nameInput.focus();
|
this.nameInput.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +127,7 @@ class EditProjectDialog extends React.Component {
|
||||||
getFormData={this.getFormData}
|
getFormData={this.getFormData}
|
||||||
reset={this.reset}
|
reset={this.reset}
|
||||||
onShow={this.onShow}
|
onShow={this.onShow}
|
||||||
leftButtons={this.props.showDuplicate ? [<button disabled={this.duplicating} onClick={this.handleDuplicate} className="btn btn-default"><i className={"fa " + (this.state.duplicating ? "fa-circle-notch fa-spin fa-fw" : "fa-copy")}></i> Duplicate</button>] : undefined}
|
leftButtons={this.props.showDuplicate ? [<button key="duplicate" disabled={this.duplicating} onClick={this.handleDuplicate} className="btn btn-default"><i className={"fa " + (this.state.duplicating ? "fa-circle-notch fa-spin fa-fw" : "fa-copy")}></i> Duplicate</button>] : undefined}
|
||||||
ref={(domNode) => { this.dialog = domNode; }}>
|
ref={(domNode) => { this.dialog = domNode; }}>
|
||||||
<ErrorMessage bind={[this, "error"]} />
|
<ErrorMessage bind={[this, "error"]} />
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
@ -138,6 +142,12 @@ class EditProjectDialog extends React.Component {
|
||||||
<textarea className="form-control" rows="3" value={this.state.descr} onChange={this.handleChange('descr')} />
|
<textarea className="form-control" rows="3" value={this.state.descr} onChange={this.handleChange('descr')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{this.props.showPermissions ?
|
||||||
|
<EditPermissionsPanel
|
||||||
|
projectId={this.props.projectId}
|
||||||
|
lazyLoad={true}
|
||||||
|
ref={(domNode) => { this.editPermissionsPanel = domNode; }} />
|
||||||
|
: ""}
|
||||||
</FormDialog>
|
</FormDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,6 +145,7 @@ class FormDialog extends React.Component {
|
||||||
leftButtons.push(<button
|
leftButtons.push(<button
|
||||||
disabled={this.state.deleting}
|
disabled={this.state.deleting}
|
||||||
className="btn btn-danger"
|
className="btn btn-danger"
|
||||||
|
key="delete"
|
||||||
onClick={this.handleDelete}>
|
onClick={this.handleDelete}>
|
||||||
{this.state.deleting ?
|
{this.state.deleting ?
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -469,6 +469,7 @@ class ProjectListItem extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { refreshing, data } = this.state;
|
const { refreshing, data } = this.state;
|
||||||
const numTasks = data.tasks.length;
|
const numTasks = data.tasks.length;
|
||||||
|
const canEdit = this.hasPermission("change");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={"project-list-item list-group-item " + (refreshing ? "refreshing" : "")}
|
<li className={"project-list-item list-group-item " + (refreshing ? "refreshing" : "")}
|
||||||
|
@ -476,6 +477,7 @@ class ProjectListItem extends React.Component {
|
||||||
ref={this.setRef("dropzone")}
|
ref={this.setRef("dropzone")}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
{canEdit ?
|
||||||
<EditProjectDialog
|
<EditProjectDialog
|
||||||
ref={(domNode) => { this.editProjectDialog = domNode; }}
|
ref={(domNode) => { this.editProjectDialog = domNode; }}
|
||||||
title={_("Edit Project")}
|
title={_("Edit Project")}
|
||||||
|
@ -488,8 +490,10 @@ class ProjectListItem extends React.Component {
|
||||||
projectDescr={data.description}
|
projectDescr={data.description}
|
||||||
projectId={data.id}
|
projectId={data.id}
|
||||||
saveAction={this.updateProject}
|
saveAction={this.updateProject}
|
||||||
|
showPermissions={this.hasPermission("change")}
|
||||||
deleteAction={this.hasPermission("delete") ? this.handleDelete : undefined}
|
deleteAction={this.hasPermission("delete") ? this.handleDelete : undefined}
|
||||||
/>
|
/>
|
||||||
|
: ""}
|
||||||
|
|
||||||
<div className="row no-margin">
|
<div className="row no-margin">
|
||||||
<ErrorMessage bind={[this, 'error']} />
|
<ErrorMessage bind={[this, 'error']} />
|
||||||
|
@ -541,9 +545,11 @@ class ProjectListItem extends React.Component {
|
||||||
</span>
|
</span>
|
||||||
: ""}
|
: ""}
|
||||||
|
|
||||||
<i className='far fa-edit'>
|
{canEdit ?
|
||||||
</i> <a href="javascript:void(0);" onClick={this.handleEditProject}> {_("Edit")}
|
[<i key="edit-icon" className='far fa-edit'>
|
||||||
</a>
|
</i>,<a key="edit-text" href="javascript:void(0);" onClick={this.handleEditProject}> {_("Edit")}
|
||||||
|
</a>]
|
||||||
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<i className="drag-drop-icon fa fa-inbox"></i>
|
<i className="drag-drop-icon fa fa-inbox"></i>
|
||||||
|
|
|
@ -640,7 +640,7 @@ class TaskListItem extends React.Component {
|
||||||
|
|
||||||
if (task.last_error){
|
if (task.last_error){
|
||||||
statusLabel = getStatusLabel(task.last_error, 'error');
|
statusLabel = getStatusLabel(task.last_error, 'error');
|
||||||
}else if (!task.processing_node && !imported){
|
}else if (!task.processing_node && !imported && this.props.hasPermission("change")){
|
||||||
statusLabel = getStatusLabel(_("Set a processing node"));
|
statusLabel = getStatusLabel(_("Set a processing node"));
|
||||||
statusIcon = "fa fa-hourglass-3";
|
statusIcon = "fa fa-hourglass-3";
|
||||||
showEditLink = true;
|
showEditLink = true;
|
||||||
|
@ -679,7 +679,7 @@ class TaskListItem extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ability to change options
|
// Ability to change options
|
||||||
if (editable || (!task.processing_node)){
|
if (editable || (!task.processing_node && this.props.hasPermission("change"))){
|
||||||
taskActions.push(<li key="edit"><a href="javascript:void(0)" onClick={this.startEditing}><i className="glyphicon glyphicon-pencil"></i>{_("Edit")}</a></li>);
|
taskActions.push(<li key="edit"><a href="javascript:void(0)" onClick={this.startEditing}><i className="glyphicon glyphicon-pencil"></i>{_("Edit")}</a></li>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
.edit-permissions-panel{
|
||||||
|
.perms-loading{
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.permission{
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.user-indicator{
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 15px;
|
||||||
|
}
|
||||||
|
.remove{
|
||||||
|
position: relative;
|
||||||
|
top: 12px;
|
||||||
|
|
||||||
|
a{
|
||||||
|
position: absolute;
|
||||||
|
left: -36px;
|
||||||
|
top: -12px;
|
||||||
|
padding: 12px;
|
||||||
|
&:hover{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.username{
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-container{
|
||||||
|
flex-grow: 0.66;
|
||||||
|
}
|
||||||
|
.role-container{
|
||||||
|
flex-grow: 0.33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-new{
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
Ładowanie…
Reference in New Issue