diff --git a/app/admin.py b/app/admin.py index ff412b5c..ef34ef52 100644 --- a/app/admin.py +++ b/app/admin.py @@ -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) + diff --git a/app/api/presets.py b/app/api/presets.py new file mode 100644 index 00000000..60a65cdb --- /dev/null +++ b/app/api/presets.py @@ -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) diff --git a/app/api/urls.py b/app/api/urls.py index df438e34..4b57ec80 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -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') diff --git a/app/boot.py b/app/boot.py index 5ee436dd..6daf412b 100644 --- a/app/boot.py +++ b/app/boot.py @@ -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)) ) diff --git a/app/migrations/0009_auto_20170721_1332.py b/app/migrations/0009_auto_20170721_1332.py new file mode 100644 index 00000000..9076765b --- /dev/null +++ b/app/migrations/0009_auto_20170721_1332.py @@ -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]), + ), + ] diff --git a/app/models/preset.py b/app/models/preset.py index 2a9d4358..170f6e70 100644 --- a/app/models/preset.py +++ b/app/models/preset.py @@ -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 - - - diff --git a/app/static/app/js/components/EditTaskForm.jsx b/app/static/app/js/components/EditTaskForm.jsx index 6677e9fd..727b2092 100644 --- a/app/static/app/js/components/EditTaskForm.jsx +++ b/app/static/app/js/components/EditTaskForm.jsx @@ -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 {