mirror of
https://github.com/inventree/InvenTree.git
synced 2026-08-11 16:06:19 +00:00
[bug] Fix getLogoImage helper (#12577)
* [bug] Fix getLogoImage helper - Do not return a file:// URI * cleanup code
This commit is contained in:
@@ -262,27 +262,42 @@ def checkStaticFile(*args) -> bool:
|
|||||||
return static_storage.exists(str(fn))
|
return static_storage.exists(str(fn))
|
||||||
|
|
||||||
|
|
||||||
def getLogoImage(as_file=False, custom=True):
|
def getLogoImage(as_file: bool = False, custom: bool = True) -> str:
|
||||||
"""Return the InvenTree logo image, or a custom logo if available."""
|
"""Return the InvenTree logo image, or a custom logo if available.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
as_file: If True, return a base64-encoded data URI of the image contents,
|
||||||
|
suitable for embedding directly into a generated report (default = False)
|
||||||
|
custom: If True, return a custom logo if one has been provided (default = True)
|
||||||
|
"""
|
||||||
|
# Note: Imported here to avoid circular imports, as the 'report' app also imports from this module
|
||||||
|
import report.helpers
|
||||||
|
from report.templatetags.report import (
|
||||||
|
get_media_file_contents,
|
||||||
|
get_static_file_contents,
|
||||||
|
)
|
||||||
|
|
||||||
if custom and settings.CUSTOM_LOGO:
|
if custom and settings.CUSTOM_LOGO:
|
||||||
static_storage = StaticFilesStorage()
|
static_storage = StaticFilesStorage()
|
||||||
|
|
||||||
if static_storage.exists(settings.CUSTOM_LOGO):
|
if static_storage.exists(settings.CUSTOM_LOGO):
|
||||||
storage = static_storage
|
|
||||||
elif default_storage.exists(settings.CUSTOM_LOGO):
|
|
||||||
storage = default_storage
|
|
||||||
else:
|
|
||||||
storage = None
|
|
||||||
|
|
||||||
if storage is not None:
|
|
||||||
if as_file:
|
if as_file:
|
||||||
return f'file://{storage.path(settings.CUSTOM_LOGO)}'
|
return report.helpers.encode_file_base64(
|
||||||
return storage.url(settings.CUSTOM_LOGO)
|
settings.CUSTOM_LOGO, get_static_file_contents(settings.CUSTOM_LOGO)
|
||||||
|
)
|
||||||
|
return static_storage.url(settings.CUSTOM_LOGO)
|
||||||
|
elif default_storage.exists(settings.CUSTOM_LOGO):
|
||||||
|
if as_file:
|
||||||
|
return report.helpers.encode_file_base64(
|
||||||
|
settings.CUSTOM_LOGO, get_media_file_contents(settings.CUSTOM_LOGO)
|
||||||
|
)
|
||||||
|
return default_storage.url(settings.CUSTOM_LOGO)
|
||||||
|
|
||||||
# If we have got to this point, return the default logo
|
# If we have got to this point, return the default logo
|
||||||
if as_file:
|
if as_file:
|
||||||
path = settings.STATIC_ROOT.joinpath('img/inventree.png')
|
return report.helpers.encode_file_base64(
|
||||||
return f'file://{path}'
|
'img/inventree.png', get_static_file_contents('img/inventree.png')
|
||||||
|
)
|
||||||
return getStaticUrl('img/inventree.png')
|
return getStaticUrl('img/inventree.png')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""Test general functions and helpers."""
|
"""Test general functions and helpers."""
|
||||||
|
|
||||||
|
import base64
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
@@ -13,6 +15,8 @@ from django.conf import settings
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -22,6 +26,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate
|
|||||||
from djmoney.contrib.exchange.models import Rate, convert_money
|
from djmoney.contrib.exchange.models import Rate, convert_money
|
||||||
from djmoney.money import Money
|
from djmoney.money import Money
|
||||||
from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode
|
from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode
|
||||||
|
from PIL import Image
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from sesame.utils import get_user
|
from sesame.utils import get_user
|
||||||
from stdimage.models import StdImageFieldFile
|
from stdimage.models import StdImageFieldFile
|
||||||
@@ -703,12 +708,59 @@ class TestHelpers(TestCase):
|
|||||||
|
|
||||||
def test_logo_image(self):
|
def test_logo_image(self):
|
||||||
"""Test for retrieving logo image."""
|
"""Test for retrieving logo image."""
|
||||||
# By default, there is no custom logo provided
|
# By default, there is no custom logo provided - return the default InvenTree logo
|
||||||
|
logo = helpers.getLogoImage()
|
||||||
|
self.assertEqual(logo, '/static/img/inventree.png')
|
||||||
|
|
||||||
|
# When requested 'as_file', the logo must be returned as an embeddable
|
||||||
|
# base64 data URI - file:// URIs are no longer permitted in reports
|
||||||
|
logo = helpers.getLogoImage(as_file=True)
|
||||||
|
self.assertNotIn('file://', logo)
|
||||||
|
self.assertTrue(logo.startswith('data:image/png;base64,'))
|
||||||
|
|
||||||
|
# Ensure the encoded data actually represents a valid image
|
||||||
|
decoded = base64.b64decode(logo.removeprefix('data:image/png;base64,'))
|
||||||
|
Image.open(BytesIO(decoded)).verify()
|
||||||
|
|
||||||
|
def test_logo_image_custom_static(self):
|
||||||
|
"""Test retrieval of a custom logo which lives in the static storage backend."""
|
||||||
|
with override_settings(CUSTOM_LOGO='img/inventree.png'):
|
||||||
logo = helpers.getLogoImage()
|
logo = helpers.getLogoImage()
|
||||||
self.assertEqual(logo, '/static/img/inventree.png')
|
self.assertEqual(logo, '/static/img/inventree.png')
|
||||||
|
|
||||||
logo = helpers.getLogoImage(as_file=True)
|
logo = helpers.getLogoImage(as_file=True)
|
||||||
self.assertEqual(logo, f'file://{settings.STATIC_ROOT}/img/inventree.png')
|
self.assertNotIn('file://', logo)
|
||||||
|
self.assertTrue(logo.startswith('data:image/png;base64,'))
|
||||||
|
|
||||||
|
# Disabling 'custom' must fall back to the default logo, even if set
|
||||||
|
logo = helpers.getLogoImage(custom=False)
|
||||||
|
self.assertEqual(logo, '/static/img/inventree.png')
|
||||||
|
|
||||||
|
def test_logo_image_custom_media(self):
|
||||||
|
"""Test retrieval of a custom logo which lives in the media (uploaded) storage backend."""
|
||||||
|
custom_logo_path = 'custom/test_logo.png'
|
||||||
|
|
||||||
|
img = Image.new('RGB', (16, 16), color='blue')
|
||||||
|
buffer = BytesIO()
|
||||||
|
img.save(buffer, 'PNG')
|
||||||
|
image_data = buffer.getvalue()
|
||||||
|
|
||||||
|
default_storage.save(custom_logo_path, ContentFile(image_data))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with override_settings(CUSTOM_LOGO=custom_logo_path):
|
||||||
|
logo = helpers.getLogoImage()
|
||||||
|
self.assertEqual(logo, default_storage.url(custom_logo_path))
|
||||||
|
|
||||||
|
logo = helpers.getLogoImage(as_file=True)
|
||||||
|
self.assertNotIn('file://', logo)
|
||||||
|
self.assertTrue(logo.startswith('data:image/png;base64,'))
|
||||||
|
|
||||||
|
# The decoded data must exactly match the uploaded image
|
||||||
|
decoded = base64.b64decode(logo.removeprefix('data:image/png;base64,'))
|
||||||
|
self.assertEqual(decoded, image_data)
|
||||||
|
finally:
|
||||||
|
default_storage.delete(custom_logo_path)
|
||||||
|
|
||||||
def test_download_image(self):
|
def test_download_image(self):
|
||||||
"""Test function for downloading image from remote URL."""
|
"""Test function for downloading image from remote URL."""
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import base64
|
import base64
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -78,6 +79,33 @@ def report_page_size_default():
|
|||||||
return page_size
|
return page_size
|
||||||
|
|
||||||
|
|
||||||
|
def encode_file_base64(filename: str, data: bytes | None) -> str:
|
||||||
|
"""Return a base-64 encoded data URI for raw file data.
|
||||||
|
|
||||||
|
Unlike encode_image_base64, this preserves the original file bytes and
|
||||||
|
format as-is (no PIL decode/re-encode), so it works for any file type
|
||||||
|
(e.g. SVG logos) and does not alter the source data.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
filename: The filename associated with the data (used to guess the mime type)
|
||||||
|
data: The raw file data to encode
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str -- Base64 encoded data URI e.g. 'data:image/png;base64,xxxxxxxxx', or an
|
||||||
|
empty string if no data is provided
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
mime_type, _encoding = mimetypes.guess_type(str(filename))
|
||||||
|
|
||||||
|
if not mime_type:
|
||||||
|
mime_type = 'application/octet-stream'
|
||||||
|
|
||||||
|
encoded = base64.b64encode(data).decode('ascii')
|
||||||
|
return f'data:{mime_type};base64,{encoded}'
|
||||||
|
|
||||||
|
|
||||||
def encode_image_base64(image, img_format: str = 'PNG') -> str:
|
def encode_image_base64(image, img_format: str = 'PNG') -> str:
|
||||||
"""Return a base-64 encoded image which can be rendered in an <img> tag.
|
"""Return a base-64 encoded image which can be rendered in an <img> tag.
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import base64
|
import base64
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@@ -323,12 +322,7 @@ def asset(filename: str, raise_error: bool = False) -> str | None:
|
|||||||
if not file_data:
|
if not file_data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
mime_type, _encoding = mimetypes.guess_type(str(filename))
|
return report.helpers.encode_file_base64(filename, file_data)
|
||||||
if not mime_type:
|
|
||||||
mime_type = 'application/octet-stream'
|
|
||||||
|
|
||||||
encoded = base64.b64encode(file_data).decode('ascii')
|
|
||||||
return f'data:{mime_type};base64,{encoded}'
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
|
|||||||
@@ -228,11 +228,18 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
|
|||||||
|
|
||||||
def test_logo_image(self):
|
def test_logo_image(self):
|
||||||
"""Unit tests for the 'logo_image' tag."""
|
"""Unit tests for the 'logo_image' tag."""
|
||||||
# By default, should return the core InvenTree logo
|
# In debug mode, a web-accessible URL to the logo is returned
|
||||||
for b in [True, False]:
|
self.debug_mode(True)
|
||||||
self.debug_mode(b)
|
|
||||||
logo = report_tags.logo_image()
|
logo = report_tags.logo_image()
|
||||||
self.assertIn('inventree.png', logo)
|
self.assertIn('inventree.png', logo)
|
||||||
|
self.assertNotIn('file://', logo)
|
||||||
|
|
||||||
|
# Outside of debug mode, an embeddable base64 data URI is returned instead
|
||||||
|
# of a file:// URL, which is no longer permitted in report templates
|
||||||
|
self.debug_mode(False)
|
||||||
|
logo = report_tags.logo_image()
|
||||||
|
self.assertNotIn('file://', logo)
|
||||||
|
self.assertTrue(logo.startswith('data:image/png;base64,'))
|
||||||
|
|
||||||
def test_string_tags(self):
|
def test_string_tags(self):
|
||||||
"""Simple tests for the string manipulation tags."""
|
"""Simple tests for the string manipulation tags."""
|
||||||
|
|||||||
Reference in New Issue
Block a user