@@ -96,6 +101,36 @@
{% block js_ready %}
{{ block.super }}
+
+ $('#part-thumb').on('drop', function(event) {
+
+ var transfer = event.originalEvent.dataTransfer;
+
+ var formData = new FormData();
+
+ if (isFileTransfer(transfer)) {
+ formData.append('image_file', transfer.files[0]);
+ } else if (isOnlineTransfer(transfer)) {
+ 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() {
launchModalForm(
"{% url 'part-qr' part.id %}",
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..219bd1c740 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -7,6 +7,10 @@ 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
from django.forms.models import model_to_dict
@@ -29,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
+from InvenTree.helpers import DownloadFile, str2bool, TestIfImage, TestIfImageURL
class PartIndex(ListView):
@@ -268,6 +272,95 @@ class PartImage(AjaxUpdateView):
}
+class UploadPartImage(AjaxView):
+ """ 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
+
+ def post(self, request, *args, **kwargs):
+
+ response = {}
+ status = 200
+
+ try:
+ part = Part.objects.get(pk=kwargs.get('pk'))
+ except Part.DoesNotExist:
+ response['error'] = 'Part not found'
+ return JsonResponse(response, status=404)
+
+ # Direct image upload
+ if 'image_file' in request.FILES:
+ image = request.FILES['image_file']
+
+ if TestIfImage(image):
+ part.image = image
+ part.clean()
+ part.save()
+
+ 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)
+
+
class PartEdit(AjaxUpdateView):
""" View for editing Part object """
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/api.js b/InvenTree/static/script/inventree/api.js
index 27feec8b79..8c67f92979 100644
--- a/InvenTree/static/script/inventree/api.js
+++ b/InvenTree/static/script/inventree/api.js
@@ -42,7 +42,44 @@ function inventreeGet(url, filters={}, options={}) {
});
}
-function inventreeUpdate(url, data={}, options={}) {
+function inventreeFormDataUpload(url, data, options={}) {
+ /* Upload 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');
+
+ 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('Form data upload success');
+ if (options.success) {
+ options.success(data, status, xhr);
+ }
+ },
+ error: function(xhr, status, error) {
+ console.log('Form data upload failure: ' + status);
+
+ if (options.error) {
+ options.error(xhr, status, error);
+ }
+ }
+ });
+}
+
+function inventreePut(url, data={}, options={}) {
var method = options.method || 'PUT';
@@ -93,9 +130,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/inventree.js b/InvenTree/static/script/inventree/inventree.js
new file mode 100644
index 0000000000..551058f101
--- /dev/null
+++ b/InvenTree/static/script/inventree/inventree.js
@@ -0,0 +1,66 @@
+function inventreeDocReady() {
+ /* Run this function when the HTML document is loaded.
+ * 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'
+ */
+ $('.dropzone').on('dragenter', function() {
+ $(this).addClass('dragover');
+ });
+
+ $('.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');
+ });
+}
+
+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
+ */
+
+ var url = transfer.getData('text/html').match(/src\s*=\s*"(.+?)"/)[1];
+
+ console.log('Image URL: ' + url);
+
+ return url;
+}
\ 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,
diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html
index 8331476cfa..2bc5cc9cd2 100644
--- a/InvenTree/templates/base.html
+++ b/InvenTree/templates/base.html
@@ -65,6 +65,8 @@ InvenTree
+
+
@@ -76,18 +78,16 @@ 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() {
- var modal = $('#modal-about');
-
- modal.modal({
- backdrop: 'static',
- keyboard: 'false',
- });
-
- modal.modal('show');
- });
});