mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 21:38:48 +00:00
Merge pull request #1410 from SchrodingersGat/image-downloader
Image downloader
This commit is contained in:
commit
57289fe141
@ -626,6 +626,53 @@
|
|||||||
z-index: 11000;
|
z-index: 11000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 35px;
|
||||||
|
color: #f1f1f1;
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover,
|
||||||
|
.modal-close:focus {
|
||||||
|
color: #bbb;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-image-content {
|
||||||
|
margin: auto;
|
||||||
|
display: block;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 700px;
|
||||||
|
text-align: center;
|
||||||
|
color: #ccc;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 700px){
|
||||||
|
.modal-image-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-image {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
padding-top: 100px;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgb(0,0,0); /* Fallback color */
|
||||||
|
background-color: rgba(0,0,0,0.85); /* Black w/ opacity */
|
||||||
|
}
|
||||||
|
|
||||||
.js-modal-form .checkbox {
|
.js-modal-form .checkbox {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,13 @@ class InvenTreeSetting(models.Model):
|
|||||||
'choices': djmoney.settings.CURRENCY_CHOICES,
|
'choices': djmoney.settings.CURRENCY_CHOICES,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'INVENTREE_DOWNLOAD_FROM_URL': {
|
||||||
|
'name': _('Download from URL'),
|
||||||
|
'description': _('Allow download of remote images and files from external URL'),
|
||||||
|
'validator': bool,
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
|
||||||
'BARCODE_ENABLE': {
|
'BARCODE_ENABLE': {
|
||||||
'name': _('Barcode Support'),
|
'name': _('Barcode Support'),
|
||||||
'description': _('Enable barcode scanner support'),
|
'description': _('Enable barcode scanner support'),
|
||||||
|
@ -7,6 +7,7 @@ from django.apps import AppConfig
|
|||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from PIL import UnidentifiedImageError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -38,9 +39,11 @@ class CompanyConfig(AppConfig):
|
|||||||
try:
|
try:
|
||||||
company.image.render_variations(replace=False)
|
company.image.render_variations(replace=False)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.warning("Image file missing")
|
logger.warning(f"Image file '{company.image}' missing")
|
||||||
company.image = None
|
company.image = None
|
||||||
company.save()
|
company.save()
|
||||||
|
except UnidentifiedImageError:
|
||||||
|
logger.warning(f"Image file '{company.image}' is invalid")
|
||||||
except (OperationalError, ProgrammingError):
|
except (OperationalError, ProgrammingError):
|
||||||
# Getting here probably meant the database was in test mode
|
# Getting here probably meant the database was in test mode
|
||||||
pass
|
pass
|
||||||
|
@ -66,6 +66,24 @@ class CompanyImageForm(HelperForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyImageDownloadForm(HelperForm):
|
||||||
|
"""
|
||||||
|
Form for downloading an image from a URL
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = django.forms.URLField(
|
||||||
|
label=_('URL'),
|
||||||
|
help_text=_('Image URL'),
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Company
|
||||||
|
fields = [
|
||||||
|
'url',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class EditSupplierPartForm(HelperForm):
|
class EditSupplierPartForm(HelperForm):
|
||||||
""" Form for editing a SupplierPart object """
|
""" Form for editing a SupplierPart object """
|
||||||
|
|
||||||
|
@ -2,19 +2,32 @@
|
|||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
InvenTree | {% trans "Company" %} - {{ company.name }}
|
InvenTree | {% trans "Company" %} - {{ company.name }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block thumbnail %}
|
{% block thumbnail %}
|
||||||
<div class='dropzone' id='company-thumb'>
|
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
|
||||||
<img class="part-thumb"
|
|
||||||
|
<div class='dropzone part-thumb-container' id='company-thumb'>
|
||||||
|
<img class="part-thumb" id='company-image'
|
||||||
{% if company.image %}
|
{% if company.image %}
|
||||||
src="{{ company.image.url }}"
|
src="{{ company.image.url }}"
|
||||||
{% else %}
|
{% else %}
|
||||||
src="{% static 'img/blank_image.png' %}"
|
src="{% static 'img/blank_image.png' %}"
|
||||||
{% endif %}/>
|
{% endif %}/>
|
||||||
|
<div class='btn-row part-thumb-overlay'>
|
||||||
|
<div class='btn-group'>
|
||||||
|
<button type='button' class='btn btn-default btn-glyph' title='{% trans "Upload new image" %}' id='company-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='company-image-url'><span class='fas fa-cloud-download-alt'></span></button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -135,7 +148,13 @@ InvenTree | {% trans "Company" %} - {{ company.name }}
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
$("#company-thumb").click(function() {
|
{% if company.image %}
|
||||||
|
$('#company-image').click(function() {
|
||||||
|
showModalImage('{{ company.image.url }}');
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
$("#company-image-upload").click(function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'company-image' company.id %}",
|
"{% url 'company-image' company.id %}",
|
||||||
{
|
{
|
||||||
@ -144,4 +163,17 @@ InvenTree | {% trans "Company" %} - {{ company.name }}
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
|
||||||
|
|
||||||
|
{% if allow_download %}
|
||||||
|
$('#company-image-url').click(function() {
|
||||||
|
launchModalForm(
|
||||||
|
'{% url "company-image-download" company.id %}',
|
||||||
|
{
|
||||||
|
reload: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -21,6 +21,7 @@ company_detail_urls = [
|
|||||||
url(r'^notes/', views.CompanyNotes.as_view(), name='company-notes'),
|
url(r'^notes/', views.CompanyNotes.as_view(), name='company-notes'),
|
||||||
|
|
||||||
url(r'^thumbnail/', views.CompanyImage.as_view(), name='company-image'),
|
url(r'^thumbnail/', views.CompanyImage.as_view(), name='company-image'),
|
||||||
|
url(r'^thumb-download/', views.CompanyImageDownloadFromURL.as_view(), name='company-image-download'),
|
||||||
|
|
||||||
# Any other URL
|
# Any other URL
|
||||||
url(r'^.*$', views.CompanyDetail.as_view(), name='company-detail'),
|
url(r'^.*$', views.CompanyDetail.as_view(), name='company-detail'),
|
||||||
|
@ -11,9 +11,14 @@ from django.views.generic import DetailView, ListView, UpdateView
|
|||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.forms import HiddenInput
|
from django.forms import HiddenInput
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
from moneyed import CURRENCIES
|
from moneyed import CURRENCIES
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
import requests
|
||||||
|
import io
|
||||||
|
|
||||||
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool
|
||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
@ -28,6 +33,7 @@ from .forms import EditCompanyForm
|
|||||||
from .forms import CompanyImageForm
|
from .forms import CompanyImageForm
|
||||||
from .forms import EditSupplierPartForm
|
from .forms import EditSupplierPartForm
|
||||||
from .forms import EditPriceBreakForm
|
from .forms import EditPriceBreakForm
|
||||||
|
from .forms import CompanyImageDownloadForm
|
||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
import common.settings
|
import common.settings
|
||||||
@ -150,6 +156,84 @@ class CompanyDetail(DetailView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyImageDownloadFromURL(AjaxUpdateView):
|
||||||
|
"""
|
||||||
|
View for downloading an image from a provided URL
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = Company
|
||||||
|
ajax_template_name = 'image_download.html'
|
||||||
|
form_class = CompanyImageDownloadForm
|
||||||
|
ajax_form_title = _('Download Image')
|
||||||
|
|
||||||
|
def validate(self, company, form):
|
||||||
|
"""
|
||||||
|
Validate that the image data are correct
|
||||||
|
"""
|
||||||
|
# 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, company, form, **kwargs):
|
||||||
|
"""
|
||||||
|
Save the downloaded image to the company
|
||||||
|
"""
|
||||||
|
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"company_{company.pk}_image.{fmt.lower()}"
|
||||||
|
|
||||||
|
company.image.save(
|
||||||
|
filename,
|
||||||
|
ContentFile(buffer.getvalue()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CompanyImage(AjaxUpdateView):
|
class CompanyImage(AjaxUpdateView):
|
||||||
""" View for uploading an image for the Company """
|
""" View for uploading an image for the Company """
|
||||||
model = Company
|
model = Company
|
||||||
|
@ -7,6 +7,7 @@ from django.db.utils import OperationalError, ProgrammingError
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from PIL import UnidentifiedImageError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -44,9 +45,11 @@ class PartConfig(AppConfig):
|
|||||||
try:
|
try:
|
||||||
part.image.render_variations(replace=False)
|
part.image.render_variations(replace=False)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.warning("Image file missing")
|
logger.warning(f"Image file '{part.image}' missing")
|
||||||
part.image = None
|
part.image = None
|
||||||
part.save()
|
part.save()
|
||||||
|
except UnidentifiedImageError:
|
||||||
|
logger.warning(f"Image file '{part.image}' is invalid")
|
||||||
except (OperationalError, ProgrammingError):
|
except (OperationalError, ProgrammingError):
|
||||||
# Exception if the database has not been migrated yet
|
# Exception if the database has not been migrated yet
|
||||||
pass
|
pass
|
||||||
|
@ -37,6 +37,24 @@ class PartModelChoiceField(forms.ModelChoiceField):
|
|||||||
return label
|
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):
|
class PartImageForm(HelperForm):
|
||||||
""" Form for uploading a Part image """
|
""" Form for uploading a Part image """
|
||||||
|
|
||||||
|
@ -206,6 +206,12 @@
|
|||||||
toggleId: '#part-menu-toggle',
|
toggleId: '#part-menu-toggle',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
{% if part.image %}
|
||||||
|
$('#part-thumb').click(function() {
|
||||||
|
showModalImage('{{ part.image.url }}');
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
enableDragAndDrop(
|
enableDragAndDrop(
|
||||||
'#part-thumb',
|
'#part-thumb',
|
||||||
"{% url 'part-image-upload' part.id %}",
|
"{% 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() {
|
$("#part-image-select").click(function() {
|
||||||
launchModalForm("{% url 'part-image-select' part.id %}",
|
launchModalForm("{% url 'part-image-select' part.id %}",
|
||||||
@ -303,7 +323,6 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if roles.part.change %}
|
|
||||||
$("#part-edit").click(function() {
|
$("#part-edit").click(function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'part-edit' part.id %}",
|
"{% url 'part-edit' part.id %}",
|
||||||
|
@ -1,20 +1,28 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
|
||||||
|
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left part-thumb-container">
|
<div class="media-left part-thumb-container">
|
||||||
<div class='dropzone' id='part-thumb'>
|
<div class='dropzone' id='part-thumb'>
|
||||||
<img class="part-thumb"
|
<img class="part-thumb" id='part-image'
|
||||||
{% if part.image %}
|
{% if part.image %}
|
||||||
src="{{ part.image.url }}"
|
src="{{ part.image.url }}"
|
||||||
{% else %}
|
{% else %}
|
||||||
src="{% static 'img/blank_image.png' %}"
|
src="{% static 'img/blank_image.png' %}"
|
||||||
{% endif %}/>
|
{% endif %}/>
|
||||||
</div>
|
</div>
|
||||||
|
{% if roles.part.change %}
|
||||||
<div class='btn-row part-thumb-overlay'>
|
<div class='btn-row part-thumb-overlay'>
|
||||||
<div class='btn-group'>
|
<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 '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>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
@ -75,6 +75,7 @@ part_detail_urls = [
|
|||||||
# Normal thumbnail with form
|
# Normal thumbnail with form
|
||||||
url(r'^thumbnail/?', views.PartImageUpload.as_view(), name='part-image-upload'),
|
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-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
|
# Any other URLs go to the part detail page
|
||||||
url(r'^.*$', views.PartDetail.as_view(), name='part-detail'),
|
url(r'^.*$', views.PartDetail.as_view(), name='part-detail'),
|
||||||
|
@ -5,6 +5,7 @@ Django views for interacting with Part app
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
@ -19,7 +20,11 @@ from django.conf import settings
|
|||||||
|
|
||||||
from moneyed import CURRENCIES
|
from moneyed import CURRENCIES
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
import requests
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
|
||||||
from rapidfuzz import fuzz
|
from rapidfuzz import fuzz
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
@ -831,6 +836,89 @@ class PartQRCode(QRCodeView):
|
|||||||
return None
|
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):
|
class PartImageUpload(AjaxUpdateView):
|
||||||
""" View for uploading a new Part image """
|
""" View for uploading a new Part image """
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_BASE_URL" icon="fa-globe" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_BASE_URL" icon="fa-globe" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_FROM_URL" icon="fa-cloud-download-alt" %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
16
InvenTree/templates/image_download.html
Normal file
16
InvenTree/templates/image_download.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "modal_form.html" %}
|
||||||
|
|
||||||
|
{% load inventree_extras %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block pre_form_content %}
|
||||||
|
<div class='alert alert-block alert-info'>
|
||||||
|
{% trans "Specify URL for downloading image" %}:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>{% trans "Must be a valid image URL" %}</li>
|
||||||
|
<li>{% trans "Remote server must be accessible" %}</li>
|
||||||
|
<li>{% trans "Remote image must not exceed maximum allowable file size" %}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -909,3 +909,42 @@ function launchModalForm(url, options = {}) {
|
|||||||
// Send the AJAX request
|
// Send the AJAX request
|
||||||
$.ajax(ajax_data);
|
$.ajax(ajax_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function hideModalImage() {
|
||||||
|
|
||||||
|
var modal = $('#modal-image-dialog');
|
||||||
|
|
||||||
|
modal.animate({
|
||||||
|
opacity: 0.0,
|
||||||
|
}, 250, function() {
|
||||||
|
modal.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function showModalImage(image_url) {
|
||||||
|
// Display full-screen modal image
|
||||||
|
|
||||||
|
console.log('showing modal image: ' + image_url);
|
||||||
|
|
||||||
|
var modal = $('#modal-image-dialog');
|
||||||
|
|
||||||
|
// Set image content
|
||||||
|
$('#modal-image').attr('src', image_url);
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
modal.animate({
|
||||||
|
opacity: 1.0,
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
$('#modal-image-close').click(function() {
|
||||||
|
hideModalImage();
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.click(function() {
|
||||||
|
hideModalImage();
|
||||||
|
});
|
||||||
|
}
|
@ -1,5 +1,12 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class='modal fade modal-image' role='dialog' id='modal-image-dialog'>
|
||||||
|
<span class='modal-close' id='modal-image-close'>×</span>
|
||||||
|
|
||||||
|
<img class='modal-image-content' id='modal-image'>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class='modal fade modal-fixed-footer modal-primary' tabindex='-1' role='dialog' id='modal-form'>
|
<div class='modal fade modal-fixed-footer modal-primary' tabindex='-1' role='dialog' id='modal-form'>
|
||||||
<div class='modal-dialog'>
|
<div class='modal-dialog'>
|
||||||
<div class='modal-content'>
|
<div class='modal-content'>
|
||||||
@ -78,7 +85,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<h3 id='modal-title'>Alert Information</h3>
|
<h3 id='modal-title'>Alert Information</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class='modal-form-content'>
|
<div class='modal-form-content-wrapper'>
|
||||||
|
<div class='modal-form-content'>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='modal-footer'>
|
<div class='modal-footer'>
|
||||||
<button type='button' class='btn btn-default' data-dismiss='modal'>{% trans "Close" %}</button>
|
<button type='button' class='btn btn-default' data-dismiss='modal'>{% trans "Close" %}</button>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user