kopia lustrzana https://github.com/OpenDroneMap/WebODM
Added presets API, unit tests
rodzic
3319a42e57
commit
704a7c34fd
|
@ -1,5 +1,7 @@
|
|||
from django.contrib import admin
|
||||
from guardian.admin import GuardedModelAdmin
|
||||
|
||||
from app.models import Preset
|
||||
from .models import Project, Task, ImageUpload
|
||||
|
||||
admin.site.register(Project, GuardedModelAdmin)
|
||||
|
@ -12,3 +14,6 @@ admin.site.register(Task, TaskAdmin)
|
|||
class ImageUploadAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('image',)
|
||||
admin.site.register(ImageUpload, ImageUploadAdmin)
|
||||
|
||||
admin.site.register(Preset, admin.ModelAdmin)
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
from django.db import transaction
|
||||
from rest_framework import permissions
|
||||
from rest_framework import serializers, viewsets
|
||||
from django.db.models import Q
|
||||
from rest_framework import status, exceptions
|
||||
from rest_framework.filters import DjangoFilterBackend, OrderingFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from app.models import Preset
|
||||
|
||||
|
||||
class PresetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Preset
|
||||
|
||||
exclude = ('owner', )
|
||||
read_only_fields = ('owner', 'created_at', 'system', )
|
||||
|
||||
|
||||
class PresetViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
Preset get/add/delete/update
|
||||
Presets represent a set of options that a user
|
||||
can save/customize for use in processing a task.
|
||||
"""
|
||||
|
||||
pagination_class = None
|
||||
serializer_class = PresetSerializer
|
||||
|
||||
# We don't use object level permissions on presets
|
||||
permission_classes = (permissions.DjangoModelPermissions, )
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter, )
|
||||
|
||||
def get_queryset(self):
|
||||
return Preset.objects.filter(Q(owner=self.request.user.id) | Q(system=True))
|
||||
|
||||
def create(self, request):
|
||||
with transaction.atomic():
|
||||
preset = Preset.objects.create(owner=self.request.user)
|
||||
|
||||
# Update other parameters
|
||||
serializer = PresetSerializer(preset, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def destroy(self, request, pk=None):
|
||||
preset = Preset.objects.get(pk=pk)
|
||||
|
||||
# Only owners can delete their own presets (except superusers)
|
||||
if preset.owner != request.user and not request.user.is_superuser:
|
||||
raise exceptions.NotFound()
|
||||
|
||||
# Even superusers cannot delete global presets via the API (must use admin backend)
|
||||
if preset.system:
|
||||
raise exceptions.PermissionDenied()
|
||||
|
||||
return super().destroy(request, pk)
|
|
@ -1,4 +1,6 @@
|
|||
from django.conf.urls import url, include
|
||||
|
||||
from app.api.presets import PresetViewSet
|
||||
from .projects import ProjectViewSet
|
||||
from .tasks import TaskViewSet, TaskTiles, TaskTilesJson, TaskDownloads, TaskAssets
|
||||
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
|
||||
|
@ -8,6 +10,8 @@ from rest_framework_jwt.views import obtain_jwt_token
|
|||
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')
|
||||
|
|
|
@ -42,7 +42,7 @@ def boot():
|
|||
|
||||
|
||||
# Add default permissions (view_project, change_project, delete_project, etc.)
|
||||
for permission in ('_project', '_task'):
|
||||
for permission in ('_project', '_task', '_preset'):
|
||||
default_group.permissions.add(
|
||||
*list(Permission.objects.filter(codename__endswith=permission))
|
||||
)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-07-21 17:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import app.models.task
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0008_preset'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='preset',
|
||||
name='options',
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=[], help_text="Options that define this preset (same format as in a Task's options).", validators=[app.models.task.validate_task_options]),
|
||||
),
|
||||
]
|
|
@ -11,14 +11,11 @@ logger = logging.getLogger('app.logger')
|
|||
|
||||
class Preset(models.Model):
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, help_text="The person who owns this preset")
|
||||
name = models.CharField(max_length=255, help_text="A label used to describe the preset")
|
||||
options = JSONField(default=dict(), blank=True, help_text="Options that define this preset (same format as in a Task's options).",
|
||||
name = models.CharField(max_length=255, blank=False, null=False, help_text="A label used to describe the preset")
|
||||
options = JSONField(default=list(), blank=True, help_text="Options that define this preset (same format as in a Task's options).",
|
||||
validators=[validate_task_options])
|
||||
created_at = models.DateTimeField(default=timezone.now, help_text="Creation date")
|
||||
system = models.BooleanField(db_index=True, default=False, help_text="Whether this preset is available to every user in the system or just to its owner.")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -34,7 +34,9 @@ class EditTaskForm extends React.Component {
|
|||
advancedOptions: props.task !== null ? props.task.options.length > 0 : false,
|
||||
loadedProcessingNodes: false,
|
||||
selectedNode: null,
|
||||
processingNodes: []
|
||||
processingNodes: [],
|
||||
selectedPreset: {key: 1, label: "TODO REMOVE"},
|
||||
presets: []
|
||||
};
|
||||
|
||||
// Refs to ProcessingNodeOption components
|
||||
|
@ -261,10 +263,11 @@ class EditTaskForm extends React.Component {
|
|||
<div className="form-group">
|
||||
<label className="col-sm-2 control-label">Options</label>
|
||||
<div className="col-sm-10">
|
||||
<div className="btn-group" role="group">
|
||||
<button type="button" className={"btn " + (!this.state.advancedOptions ? "btn-default" : "btn-secondary")} onClick={this.setAdvancedOptions(false)}>Use Defaults</button>
|
||||
<button type="button" className={"btn " + (this.state.advancedOptions ? "btn-default" : "btn-secondary")} onClick={this.setAdvancedOptions(true)}>Set Options</button>
|
||||
</div>
|
||||
<select className="form-control" value={this.state.selectedPreset.key} onChange={this.handleSelectPreset}>
|
||||
{this.state.presets.map(preset =>
|
||||
<option value={preset.key} key={preset.key}>{preset.label}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"form-group " + (!this.state.advancedOptions ? "hide" : "")}>
|
||||
|
@ -278,6 +281,10 @@ class EditTaskForm extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
/*<div className="btn-group" role="group">
|
||||
<button type="button" className={"btn " + (!this.state.advancedOptions ? "btn-default" : "btn-secondary")} onClick={this.setAdvancedOptions(false)}>Use Defaults</button>
|
||||
<button type="button" className={"btn " + (this.state.advancedOptions ? "btn-default" : "btn-secondary")} onClick={this.setAdvancedOptions(true)}>Set Options</button>
|
||||
</div>*/
|
||||
}else{
|
||||
processingNodesOptions = (<div className="form-group">
|
||||
<div className="col-sm-offset-2 col-sm-10">Loading processing nodes... <i className="fa fa-refresh fa-spin fa-fw"></i></div>
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
import logging
|
||||
|
||||
import json
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from app.models import Preset
|
||||
from app.tests.classes import BootTestCase
|
||||
|
||||
logger = logging.getLogger('app.logger')
|
||||
|
||||
class TestApiPreset(BootTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
superuser = User.objects.get(username='testsuperuser')
|
||||
|
||||
Preset.objects.create(owner=superuser, name='Global Preset #1', system=True, options=[{'test': True}])
|
||||
Preset.objects.create(owner=superuser, name='Global Preset #2', system=True, options=[{'test2': True}])
|
||||
Preset.objects.create(owner=superuser, name='Local Preset #1', system=False, options=[{'test3': True}])
|
||||
|
||||
def test_preset(self):
|
||||
client = APIClient()
|
||||
|
||||
# Cannot list presets without authentication
|
||||
res = client.get("/api/presets/")
|
||||
self.assertTrue(res.status_code == status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Cannot create presets without authentication
|
||||
res = client.post("/api/presets/", {
|
||||
'name': 'test',
|
||||
})
|
||||
self.assertTrue(res.status_code == status.HTTP_403_FORBIDDEN)
|
||||
|
||||
user = User.objects.get(username="testuser")
|
||||
self.assertFalse(user.is_superuser)
|
||||
|
||||
other_user = User.objects.get(username="testuser2")
|
||||
|
||||
client.login(username="testuser", password="test1234")
|
||||
|
||||
# Create local preset
|
||||
Preset.objects.create(owner=user, name='My Local Preset')
|
||||
|
||||
# Can list presets
|
||||
res = client.get("/api/presets/")
|
||||
self.assertTrue(res.status_code == status.HTTP_200_OK)
|
||||
|
||||
# Only ours and global presets are available
|
||||
self.assertTrue(len(res.data) == 3)
|
||||
self.assertTrue('My Local Preset' in [preset['name'] for preset in res.data])
|
||||
self.assertTrue('Global Preset #1' in [preset['name'] for preset in res.data])
|
||||
self.assertTrue('Global Preset #2' in [preset['name'] for preset in res.data])
|
||||
self.assertFalse('Local Preset #1' in [preset['name'] for preset in res.data])
|
||||
|
||||
# Owner field does not exist
|
||||
self.assertFalse('owner' in res.data[0])
|
||||
|
||||
# Can create preset when authenticated
|
||||
res = client.post("/api/presets/", {
|
||||
'name': 'test',
|
||||
'system': True
|
||||
})
|
||||
self.assertTrue(res.status_code == status.HTTP_201_CREATED)
|
||||
|
||||
# Result is not a system preset even though we tried to set it as such
|
||||
self.assertFalse(res.data['system'])
|
||||
|
||||
# Cannot create a preset and set it as somebody else's preset
|
||||
res = client.post("/api/presets/", {
|
||||
'name': 'test',
|
||||
'owner': other_user.id
|
||||
})
|
||||
self.assertTrue(res.status_code == status.HTTP_201_CREATED)
|
||||
|
||||
preset = Preset.objects.get(pk=res.data['id'])
|
||||
|
||||
# Still ours
|
||||
self.assertTrue(preset.owner == user)
|
||||
|
||||
# Cannot update one of our existing preset with a different user, or set it to system
|
||||
res = client.patch("/api/presets/{}/".format(preset.id),{
|
||||
'owner': other_user.id,
|
||||
'system': True
|
||||
})
|
||||
self.assertTrue(res.status_code == status.HTTP_200_OK)
|
||||
self.assertFalse(res.data['system'])
|
||||
preset.refresh_from_db()
|
||||
self.assertTrue(preset.owner == user)
|
||||
|
||||
# Can update name and options fields
|
||||
res = client.patch("/api/presets/{}/".format(preset.id), {
|
||||
'name': 'changed',
|
||||
'options': json.dumps([{'name': 'optname', 'value': 'optvalue'}])
|
||||
})
|
||||
self.assertTrue(res.status_code == status.HTTP_200_OK)
|
||||
self.assertTrue(res.data['name'] == 'changed')
|
||||
self.assertTrue('name' in res.data['options'][0])
|
||||
|
||||
# Cannot set an invalid options value
|
||||
res = client.patch("/api/presets/{}/".format(preset.id), {
|
||||
'options': json.dumps([{'invalid': 'value'}])
|
||||
})
|
||||
self.assertTrue(res.status_code == status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Can delete our own preset
|
||||
res = client.delete("/api/presets/{}/".format(preset.id))
|
||||
self.assertTrue(res.status_code == status.HTTP_204_NO_CONTENT)
|
||||
self.assertFalse(Preset.objects.filter(pk=preset.id).exists())
|
||||
|
||||
# Cannot delete somebody else's preset
|
||||
other_preset = Preset.objects.get(name="Local Preset #1")
|
||||
self.assertTrue(other_preset.owner != user)
|
||||
res = client.delete("/api/presets/{}/".format(other_preset.id))
|
||||
self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Cannot update somebody else's preset
|
||||
res = client.patch("/api/presets/{}/".format(other_preset.id), {
|
||||
'name': 'test'
|
||||
})
|
||||
self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Cannot delete a system preset (even as a superuser)
|
||||
system_preset = Preset.objects.get(name="Global Preset #1")
|
||||
res = client.delete("/api/presets/{}/".format(system_preset.id))
|
||||
self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND)
|
||||
|
||||
client.login(username="testsuperuser", password="test1234")
|
||||
res = client.delete("/api/presets/{}/".format(system_preset.id))
|
||||
self.assertTrue(res.status_code == status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
|
Ładowanie…
Reference in New Issue