mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
364f78d20c
@ -4,12 +4,36 @@ Provides helper functions used throughout the InvenTree project
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import os.path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from wsgiref.util import FileWrapper
|
from wsgiref.util import FileWrapper
|
||||||
from django.http import StreamingHttpResponse
|
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 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):
|
def str2bool(text, test=True):
|
||||||
""" Test if a string 'looks' like a boolean value.
|
""" Test if a string 'looks' like a boolean value.
|
||||||
|
|
||||||
|
@ -78,7 +78,6 @@ class EditPartForm(HelperForm):
|
|||||||
'purchaseable',
|
'purchaseable',
|
||||||
'salable',
|
'salable',
|
||||||
'notes',
|
'notes',
|
||||||
'image',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@
|
|||||||
{
|
{
|
||||||
accept_text: 'Activate',
|
accept_text: 'Activate',
|
||||||
accept: function() {
|
accept: function() {
|
||||||
inventreeUpdate(
|
inventreePut(
|
||||||
"{% url 'api-part-detail' part.id %}",
|
"{% url 'api-part-detail' part.id %}",
|
||||||
{
|
{
|
||||||
active: true,
|
active: true,
|
||||||
@ -176,7 +176,7 @@
|
|||||||
{
|
{
|
||||||
accept_text: 'Deactivate',
|
accept_text: 'Deactivate',
|
||||||
accept: function() {
|
accept: function() {
|
||||||
inventreeUpdate(
|
inventreePut(
|
||||||
"{% url 'api-part-detail' part.id %}",
|
"{% url 'api-part-detail' part.id %}",
|
||||||
{
|
{
|
||||||
active: false,
|
active: false,
|
||||||
|
@ -13,12 +13,17 @@
|
|||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<img class="part-thumb" id="part-thumb"
|
<form id='upload-photo' method='post' action="{% url 'part-image-upload' part.id %}">
|
||||||
{% if part.image %}
|
<div class='dropzone' id='part-thumb'>
|
||||||
src="{{ part.image.url }}"
|
<img class="part-thumb"
|
||||||
{% else %}
|
{% if part.image %}
|
||||||
src="{% static 'img/blank_image.png' %}"
|
src="{{ part.image.url }}"
|
||||||
{% endif %}/>
|
{% else %}
|
||||||
|
src="{% static 'img/blank_image.png' %}"
|
||||||
|
{% endif %}/>
|
||||||
|
</div>
|
||||||
|
{% csrf_token %}
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<h4>
|
<h4>
|
||||||
@ -96,6 +101,36 @@
|
|||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ 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() {
|
$("#show-qr-code").click(function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'part-qr' part.id %}",
|
"{% url 'part-qr' part.id %}",
|
||||||
|
@ -47,6 +47,10 @@ part_detail_urls = [
|
|||||||
|
|
||||||
url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),
|
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'),
|
url(r'^thumbnail/?', views.PartImage.as_view(), name='part-image'),
|
||||||
|
|
||||||
# Any other URLs go to the part detail page
|
# Any other URLs go to the part detail page
|
||||||
|
@ -7,6 +7,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
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.urls import reverse_lazy
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
from django.forms.models import model_to_dict
|
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 AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||||
from InvenTree.views import QRCodeView
|
from InvenTree.views import QRCodeView
|
||||||
|
|
||||||
from InvenTree.helpers import DownloadFile, str2bool
|
from InvenTree.helpers import DownloadFile, str2bool, TestIfImage, TestIfImageURL
|
||||||
|
|
||||||
|
|
||||||
class PartIndex(ListView):
|
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):
|
class PartEdit(AjaxUpdateView):
|
||||||
""" View for editing Part object """
|
""" View for editing Part object """
|
||||||
|
|
||||||
|
@ -10,6 +10,25 @@
|
|||||||
color: #ffcc00;
|
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 {
|
.btn-glyph {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
@ -28,7 +47,6 @@
|
|||||||
.part-thumb {
|
.part-thumb {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
border: 1px black solid;
|
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
@ -62,7 +80,7 @@
|
|||||||
margin-right: 50px;
|
margin-right: 50px;
|
||||||
margin-left: 50px;
|
margin-left: 50px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
//transition: 0.1s;
|
transition: 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
@ -182,3 +200,4 @@
|
|||||||
from { transform: scale(1) rotate(0deg);}
|
from { transform: scale(1) rotate(0deg);}
|
||||||
to { transform: scale(1) rotate(360deg);}
|
to { transform: scale(1) rotate(360deg);}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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';
|
var method = options.method || 'PUT';
|
||||||
|
|
||||||
@ -93,9 +130,9 @@ function getCompanies(filters={}, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateStockItem(pk, data, final=false) {
|
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) {
|
function updatePart(pk, data, final=false) {
|
||||||
return inventreeUpdate('/api/part/' + pk + '/', data, final);
|
return inventreePut('/api/part/' + pk + '/', data, final);
|
||||||
}
|
}
|
66
InvenTree/static/script/inventree/inventree.js
Normal file
66
InvenTree/static/script/inventree/inventree.js
Normal file
@ -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;
|
||||||
|
}
|
@ -41,7 +41,7 @@ function toggleStar(options) {
|
|||||||
if (response.length == 0) {
|
if (response.length == 0) {
|
||||||
// Zero length response = star does not exist
|
// Zero length response = star does not exist
|
||||||
// So let's add one!
|
// So let's add one!
|
||||||
inventreeUpdate(
|
inventreePut(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
part: options.part,
|
part: options.part,
|
||||||
@ -57,7 +57,7 @@ function toggleStar(options) {
|
|||||||
} else {
|
} else {
|
||||||
var pk = response[0].pk;
|
var pk = response[0].pk;
|
||||||
// There IS a star (delete it!)
|
// There IS a star (delete it!)
|
||||||
inventreeUpdate(
|
inventreePut(
|
||||||
url + pk + "/",
|
url + pk + "/",
|
||||||
{
|
{
|
||||||
},
|
},
|
||||||
|
@ -141,7 +141,7 @@ function updateStock(items, options={}) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
inventreeUpdate("/api/stock/stocktake/",
|
inventreePut("/api/stock/stocktake/",
|
||||||
{
|
{
|
||||||
'action': options.action,
|
'action': options.action,
|
||||||
'items[]': stocktake,
|
'items[]': stocktake,
|
||||||
@ -226,7 +226,7 @@ function moveStockItems(items, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function doMove(location, parts, notes) {
|
function doMove(location, parts, notes) {
|
||||||
inventreeUpdate("/api/stock/move/",
|
inventreePut("/api/stock/move/",
|
||||||
{
|
{
|
||||||
location: location,
|
location: location,
|
||||||
'parts[]': parts,
|
'parts[]': parts,
|
||||||
|
@ -65,6 +65,8 @@ InvenTree
|
|||||||
<script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script>
|
<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/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/tables.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/inventree/modals.js' %}"></script>
|
<script type='text/javascript' src="{% static 'script/inventree/modals.js' %}"></script>
|
||||||
@ -76,18 +78,16 @@ InvenTree
|
|||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
/* Run document-ready scripts.
|
||||||
|
* Ref: static/script/inventree/inventree.js
|
||||||
|
*/
|
||||||
|
inventreeDocReady();
|
||||||
|
|
||||||
|
/* Display any cached alert messages
|
||||||
|
* Ref: static/script/inventree/notification.js
|
||||||
|
*/
|
||||||
showCachedAlerts();
|
showCachedAlerts();
|
||||||
|
|
||||||
$('#launch-about').click(function() {
|
|
||||||
var modal = $('#modal-about');
|
|
||||||
|
|
||||||
modal.modal({
|
|
||||||
backdrop: 'static',
|
|
||||||
keyboard: 'false',
|
|
||||||
});
|
|
||||||
|
|
||||||
modal.modal('show');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user