From abb89307020b59ef1dd24b399d06d3b9b41fd6bf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 6 May 2019 18:05:29 +1000 Subject: [PATCH 1/8] Limit SupplierPart choices in EditStockItem view --- InvenTree/stock/views.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index fc174fa3f1..1f6f85ea1a 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -115,6 +115,22 @@ class StockItemEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = 'Edit Stock Item' + def get_form(self): + """ Get form for StockItem editing. + + Limit the choices for supplier_part + """ + + form = super(AjaxUpdateView, self).get_form() + + item = self.get_object() + + query = form.fields['supplier_part'].queryset + query = query.filter(part=item.part.id) + form.fields['supplier_part'].queryset = query + + return form + class StockLocationCreate(AjaxCreateView): """ From dca26b581072501b951e2383ebaf866bc0305369 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 6 May 2019 19:21:14 +1000 Subject: [PATCH 2/8] Change the display of the part thumbnail when dragging a file overhead Refs: - https://stackoverflow.com/questions/26756176/jquery-dragenter-or-dragover-to-include-children#26777526 - https://stackoverflow.com/questions/10867506/dragleave-of-parent-element-fires-when-dragging-over-children-elements Thanks, StackOverflow! --- InvenTree/part/templates/part/part_base.html | 17 ++++++++----- InvenTree/static/css/inventree.css | 25 ++++++++++++++++--- .../static/script/inventree/inventree.js | 16 ++++++++++++ InvenTree/templates/base.html | 11 ++++++++ 4 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 InvenTree/static/script/inventree/inventree.js diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 5dc7add027..c1836b9f8f 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -13,12 +13,17 @@
- +
+
+ +
+ {% csrf_token %} +

diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index 3a9c1be184..78483daac5 100644 --- a/InvenTree/static/css/inventree.css +++ b/InvenTree/static/css/inventree.css @@ -10,6 +10,25 @@ color: #ffcc00; } +/* dropzone class - for Drag-n-Drop file uploads */ +.dropzone { + border: 1px solid #555; + z-index: 2; +} + +.dropzone * { + pointer-events: none; +} + +.dragover { + background-color: #55A; + border: 1px dashed #111; + opacity: 0.1; + -moz-opacity: 10%; + -webkit-opacity: 10%; +} + + .btn-glyph { padding-left: 6px; padding-right: 6px; @@ -28,7 +47,6 @@ .part-thumb { width: 150px; height: 150px; - border: 1px black solid; margin: 5px; padding: 5px; object-fit: contain; @@ -62,7 +80,7 @@ margin-right: 50px; margin-left: 50px; width: 100%; - //transition: 0.1s; + transition: 0.1s; } .body { @@ -181,4 +199,5 @@ @keyframes spin { from { transform: scale(1) rotate(0deg);} to { transform: scale(1) rotate(360deg);} -} \ No newline at end of file +} + diff --git a/InvenTree/static/script/inventree/inventree.js b/InvenTree/static/script/inventree/inventree.js new file mode 100644 index 0000000000..e47e8f218b --- /dev/null +++ b/InvenTree/static/script/inventree/inventree.js @@ -0,0 +1,16 @@ +function inventreeDocReady() { + /* Run this function when the HTML document is loaded. + * This will be called for every page that extends "base.html" + */ + + /* Add drag-n-drop functionality to any element + * marked with the class 'dropzone' + */ + $('.dropzone').on('dragenter', function() { + $(this).addClass('dragover'); + }); + + $('.dropzone').on('dragleave', function() { + $(this).removeClass('dragover'); + }); +} \ No newline at end of file diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 8331476cfa..21b1779924 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -65,6 +65,8 @@ InvenTree + + @@ -76,6 +78,15 @@ InvenTree $(document).ready(function () { {% block js_ready %} {% endblock %} + + /* Run document-ready scripts. + * Ref: static/script/inventree/inventree.js + */ + inventreeDocReady(); + + /* Display any cached alert messages + * Ref: static/script/inventree/notification.js + */ showCachedAlerts(); $('#launch-about').click(function() { From eec0fc34d2bf34a573d19fa6adae232e83b354cc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 6 May 2019 21:22:31 +1000 Subject: [PATCH 3/8] Provide function callback when file is dropped - https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file - --- InvenTree/part/templates/part/part_base.html | 12 ++++++++++ .../static/script/inventree/inventree.js | 24 ++++++++++++++++++- InvenTree/templates/base.html | 11 --------- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index c1836b9f8f..c1c64dddea 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -101,6 +101,18 @@ {% block js_ready %} {{ block.super }} + + $('#part-thumb').on('drop', function(event) { + + var transfer = event.originalEvent.dataTransfer; + + var files = transfer.files; + + console.log('dropped'); + + //$(this).removeClass('dragover'); + }); + $("#show-qr-code").click(function() { launchModalForm( "{% url 'part-qr' part.id %}", diff --git a/InvenTree/static/script/inventree/inventree.js b/InvenTree/static/script/inventree/inventree.js index e47e8f218b..35f446f9ef 100644 --- a/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/static/script/inventree/inventree.js @@ -3,6 +3,16 @@ function inventreeDocReady() { * This will be called for every page that extends "base.html" */ + window.addEventListener("dragover",function(e){ + e = e || event; + e.preventDefault(); + },false); + + window.addEventListener("drop",function(e){ + e = e || event; + e.preventDefault(); + },false); + /* Add drag-n-drop functionality to any element * marked with the class 'dropzone' */ @@ -10,7 +20,19 @@ function inventreeDocReady() { $(this).addClass('dragover'); }); - $('.dropzone').on('dragleave', function() { + $('.dropzone').on('dragleave drop', function() { $(this).removeClass('dragover'); }); + + // Callback to launch the 'About' window + $('#launch-about').click(function() { + var modal = $('#modal-about'); + + modal.modal({ + backdrop: 'static', + keyboard: 'false', + }); + + modal.modal('show'); + }); } \ No newline at end of file diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 21b1779924..2bc5cc9cd2 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -88,17 +88,6 @@ $(document).ready(function () { * Ref: static/script/inventree/notification.js */ showCachedAlerts(); - - $('#launch-about').click(function() { - var modal = $('#modal-about'); - - modal.modal({ - backdrop: 'static', - keyboard: 'false', - }); - - modal.modal('show'); - }); }); From c88149b9aae63746875a290be96c1b6b1f6c5187 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 6 May 2019 21:49:01 +1000 Subject: [PATCH 4/8] POST image data to View - https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects - https://stackoverflow.com/questions/25390598/append-called-on-an-object-that-does-not-implement-interface-formdata#25390646 --- InvenTree/part/templates/part/part_base.html | 42 +++++++++++++++++++- InvenTree/part/urls.py | 4 ++ InvenTree/part/views.py | 26 ++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index c1c64dddea..8abf7717f3 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -110,7 +110,47 @@ console.log('dropped'); - //$(this).removeClass('dragover'); + if (files.length > 0) { + var file = files[0]; + + var formData = new FormData(); + + var token = getCookie('csrftoken'); + + formData.append('file', file); + + $.ajax({ + beforeSend: function(xhr, settings) { + xhr.setRequestHeader('X-CSRFToken', token); + }, + url: "{% url 'part-image-upload' part.id %}", + type: 'POST', + data: formData, + processData: false, + contentType: false, + success: function(data, status, xhr) { + //location.reload(); + }, + error: function(xhr, status, error) { + console.log('Error uploading thumbnail: ' + status); + console.log(error); + } + }); + + /* + inventreeUpdate( + "{% url 'part-image-upload' part.id %}", + formData, + { + method: 'POST', + dataType: 'json', + } + ); + */ + + console.log('submitted'); + } + }); $("#show-qr-code").click(function() { diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 4cda10a0f0..7663318f96 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -47,6 +47,10 @@ part_detail_urls = [ url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'), + # Drag-and-drop thumbnail upload + url(r'^thumbnail-upload/?', views.UploadPartImage.as_view(), name='part-image-upload'), + + # Normal thumbnail with form url(r'^thumbnail/?', views.PartImage.as_view(), name='part-image'), # Any other URLs go to the part detail page diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index c97808888e..6fbd18318e 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals from django.shortcuts import get_object_or_404 +from django.http import JsonResponse from django.urls import reverse_lazy from django.views.generic import DetailView, ListView from django.forms.models import model_to_dict @@ -268,6 +269,31 @@ class PartImage(AjaxUpdateView): } +class UploadPartImage(AjaxView): + """ View for uploading a Part image """ + + model = Part + + def post(self, request, *args, **kwargs): + try: + part = Part.objects.get(pk=kwargs.get('pk')) + except Part.DoesNotExist: + error_dict = { + 'error': 'Part not found', + } + return JsonResponse(error_dict, status=404) + + print("Files:", request.FILES) + uploaded_file = request.FILES['file'] + + response_dict = { + 'success': 'File was uploaded successfully', + } + + return JsonResponse(response_dict, status=200) + + + class PartEdit(AjaxUpdateView): """ View for editing Part object """ From bb702367b6492eb99a1e151b838aeb71541aabaa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 6 May 2019 22:20:06 +1000 Subject: [PATCH 5/8] Test that uploaded image is an image file, return error otherwise - New javascript function inventreeFileUpload --- InvenTree/InvenTree/helpers.py | 10 ++++ InvenTree/part/forms.py | 1 - InvenTree/part/templates/part/detail.html | 4 +- InvenTree/part/templates/part/part_base.html | 43 ++++-------------- InvenTree/part/views.py | 25 ++++++---- InvenTree/static/script/inventree/api.js | 48 ++++++++++++++++++-- InvenTree/static/script/inventree/part.js | 4 +- InvenTree/static/script/inventree/stock.js | 4 +- 8 files changed, 86 insertions(+), 53 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 2843dc4eb8..e14efd5298 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -5,11 +5,21 @@ Provides helper functions used throughout the InvenTree project import io import json from datetime import datetime +from PIL import Image from wsgiref.util import FileWrapper from django.http import StreamingHttpResponse +def TestIfImage(img): + """ Test if an image file is indeed an image """ + try: + Image.open(img).verify() + return True + except: + return False + + def str2bool(text, test=True): """ Test if a string 'looks' like a boolean value. diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index e97386d229..1b2814ba52 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -78,7 +78,6 @@ class EditPartForm(HelperForm): 'purchaseable', 'salable', 'notes', - 'image', ] diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index a99839d34a..a59b97ce47 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -153,7 +153,7 @@ { accept_text: 'Activate', accept: function() { - inventreeUpdate( + inventreePut( "{% url 'api-part-detail' part.id %}", { active: true, @@ -176,7 +176,7 @@ { accept_text: 'Deactivate', accept: function() { - inventreeUpdate( + inventreePut( "{% url 'api-part-detail' part.id %}", { active: false, diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 8abf7717f3..29e1638d55 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -108,47 +108,22 @@ var files = transfer.files; - console.log('dropped'); - if (files.length > 0) { var file = files[0]; - var formData = new FormData(); - - var token = getCookie('csrftoken'); - - formData.append('file', file); - - $.ajax({ - beforeSend: function(xhr, settings) { - xhr.setRequestHeader('X-CSRFToken', token); - }, - url: "{% url 'part-image-upload' part.id %}", - type: 'POST', - data: formData, - processData: false, - contentType: false, - success: function(data, status, xhr) { - //location.reload(); - }, - error: function(xhr, status, error) { - console.log('Error uploading thumbnail: ' + status); - console.log(error); - } - }); - - /* - inventreeUpdate( + inventreeFileUpload( "{% url 'part-image-upload' part.id %}", - formData, + file, + {}, { - method: 'POST', - dataType: 'json', + success: function(data, status, xhr) { + location.reload(); + }, + error: function(xhr, status, error) { + showAlertDialog('Error uploading image', renderErrorMessage(xhr)); + } } ); - */ - - console.log('submitted'); } }); diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 6fbd18318e..1bfef2962a 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -30,7 +30,7 @@ from .forms import EditSupplierPartForm from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import QRCodeView -from InvenTree.helpers import DownloadFile, str2bool +from InvenTree.helpers import DownloadFile, str2bool, TestIfImage class PartIndex(ListView): @@ -275,22 +275,29 @@ class UploadPartImage(AjaxView): model = Part def post(self, request, *args, **kwargs): + + response = {} + status = 200 + try: part = Part.objects.get(pk=kwargs.get('pk')) except Part.DoesNotExist: - error_dict = { - 'error': 'Part not found', - } + response['error'] = 'Part not found' return JsonResponse(error_dict, status=404) - print("Files:", request.FILES) uploaded_file = request.FILES['file'] - response_dict = { - 'success': 'File was uploaded successfully', - } + if TestIfImage(uploaded_file): + part.image = uploaded_file + part.clean() + part.save() - return JsonResponse(response_dict, status=200) + response['success'] = 'File was uploaded successfully' + else: + response['error'] = 'Not a valid image file' + status = 400 + + return JsonResponse(response, status=status) diff --git a/InvenTree/static/script/inventree/api.js b/InvenTree/static/script/inventree/api.js index 27feec8b79..015186d83b 100644 --- a/InvenTree/static/script/inventree/api.js +++ b/InvenTree/static/script/inventree/api.js @@ -42,7 +42,49 @@ function inventreeGet(url, filters={}, options={}) { }); } -function inventreeUpdate(url, data={}, options={}) { +function inventreeFileUpload(url, file, data={}, options={}) { + /* Upload a file via AJAX using the FormData approach. + * + * Note that the following AJAX parameters are required for FormData upload + * + * processData: false + * contentType: false + */ + + // CSRF cookie token + var csrftoken = getCookie('csrftoken'); + + var data = new FormData(); + + data.append('file', file); + + return $.ajax({ + beforeSend: function(xhr, settings) { + xhr.setRequestHeader('X-CSRFToken', csrftoken); + }, + url: url, + method: 'POST', + data: data, + processData: false, + contentType: false, + success: function(data, status, xhr) { + console.log('Uploaded file - ' + file.name); + + if (options.success) { + options.success(data, status, xhr); + } + }, + error: function(xhr, status, error) { + console.log('Error uploading file: ' + status); + + if (options.error) { + options.error(xhr, status, error); + } + } + }); +} + +function inventreePut(url, data={}, options={}) { var method = options.method || 'PUT'; @@ -93,9 +135,9 @@ function getCompanies(filters={}, options={}) { } function updateStockItem(pk, data, final=false) { - return inventreeUpdate('/api/stock/' + pk + '/', data, final); + return inventreePut('/api/stock/' + pk + '/', data, final); } function updatePart(pk, data, final=false) { - return inventreeUpdate('/api/part/' + pk + '/', data, final); + return inventreePut('/api/part/' + pk + '/', data, final); } \ No newline at end of file diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js index 7682d5a6c1..c3b0f8182d 100644 --- a/InvenTree/static/script/inventree/part.js +++ b/InvenTree/static/script/inventree/part.js @@ -41,7 +41,7 @@ function toggleStar(options) { if (response.length == 0) { // Zero length response = star does not exist // So let's add one! - inventreeUpdate( + inventreePut( url, { part: options.part, @@ -57,7 +57,7 @@ function toggleStar(options) { } else { var pk = response[0].pk; // There IS a star (delete it!) - inventreeUpdate( + inventreePut( url + pk + "/", { }, diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 73199466f6..71bbed2ab3 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -141,7 +141,7 @@ function updateStock(items, options={}) { return false; } - inventreeUpdate("/api/stock/stocktake/", + inventreePut("/api/stock/stocktake/", { 'action': options.action, 'items[]': stocktake, @@ -226,7 +226,7 @@ function moveStockItems(items, options) { } function doMove(location, parts, notes) { - inventreeUpdate("/api/stock/move/", + inventreePut("/api/stock/move/", { location: location, 'parts[]': parts, From 3c7238f29cbae89bafe5c4510cb2af456da59cdf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 6 May 2019 22:34:38 +1000 Subject: [PATCH 6/8] Extract img URL from a drag-and-dropped image from another website - https://stackoverflow.com/a/19268449 --- InvenTree/part/templates/part/part_base.html | 19 +++++++++++---- .../static/script/inventree/inventree.js | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 29e1638d55..fa6b077f8e 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -106,11 +106,10 @@ var transfer = event.originalEvent.dataTransfer; - var files = transfer.files; - - if (files.length > 0) { - var file = files[0]; + if (isFileTransfer(transfer)) { + var file = transfer.files[0]; + inventreeFileUpload( "{% url 'part-image-upload' part.id %}", file, @@ -124,6 +123,18 @@ } } ); + } else if (isOnlineTransfer(transfer)) { + + getImageUrlFromTransfer(transfer); + /* + for (var i = 0; i < 12; i++) { + transfer.items[i].getAsString(function(text) { + console.log('item ' + i + ' - ' + text); + }); + } + */ + } else { + console.log('Unknown transfer'); } }); diff --git a/InvenTree/static/script/inventree/inventree.js b/InvenTree/static/script/inventree/inventree.js index 35f446f9ef..b9454e96a7 100644 --- a/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/static/script/inventree/inventree.js @@ -35,4 +35,28 @@ function inventreeDocReady() { modal.modal('show'); }); +} + +function isFileTransfer(transfer) { + /* Determine if a transfer (e.g. drag-and-drop) is a file transfer + */ + + return transfer.files.length > 0; +} + + +function isOnlineTransfer(transfer) { + /* Determine if a drag-and-drop transfer is from another website. + * e.g. dragged from another browser window + */ + + return transfer.items.length > 0; +} + + +function getImageUrlFromTransfer(transfer) { + /* Extract external image URL from a drag-and-dropped image + */ + + console.log(transfer.getData('text/html').match(/src\s*=\s*"(.+?)"/)[1]); } \ No newline at end of file From fe4acd48a77f31dd400030f13d949497046e2519 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 7 May 2019 00:04:35 +1000 Subject: [PATCH 7/8] Add the ability to extract image URL information when drag-and-dropping image URL from a browser window - Can't do anything with it yet... - Code is almost there but leaving for now --- InvenTree/InvenTree/helpers.py | 14 ++++ InvenTree/part/templates/part/part_base.html | 44 ++++------ InvenTree/part/views.py | 83 ++++++++++++++++--- InvenTree/static/script/inventree/api.js | 13 +-- .../static/script/inventree/inventree.js | 6 +- 5 files changed, 113 insertions(+), 47 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index e14efd5298..68417fc2c2 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -4,6 +4,7 @@ Provides helper functions used throughout the InvenTree project import io import json +import os.path from datetime import datetime from PIL import Image @@ -20,6 +21,19 @@ def TestIfImage(img): return False +def TestIfImageURL(url): + """ Test if an image URL (or filename) looks like a valid image format. + + Simply tests the extension against a set of allowed values + """ + return os.path.splitext(os.path.basename(url))[-1].lower() in [ + '.jpg', '.jpeg', + '.png', '.bmp', + '.tif', '.tiff', + '.webp', + ] + + def str2bool(text, test=True): """ Test if a string 'looks' like a boolean value. diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index fa6b077f8e..7b6ce44acf 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -106,37 +106,29 @@ var transfer = event.originalEvent.dataTransfer; + var formData = new FormData(); + if (isFileTransfer(transfer)) { - - var file = transfer.files[0]; - - inventreeFileUpload( - "{% url 'part-image-upload' part.id %}", - file, - {}, - { - success: function(data, status, xhr) { - location.reload(); - }, - error: function(xhr, status, error) { - showAlertDialog('Error uploading image', renderErrorMessage(xhr)); - } - } - ); + formData.append('image_file', transfer.files[0]); } else if (isOnlineTransfer(transfer)) { - - getImageUrlFromTransfer(transfer); - /* - for (var i = 0; i < 12; i++) { - transfer.items[i].getAsString(function(text) { - console.log('item ' + i + ' - ' + text); - }); - } - */ + formData.append('image_url', getImageUrlFromTransfer(transfer)); } else { console.log('Unknown transfer'); + return; } - + + inventreeFormDataUpload( + "{% url 'part-image-upload' part.id %}", + formData, + { + success: function(data, status, xhr) { + location.reload(); + }, + error: function(xhr, status, error) { + showAlertDialog('Error uploading image', renderErrorMessage(xhr)); + } + } + ); }); $("#show-qr-code").click(function() { diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 1bfef2962a..86d90569fa 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -7,6 +7,9 @@ from __future__ import unicode_literals from django.shortcuts import get_object_or_404 +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError + from django.http import JsonResponse from django.urls import reverse_lazy from django.views.generic import DetailView, ListView @@ -30,7 +33,7 @@ from .forms import EditSupplierPartForm from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import QRCodeView -from InvenTree.helpers import DownloadFile, str2bool, TestIfImage +from InvenTree.helpers import DownloadFile, str2bool, TestIfImage, TestIfImageURL class PartIndex(ListView): @@ -270,7 +273,20 @@ class PartImage(AjaxUpdateView): class UploadPartImage(AjaxView): - """ View for uploading a Part image """ + """ View for uploading a Part image via AJAX request. + e.g. via a "drag and drop" event. + + There are two ways to upload a file: + + 1. Attach an image file as request.FILES['image_file'] + - Image is validated saved + - Part object is saved + 2. Attach an iamge URL as request.POST['image_url'] + NOT YET IMPLEMENTED + - Image URL is valiated + - Image is downloaded and validated + - Part object is saved + """ model = Part @@ -285,18 +301,63 @@ class UploadPartImage(AjaxView): response['error'] = 'Part not found' return JsonResponse(error_dict, status=404) - uploaded_file = request.FILES['file'] + # Direct image upload + if 'image_file' in request.FILES: + image = request.FILES['image_file'] - if TestIfImage(uploaded_file): - part.image = uploaded_file - part.clean() - part.save() + if TestIfImage(image): + part.image = image + part.clean() + part.save() - response['success'] = 'File was uploaded successfully' - else: - response['error'] = 'Not a valid image file' - status = 400 + response['success'] = 'File was uploaded successfully' + status = 200 + else: + response['error'] = 'Not a valid image file' + status = 400 + return JsonResponse(response, status=status) + + elif 'image_url' in request.POST: + image_url = request.POST['image_url'] + + validator = URLValidator() + + try: + validator(image_url) + except ValidationError: + response['error'] = 'Invalid image URL' + response['url'] = image_url + + return JsonResponse(response, status=400) + + # Test the the URL at least looks like an image + if not TestIfImageURL(image_url): + response['error'] = 'Invalid image URL' + return JsonResponse(response, status=400) + + response['error'] = 'Cannot download cross-site images (yet)' + response['url'] = image_url + response['apology'] = 'deepest' + + return JsonResponse(response, status=400) + + # TODO - Attempt to download the image file here + + """ + head = requests.head(url, stream=True, headers={'User-agent': 'Mozilla/5.0'}) + + if head.headers['Content-Length'] < SOME_MAX_LENGTH: + + image = requests.get(url, stream=True, headers={'User-agent': 'Mozilla/5.0'}) + + file = io.BytesIO(image.raw.read()) + + - Save the file? + + """ + + # Default response return JsonResponse(response, status=status) diff --git a/InvenTree/static/script/inventree/api.js b/InvenTree/static/script/inventree/api.js index 015186d83b..8c67f92979 100644 --- a/InvenTree/static/script/inventree/api.js +++ b/InvenTree/static/script/inventree/api.js @@ -42,8 +42,8 @@ function inventreeGet(url, filters={}, options={}) { }); } -function inventreeFileUpload(url, file, data={}, options={}) { - /* Upload a file via AJAX using the FormData approach. +function inventreeFormDataUpload(url, data, options={}) { + /* Upload via AJAX using the FormData approach. * * Note that the following AJAX parameters are required for FormData upload * @@ -53,10 +53,6 @@ function inventreeFileUpload(url, file, data={}, options={}) { // CSRF cookie token var csrftoken = getCookie('csrftoken'); - - var data = new FormData(); - - data.append('file', file); return $.ajax({ beforeSend: function(xhr, settings) { @@ -68,14 +64,13 @@ function inventreeFileUpload(url, file, data={}, options={}) { processData: false, contentType: false, success: function(data, status, xhr) { - console.log('Uploaded file - ' + file.name); - + console.log('Form data upload success'); if (options.success) { options.success(data, status, xhr); } }, error: function(xhr, status, error) { - console.log('Error uploading file: ' + status); + console.log('Form data upload failure: ' + status); if (options.error) { options.error(xhr, status, error); diff --git a/InvenTree/static/script/inventree/inventree.js b/InvenTree/static/script/inventree/inventree.js index b9454e96a7..551058f101 100644 --- a/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/static/script/inventree/inventree.js @@ -58,5 +58,9 @@ function getImageUrlFromTransfer(transfer) { /* Extract external image URL from a drag-and-dropped image */ - console.log(transfer.getData('text/html').match(/src\s*=\s*"(.+?)"/)[1]); + var url = transfer.getData('text/html').match(/src\s*=\s*"(.+?)"/)[1]; + + console.log('Image URL: ' + url); + + return url; } \ No newline at end of file From 22ff085b153f098cd3e605b42b18a29812216ab2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 7 May 2019 00:11:27 +1000 Subject: [PATCH 8/8] PEP fixes --- InvenTree/InvenTree/helpers.py | 4 ++-- InvenTree/part/views.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 68417fc2c2..068e3c1bc7 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -30,10 +30,10 @@ def TestIfImageURL(url): '.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff', - '.webp', + '.webp', ] - + def str2bool(text, test=True): """ Test if a string 'looks' like a boolean value. diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 86d90569fa..219bd1c740 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -299,7 +299,7 @@ class UploadPartImage(AjaxView): part = Part.objects.get(pk=kwargs.get('pk')) except Part.DoesNotExist: response['error'] = 'Part not found' - return JsonResponse(error_dict, status=404) + return JsonResponse(response, status=404) # Direct image upload if 'image_file' in request.FILES: @@ -325,7 +325,7 @@ class UploadPartImage(AjaxView): try: validator(image_url) - except ValidationError: + except ValidationError: response['error'] = 'Invalid image URL' response['url'] = image_url @@ -361,7 +361,6 @@ class UploadPartImage(AjaxView): return JsonResponse(response, status=status) - class PartEdit(AjaxUpdateView): """ View for editing Part object """