mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +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:
		| @@ -2,13 +2,16 @@ | |||||||
|  |  | ||||||
| import io | import io | ||||||
| import json | import json | ||||||
|  | import logging | ||||||
| import os.path | import os.path | ||||||
| import re | import re | ||||||
| from decimal import Decimal, InvalidOperation | from decimal import Decimal, InvalidOperation | ||||||
| from wsgiref.util import FileWrapper | from wsgiref.util import FileWrapper | ||||||
|  |  | ||||||
|  | from django.conf import settings | ||||||
| from django.contrib.auth.models import Permission | from django.contrib.auth.models import Permission | ||||||
| from django.core.exceptions import FieldError, ValidationError | from django.core.exceptions import FieldError, ValidationError | ||||||
|  | from django.core.files.storage import default_storage | ||||||
| from django.http import StreamingHttpResponse | from django.http import StreamingHttpResponse | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| @@ -25,6 +28,8 @@ from common.settings import currency_code_default | |||||||
| from .api_tester import UserMixin | from .api_tester import UserMixin | ||||||
| from .settings import MEDIA_URL, STATIC_URL | from .settings import MEDIA_URL, STATIC_URL | ||||||
|  |  | ||||||
|  | logger = logging.getLogger('inventree') | ||||||
|  |  | ||||||
|  |  | ||||||
| def getSetting(key, backup_value=None): | def getSetting(key, backup_value=None): | ||||||
|     """Shortcut for reading a setting value from the database.""" |     """Shortcut for reading a setting value from the database.""" | ||||||
| @@ -82,6 +87,15 @@ def construct_absolute_url(*arg): | |||||||
|     return url |     return url | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def TestIfImage(img): | ||||||
|  |     """Test if an image file is indeed an image.""" | ||||||
|  |     try: | ||||||
|  |         Image.open(img).verify() | ||||||
|  |         return True | ||||||
|  |     except Exception: | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
| def getBlankImage(): | def getBlankImage(): | ||||||
|     """Return the qualified path for the 'blank image' placeholder.""" |     """Return the qualified path for the 'blank image' placeholder.""" | ||||||
|     return getStaticUrl("img/blank_image.png") |     return getStaticUrl("img/blank_image.png") | ||||||
| @@ -92,13 +106,23 @@ def getBlankThumbnail(): | |||||||
|     return getStaticUrl("img/blank_image.thumbnail.png") |     return getStaticUrl("img/blank_image.thumbnail.png") | ||||||
|  |  | ||||||
|  |  | ||||||
| def TestIfImage(img): | def getLogoImage(as_file=False): | ||||||
|     """Test if an image file is indeed an image.""" |     """Return the InvenTree logo image, or a custom logo if available.""" | ||||||
|     try: |  | ||||||
|         Image.open(img).verify() |     """Return the path to the logo-file.""" | ||||||
|         return True |     if settings.CUSTOM_LOGO: | ||||||
|     except Exception: |  | ||||||
|         return False |         if as_file: | ||||||
|  |             return f"file://{default_storage.path(settings.CUSTOM_LOGO)}" | ||||||
|  |         else: | ||||||
|  |             return default_storage.url(settings.CUSTOM_LOGO) | ||||||
|  |  | ||||||
|  |     else: | ||||||
|  |         if as_file: | ||||||
|  |             path = os.path.join(settings.STATIC_ROOT, 'img/inventree.png') | ||||||
|  |             return f"file://{path}" | ||||||
|  |         else: | ||||||
|  |             return getStaticUrl('img/inventree.png') | ||||||
|  |  | ||||||
|  |  | ||||||
| def TestIfImageURL(url): | def TestIfImageURL(url): | ||||||
|   | |||||||
| @@ -967,5 +967,5 @@ CUSTOM_LOGO = get_setting( | |||||||
|  |  | ||||||
| # check that the logo-file exsists in media | # check that the logo-file exsists in media | ||||||
| if CUSTOM_LOGO and not default_storage.exists(CUSTOM_LOGO):  # pragma: no cover | if CUSTOM_LOGO and not default_storage.exists(CUSTOM_LOGO):  # pragma: no cover | ||||||
|  |     logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the default media storage") | ||||||
|     CUSTOM_LOGO = False |     CUSTOM_LOGO = False | ||||||
|     logger.warning("The custom logo file could not be found in the default media storage") |  | ||||||
|   | |||||||
| @@ -240,6 +240,17 @@ class TestHelpers(TestCase): | |||||||
|         self.assertEqual(helpers.decimal2string(Decimal('1.2345000')), '1.2345') |         self.assertEqual(helpers.decimal2string(Decimal('1.2345000')), '1.2345') | ||||||
|         self.assertEqual(helpers.decimal2string('test'), 'test') |         self.assertEqual(helpers.decimal2string('test'), 'test') | ||||||
|  |  | ||||||
|  |     def test_logo_image(self): | ||||||
|  |         """Test for retrieving logo image""" | ||||||
|  |  | ||||||
|  |         # By default, there is no custom logo provided | ||||||
|  |  | ||||||
|  |         logo = helpers.getLogoImage() | ||||||
|  |         self.assertEqual(logo, '/static/img/inventree.png') | ||||||
|  |  | ||||||
|  |         logo = helpers.getLogoImage(as_file=True) | ||||||
|  |         self.assertEqual(logo, f'file://{settings.STATIC_ROOT}/img/inventree.png') | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestQuoteWrap(TestCase): | class TestQuoteWrap(TestCase): | ||||||
|     """Tests for string wrapping.""" |     """Tests for string wrapping.""" | ||||||
|   | |||||||
| @@ -17,10 +17,10 @@ from stdimage.models import StdImageField | |||||||
| import common.models | import common.models | ||||||
| import common.settings | import common.settings | ||||||
| import InvenTree.fields | import InvenTree.fields | ||||||
|  | import InvenTree.helpers | ||||||
| import InvenTree.validators | import InvenTree.validators | ||||||
| from common.settings import currency_code_default | from common.settings import currency_code_default | ||||||
| from InvenTree.fields import InvenTreeURLField | from InvenTree.fields import InvenTreeURLField | ||||||
| from InvenTree.helpers import getBlankImage, getBlankThumbnail, getMediaUrl |  | ||||||
| from InvenTree.models import InvenTreeAttachment | from InvenTree.models import InvenTreeAttachment | ||||||
| from InvenTree.status_codes import PurchaseOrderStatus | from InvenTree.status_codes import PurchaseOrderStatus | ||||||
|  |  | ||||||
| @@ -177,16 +177,16 @@ class Company(models.Model): | |||||||
|     def get_image_url(self): |     def get_image_url(self): | ||||||
|         """Return the URL of the image for this company.""" |         """Return the URL of the image for this company.""" | ||||||
|         if self.image: |         if self.image: | ||||||
|             return getMediaUrl(self.image.url) |             return InvenTree.helpers.getMediaUrl(self.image.url) | ||||||
|         else: |         else: | ||||||
|             return getBlankImage() |             return InvenTree.helpers.getBlankImage() | ||||||
|  |  | ||||||
|     def get_thumbnail_url(self): |     def get_thumbnail_url(self): | ||||||
|         """Return the URL for the thumbnail image for this Company.""" |         """Return the URL for the thumbnail image for this Company.""" | ||||||
|         if self.image: |         if self.image: | ||||||
|             return getMediaUrl(self.image.thumbnail.url) |             return InvenTree.helpers.getMediaUrl(self.image.thumbnail.url) | ||||||
|         else: |         else: | ||||||
|             return getBlankThumbnail() |             return InvenTree.helpers.getBlankThumbnail() | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def parts(self): |     def parts(self): | ||||||
|   | |||||||
| @@ -7,8 +7,7 @@ from datetime import date, datetime | |||||||
|  |  | ||||||
| from django import template | from django import template | ||||||
| from django.conf import settings as djangosettings | from django.conf import settings as djangosettings | ||||||
| from django.core.files.storage import default_storage | from django.templatetags.static import StaticNode | ||||||
| from django.templatetags.static import StaticNode, static |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.html import format_html | from django.utils.html import format_html | ||||||
| from django.utils.safestring import mark_safe | from django.utils.safestring import mark_safe | ||||||
| @@ -174,6 +173,16 @@ def inventree_title(*args, **kwargs): | |||||||
|     return version.inventreeInstanceTitle() |     return version.inventreeInstanceTitle() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @register.simple_tag() | ||||||
|  | def inventree_logo(**kwargs): | ||||||
|  |     """Return the InvenTree logo, *or* a custom logo if the user has uploaded one. | ||||||
|  |  | ||||||
|  |     Returns a path to an image file, which can be rendered in the web interface | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     return InvenTree.helpers.getLogoImage(**kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
| @register.simple_tag() | @register.simple_tag() | ||||||
| def inventree_base_url(*args, **kwargs): | def inventree_base_url(*args, **kwargs): | ||||||
|     """Return the INVENTREE_BASE_URL setting.""" |     """Return the INVENTREE_BASE_URL setting.""" | ||||||
| @@ -473,14 +482,6 @@ def inventree_customize(reference, *args, **kwargs): | |||||||
|     return djangosettings.CUSTOMIZE.get(reference, '') |     return djangosettings.CUSTOMIZE.get(reference, '') | ||||||
|  |  | ||||||
|  |  | ||||||
| @register.simple_tag() |  | ||||||
| def inventree_logo(*args, **kwargs): |  | ||||||
|     """Return the path to the logo-file.""" |  | ||||||
|     if settings.CUSTOM_LOGO: |  | ||||||
|         return default_storage.url(settings.CUSTOM_LOGO) |  | ||||||
|     return static('img/inventree.png') |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class I18nStaticNode(StaticNode): | class I18nStaticNode(StaticNode): | ||||||
|     """Custom StaticNode. |     """Custom StaticNode. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -535,14 +535,23 @@ class ReportAsset(models.Model): | |||||||
|     and can be loaded in a template using the {% report_asset <filename> %} tag. |     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): |     def __str__(self): | ||||||
|         """String representation of a ReportAsset instance""" |         """String representation of a ReportAsset instance""" | ||||||
|         return os.path.basename(self.asset.name) |         return os.path.basename(self.asset.name) | ||||||
|  |  | ||||||
|  |     # Asset file | ||||||
|     asset = models.FileField( |     asset = models.FileField( | ||||||
|         upload_to=rename_asset, |         upload_to=rename_asset, | ||||||
|         verbose_name=_('Asset'), |         verbose_name=_('Asset'), | ||||||
|         help_text=_("Report asset file"), |         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") | ||||||
|  |     ) | ||||||
|   | |||||||
| @@ -78,8 +78,7 @@ content: "v{{report_revision}} - {{ date.isoformat }}"; | |||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block header_content %} | {% block header_content %} | ||||||
|     <!-- TODO - Make the company logo asset generic --> |     <img class='logo' src="{% logo_image %}" alt="logo" width="150"> | ||||||
|     <img class='logo' src="{% asset 'company_logo.png' %}" alt="logo" width="150"> |  | ||||||
|  |  | ||||||
|     <div class='header-right'> |     <div class='header-right'> | ||||||
|         <h3> |         <h3> | ||||||
|   | |||||||
| @@ -28,21 +28,29 @@ def image_data(img, fmt='PNG'): | |||||||
| def qrcode(data, **kwargs): | def qrcode(data, **kwargs): | ||||||
|     """Return a byte-encoded QR code image. |     """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 |     # Construct "default" values | ||||||
|     params = dict( |     params = dict( | ||||||
|         box_size=20, |         box_size=20, | ||||||
|         border=1, |         border=1, | ||||||
|  |         version=1, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     fill_color = kwargs.pop('fill_color', 'black') |     fill_color = kwargs.pop('fill_color', 'black') | ||||||
|     back_color = kwargs.pop('back_color', 'white') |     back_color = kwargs.pop('back_color', 'white') | ||||||
|  |  | ||||||
|  |     format = kwargs.pop('format', 'PNG') | ||||||
|  |  | ||||||
|     params.update(**kwargs) |     params.update(**kwargs) | ||||||
|  |  | ||||||
|     qr = python_qrcode.QRCode(**params) |     qr = python_qrcode.QRCode(**params) | ||||||
| @@ -50,9 +58,13 @@ def qrcode(data, **kwargs): | |||||||
|     qr.add_data(data, optimize=20) |     qr.add_data(data, optimize=20) | ||||||
|     qr.make(fit=True) |     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() | @register.simple_tag() | ||||||
| @@ -60,6 +72,8 @@ def barcode(data, barcode_class='code128', **kwargs): | |||||||
|     """Render a barcode.""" |     """Render a barcode.""" | ||||||
|     constructor = python_barcode.get_barcode_class(barcode_class) |     constructor = python_barcode.get_barcode_class(barcode_class) | ||||||
|  |  | ||||||
|  |     format = kwargs.pop('format', 'PNG') | ||||||
|  |  | ||||||
|     data = str(data).zfill(constructor.digits) |     data = str(data).zfill(constructor.digits) | ||||||
|  |  | ||||||
|     writer = python_barcode.writer.ImageWriter |     writer = python_barcode.writer.ImageWriter | ||||||
| @@ -68,5 +82,5 @@ def barcode(data, barcode_class='code128', **kwargs): | |||||||
|  |  | ||||||
|     image = barcode_image.render(writer_options=kwargs) |     image = barcode_image.render(writer_options=kwargs) | ||||||
|  |  | ||||||
|     # Render to byte-encoded PNG |     # Render to byte-encoded image | ||||||
|     return image_data(image) |     return image_data(image, fmt=format) | ||||||
|   | |||||||
| @@ -10,89 +10,133 @@ import InvenTree.helpers | |||||||
| from common.models import InvenTreeSetting | from common.models import InvenTreeSetting | ||||||
| from company.models import Company | from company.models import Company | ||||||
| from part.models import Part | from part.models import Part | ||||||
| from stock.models import StockItem |  | ||||||
|  |  | ||||||
| register = template.Library() | register = template.Library() | ||||||
|  |  | ||||||
|  |  | ||||||
| @register.simple_tag() | @register.simple_tag() | ||||||
| def asset(filename): | 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 |     # If in debug mode, return URL to the image, not a local file | ||||||
|     debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE') |     debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE') | ||||||
|  |  | ||||||
|     if debug_mode: |     # Test if the file actually exists | ||||||
|         path = os.path.join(settings.MEDIA_URL, 'report', 'assets', filename) |     full_path = os.path.join(settings.MEDIA_ROOT, 'report', 'assets', filename) | ||||||
|     else: |  | ||||||
|  |  | ||||||
|         path = os.path.join(settings.MEDIA_ROOT, 'report', 'assets', filename) |     if not os.path.exists(full_path) or not os.path.isfile(full_path): | ||||||
|         path = os.path.abspath(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}" |         return f"file://{path}" | ||||||
|  |  | ||||||
|  |  | ||||||
| @register.simple_tag() | @register.simple_tag() | ||||||
| def part_image(part): | def part_image(part): | ||||||
|     """Return a fully-qualified path for a part image.""" |     """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') |     Arguments: | ||||||
|  |         part: a Part model instance | ||||||
|  |  | ||||||
|  |     Raises: | ||||||
|  |         TypeError if provided part is not a Part instance | ||||||
|  |     """ | ||||||
|  |  | ||||||
|     if type(part) is Part: |     if type(part) is Part: | ||||||
|         img = part.image.name |         img = part.image.name | ||||||
|  |  | ||||||
|     elif type(part) is StockItem: |  | ||||||
|         img = part.part.image.name |  | ||||||
|  |  | ||||||
|     else: |     else: | ||||||
|         img = '' |         raise TypeError("part_image tag requires a Part instance") | ||||||
|  |  | ||||||
|     if debug_mode: |     return uploaded_image(img) | ||||||
|         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}" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @register.simple_tag() | @register.simple_tag() | ||||||
| def company_image(company): | def company_image(company): | ||||||
|     """Return a fully-qualified path for a company image.""" |     """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') |     Arguments: | ||||||
|  |         company: a Company model instance | ||||||
|  |  | ||||||
|  |     Raises: | ||||||
|  |         TypeError if provided company is not a Company instance | ||||||
|  |     """ | ||||||
|  |  | ||||||
|     if type(company) is Company: |     if type(company) is Company: | ||||||
|         img = company.image.name |         img = company.image.name | ||||||
|     else: |     else: | ||||||
|         img = '' |         raise TypeError("company_image tag requires a Company instance") | ||||||
|  |  | ||||||
|     if debug_mode: |     return uploaded_image(img) | ||||||
|         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): | @register.simple_tag() | ||||||
|             # Image does not exist | def logo_image(): | ||||||
|             # Return the 'blank' image |     """Return a fully-qualified path for the logo image. | ||||||
|             path = os.path.join(settings.STATIC_ROOT, 'img', 'blank_image.png') |  | ||||||
|             path = os.path.abspath(path) |  | ||||||
|  |  | ||||||
|         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() | @register.simple_tag() | ||||||
|   | |||||||
| @@ -6,15 +6,142 @@ import shutil | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.http.response import StreamingHttpResponse | from django.http.response import StreamingHttpResponse | ||||||
|  | from django.test import TestCase | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| import report.models as report_models | import report.models as report_models | ||||||
| from build.models import Build | from build.models import Build | ||||||
| from common.models import InvenTreeSetting, InvenTreeUserSetting | from common.models import InvenTreeSetting, InvenTreeUserSetting | ||||||
| from InvenTree.api_tester import InvenTreeAPITestCase | 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 | 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): | class ReportTest(InvenTreeAPITestCase): | ||||||
|     """Base class for unit testing reporting models""" |     """Base class for unit testing reporting models""" | ||||||
|     fixtures = [ |     fixtures = [ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user