2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-08-09 21:30:54 +00:00

Refctor image downloader (#3393)

* Adds configurable setting for maximum remote image size

* Add helper function for downloading image from remote URL

- Will replace existing function
- Performs more thorough sanity checking

* Replace existing image downloading code

- part image uses new generic function
- company image uses new generic function

* Rearrange settings

* Refactor and cleanup existing views / forms

* Add unit testing for image downloader function

* Refactor image downloader forms

- Part image download now uses the API
- Company image download now uses the API
- Remove outdated forms / views / templates

* Increment API version

* Prevent remote image download via API if the setting is not enabled

* Do not attempt to validate or extract image from blank URL

* Fix custom save() serializer methods
This commit is contained in:
Oliver
2022-07-25 11:17:59 +10:00
committed by GitHub
parent d32054b53b
commit eecb26676e
18 changed files with 279 additions and 301 deletions

View File

@@ -1,48 +0,0 @@
"""Django Forms for interacting with Company app."""
import django.forms
from django.utils.translation import gettext_lazy as _
from InvenTree.fields import RoundingDecimalFormField
from InvenTree.forms import HelperForm
from .models import Company, SupplierPriceBreak
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:
"""Metaclass options."""
model = Company
fields = [
'url',
]
class EditPriceBreakForm(HelperForm):
"""Form for creating / editing a supplier price break."""
quantity = RoundingDecimalFormField(
max_digits=10,
decimal_places=5,
label=_('Quantity'),
help_text=_('Price break quantity'),
)
class Meta:
"""Metaclass options."""
model = SupplierPriceBreak
fields = [
'part',
'quantity',
'price',
]

View File

@@ -1,5 +1,8 @@
"""JSON serializers for Company app."""
import io
from django.core.files.base import ContentFile
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@@ -11,7 +14,7 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializer,
InvenTreeDecimalField,
InvenTreeImageSerializerField,
InvenTreeModelSerializer,
InvenTreeMoneySerializer)
InvenTreeMoneySerializer, RemoteImageMixin)
from part.serializers import PartBriefSerializer
from .models import (Company, ManufacturerPart, ManufacturerPartAttachment,
@@ -39,7 +42,7 @@ class CompanyBriefSerializer(InvenTreeModelSerializer):
]
class CompanySerializer(InvenTreeModelSerializer):
class CompanySerializer(RemoteImageMixin, InvenTreeModelSerializer):
"""Serializer for Company object (full detail)"""
@staticmethod
@@ -95,8 +98,33 @@ class CompanySerializer(InvenTreeModelSerializer):
'notes',
'parts_supplied',
'parts_manufactured',
'remote_image',
]
def save(self):
"""Save the Company instance"""
super().save()
company = self.instance
# Check if an image was downloaded from a remote URL
remote_img = getattr(self, 'remote_image_file', None)
if remote_img and company:
fmt = remote_img.format or 'PNG'
buffer = io.BytesIO()
remote_img.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()),
)
return self.instance
class ManufacturerPartSerializer(InvenTreeModelSerializer):
"""Serializer for ManufacturerPart object."""

View File

@@ -216,12 +216,19 @@
if (global_settings.INVENTREE_DOWNLOAD_FROM_URL) {
$('#company-image-url').click(function() {
launchModalForm(
'{% url "company-image-download" company.id %}',
constructForm(
'{% url "api-company-detail" company.pk %}',
{
reload: true,
method: 'PATCH',
title: '{% trans "Download Image" %}',
fields: {
remote_image: {},
},
onSuccess: function(data) {
reloadImage(data);
}
}
)
);
});
}

View File

@@ -4,18 +4,12 @@ from django.urls import include, re_path
from . import views
company_detail_urls = [
re_path(r'^thumb-download/', views.CompanyImageDownloadFromURL.as_view(), name='company-image-download'),
# Any other URL
re_path(r'^.*$', views.CompanyDetail.as_view(), name='company-detail'),
]
company_urls = [
re_path(r'^(?P<pk>\d+)/', include(company_detail_urls)),
# Detail URLs for a specific Company instance
re_path(r'^(?P<pk>\d+)/', include([
re_path(r'^.*$', views.CompanyDetail.as_view(), name='company-detail'),
])),
re_path(r'suppliers/', views.CompanyIndex.as_view(), name='supplier-index'),
re_path(r'manufacturers/', views.CompanyIndex.as_view(), name='manufacturer-index'),

View File

@@ -1,19 +1,12 @@
"""Django views for interacting with Company app."""
import io
from django.core.files.base import ContentFile
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView
import requests
from PIL import Image
from InvenTree.views import AjaxUpdateView, InvenTreeRoleMixin
from InvenTree.views import InvenTreeRoleMixin
from plugin.views import InvenTreePluginViewMixin
from .forms import CompanyImageDownloadForm
from .models import Company, ManufacturerPart, SupplierPart
@@ -103,78 +96,6 @@ class CompanyDetail(InvenTreePluginViewMixin, DetailView):
permission_required = 'company.view_company'
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 response.status_code != 200:
form.add_error('url', _('Invalid response: {code}').format(code=response.status_code))
return
response.raw.decode_content = True
try:
self.image = Image.open(response.raw).convert()
self.image.verify()
except Exception:
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 ManufacturerPartDetail(InvenTreePluginViewMixin, DetailView):
"""Detail view for ManufacturerPart."""
model = ManufacturerPart