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.decorators import detail_route
|
||||
from rest_framework.response import Response
|
||||
|
@ -9,6 +9,9 @@ from .tasks import TaskIDsSerializer
|
|||
from .common import get_and_check_project
|
||||
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):
|
||||
tasks = TaskIDsSerializer(many=True, read_only=True)
|
||||
owner = serializers.HiddenField(
|
||||
|
@ -19,7 +22,7 @@ class ProjectSerializer(serializers.ModelSerializer):
|
|||
|
||||
def get_permissions(self, obj):
|
||||
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:
|
||||
# Cannot list permissions, no user is associated with request (happens when serializing ui test mocks)
|
||||
return []
|
||||
|
@ -60,3 +63,17 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
|||
return Response({'success': True, 'project': ProjectSerializer(new_project).data}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
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 PropTypes from 'prop-types';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import EditPermissionsPanel from './EditPermissionsPanel';
|
||||
import { _ } from '../classes/gettext';
|
||||
|
||||
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?"),
|
||||
show: false,
|
||||
showDuplicate: false,
|
||||
showPermissions: false,
|
||||
onDuplicated: () => {}
|
||||
};
|
||||
|
||||
|
@ -33,6 +35,7 @@ class EditProjectDialog extends React.Component {
|
|||
deleteWarning: PropTypes.string,
|
||||
show: PropTypes.bool,
|
||||
showDuplicate: PropTypes.bool,
|
||||
showPermissions: PropTypes.bool,
|
||||
onDuplicated: PropTypes.func
|
||||
};
|
||||
|
||||
|
@ -69,6 +72,7 @@ class EditProjectDialog extends React.Component {
|
|||
}
|
||||
|
||||
onShow(){
|
||||
this.editPermissionsPanel.loadPermissions();
|
||||
this.nameInput.focus();
|
||||
}
|
||||
|
||||
|
@ -123,7 +127,7 @@ class EditProjectDialog extends React.Component {
|
|||
getFormData={this.getFormData}
|
||||
reset={this.reset}
|
||||
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; }}>
|
||||
<ErrorMessage bind={[this, "error"]} />
|
||||
<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')} />
|
||||
</div>
|
||||
</div>
|
||||
{this.props.showPermissions ?
|
||||
<EditPermissionsPanel
|
||||
projectId={this.props.projectId}
|
||||
lazyLoad={true}
|
||||
ref={(domNode) => { this.editPermissionsPanel = domNode; }} />
|
||||
: ""}
|
||||
</FormDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -144,7 +144,8 @@ class FormDialog extends React.Component {
|
|||
if (this.props.deleteAction){
|
||||
leftButtons.push(<button
|
||||
disabled={this.state.deleting}
|
||||
className="btn btn-danger"
|
||||
className="btn btn-danger"
|
||||
key="delete"
|
||||
onClick={this.handleDelete}>
|
||||
{this.state.deleting ?
|
||||
<span>
|
||||
|
|
|
@ -469,27 +469,31 @@ class ProjectListItem extends React.Component {
|
|||
render() {
|
||||
const { refreshing, data } = this.state;
|
||||
const numTasks = data.tasks.length;
|
||||
const canEdit = this.hasPermission("change");
|
||||
|
||||
return (
|
||||
<li className={"project-list-item list-group-item " + (refreshing ? "refreshing" : "")}
|
||||
href="javascript:void(0);"
|
||||
ref={this.setRef("dropzone")}
|
||||
>
|
||||
|
||||
<EditProjectDialog
|
||||
ref={(domNode) => { this.editProjectDialog = domNode; }}
|
||||
title={_("Edit Project")}
|
||||
saveLabel={_("Save Changes")}
|
||||
savingLabel={_("Saving changes...")}
|
||||
saveIcon="far fa-edit"
|
||||
showDuplicate={true}
|
||||
onDuplicated={this.props.onProjectDuplicated}
|
||||
projectName={data.name}
|
||||
projectDescr={data.description}
|
||||
projectId={data.id}
|
||||
saveAction={this.updateProject}
|
||||
deleteAction={this.hasPermission("delete") ? this.handleDelete : undefined}
|
||||
/>
|
||||
|
||||
{canEdit ?
|
||||
<EditProjectDialog
|
||||
ref={(domNode) => { this.editProjectDialog = domNode; }}
|
||||
title={_("Edit Project")}
|
||||
saveLabel={_("Save Changes")}
|
||||
savingLabel={_("Saving changes...")}
|
||||
saveIcon="far fa-edit"
|
||||
showDuplicate={true}
|
||||
onDuplicated={this.props.onProjectDuplicated}
|
||||
projectName={data.name}
|
||||
projectDescr={data.description}
|
||||
projectId={data.id}
|
||||
saveAction={this.updateProject}
|
||||
showPermissions={this.hasPermission("change")}
|
||||
deleteAction={this.hasPermission("delete") ? this.handleDelete : undefined}
|
||||
/>
|
||||
: ""}
|
||||
|
||||
<div className="row no-margin">
|
||||
<ErrorMessage bind={[this, 'error']} />
|
||||
|
@ -541,9 +545,11 @@ class ProjectListItem extends React.Component {
|
|||
</span>
|
||||
: ""}
|
||||
|
||||
<i className='far fa-edit'>
|
||||
</i> <a href="javascript:void(0);" onClick={this.handleEditProject}> {_("Edit")}
|
||||
</a>
|
||||
{canEdit ?
|
||||
[<i key="edit-icon" className='far fa-edit'>
|
||||
</i>,<a key="edit-text" href="javascript:void(0);" onClick={this.handleEditProject}> {_("Edit")}
|
||||
</a>]
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
<i className="drag-drop-icon fa fa-inbox"></i>
|
||||
|
|
|
@ -640,7 +640,7 @@ class TaskListItem extends React.Component {
|
|||
|
||||
if (task.last_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"));
|
||||
statusIcon = "fa fa-hourglass-3";
|
||||
showEditLink = true;
|
||||
|
@ -679,7 +679,7 @@ class TaskListItem extends React.Component {
|
|||
}
|
||||
|
||||
// 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>);
|
||||
}
|
||||
|
||||
|
|
|
@ -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