mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-20 22:06:28 +00:00
Merge pull request #1410 from SchrodingersGat/image-downloader
Image downloader
This commit is contained in:
@ -7,6 +7,7 @@ from django.db.utils import OperationalError, ProgrammingError
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
from PIL import UnidentifiedImageError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -44,9 +45,11 @@ class PartConfig(AppConfig):
|
||||
try:
|
||||
part.image.render_variations(replace=False)
|
||||
except FileNotFoundError:
|
||||
logger.warning("Image file missing")
|
||||
logger.warning(f"Image file '{part.image}' missing")
|
||||
part.image = None
|
||||
part.save()
|
||||
except UnidentifiedImageError:
|
||||
logger.warning(f"Image file '{part.image}' is invalid")
|
||||
except (OperationalError, ProgrammingError):
|
||||
# Exception if the database has not been migrated yet
|
||||
pass
|
||||
|
@ -37,6 +37,24 @@ class PartModelChoiceField(forms.ModelChoiceField):
|
||||
return label
|
||||
|
||||
|
||||
class PartImageDownloadForm(HelperForm):
|
||||
"""
|
||||
Form for downloading an image from a URL
|
||||
"""
|
||||
|
||||
url = forms.URLField(
|
||||
label=_('URL'),
|
||||
help_text=_('Image URL'),
|
||||
required=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Part
|
||||
fields = [
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
class PartImageForm(HelperForm):
|
||||
""" Form for uploading a Part image """
|
||||
|
||||
|
@ -206,6 +206,12 @@
|
||||
toggleId: '#part-menu-toggle',
|
||||
});
|
||||
|
||||
{% if part.image %}
|
||||
$('#part-thumb').click(function() {
|
||||
showModalImage('{{ part.image.url }}');
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
enableDragAndDrop(
|
||||
'#part-thumb',
|
||||
"{% url 'part-image-upload' part.id %}",
|
||||
@ -294,6 +300,20 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{% if roles.part.change %}
|
||||
|
||||
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
|
||||
{% if allow_download %}
|
||||
$("#part-image-url").click(function() {
|
||||
launchModalForm(
|
||||
'{% url "part-image-download" part.id %}',
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
$("#part-image-select").click(function() {
|
||||
launchModalForm("{% url 'part-image-select' part.id %}",
|
||||
@ -303,7 +323,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
{% if roles.part.change %}
|
||||
$("#part-edit").click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'part-edit' part.id %}",
|
||||
|
@ -1,20 +1,28 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
|
||||
|
||||
<div class="media">
|
||||
<div class="media-left part-thumb-container">
|
||||
<div class='dropzone' id='part-thumb'>
|
||||
<img class="part-thumb"
|
||||
<img class="part-thumb" id='part-image'
|
||||
{% if part.image %}
|
||||
src="{{ part.image.url }}"
|
||||
{% else %}
|
||||
src="{% static 'img/blank_image.png' %}"
|
||||
{% endif %}/>
|
||||
</div>
|
||||
{% if roles.part.change %}
|
||||
<div class='btn-row part-thumb-overlay'>
|
||||
<div class='btn-group'>
|
||||
<button type='button' class='btn btn-default btn-glyph' title="{% trans 'Select from existing images' %}" id='part-image-select'><span class='fas fa-th'></span></button>
|
||||
<button type='button' class='btn btn-default btn-glyph' title="{% trans 'Upload new image' %}" id='part-image-upload'><span class='fas fa-file-image'></span></button>
|
||||
<button type='button' class='btn btn-default btn-glyph' title="{% trans 'Upload new image' %}" id='part-image-upload'><span class='fas fa-file-upload'></span></button>
|
||||
{% if allow_download %}
|
||||
<button type='button' class='btn btn-default btn-glyph' title="{% trans 'Download image from URL' %}" id='part-image-url'><span class='fas fa-cloud-download-alt'></span></button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
@ -75,6 +75,7 @@ part_detail_urls = [
|
||||
# Normal thumbnail with form
|
||||
url(r'^thumbnail/?', views.PartImageUpload.as_view(), name='part-image-upload'),
|
||||
url(r'^thumb-select/?', views.PartImageSelect.as_view(), name='part-image-select'),
|
||||
url(r'^thumb-download/', views.PartImageDownloadFromURL.as_view(), name='part-image-download'),
|
||||
|
||||
# Any other URLs go to the part detail page
|
||||
url(r'^.*$', views.PartDetail.as_view(), name='part-detail'),
|
||||
|
@ -5,6 +5,7 @@ Django views for interacting with Part app
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.utils import IntegrityError
|
||||
@ -19,7 +20,11 @@ from django.conf import settings
|
||||
|
||||
from moneyed import CURRENCIES
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import requests
|
||||
import os
|
||||
import io
|
||||
|
||||
from rapidfuzz import fuzz
|
||||
from decimal import Decimal, InvalidOperation
|
||||
@ -831,6 +836,89 @@ class PartQRCode(QRCodeView):
|
||||
return None
|
||||
|
||||
|
||||
class PartImageDownloadFromURL(AjaxUpdateView):
|
||||
"""
|
||||
View for downloading an image from a provided URL
|
||||
"""
|
||||
|
||||
model = Part
|
||||
|
||||
ajax_template_name = 'image_download.html'
|
||||
form_class = part_forms.PartImageDownloadForm
|
||||
ajax_form_title = _('Download Image')
|
||||
|
||||
def validate(self, part, form):
|
||||
"""
|
||||
Validate that the image data are correct.
|
||||
|
||||
- Try to download the image!
|
||||
"""
|
||||
|
||||
# First ensure that the normal validation routines pass
|
||||
if not form.is_valid():
|
||||
return
|
||||
|
||||
# We can now extract a valid URL from the form data
|
||||
url = form.cleaned_data.get('url', None)
|
||||
|
||||
# Download the file
|
||||
response = requests.get(url, stream=True)
|
||||
|
||||
# Look at response header, reject if too large
|
||||
content_length = response.headers.get('Content-Length', '0')
|
||||
|
||||
try:
|
||||
content_length = int(content_length)
|
||||
except (ValueError):
|
||||
# If we cannot extract meaningful length, just assume it's "small enough"
|
||||
content_length = 0
|
||||
|
||||
# TODO: Factor this out into a configurable setting
|
||||
MAX_IMG_LENGTH = 10 * 1024 * 1024
|
||||
|
||||
if content_length > MAX_IMG_LENGTH:
|
||||
form.add_error('url', _('Image size exceeds maximum allowable size for download'))
|
||||
return
|
||||
|
||||
self.response = response
|
||||
|
||||
# Check for valid response code
|
||||
if not response.status_code == 200:
|
||||
form.add_error('url', f"{_('Invalid response')}: {response.status_code}")
|
||||
return
|
||||
|
||||
response.raw.decode_content = True
|
||||
|
||||
try:
|
||||
self.image = Image.open(response.raw).convert()
|
||||
self.image.verify()
|
||||
except:
|
||||
form.add_error('url', _("Supplied URL is not a valid image file"))
|
||||
return
|
||||
|
||||
def save(self, part, form, **kwargs):
|
||||
"""
|
||||
Save the downloaded image to the part
|
||||
"""
|
||||
|
||||
fmt = self.image.format
|
||||
|
||||
if not fmt:
|
||||
fmt = 'PNG'
|
||||
|
||||
buffer = io.BytesIO()
|
||||
|
||||
self.image.save(buffer, format=fmt)
|
||||
|
||||
# Construct a simplified name for the image
|
||||
filename = f"part_{part.pk}_image.{fmt.lower()}"
|
||||
|
||||
part.image.save(
|
||||
filename,
|
||||
ContentFile(buffer.getvalue()),
|
||||
)
|
||||
|
||||
|
||||
class PartImageUpload(AjaxUpdateView):
|
||||
""" View for uploading a new Part image """
|
||||
|
||||
|
Reference in New Issue
Block a user