kopia lustrzana https://github.com/OpenDroneMap/WebODM
commit
9e4c626d42
|
@ -10,7 +10,7 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
class AdminUserViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
|
@ -34,7 +34,7 @@ class GroupSerializer(serializers.ModelSerializer):
|
|||
model = Group
|
||||
fields = '__all__'
|
||||
|
||||
class GroupViewSet(viewsets.ModelViewSet):
|
||||
class AdminGroupViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = GroupSerializer
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
from guardian.shortcuts import get_perms
|
||||
from guardian.shortcuts import get_perms, get_users_with_perms, assign_perm, remove_perm
|
||||
from rest_framework import serializers, viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.db import transaction
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from app import models
|
||||
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 +24,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 +65,74 @@ 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, with_group_users=False)
|
||||
for user in perms:
|
||||
result.append({'username': user.username,
|
||||
'owner': project.owner == user,
|
||||
'permissions': normalized_perm_names(perms[user])})
|
||||
|
||||
result.sort(key=lambda r: r['owner'], reverse=True)
|
||||
return Response(result, status=status.HTTP_200_OK)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
def edit(self, request, pk=None):
|
||||
project = get_and_check_project(request, pk, ('change_project', ))
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
project.name = request.data.get('name', '')
|
||||
project.description = request.data.get('description', '')
|
||||
project.save()
|
||||
|
||||
form_perms = request.data.get('permissions')
|
||||
if form_perms is not None:
|
||||
# Build perms map (ignore owners, empty usernames)
|
||||
perms_map = {}
|
||||
for perm in form_perms:
|
||||
if not perm.get('owner') and perm.get('username'):
|
||||
perms_map[perm['username']] = perm['permissions']
|
||||
|
||||
db_perms = get_users_with_perms(project, attach_perms=True, with_group_users=False)
|
||||
|
||||
# Check users to remove
|
||||
for user in db_perms:
|
||||
|
||||
# Never modify owner permissions
|
||||
if project.owner == user:
|
||||
continue
|
||||
|
||||
if perms_map.get(user.username) is None:
|
||||
for p in db_perms[user]:
|
||||
remove_perm(p, user, project)
|
||||
|
||||
# Check users to add/edit
|
||||
for username in perms_map:
|
||||
for p in ["add", "change", "delete", "view"]:
|
||||
perm = p + "_project"
|
||||
user = User.objects.get(username=username)
|
||||
|
||||
# Skip owners
|
||||
if project.owner == user:
|
||||
continue
|
||||
|
||||
# Has permission in database but not in form?
|
||||
if user.has_perm(perm, project) and not p in perms_map[username]:
|
||||
remove_perm(perm, user, project)
|
||||
|
||||
# Has permission in form but not in database?
|
||||
elif p in perms_map[username] and not user.has_perm(perm, project):
|
||||
assign_perm(perm, user, project)
|
||||
|
||||
except User.DoesNotExist as e:
|
||||
return Response({'error': _("Invalid user in permissions list")}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except AttributeError as e:
|
||||
return Response({'error': _("Invalid permissions")}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response({'success': True}, status=status.HTTP_200_OK)
|
|
@ -192,6 +192,7 @@ class Metadata(TaskNestedView):
|
|||
info['statistics'][b]['max'] = hrange[1]
|
||||
|
||||
cmap_labels = {
|
||||
"viridis": "Viridis",
|
||||
"jet": "Jet",
|
||||
"terrain": "Terrain",
|
||||
"gist_earth": "Earth",
|
||||
|
@ -208,7 +209,7 @@ class Metadata(TaskNestedView):
|
|||
colormaps = []
|
||||
algorithms = []
|
||||
if tile_type in ['dsm', 'dtm']:
|
||||
colormaps = ['jet', 'terrain', 'gist_earth', 'pastel1']
|
||||
colormaps = ['viridis', 'jet', 'terrain', 'gist_earth', 'pastel1']
|
||||
elif formula and bands:
|
||||
colormaps = ['rdylgn', 'spectral', 'rdylgn_r', 'spectral_r', 'rplumbo', 'discrete_ndvi',
|
||||
'better_discrete_ndvi']
|
||||
|
|
|
@ -6,25 +6,26 @@ from .projects import ProjectViewSet
|
|||
from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskAssetsImport
|
||||
from .imageuploads import Thumbnail, ImageDownload
|
||||
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
|
||||
from .admin import UserViewSet, GroupViewSet
|
||||
from .admin import AdminUserViewSet, AdminGroupViewSet
|
||||
from rest_framework_nested import routers
|
||||
from rest_framework_jwt.views import obtain_jwt_token
|
||||
from .tiler import TileJson, Bounds, Metadata, Tiles, Export
|
||||
from .potree import Scene, CameraView
|
||||
from .workers import CheckTask, GetTaskResult
|
||||
from .users import UsersList
|
||||
from webodm import settings
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'projects', ProjectViewSet)
|
||||
router.register(r'processingnodes', ProcessingNodeViewSet)
|
||||
router.register(r'presets', PresetViewSet, base_name='presets')
|
||||
|
||||
|
||||
tasks_router = routers.NestedSimpleRouter(router, r'projects', lookup='project')
|
||||
tasks_router.register(r'tasks', TaskViewSet, base_name='projects-tasks')
|
||||
|
||||
admin_router = routers.DefaultRouter()
|
||||
admin_router.register(r'admin/users', UserViewSet, basename='user')
|
||||
admin_router.register(r'admin/groups', GroupViewSet, basename='group')
|
||||
admin_router.register(r'admin/users', AdminUserViewSet, base_name='admin-users')
|
||||
admin_router.register(r'admin/groups', AdminGroupViewSet, base_name='admin-groups')
|
||||
|
||||
urlpatterns = [
|
||||
url(r'processingnodes/options/$', ProcessingNodeOptionsView.as_view()),
|
||||
|
@ -57,3 +58,7 @@ urlpatterns = [
|
|||
|
||||
url(r'^plugins/(?P<plugin_name>[^/.]+)/(.*)$', api_view_handler)
|
||||
]
|
||||
|
||||
if settings.ENABLE_USERS_API:
|
||||
urlpatterns.append(url(r'users', UsersList.as_view()))
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
from django.contrib.auth.models import User
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import exceptions, permissions, parsers
|
||||
from rest_framework.response import Response
|
||||
|
||||
class UsersList(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
parser_classes = (parsers.JSONParser, parsers.FormParser,)
|
||||
|
||||
def get(self, request):
|
||||
qs = User.objects.all()
|
||||
|
||||
search = self.request.query_params.get('search', None)
|
||||
if search is not None:
|
||||
qs = qs.filter(username__istartswith=search) | qs.filter(email__istartswith=search)
|
||||
|
||||
limit = self.request.query_params.get('limit', None)
|
||||
if limit is not None:
|
||||
try:
|
||||
qs = qs[:abs(int(limit))]
|
||||
except ValueError:
|
||||
raise exceptions.ValidationError(detail="Invalid query parameters")
|
||||
|
||||
return Response([{'username': u.username, 'email': u.email} for u in qs])
|
|
@ -0,0 +1,264 @@
|
|||
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: {},
|
||||
validatingUser: false,
|
||||
validationUnavailable: false
|
||||
};
|
||||
|
||||
this.backgroundFailedColor = Css.getValue('btn-danger', 'backgroundColor');
|
||||
this.autocompleteBorderColor = Css.getValue('btn-default', 'backgroundColor');
|
||||
this.backgroundColor = Css.getValue('theme-secondary', 'backgroundColor');
|
||||
this.highlightColor = Css.getValue('theme-background-highlight', 'backgroundColor');
|
||||
}
|
||||
|
||||
loadPermissions = () => {
|
||||
this.setState({loading: true, permissions: []});
|
||||
|
||||
this.permsRequest =
|
||||
$.getJSON(`/api/projects/${this.props.projectId}/permissions/`, json => {
|
||||
let validUsernames = {};
|
||||
json.forEach(p => validUsernames[p.username] = true);
|
||||
this.setState({validUsernames, permissions: json});
|
||||
})
|
||||
.fail(() => {
|
||||
this.setState({error: _("Cannot load permissions.")});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({loading: false});
|
||||
});
|
||||
}
|
||||
|
||||
getPermissions = () => {
|
||||
// Cleanup temporary objects then return
|
||||
this.state.permissions.forEach(perm => delete(perm.autocomplete));
|
||||
return this.state.permissions;
|
||||
}
|
||||
|
||||
autocomplete = (perm) => {
|
||||
if (this.validateReq){
|
||||
this.validateReq.abort();
|
||||
this.validateReq = null;
|
||||
}
|
||||
|
||||
if (this.validateTimeout){
|
||||
clearTimeout(this.validateTimeout);
|
||||
this.validateTimeout = null;
|
||||
}
|
||||
|
||||
|
||||
// Empty case
|
||||
if (perm.username === ""){
|
||||
delete(perm.autocomplete);
|
||||
this.setState({permissions: this.state.permissions});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({validatingUser: true});
|
||||
this.validateTimeout = setTimeout(() => {
|
||||
this.validateReq = $.getJSON(`/api/users/?limit=30&search=${encodeURIComponent(perm.username)}`)
|
||||
.done((json) => {
|
||||
json.forEach(u => {
|
||||
this.state.validUsernames[u.username] = true;
|
||||
});
|
||||
|
||||
this.state.permissions.forEach(p => delete(p.autocomplete));
|
||||
|
||||
if (this.textFocused) perm.autocomplete = json;
|
||||
|
||||
this.setState({validUsernames: this.state.validUsernames, permissions: this.state.permissions});
|
||||
}).fail(jqXHR => {
|
||||
// Perhaps the user API is not enabled
|
||||
if (jqXHR.statusText !== "abort"){
|
||||
this.setState({validationUnavailable: true});
|
||||
}
|
||||
}).always(() => {
|
||||
this.setState({validatingUser: false});
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
if (!this.props.lazyLoad) this.loadPermissions();
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
if (this.permsRequest) this.permsRequest.abort();
|
||||
if (this.validateReq) this.validateReq.abort();
|
||||
if (this.validateTimeout) clearTimeout(this.validateTimeout);
|
||||
}
|
||||
|
||||
handleChangePermissionRole = perm => {
|
||||
return e => {
|
||||
perm.permissions = this.extendedPermissions(e.target.value);
|
||||
this.setState({permissions: this.state.permissions});
|
||||
}
|
||||
}
|
||||
|
||||
handleChangePermissionUser = perm => {
|
||||
return e => {
|
||||
perm.username = e.target.value;
|
||||
|
||||
this.autocomplete(perm);
|
||||
|
||||
// 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 "";
|
||||
}
|
||||
|
||||
extendedPermissions = simPerm => {
|
||||
if (simPerm == "rw"){
|
||||
return ["add", "change", "delete", "view"];
|
||||
}else if (simPerm == "r"){
|
||||
return ["view"];
|
||||
}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.validationUnavailable || this.state.validatingUser || this.state.validUsernames[username]) return "";
|
||||
else return this.backgroundFailedColor;
|
||||
}
|
||||
|
||||
addNewPermission = () => {
|
||||
this.setState(update(this.state, {
|
||||
permissions: {$push: [{username: "", 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)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
acOnMouseEnter = e => {
|
||||
e.target.style.backgroundColor = this.highlightColor;
|
||||
}
|
||||
|
||||
acOnMouseLeave = e => {
|
||||
e.target.style.backgroundColor = "";
|
||||
}
|
||||
|
||||
acOnClick = (perm, acEntry) => {
|
||||
return e => {
|
||||
perm.username = acEntry.username;
|
||||
delete(perm.autocomplete);
|
||||
this.setState({permissions: this.state.permissions});
|
||||
}
|
||||
}
|
||||
|
||||
onFocus = e => {
|
||||
this.textFocused = true;
|
||||
}
|
||||
|
||||
onBlur = perm => {
|
||||
return e => {
|
||||
delete(perm.autocomplete);
|
||||
this.setState({permissions: this.state.permissions});
|
||||
this.textFocused = false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const permissions = this.state.permissions.map((p, i) => <form autoComplete="off" key={i}>
|
||||
<div className="permission">
|
||||
<div className="username-container">
|
||||
<i className="fa fa-user user-indicator"/>
|
||||
<input
|
||||
style={{color: this.getColorFor(p.username)}}
|
||||
onChange={this.handleChangePermissionUser(p)}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur(p)}
|
||||
disabled={p.owner}
|
||||
value={p.username}
|
||||
className="form-control username"
|
||||
placeholder={_("Username")}
|
||||
ref={(domNode) => this.lastTextbox = domNode} />
|
||||
{p.autocomplete && p.autocomplete.length > 0 ? <div className="autocomplete" style={{borderColor: this.autocompleteBorderColor, backgroundColor: this.backgroundColor}}>
|
||||
{p.autocomplete.map(ac => <div key={ac.username} onMouseDown={this.acOnClick(p, ac)} className="ac-entry" onMouseEnter={this.acOnMouseEnter} onMouseLeave={this.acOnMouseLeave} style={{borderColor: this.autocompleteBorderColor}}>
|
||||
<div className="ac-user">{ac.username}</div>
|
||||
<div className="ac-email">{ac.email}</div>
|
||||
</div>)}
|
||||
</div> : ""}
|
||||
</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(p)}>
|
||||
{this.allPermissions().map(p => <option key={p.key} value={p.key}>{p.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>);
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
|
@ -62,13 +65,20 @@ class EditProjectDialog extends React.Component {
|
|||
}
|
||||
|
||||
getFormData(){
|
||||
return {
|
||||
const res = {
|
||||
name: this.state.name,
|
||||
descr: this.state.descr,
|
||||
};
|
||||
|
||||
if (this.editPermissionsPanel){
|
||||
res.permissions = this.editPermissionsPanel.getPermissions();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
onShow(){
|
||||
this.editPermissionsPanel.loadPermissions();
|
||||
this.nameInput.focus();
|
||||
}
|
||||
|
||||
|
@ -123,7 +133,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 +148,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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ class FormDialog extends React.Component {
|
|||
this.serverRequest = this.props.saveAction(formData);
|
||||
if (this.serverRequest){
|
||||
this.serverRequest.fail(e => {
|
||||
this.setState({error: e.message || (e.responseJSON || {}).detail || e.responseText || _("Could not apply changes")});
|
||||
this.setState({error: e.message || (e.responseJSON || {}).detail || (e.responseJSON || {}).error || e.responseText || _("Could not apply changes")});
|
||||
}).always(() => {
|
||||
this.setState({saving: false});
|
||||
this.serverRequest = null;
|
||||
|
@ -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>
|
||||
|
|
|
@ -126,7 +126,7 @@ class Map extends React.Component {
|
|||
let metaUrl = url + "metadata";
|
||||
|
||||
if (type == "plant") metaUrl += "?formula=NDVI&bands=RGN&color_map=rdylgn";
|
||||
if (type == "dsm" || type == "dtm") metaUrl += "?hillshade=6&color_map=jet";
|
||||
if (type == "dsm" || type == "dtm") metaUrl += "?hillshade=6&color_map=viridis";
|
||||
|
||||
this.tileJsonRequests.push($.getJSON(metaUrl)
|
||||
.done(mres => {
|
||||
|
|
|
@ -378,14 +378,15 @@ class ProjectListItem extends React.Component {
|
|||
|
||||
updateProject(project){
|
||||
return $.ajax({
|
||||
url: `/api/projects/${this.state.data.id}/`,
|
||||
url: `/api/projects/${this.state.data.id}/edit/`,
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
name: project.name,
|
||||
description: project.descr,
|
||||
permissions: project.permissions
|
||||
}),
|
||||
dataType: 'json',
|
||||
type: 'PATCH'
|
||||
type: 'POST'
|
||||
}).done(() => {
|
||||
this.refresh();
|
||||
});
|
||||
|
@ -469,27 +470,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 +546,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,77 @@
|
|||
.edit-permissions-panel{
|
||||
.perms-loading{
|
||||
margin-top: 14px;
|
||||
}
|
||||
.permission{
|
||||
position: relative;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
}
|
||||
.user-indicator{
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 16px;
|
||||
}
|
||||
.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;
|
||||
position: relative;
|
||||
|
||||
.autocomplete{
|
||||
left: 0;
|
||||
top: 44px;
|
||||
z-index: 2;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
border-radius: 2px;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
.ac-entry{
|
||||
padding: 10px 15px 10px 15px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ac-entry:last-child{
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ac-email{
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
.role-container{
|
||||
flex-grow: 0.33;
|
||||
}
|
||||
|
||||
.add-new{
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import logging
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
from app.models import Project
|
||||
from .classes import BootTestCase
|
||||
from guardian.shortcuts import get_perms
|
||||
|
||||
from webodm import settings
|
||||
logger = logging.getLogger('app.logger')
|
||||
|
||||
|
||||
class TestApiProjects(BootTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
|
||||
def test_project(self):
|
||||
client = APIClient()
|
||||
|
||||
user = User.objects.get(username="testuser")
|
||||
project = Project.objects.create(
|
||||
owner=user,
|
||||
name="test project"
|
||||
)
|
||||
|
||||
# Cannot edit project (anonymous)
|
||||
res = client.post("/api/projects/{}/edit/".format(project.id), {
|
||||
'name': 'edited'
|
||||
})
|
||||
self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
client.login(username="testuser", password="test1234")
|
||||
|
||||
# Can edit project
|
||||
res = client.post("/api/projects/{}/edit/".format(project.id), {
|
||||
'name': 'edited'
|
||||
})
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
project.refresh_from_db()
|
||||
|
||||
self.assertEqual(project.name, 'edited')
|
||||
self.assertEqual(project.description, '')
|
||||
|
||||
other_user = User.objects.get(username="testuser2")
|
||||
|
||||
other_client = APIClient()
|
||||
other_client.login(username="testuser2", password="test1234")
|
||||
|
||||
# Other user cannot edit project
|
||||
res = other_client.post("/api/projects/{}/edit/".format(project.id), {
|
||||
'name': 'edited2'
|
||||
})
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Other user cannot see project
|
||||
res = other_client.get("/api/projects/{}/".format(project.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Change permissions via API
|
||||
res = client.post("/api/projects/{}/edit/".format(project.id), {
|
||||
'permissions': [{'username': 'testuser2', 'permissions': ['view']}]
|
||||
}, format="json")
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Other user can see project
|
||||
res = other_client.get("/api/projects/{}/".format(project.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Other user still cannot edit project
|
||||
res = other_client.post("/api/projects/{}/edit/".format(project.id), {
|
||||
'name': 'edited2'
|
||||
})
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Change permissions again
|
||||
res = client.post("/api/projects/{}/edit/".format(project.id), {
|
||||
'permissions': [{'username': 'testuser2', 'permissions': ['view', 'add', 'change', 'delete']}]
|
||||
}, format="json")
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Other user can now edit
|
||||
res = other_client.post("/api/projects/{}/edit/".format(project.id), {
|
||||
'name': 'edited3'
|
||||
})
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
project.refresh_from_db()
|
||||
self.assertEqual(project.name, 'edited3')
|
||||
|
||||
# Can remove permissions
|
||||
res = client.post("/api/projects/{}/edit/".format(project.id), {
|
||||
'permissions': []
|
||||
}, format="json")
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
|
||||
# Other user cannot see project
|
||||
res = other_client.get("/api/projects/{}/".format(project.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Current user (owner) still has permissions
|
||||
res = client.get("/api/projects/{}/".format(project.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
|
||||
perms = get_perms(user, project)
|
||||
self.assertEqual(len(perms), 4)
|
|
@ -0,0 +1,52 @@
|
|||
import logging
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
from app.models import Project
|
||||
from .classes import BootTestCase
|
||||
|
||||
from webodm import settings
|
||||
logger = logging.getLogger('app.logger')
|
||||
|
||||
|
||||
class TestApiUsers(BootTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
|
||||
def test_users(self):
|
||||
client = APIClient()
|
||||
|
||||
user = User.objects.get(username="testuser")
|
||||
|
||||
# Cannot list users (anonymous)
|
||||
res = client.get("/api/users/?limit=30")
|
||||
self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
client.login(username="testuser", password="test1234")
|
||||
|
||||
# Can list users (authenticated)
|
||||
res = client.get("/api/users/?limit=30")
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue([u for u in res.data if u['username'] == 'testuser'])
|
||||
self.assertTrue([u for u in res.data if u['username'] == 'testsuperuser'])
|
||||
|
||||
# Can search for users
|
||||
res = client.get("/api/users/?search=super")
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertFalse([u for u in res.data if u['username'] == 'testuser'])
|
||||
self.assertTrue([u for u in res.data if u['username'] == 'testsuperuser'])
|
||||
|
||||
# Can search for users and limit
|
||||
res = client.get("/api/users/?search=super&limit=1")
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertFalse([u for u in res.data if u['username'] == 'testuser'])
|
||||
self.assertTrue([u for u in res.data if u['username'] == 'testsuperuser'])
|
||||
|
||||
# Handle invalid limits
|
||||
res = client.get("/api/users/?search=super&limit=-1")
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(res.data), 1)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "WebODM",
|
||||
"version": "1.9.9",
|
||||
"version": "1.9.10",
|
||||
"description": "User-friendly, extendable application and API for processing aerial imagery.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -69,6 +69,12 @@ SINGLE_USER_MODE = False
|
|||
# URL to redirect to if there are no processing nodes when visiting the dashboard
|
||||
PROCESSING_NODES_ONBOARDING = None
|
||||
|
||||
# Enable the /api/users endpoint which is used for autocompleting
|
||||
# usernames when handling project permissions. This can be disabled
|
||||
# for security reasons if you don't want to let authenticated users
|
||||
# retrieve the user list.
|
||||
ENABLE_USERS_API = True
|
||||
|
||||
# Enable desktop mode. In desktop mode some styling changes
|
||||
# are applied to make the application look nicer on desktop
|
||||
# as well as disabling certain features (e.g. sharing)
|
||||
|
|
Ładowanie…
Reference in New Issue