2
0
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:
Oliver
2021-03-18 11:10:07 +11:00
committed by GitHub
17 changed files with 403 additions and 9 deletions

View File

@ -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

View File

@ -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 """

View File

@ -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 %}",

View File

@ -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>

View File

@ -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'),

View File

@ -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 """