From dca26b581072501b951e2383ebaf866bc0305369 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Mon, 6 May 2019 19:21:14 +1000
Subject: [PATCH 1/7] 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 @@
 <div class="col-sm-6">
     <div class="media">
       <div class="media-left">
-        <img class="part-thumb" id="part-thumb"
-        {% if part.image %}
-        src="{{ part.image.url }}"
-        {% else %}
-        src="{% static 'img/blank_image.png' %}"
-        {% endif %}/>
+        <form id='upload-photo' method='post' action="{% url 'part-image-upload' part.id %}">
+            <div class='dropzone' id='part-thumb'>
+                <img class="part-thumb"
+                {% if part.image %}
+                src="{{ part.image.url }}"
+                {% else %}
+                src="{% static 'img/blank_image.png' %}"
+                {% endif %}/>
+            </div>
+                {% csrf_token %}
+        </form>
       </div>
     <div class="media-body">
         <h4>
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
 <script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script>
 <script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
 
+<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
+<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
 <script type='text/javascript' src="{% static 'script/inventree/tables.js' %}"></script>
 <script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
 <script type='text/javascript' src="{% static 'script/inventree/modals.js' %}"></script>
@@ -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 <oliver.henry.walters@gmail.com>
Date: Mon, 6 May 2019 21:22:31 +1000
Subject: [PATCH 2/7] 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');
-    });
 });
 </script>
 

From c88149b9aae63746875a290be96c1b6b1f6c5187 Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Mon, 6 May 2019 21:49:01 +1000
Subject: [PATCH 3/7] 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 <oliver.henry.walters@gmail.com>
Date: Mon, 6 May 2019 22:20:06 +1000
Subject: [PATCH 4/7] 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 <oliver.henry.walters@gmail.com>
Date: Mon, 6 May 2019 22:34:38 +1000
Subject: [PATCH 5/7] 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 <oliver.henry.walters@gmail.com>
Date: Tue, 7 May 2019 00:04:35 +1000
Subject: [PATCH 6/7] 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 <oliver.henry.walters@gmail.com>
Date: Tue, 7 May 2019 00:11:27 +1000
Subject: [PATCH 7/7] 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 """