diff --git a/src/backend/InvenTree/InvenTree/helpers_model.py b/src/backend/InvenTree/InvenTree/helpers_model.py index 3461b26254..41503f5741 100644 --- a/src/backend/InvenTree/InvenTree/helpers_model.py +++ b/src/backend/InvenTree/InvenTree/helpers_model.py @@ -211,16 +211,16 @@ def render_currency( except Exception: pass - if min_decimal_places is None: + if min_decimal_places is None or not isinstance(min_decimal_places, (int, float)): min_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES_MIN', 0) - if max_decimal_places is None: + if max_decimal_places is None or not isinstance(max_decimal_places, (int, float)): max_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES', 6) value = Decimal(str(money.amount)).normalize() value = str(value) - if decimal_places is not None: + if decimal_places is not None and isinstance(decimal_places, (int, float)): # Decimal place count is provided, use it pass elif '.' in value: diff --git a/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py b/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py index ca85a755b3..73130d0c60 100644 --- a/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py +++ b/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py @@ -64,12 +64,6 @@ def render_date(date_object): return date_object -@register.simple_tag -def render_currency(money, **kwargs): - """Render a currency / Money object.""" - return InvenTree.helpers_model.render_currency(money, **kwargs) - - @register.simple_tag() def str2bool(x, *args, **kwargs): """Convert a string to a boolean value.""" diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index bd99f1186b..36b107bb91 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -39,6 +39,48 @@ from stock.models import StockItem, StockLocation from stock.status_codes import StockStatus +class PartImageTestMixin: + """Mixin for testing part images.""" + + roles = [ + 'part.change', + 'part.add', + 'part.delete', + 'part_category.change', + 'part_category.add', + ] + + @classmethod + def setUpTestData(cls): + """Custom setup routine for this class.""" + super().setUpTestData() + + # Create a custom APIClient for file uploads + # Ref: https://stackoverflow.com/questions/40453947/how-to-generate-a-file-upload-test-request-with-django-rest-frameworks-apireq + cls.upload_client = APIClient() + cls.upload_client.force_authenticate(user=cls.user) + + def create_test_image(self): + """Create a test image file.""" + p = Part.objects.first() + + fn = BASE_DIR / '_testfolder' / 'part_image_123abc.png' + + img = PIL.Image.new('RGB', (128, 128), color='blue') + img.save(fn) + + with open(fn, 'rb') as img_file: + response = self.upload_client.patch( + reverse('api-part-detail', kwargs={'pk': p.pk}), + {'image': img_file}, + expected_code=200, + ) + print(response.data) + image_name = response.data['image'] + self.assertTrue(image_name.startswith('/media/part_images/part_image')) + return image_name + + class PartCategoryAPITest(InvenTreeAPITestCase): """Unit tests for the PartCategory API.""" @@ -1463,7 +1505,7 @@ class PartCreationTests(PartAPITestBase): self.assertEqual(prt.parameters.count(), 3) -class PartDetailTests(PartAPITestBase): +class PartDetailTests(PartImageTestMixin, PartAPITestBase): """Test that we can create / edit / delete Part objects via the API.""" @classmethod @@ -1656,22 +1698,7 @@ class PartDetailTests(PartAPITestBase): def test_existing_image(self): """Test that we can allocate an existing uploaded image to a new Part.""" # First, upload an image for an existing part - p = Part.objects.first() - - fn = BASE_DIR / '_testfolder' / 'part_image_123abc.png' - - img = PIL.Image.new('RGB', (128, 128), color='blue') - img.save(fn) - - with open(fn, 'rb') as img_file: - response = self.upload_client.patch( - reverse('api-part-detail', kwargs={'pk': p.pk}), - {'image': img_file}, - expected_code=200, - ) - - image_name = response.data['image'] - self.assertTrue(image_name.startswith('/media/part_images/part_image')) + image_name = self.create_test_image() # Attempt to create, but with an invalid image name response = self.post( diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index 9c84d20732..a8556166e4 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -44,6 +44,8 @@ def filter_queryset(queryset: QuerySet, **kwargs) -> QuerySet: Example: {% filter_queryset companies is_supplier=True as suppliers %} """ + if not isinstance(queryset, QuerySet): + return queryset return queryset.filter(**kwargs) @@ -60,7 +62,10 @@ def filter_db_model(model_name: str, **kwargs) -> QuerySet: Example: {% filter_db_model 'part.partcategory' is_template=True as template_parts %} """ - app_name, model_name = model_name.split('.') + try: + app_name, model_name = model_name.split('.') + except ValueError: + return None try: model = apps.get_model(app_name, model_name) @@ -93,13 +98,7 @@ def getindex(container: list, index: int) -> Any: if index < 0 or index >= len(container): return None - - try: - value = container[index] - except IndexError: - value = None - - return value + return container[index] @register.simple_tag() @@ -191,8 +190,8 @@ def uploaded_image( try: full_path = settings.MEDIA_ROOT.joinpath(filename).resolve() exists = full_path.exists() and full_path.is_file() - except Exception: - exists = False + except Exception: # pragma: no cover + exists = False # pragma: no cover if exists and validate and not InvenTree.helpers.TestIfImage(full_path): logger.warning("File '%s' is not a valid image", filename) @@ -302,10 +301,14 @@ def part_image(part: Part, preview: bool = False, thumbnail: bool = False, **kwa if type(part) is not Part: raise TypeError(_('part_image tag requires a Part instance')) - if preview: - img = part.image.preview.name + if not part.image: + img = None + elif preview: + img = None if not hasattr(part.image, 'preview') else part.image.preview.name elif thumbnail: - img = part.image.thumbnail.name + img = ( + None if not hasattr(part.image, 'thumbnail') else part.image.thumbnail.name + ) else: img = part.image.name @@ -376,10 +379,14 @@ def internal_link(link, text) -> str: """ text = str(text) - url = InvenTree.helpers_model.construct_absolute_url(link) + try: + url = InvenTree.helpers_model.construct_absolute_url(link) + except Exception: + url = None # If the base URL is not set, just return the text if not url: + logger.warning('Failed to construct absolute URL for internal link') return text return mark_safe(f'{text}') @@ -525,7 +532,10 @@ def format_date(dt: date, timezone: Optional[str] = None, fmt: Optional[str] = N timezone: The timezone to use for the date (defaults to the server timezone) fmt: The format string to use (defaults to ISO formatting) """ - dt = InvenTree.helpers.to_local_time(dt, timezone).date() + try: + dt = InvenTree.helpers.to_local_time(dt, timezone).date() + except TypeError: + return str(dt) if fmt: return dt.strftime(fmt) @@ -558,15 +568,18 @@ def icon(name, **kwargs): @register.simple_tag() -def include_icon_fonts(): +def include_icon_fonts(ttf: bool = False, woff: bool = False): """Return the CSS font-face rule for the icon fonts used on the current page (or all).""" fonts = [] + if not ttf and not woff: + ttf = woff = True + for font in common.icons.get_icon_packs().values(): # generate the font src string (prefer ttf over woff, woff2 is not supported by weasyprint) - if 'truetype' in font.fonts: + if 'truetype' in font.fonts and ttf: font_format, url = 'truetype', font.fonts['truetype'] - elif 'woff' in font.fonts: + elif 'woff' in font.fonts and woff: font_format, url = 'woff', font.fonts['woff'] fonts.append(f""" diff --git a/src/backend/InvenTree/report/test_tags.py b/src/backend/InvenTree/report/test_tags.py new file mode 100644 index 0000000000..bf6265a354 --- /dev/null +++ b/src/backend/InvenTree/report/test_tags.py @@ -0,0 +1,447 @@ +"""Test for custom report tags.""" + +from zoneinfo import ZoneInfo + +from django.conf import settings +from django.test import TestCase, override_settings +from django.utils import timezone +from django.utils.safestring import SafeString + +from djmoney.money import Money +from PIL import Image + +from common.models import InvenTreeSetting +from InvenTree.unit_test import InvenTreeTestCase +from part.models import Part, PartParameter, PartParameterTemplate +from part.test_api import PartImageTestMixin +from report.templatetags import barcode as barcode_tags +from report.templatetags import report as report_tags + + +class ReportTagTest(PartImageTestMixin, InvenTreeTestCase): + """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_getindex(self): + """Tests for the 'getindex' template tag.""" + fn = report_tags.getindex + data = [1, 2, 3, 4, 5, 6] + + # Out of bounds or invalid + self.assertEqual(fn(data, -1), None) + self.assertEqual(fn(data, 99), None) + self.assertEqual(fn(data, 'xx'), None) + + for idx in range(len(data)): + self.assertEqual(fn(data, idx), data[idx]) + + def test_getkey(self): + """Tests for the 'getkey' template tag.""" + data = {'hello': 'world', 'foo': 'bar', 'with spaces': 'withoutspaces', 1: 2} + + # Valid case + for k, v in data.items(): + self.assertEqual(report_tags.getkey(data, k), v) + + # Error case + self.assertEqual( + None, report_tags.getkey('not a container', 'not-a-key', 'a value') + ) + + 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 = settings.MEDIA_ROOT.joinpath('report', 'assets') + asset_dir.mkdir(parents=True, exist_ok=True) + asset_path = asset_dir.joinpath('test.txt') + + asset_path.write_text('dummy data') + + self.debug_mode(True) + asset = report_tags.asset('test.txt') + self.assertEqual(asset, '/media/report/assets/test.txt') + + # Ensure that a 'safe string' also works + asset = report_tags.asset(SafeString('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 = str(report_tags.uploaded_image('/part/something/other.png')) + + if b: + self.assertIn('blank_image.png', img) + else: + self.assertIn('data:image/png;charset=utf-8;base64,', img) + + # Create a dummy image + img_path = 'part/images/' + img_path = settings.MEDIA_ROOT.joinpath(img_path) + img_file = img_path.joinpath('test.jpg') + + img_path.mkdir(parents=True, exist_ok=True) + img_file.write_text('dummy data') + + # Test in debug mode. Returns blank image as dummy file is not a valid image + self.debug_mode(True) + img = report_tags.uploaded_image('part/images/test.jpg') + self.assertEqual(img, '/static/img/blank_image.png') + + # Now, let's create a proper image + img = Image.new('RGB', (128, 128), color='RED') + img.save(img_file) + + # Try again + img = report_tags.uploaded_image('part/images/test.jpg') + self.assertEqual(img, '/media/part/images/test.jpg') + + # Ensure that a 'safe string' also works + img = report_tags.uploaded_image(SafeString('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.assertTrue(img.startswith('data:image/png;charset=utf-8;base64,')) + + img = report_tags.uploaded_image(SafeString('part/images/test.jpg')) + self.assertTrue(img.startswith('data:image/png;charset=utf-8;base64,')) + + # Check width, height, rotate + img = report_tags.uploaded_image( + 'part/images/test.jpg', width=100, height=200, rotate=90 + ) + self.assertTrue(img.startswith('data:image/png;charset=utf-8;base64,')) + + img = report_tags.uploaded_image('part/images/test.jpg', width=100) + self.assertTrue(img.startswith('data:image/png;charset=utf-8;base64,')) + + # Invalid args + img = report_tags.uploaded_image( + 'part/images/test.jpg', width='a', height='b', rotate='c' + ) + self.assertTrue(img.startswith('data:image/png;charset=utf-8;base64,')) + + def test_part_image(self): + """Unit tests for the 'part_image' tag.""" + with self.assertRaises(TypeError): + report_tags.part_image(None) + + obj = Part.objects.create(name='test', description='test') + self.create_test_image() + + report_tags.part_image(obj, preview=True) + report_tags.part_image(obj, thumbnail=True) + + def test_company_image(self): + """Unit tests for the 'company_image' tag.""" + with self.assertRaises(TypeError): + report_tags.company_image(None) + with self.assertRaises(TypeError): + report_tags.company_image(None, preview=True) + with self.assertRaises(TypeError): + report_tags.company_image(None, thumbnail=True) + + def test_internal_link(self): + """Unit tests for the 'internal_link' tag.""" + # Test with a valid object + obj = Part.objects.create(name='test', description='test') + self.assertEqual(report_tags.internal_link(obj, 'test123'), 'test123') + link = report_tags.internal_link(obj.get_absolute_url(), 'test') + self.assertEqual( + link, f'test' + ) + + # Test with an invalid object + link = report_tags.internal_link(None, None) + self.assertEqual(link, '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) + + def test_maths_tags(self): + """Simple tests for mathematical operator tags.""" + self.assertEqual(report_tags.add(1, 2), 3) + self.assertEqual(report_tags.subtract(10, 4.2), 5.8) + self.assertEqual(report_tags.multiply(2.3, 4), 9.2) + self.assertEqual(report_tags.divide(100, 5), 20) + + def test_number_tags(self): + """Simple tests for number formatting tags.""" + fn = report_tags.format_number + + self.assertEqual(fn(1234), '1234') + self.assertEqual(fn(1234.5678, decimal_places=2), '1234.57') + self.assertEqual(fn(1234.5678, decimal_places=3), '1234.568') + self.assertEqual(fn(-9999.5678, decimal_places=2, separator=','), '-9,999.57') + self.assertEqual( + fn(9988776655.4321, integer=True, separator=' '), '9 988 776 655' + ) + + # Failure cases + self.assertEqual(fn('abc'), 'abc') + self.assertEqual(fn(1234.456, decimal_places='a'), '1234.456') + self.assertEqual(fn(1234.456, leading='a'), '1234.456') + + @override_settings(TIME_ZONE='America/New_York') + def test_date_tags(self): + """Test for date formatting tags. + + - Source timezone is Australia/Sydney + - Server timezone is America/New York + """ + time = timezone.datetime( + year=2024, + month=3, + day=13, + hour=12, + minute=30, + second=0, + tzinfo=ZoneInfo('Australia/Sydney'), + ) + + # Format a set of tests: timezone, format, expected + tests = [ + (None, None, '2024-03-12T21:30:00-04:00'), + (None, '%d-%m-%y', '12-03-24'), + ('UTC', None, '2024-03-13T01:30:00+00:00'), + ('UTC', '%d-%B-%Y', '13-March-2024'), + ('Europe/Amsterdam', None, '2024-03-13T02:30:00+01:00'), + ('Europe/Amsterdam', '%y-%m-%d %H:%M', '24-03-13 02:30'), + ] + + for tz, fmt, expected in tests: + result = report_tags.format_datetime(time, tz, fmt) + self.assertEqual(result, expected) + + def test_icon(self): + """Test the icon template tag.""" + for icon in [None, '', 'not:the-correct-format', 'any-non-existent-icon']: + self.assertEqual(report_tags.icon(icon), '') + + self.assertEqual( + report_tags.icon('ti:package:outline'), + f'{chr(int("eaff", 16))}', + ) + self.assertEqual( + report_tags.icon( + 'ti:package:outline', **{'class': 'my-custom-class my-seconds-class'} + ), + f'{chr(int("eaff", 16))}', + ) + + def test_include_icon_fonts(self): + """Test the include_icon_fonts template tag.""" + # ttf + style = report_tags.include_icon_fonts() + + self.assertIn('@font-face {', style) + self.assertIn("font-family: 'inventree-icon-font-ti';", style) + self.assertIn('tabler-icons/tabler-icons.ttf', style) + self.assertIn('.icon {', style) + + # woff + style = report_tags.include_icon_fonts(woff=True) + self.assertIn('tabler-icons/tabler-icons.woff', style) + + def test_filter_queryset(self): + """Test the filter_queryset template tag.""" + # Test with a valid queryset + qs = Part.objects.all() + self.assertEqual( + list(report_tags.filter_queryset(qs, name='test')), + list(qs.filter(name='test')), + ) + + # Test with an invalid queryset + self.assertEqual(report_tags.filter_queryset(None, name='test'), None) + + def test_filter_db_model(self): + """Test the filter_db_model template tag.""" + self.assertEqual(list(report_tags.filter_db_model('part.part')), []) + + part = Part.objects.create(name='test', description='test') + self.assertEqual( + list(report_tags.filter_db_model('part.part', name='test')), [part] + ) + self.assertEqual( + list(report_tags.filter_db_model('part.part', name='test1')), [] + ) + + # Invalid model + self.assertEqual(report_tags.filter_db_model('part.abcd'), None) + self.assertEqual(report_tags.filter_db_model(''), None) + + def test_encode_svg_image(self): + """Test the encode_svg_image template tag.""" + # Generate smallest possible SVG for testing + svg_path = settings.BASE_DIR / '_testfolder' / 'part_image_123abc.png' + with open(svg_path, 'w', encoding='utf8') as f: + f.write('hello world'), 'hello world' + ) + self.assertEqual( + report_tags.render_html_text('hello world', bold=True), + 'hello world', + ) + self.assertEqual( + report_tags.render_html_text('hello world', italic=True), + 'hello world', + ) + self.assertEqual( + report_tags.render_html_text('hello world', heading='h1'), + '

