2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 11:10:54 +00:00

Refactor template helpers for displaying uploaded images (#3377)

* Refactor template helpers for displaying uploaded images

* Unit test for asset tag

* Unit tests for 'uploaded_image' tag

* Add simple tests for part_image and company_image functions

* Unit test for barcode constructor

* Unit tests for qrcode

* Refactor the 'company_logo.png' to be a new template tag

- Add unit tests

* Adds a new field to the report asset model

- Unique key which can be used to identify particular assets
- e.g. company logo

* Refactor logo image tags

- Make use of existing CUSTOM_LOGO setting
- Adds a "logo_image" template tag for reports

* Remove previous migration - strategy no longer required
This commit is contained in:
Oliver
2022-07-22 12:01:56 +10:00
committed by GitHub
parent 2bc8556993
commit d2ab6b012d
10 changed files with 312 additions and 83 deletions

View File

@ -535,14 +535,23 @@ class ReportAsset(models.Model):
and can be loaded in a template using the {% report_asset <filename> %} tag.
"""
# String keys used for uniquely indentifying particular assets
ASSET_COMPANY_LOGO = "COMPANY_LOGO"
def __str__(self):
"""String representation of a ReportAsset instance"""
return os.path.basename(self.asset.name)
# Asset file
asset = models.FileField(
upload_to=rename_asset,
verbose_name=_('Asset'),
help_text=_("Report asset file"),
)
description = models.CharField(max_length=250, verbose_name=_('Description'), help_text=_("Asset file description"))
# Asset description (user facing string, not used internally)
description = models.CharField(
max_length=250,
verbose_name=_('Description'),
help_text=_("Asset file description")
)

View File

@ -78,8 +78,7 @@ content: "v{{report_revision}} - {{ date.isoformat }}";
{% endblock %}
{% block header_content %}
<!-- TODO - Make the company logo asset generic -->
<img class='logo' src="{% asset 'company_logo.png' %}" alt="logo" width="150">
<img class='logo' src="{% logo_image %}" alt="logo" width="150">
<div class='header-right'>
<h3>

View File

@ -28,21 +28,29 @@ def image_data(img, fmt='PNG'):
def qrcode(data, **kwargs):
"""Return a byte-encoded QR code image.
Optional kwargs
---------------
kwargs:
fill_color: Fill color (default = black)
back_color: Background color (default = white)
version: Default = 1
box_size: Default = 20
border: Default = 1
Returns:
base64 encoded image data
fill_color: Fill color (default = black)
back_color: Background color (default = white)
"""
# Construct "default" values
params = dict(
box_size=20,
border=1,
version=1,
)
fill_color = kwargs.pop('fill_color', 'black')
back_color = kwargs.pop('back_color', 'white')
format = kwargs.pop('format', 'PNG')
params.update(**kwargs)
qr = python_qrcode.QRCode(**params)
@ -50,9 +58,13 @@ def qrcode(data, **kwargs):
qr.add_data(data, optimize=20)
qr.make(fit=True)
qri = qr.make_image(fill_color=fill_color, back_color=back_color)
qri = qr.make_image(
fill_color=fill_color,
back_color=back_color
)
return image_data(qri)
# Render to byte-encoded image
return image_data(qri, fmt=format)
@register.simple_tag()
@ -60,6 +72,8 @@ def barcode(data, barcode_class='code128', **kwargs):
"""Render a barcode."""
constructor = python_barcode.get_barcode_class(barcode_class)
format = kwargs.pop('format', 'PNG')
data = str(data).zfill(constructor.digits)
writer = python_barcode.writer.ImageWriter
@ -68,5 +82,5 @@ def barcode(data, barcode_class='code128', **kwargs):
image = barcode_image.render(writer_options=kwargs)
# Render to byte-encoded PNG
return image_data(image)
# Render to byte-encoded image
return image_data(image, fmt=format)

View File

@ -10,89 +10,133 @@ import InvenTree.helpers
from common.models import InvenTreeSetting
from company.models import Company
from part.models import Part
from stock.models import StockItem
register = template.Library()
@register.simple_tag()
def asset(filename):
"""Return fully-qualified path for an upload report asset file."""
"""Return fully-qualified path for an upload report asset file.
Arguments:
filename: Asset filename (relative to the 'assets' media directory)
Raises:
FileNotFoundError if file does not exist
"""
# If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
if debug_mode:
path = os.path.join(settings.MEDIA_URL, 'report', 'assets', filename)
else:
# Test if the file actually exists
full_path = os.path.join(settings.MEDIA_ROOT, 'report', 'assets', filename)
path = os.path.join(settings.MEDIA_ROOT, 'report', 'assets', filename)
path = os.path.abspath(path)
if not os.path.exists(full_path) or not os.path.isfile(full_path):
raise FileNotFoundError(f"Asset file '{filename}' does not exist")
if debug_mode:
return os.path.join(settings.MEDIA_URL, 'report', 'assets', filename)
else:
return f"file://{full_path}"
@register.simple_tag()
def uploaded_image(filename, replace_missing=True, replacement_file='blank_image.png'):
"""Return a fully-qualified path for an 'uploaded' image.
Arguments:
filename: The filename of the image relative to the MEDIA_ROOT directory
replace_missing: Optionally return a placeholder image if the provided filename does not exist
Returns:
A fully qualified path to the image
"""
# If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
# Check if the file exists
if not filename:
exists = False
else:
try:
full_path = os.path.join(settings.MEDIA_ROOT, filename)
full_path = os.path.abspath(full_path)
exists = os.path.exists(full_path) and os.path.isfile(full_path)
except Exception:
exists = False
if not exists and not replace_missing:
raise FileNotFoundError(f"Image file '{filename}' not found")
if debug_mode:
# In debug mode, return a web path
if exists:
return os.path.join(settings.MEDIA_URL, filename)
else:
return os.path.join(settings.STATIC_URL, 'img', replacement_file)
else:
# Return file path
if exists:
path = os.path.join(settings.MEDIA_ROOT, filename)
path = os.path.abspath(path)
else:
path = os.path.join(settings.STATIC_ROOT, 'img', replacement_file)
path = os.path.abspath(path)
return f"file://{path}"
@register.simple_tag()
def part_image(part):
"""Return a fully-qualified path for a part image."""
# If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
"""Return a fully-qualified path for a part image.
Arguments:
part: a Part model instance
Raises:
TypeError if provided part is not a Part instance
"""
if type(part) is Part:
img = part.image.name
elif type(part) is StockItem:
img = part.part.image.name
else:
img = ''
raise TypeError("part_image tag requires a Part instance")
if debug_mode:
if img:
return os.path.join(settings.MEDIA_URL, img)
else:
return os.path.join(settings.STATIC_URL, 'img', 'blank_image.png')
else:
path = os.path.join(settings.MEDIA_ROOT, img)
path = os.path.abspath(path)
if not os.path.exists(path) or not os.path.isfile(path):
# Image does not exist
# Return the 'blank' image
path = os.path.join(settings.STATIC_ROOT, 'img', 'blank_image.png')
path = os.path.abspath(path)
return f"file://{path}"
return uploaded_image(img)
@register.simple_tag()
def company_image(company):
"""Return a fully-qualified path for a company image."""
# If in debug mode, return the URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
"""Return a fully-qualified path for a company image.
Arguments:
company: a Company model instance
Raises:
TypeError if provided company is not a Company instance
"""
if type(company) is Company:
img = company.image.name
else:
img = ''
raise TypeError("company_image tag requires a Company instance")
if debug_mode:
if img:
return os.path.join(settings.MEDIA_URL, img)
else:
return os.path.join(settings.STATIC_URL, 'img', 'blank_image.png')
return uploaded_image(img)
else:
path = os.path.join(settings.MEDIA_ROOT, img)
path = os.path.abspath(path)
if not os.path.exists(path) or not os.path.isfile(path):
# Image does not exist
# Return the 'blank' image
path = os.path.join(settings.STATIC_ROOT, 'img', 'blank_image.png')
path = os.path.abspath(path)
@register.simple_tag()
def logo_image():
"""Return a fully-qualified path for the logo image.
return f"file://{path}"
- If a custom logo has been provided, return a path to that logo
- Otherwise, return a path to the default InvenTree logo
"""
# If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
return InvenTree.helpers.getLogoImage(as_file=not debug_mode)
@register.simple_tag()

View File

@ -6,15 +6,142 @@ import shutil
from django.conf import settings
from django.core.cache import cache
from django.http.response import StreamingHttpResponse
from django.test import TestCase
from django.urls import reverse
import report.models as report_models
from build.models import Build
from common.models import InvenTreeSetting, InvenTreeUserSetting
from InvenTree.api_tester import InvenTreeAPITestCase
from report.templatetags import barcode as barcode_tags
from report.templatetags import report as report_tags
from stock.models import StockItem, StockItemAttachment
class ReportTagTest(TestCase):
"""Unit tests for the report template tags"""
def debug_mode(self, value: bool):
"""Enable or disable debug mode for reports"""
InvenTreeSetting.set_setting('REPORT_DEBUG_MODE', value, change_user=None)
def test_asset(self):
"""Tests for asset files"""
# Test that an error is raised if the file does not exist
for b in [True, False]:
self.debug_mode(b)
with self.assertRaises(FileNotFoundError):
report_tags.asset("bad_file.txt")
# Create an asset file
asset_dir = os.path.join(settings.MEDIA_ROOT, 'report', 'assets')
os.makedirs(asset_dir, exist_ok=True)
asset_path = os.path.join(asset_dir, 'test.txt')
with open(asset_path, 'w') as f:
f.write("dummy data")
self.debug_mode(True)
asset = report_tags.asset('test.txt')
self.assertEqual(asset, '/media/report/assets/test.txt')
self.debug_mode(False)
asset = report_tags.asset('test.txt')
self.assertEqual(asset, f'file://{asset_dir}/test.txt')
def test_uploaded_image(self):
"""Tests for retrieving uploaded images"""
# Test for a missing image
for b in [True, False]:
self.debug_mode(b)
with self.assertRaises(FileNotFoundError):
report_tags.uploaded_image('/part/something/test.png', replace_missing=False)
img = report_tags.uploaded_image('/part/something/other.png')
self.assertTrue('blank_image.png' in img)
# Create a dummy image
img_path = 'part/images/'
img_path = os.path.join(settings.MEDIA_ROOT, img_path)
img_file = os.path.join(img_path, 'test.jpg')
os.makedirs(img_path, exist_ok=True)
with open(img_file, 'w') as f:
f.write("dummy data")
# Test in debug mode
self.debug_mode(True)
img = report_tags.uploaded_image('part/images/test.jpg')
self.assertEqual(img, '/media/part/images/test.jpg')
self.debug_mode(False)
img = report_tags.uploaded_image('part/images/test.jpg')
self.assertEqual(img, f'file://{img_path}test.jpg')
def test_part_image(self):
"""Unit tests for the 'part_image' tag"""
with self.assertRaises(TypeError):
report_tags.part_image(None)
def test_company_image(self):
"""Unit tests for the 'company_image' tag"""
with self.assertRaises(TypeError):
report_tags.company_image(None)
def test_logo_image(self):
"""Unit tests for the 'logo_image' tag"""
# By default, should return the core InvenTree logo
for b in [True, False]:
self.debug_mode(b)
logo = report_tags.logo_image()
self.assertIn('inventree.png', logo)
class BarcodeTagTest(TestCase):
"""Unit tests for the barcode template tags"""
def test_barcode(self):
"""Test the barcode generation tag"""
barcode = barcode_tags.barcode("12345")
self.assertTrue(type(barcode) == str)
self.assertTrue(barcode.startswith('data:image/png;'))
# Try with a different format
barcode = barcode_tags.barcode('99999', format='BMP')
self.assertTrue(type(barcode) == str)
self.assertTrue(barcode.startswith('data:image/bmp;'))
def test_qrcode(self):
"""Test the qrcode generation tag"""
# Test with default settings
qrcode = barcode_tags.qrcode("hello world")
self.assertTrue(type(qrcode) == str)
self.assertTrue(qrcode.startswith('data:image/png;'))
self.assertEqual(len(qrcode), 700)
# Generate a much larger qrcode
qrcode = barcode_tags.qrcode(
"hello_world",
version=2,
box_size=50,
format='BMP',
)
self.assertTrue(type(qrcode) == str)
self.assertTrue(qrcode.startswith('data:image/bmp;'))
self.assertEqual(len(qrcode), 309720)
class ReportTest(InvenTreeAPITestCase):
"""Base class for unit testing reporting models"""
fixtures = [