Started working on project permission management

pull/1075/head
Piero Toffanin 2021-10-18 15:19:16 -04:00
rodzic 7e77b8521f
commit 2beaad3910
7 zmienionych plików z 259 dodań i 24 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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;

Wyświetl plik

@ -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>
);
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>);
}

Wyświetl plik

@ -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;
}
}