hello world

', + ) + + def test_format_date(self): + """Test the format_date template tag.""" + # Test with a valid date + date = timezone.datetime(year=2024, month=3, day=13) + self.assertEqual(report_tags.format_date(date), '2024-03-13') + self.assertEqual(report_tags.format_date(date, fmt='%d-%m-%y'), '13-03-24') + + # Test with an invalid date + self.assertEqual(report_tags.format_date('abc'), 'abc') + self.assertEqual(report_tags.format_date(date, fmt='a'), 'a') + + +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.assertIsInstance(barcode, str) + self.assertTrue(barcode.startswith('data:image/png;')) + + # Try with a different format + barcode = barcode_tags.barcode('99999', format='BMP') + self.assertIsInstance(barcode, str) + self.assertTrue(barcode.startswith('data:image/bmp;')) + + # Test empty tag + with self.assertRaises(ValueError): + barcode_tags.barcode('') + + def test_qrcode(self): + """Test the qrcode generation tag.""" + # Test with default settings + qrcode = barcode_tags.qrcode('hello world') + self.assertIsInstance(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.assertIsInstance(qrcode, str) + self.assertTrue(qrcode.startswith('data:image/bmp;')) + self.assertEqual(len(qrcode), 309720) + + # Test empty tag + with self.assertRaises(ValueError): + barcode_tags.qrcode('') + + def test_datamatrix(self): + """Test the datamatrix generation tag.""" + # Test with default settings + datamatrix = barcode_tags.datamatrix('hello world') + self.assertEqual( + datamatrix, + 'data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAAlElEQVR4nJ1TQQ7AIAgri///cncw6wroEseBgEFbCgZJnNsFICKOPAAIjeSM5T11IznK5f5WRMgnkhP9JfCcTC/MxFZ5hxLOgqrn3o/z/OqtsNpdSL31Iu9W4Dq8Sulu+q5Nuqa3XYOdnuidlICPpXhZVBruyzAKSZehT+yNlzvZQcq6JiW7Ni592swf/43kdlDfdgMk1eOtR7kWpAAAAABJRU5ErkJggg==', + ) + + datamatrix = barcode_tags.datamatrix( + 'hello world', border=3, fill_color='red', back_color='blue' + ) + self.assertEqual( + datamatrix, + 'data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAIAAABL1vtsAAAAqElEQVR4nN1UQQ6AMAgrxv9/GQ9mpJYSY/QkBxM3KLUUA0i8i+1l/dcQiXj09CwSEU2aQJ7nE8ou2faVUXoPZSEkq+dZKVxWg4UqxUHnVdkp6IdwMXMulGvzNBDMk4WwPSrUF3LNnQNZBJmOsZaVXa44QSEKnvWb5mIgKon1E1H6aPyOcIa15uhONP9aR4hSCiGmYAoYpj4uO+vK4+ybMhr8Nkjmn/z4Dvoldi8uJu4iAAAAAElFTkSuQmCC', + ) + + # Test empty tag + with self.assertRaises(ValueError): + barcode_tags.datamatrix('') diff --git a/src/backend/InvenTree/report/tests.py b/src/backend/InvenTree/report/tests.py index 3b22b37abb..e72b0bd890 100644 --- a/src/backend/InvenTree/report/tests.py +++ b/src/backend/InvenTree/report/tests.py @@ -1,291 +1,22 @@ """Unit testing for the various report models.""" from io import StringIO -from zoneinfo import ZoneInfo from django.apps import apps -from django.conf import settings from django.core.cache import cache -from django.test import TestCase, override_settings from django.urls import reverse -from django.utils import timezone -from django.utils.safestring import SafeString - -from PIL import Image import report.models as report_models from build.models import Build -from common.models import Attachment, InvenTreeSetting +from common.models import Attachment from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase from order.models import ReturnOrder, SalesOrder from part.models import Part from plugin.registry import registry from report.models import LabelTemplate, ReportTemplate -from report.templatetags import barcode as barcode_tags -from report.templatetags import report as report_tags from stock.models import StockItem -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_getindex(self): - """Tests for the 'getindex' template tag.""" - fn = report_tags.getindex - data = [1, 2, 3, 4, 5, 6] - - # Out of bounds or invalid - self.assertEqual(fn(data, -1), None) - self.assertEqual(fn(data, 99), None) - self.assertEqual(fn(data, 'xx'), None) - - for idx in range(len(data)): - self.assertEqual(fn(data, idx), data[idx]) - - def test_getkey(self): - """Tests for the 'getkey' template tag.""" - data = {'hello': 'world', 'foo': 'bar', 'with spaces': 'withoutspaces', 1: 2} - - for k, v in data.items(): - self.assertEqual(report_tags.getkey(data, k), v) - - 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 = settings.MEDIA_ROOT.joinpath('report', 'assets') - asset_dir.mkdir(parents=True, exist_ok=True) - asset_path = asset_dir.joinpath('test.txt') - - asset_path.write_text('dummy data') - - self.debug_mode(True) - asset = report_tags.asset('test.txt') - self.assertEqual(asset, '/media/report/assets/test.txt') - - # Ensure that a 'safe string' also works - asset = report_tags.asset(SafeString('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 = str(report_tags.uploaded_image('/part/something/other.png')) - - if b: - self.assertIn('blank_image.png', img) - else: - self.assertIn('data:image/png;charset=utf-8;base64,', img) - - # Create a dummy image - img_path = 'part/images/' - img_path = settings.MEDIA_ROOT.joinpath(img_path) - img_file = img_path.joinpath('test.jpg') - - img_path.mkdir(parents=True, exist_ok=True) - img_file.write_text('dummy data') - - # Test in debug mode. Returns blank image as dummy file is not a valid image - self.debug_mode(True) - img = report_tags.uploaded_image('part/images/test.jpg') - self.assertEqual(img, '/static/img/blank_image.png') - - # Now, let's create a proper image - img = Image.new('RGB', (128, 128), color='RED') - img.save(img_file) - - # Try again - img = report_tags.uploaded_image('part/images/test.jpg') - self.assertEqual(img, '/media/part/images/test.jpg') - - # Ensure that a 'safe string' also works - img = report_tags.uploaded_image(SafeString('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.assertTrue(img.startswith('data:image/png;charset=utf-8;base64,')) - - img = report_tags.uploaded_image(SafeString('part/images/test.jpg')) - self.assertTrue(img.startswith('data:image/png;charset=utf-8;base64,')) - - 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) - - def test_maths_tags(self): - """Simple tests for mathematical operator tags.""" - self.assertEqual(report_tags.add(1, 2), 3) - self.assertEqual(report_tags.subtract(10, 4.2), 5.8) - self.assertEqual(report_tags.multiply(2.3, 4), 9.2) - self.assertEqual(report_tags.divide(100, 5), 20) - - def test_number_tags(self): - """Simple tests for number formatting tags.""" - fn = report_tags.format_number - - self.assertEqual(fn(1234), '1234') - self.assertEqual(fn(1234.5678, decimal_places=2), '1234.57') - self.assertEqual(fn(1234.5678, decimal_places=3), '1234.568') - self.assertEqual(fn(-9999.5678, decimal_places=2, separator=','), '-9,999.57') - self.assertEqual( - fn(9988776655.4321, integer=True, separator=' '), '9 988 776 655' - ) - - @override_settings(TIME_ZONE='America/New_York') - def test_date_tags(self): - """Test for date formatting tags. - - - Source timezone is Australia/Sydney - - Server timezone is America/New York - """ - time = timezone.datetime( - year=2024, - month=3, - day=13, - hour=12, - minute=30, - second=0, - tzinfo=ZoneInfo('Australia/Sydney'), - ) - - # Format a set of tests: timezone, format, expected - tests = [ - (None, None, '2024-03-12T21:30:00-04:00'), - (None, '%d-%m-%y', '12-03-24'), - ('UTC', None, '2024-03-13T01:30:00+00:00'), - ('UTC', '%d-%B-%Y', '13-March-2024'), - ('Europe/Amsterdam', None, '2024-03-13T02:30:00+01:00'), - ('Europe/Amsterdam', '%y-%m-%d %H:%M', '24-03-13 02:30'), - ] - - for tz, fmt, expected in tests: - result = report_tags.format_datetime(time, tz, fmt) - self.assertEqual(result, expected) - - def test_icon(self): - """Test the icon template tag.""" - for icon in [None, '', 'not:the-correct-format', 'any-non-existent-icon']: - self.assertEqual(report_tags.icon(icon), '') - - self.assertEqual( - report_tags.icon('ti:package:outline'), - f'{chr(int("eaff", 16))}', - ) - self.assertEqual( - report_tags.icon( - 'ti:package:outline', **{'class': 'my-custom-class my-seconds-class'} - ), - f'{chr(int("eaff", 16))}', - ) - - def test_include_icon_fonts(self): - """Test the include_icon_fonts template tag.""" - style = report_tags.include_icon_fonts() - - self.assertIn('@font-face {', style) - self.assertIn("font-family: 'inventree-icon-font-ti';", style) - self.assertIn('tabler-icons/tabler-icons.ttf', style) - self.assertIn('.icon {', style) - - -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.assertIsInstance(barcode, str) - self.assertTrue(barcode.startswith('data:image/png;')) - - # Try with a different format - barcode = barcode_tags.barcode('99999', format='BMP') - self.assertIsInstance(barcode, str) - self.assertTrue(barcode.startswith('data:image/bmp;')) - - # Test empty tag - with self.assertRaises(ValueError): - barcode_tags.barcode('') - - def test_qrcode(self): - """Test the qrcode generation tag.""" - # Test with default settings - qrcode = barcode_tags.qrcode('hello world') - self.assertIsInstance(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.assertIsInstance(qrcode, str) - self.assertTrue(qrcode.startswith('data:image/bmp;')) - self.assertEqual(len(qrcode), 309720) - - # Test empty tag - with self.assertRaises(ValueError): - barcode_tags.qrcode('') - - def test_datamatrix(self): - """Test the datamatrix generation tag.""" - # Test with default settings - datamatrix = barcode_tags.datamatrix('hello world') - self.assertEqual( - datamatrix, - 'data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAAlElEQVR4nJ1TQQ7AIAgri///cncw6wroEseBgEFbCgZJnNsFICKOPAAIjeSM5T11IznK5f5WRMgnkhP9JfCcTC/MxFZ5hxLOgqrn3o/z/OqtsNpdSL31Iu9W4Dq8Sulu+q5Nuqa3XYOdnuidlICPpXhZVBruyzAKSZehT+yNlzvZQcq6JiW7Ni592swf/43kdlDfdgMk1eOtR7kWpAAAAABJRU5ErkJggg==', - ) - - datamatrix = barcode_tags.datamatrix( - 'hello world', border=3, fill_color='red', back_color='blue' - ) - self.assertEqual( - datamatrix, - 'data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAIAAABL1vtsAAAAqElEQVR4nN1UQQ6AMAgrxv9/GQ9mpJYSY/QkBxM3KLUUA0i8i+1l/dcQiXj09CwSEU2aQJ7nE8ou2faVUXoPZSEkq+dZKVxWg4UqxUHnVdkp6IdwMXMulGvzNBDMk4WwPSrUF3LNnQNZBJmOsZaVXa44QSEKnvWb5mIgKon1E1H6aPyOcIa15uhONP9aR4hSCiGmYAoYpj4uO+vK4+ybMhr8Nkjmn/z4Dvoldi8uJu4iAAAAAElFTkSuQmCC', - ) - - # Test empty tag - with self.assertRaises(ValueError): - barcode_tags.datamatrix('') - - class ReportTest(InvenTreeAPITestCase): """Base class for unit testing reporting models.""" @@ -350,9 +81,20 @@ class ReportTest(InvenTreeAPITestCase): # Filter by items part_pk = Part.objects.first().pk report = ReportTemplate.objects.filter(model_type='part').first() - return - # TODO @matmair re-enable this (in GitHub Actions) flaky test - response = self.get(url, {'model_type': 'part', 'items': part_pk}) + + try: + response = self.get( + url, {'model_type': 'part', 'items': part_pk}, expected_code=400 + ) + self.assertIn('model_type', response.data) + self.assertIn( + 'Select a valid choice. part is not one of the available choices.', + str(response.data), + ) + return # pragma: no cover + except AssertionError: + response = self.get(url, {'model_type': 'part', 'items': part_pk}) + self.assertEqual(len(response.data), 1) self.assertEqual(response.data[0]['pk'], report.pk) self.assertEqual(response.data[0]['name'], report.name)