Added presets API, unit tests

pull/246/head
Piero Toffanin 2017-07-21 16:48:01 -04:00
rodzic 3319a42e57
commit 704a7c34fd
8 zmienionych plików z 239 dodań i 11 usunięć

Wyświetl plik

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

59
app/api/presets.py 100644
Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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