From 49e7c5dfa5afb7e896845d16d7495f68df34a82d Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 13 Oct 2016 16:28:32 -0400 Subject: [PATCH] Django CSRF ajax support, file upload example --- app/admin.py | 8 +++- app/api/tasks.py | 29 +++++++++++--- app/media/.gitignore | 1 + app/models.py | 23 ++++++++++- .../app/js/components/ProjectListItem.jsx | 8 +++- app/static/app/js/django/csrf.js | 38 +++++++++++++++++++ app/static/app/js/main.jsx | 1 + webodm/settings.py | 2 +- 8 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 app/media/.gitignore create mode 100644 app/static/app/js/django/csrf.js diff --git a/app/admin.py b/app/admin.py index b9e1db3d..6c60f476 100644 --- a/app/admin.py +++ b/app/admin.py @@ -1,6 +1,10 @@ from django.contrib import admin from guardian.admin import GuardedModelAdmin -from .models import Project, Task +from .models import Project, Task, ImageUpload admin.site.register(Project, GuardedModelAdmin) -admin.site.register(Task, GuardedModelAdmin) +admin.site.register(Task, admin.ModelAdmin) + +class ImageUploadAdmin(admin.ModelAdmin): + readonly_fields = ('image',) +admin.site.register(ImageUpload, ImageUploadAdmin) diff --git a/app/api/tasks.py b/app/api/tasks.py index 0f544ded..e9f15703 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -1,7 +1,8 @@ from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from rest_framework import serializers, viewsets, filters, exceptions, permissions +from rest_framework import status, serializers, viewsets, filters, exceptions, permissions, parsers from rest_framework.response import Response +from rest_framework.decorators import parser_classes, api_view from app import models from nodeodm.models import ProcessingNode @@ -19,22 +20,25 @@ class TaskSerializer(serializers.ModelSerializer): class TaskViewSet(viewsets.ViewSet): """ - TODO: permissions! + A task represents a set of images and other input to be sent to a processing node. + Once a processing node completes processing, results are stored in the task. """ queryset = models.Task.objects.all() # We don't use object level permissions on tasks, relying on # project's object permissions instead (but standard model permissions still apply) permission_classes = (permissions.DjangoModelPermissions, ) + parser_classes = (parsers.MultiPartParser, ) - def get_and_check_project(self, request, project_pk): + def get_and_check_project(self, request, project_pk, perms = ('view_project', )): ''' Retrieves a project and raises an exeption if the current user has no access to it. ''' try: project = models.Project.objects.get(pk=project_pk) - if not request.user.has_perm('view_project', project): raise ObjectDoesNotExist() + for perm in perms: + if not request.user.has_perm(perm, project): raise ObjectDoesNotExist() except ObjectDoesNotExist: raise exceptions.NotFound() return project @@ -52,4 +56,19 @@ class TaskViewSet(viewsets.ViewSet): except ObjectDoesNotExist: raise exceptions.NotFound() serializer = TaskSerializer(task) - return Response(serializer.data) \ No newline at end of file + return Response(serializer.data) + + def create(self, request, project_pk=None): + project = self.get_and_check_project(request, project_pk, ('change_project', )) + + # MultiValueDict in, flat array of files out + files = [file for filesList in map( + lambda key: request.FILES.getlist(key), + [keys for keys in request.FILES]) + for file in filesList] + + task = models.Task.create_from_images(files, project) + if task != None: + return Response({"id": task.id}, status=status.HTTP_201_CREATED) + else: + raise exceptions.ValidationError(detail="Cannot create task, input provided is not valid.") diff --git a/app/media/.gitignore b/app/media/.gitignore new file mode 100644 index 00000000..f59ec20a --- /dev/null +++ b/app/media/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/app/models.py b/app/models.py index b46130bb..cb1c986f 100644 --- a/app/models.py +++ b/app/models.py @@ -10,6 +10,7 @@ from django.dispatch import receiver from guardian.shortcuts import get_perms_for_model, assign_perm from guardian.models import UserObjectPermissionBase from guardian.models import GroupObjectPermissionBase +from django.db import transaction def assets_directory_path(taskId, projectId, filename): # files will be uploaded to MEDIA_ROOT/project_/task_/ @@ -79,7 +80,25 @@ class Task(models.Model): created_at = models.DateTimeField(default=timezone.now, help_text="Creation date") def __str__(self): - return '{} {}'.format(self.name, self.uuid) + return 'Task ID: {}'.format(self.id) + + @staticmethod + def create_from_images(images, project): + ''' + Create a new task from a set of input images (such as the ones coming from request.FILES). + This will happen inside a transaction so if one of the images + fails to load, the task will not be created. + ''' + with transaction.atomic(): + task = Task.objects.create(project=project) + + for image in images: + ImageUpload.objects.create(task=task, image=image) + + return task + + # In case of error + return None class Meta: permissions = ( @@ -87,7 +106,7 @@ class Task(models.Model): ) -def image_directory_path(task, filename): +def image_directory_path(imageUpload, filename): return assets_directory_path(imageUpload.task.id, imageUpload.task.project.id, filename) class ImageUpload(models.Model): diff --git a/app/static/app/js/components/ProjectListItem.jsx b/app/static/app/js/components/ProjectListItem.jsx index ec2cbee8..dbc35dad 100644 --- a/app/static/app/js/components/ProjectListItem.jsx +++ b/app/static/app/js/components/ProjectListItem.jsx @@ -1,6 +1,7 @@ import React from 'react'; import ProjectListItemPanel from './ProjectListItemPanel'; import Dropzone from '../vendor/dropzone'; +import csrf from '../django/csrf'; import $ from 'jquery'; class ProjectListItem extends React.Component { @@ -20,7 +21,12 @@ class ProjectListItem extends React.Component { Dropzone.autoDiscover = false; let dropzone = new Dropzone(domNode, { - url : '/api/upload' + url : `/api/projects/${this.props.data.id}/tasks/`, + parallelUploads: 9999999, + uploadMultiple: true, + headers: { + [csrf.header]: csrf.token + } }); dropzone.on("complete", function(file) { diff --git a/app/static/app/js/django/csrf.js b/app/static/app/js/django/csrf.js new file mode 100644 index 00000000..653c9d4d --- /dev/null +++ b/app/static/app/js/django/csrf.js @@ -0,0 +1,38 @@ +import $ from 'jquery'; + +function getCookie(name) { + var cookieValue = null; + if (document.cookie && document.cookie !== '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = jQuery.trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} + +function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +} + +let header = "X-CSRFToken", + token = getCookie('csrftoken'); + +// Automatically setup jQuery to send a CSRF header +$.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader(header, token); + } + } +}); + +export default { + header, token +}; \ No newline at end of file diff --git a/app/static/app/js/main.jsx b/app/static/app/js/main.jsx index 383668a6..d733f4c8 100644 --- a/app/static/app/js/main.jsx +++ b/app/static/app/js/main.jsx @@ -1,4 +1,5 @@ import '../css/main.scss'; +import './django/csrf'; import React from 'react'; import ReactDOM from 'react-dom'; import Dashboard from './Dashboard'; diff --git a/webodm/settings.py b/webodm/settings.py index 4887cfcf..c5f05ddc 100644 --- a/webodm/settings.py +++ b/webodm/settings.py @@ -191,7 +191,7 @@ LOGIN_REDIRECT_URL = '/dashboard/' LOGIN_URL = '/login/' # File uploads -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_ROOT = os.path.join(BASE_DIR, 'app', 'media') # Store flash messages in cookies MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'