diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index 5efbff518a..cb7ea12598 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -4,12 +4,11 @@ from django.conf import settings from django.core.exceptions import FieldError, ValidationError from django.http import HttpResponse, JsonResponse from django.urls import include, re_path -from django.utils.translation import gettext_lazy as _ from django_filters.rest_framework import DjangoFilterBackend from PIL import Image from rest_framework import filters, generics -from rest_framework.response import Response +from rest_framework.exceptions import NotFound import common.models import InvenTree.helpers @@ -62,10 +61,14 @@ class LabelPrintMixin: """ if not settings.PLUGINS_ENABLED: - return None + return None # pragma: no cover plugin_key = request.query_params.get('plugin', None) + # No plugin provided, and that's OK + if plugin_key is None: + return None + plugin = registry.get_plugin(plugin_key) if plugin: @@ -74,9 +77,10 @@ class LabelPrintMixin: if config and config.active: # Only return the plugin if it is enabled! return plugin - - # No matches found - return None + else: + raise ValidationError(f"Plugin '{plugin_key}' is not enabled") + else: + raise NotFound(f"Plugin '{plugin_key}' not found") def print(self, request, items_to_print): """ @@ -85,13 +89,11 @@ class LabelPrintMixin: # Check the request to determine if the user has selected a label printing plugin plugin = self.get_plugin(request) + if len(items_to_print) == 0: # No valid items provided, return an error message - data = { - 'error': _('No valid objects provided to template'), - } - return Response(data, status=400) + raise ValidationError('No valid objects provided to label template') outputs = [] @@ -281,7 +283,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin): # Filter string defined for the StockItemLabel object try: filters = InvenTree.helpers.validateFilterString(label.filters) - except ValidationError: + except ValidationError: # pragma: no cover continue for item in items: @@ -300,7 +302,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin): if matches: valid_label_ids.add(label.pk) else: - continue + continue # pragma: no cover # Reduce queryset to only valid matches queryset = queryset.filter(pk__in=[pk for pk in valid_label_ids]) @@ -412,7 +414,7 @@ class StockLocationLabelList(LabelListView, StockLocationLabelMixin): # Filter string defined for the StockLocationLabel object try: filters = InvenTree.helpers.validateFilterString(label.filters) - except: + except: # pragma: no cover # Skip if there was an error validating the filters... continue @@ -432,7 +434,7 @@ class StockLocationLabelList(LabelListView, StockLocationLabelMixin): if matches: valid_label_ids.add(label.pk) else: - continue + continue # pragma: no cover # Reduce queryset to only valid matches queryset = queryset.filter(pk__in=[pk for pk in valid_label_ids]) @@ -519,7 +521,7 @@ class PartLabelList(LabelListView, PartLabelMixin): try: filters = InvenTree.helpers.validateFilterString(label.filters) - except ValidationError: + except ValidationError: # pragma: no cover continue for part in parts: diff --git a/InvenTree/label/apps.py b/InvenTree/label/apps.py index 01c9fe1fdf..b26b7fb692 100644 --- a/InvenTree/label/apps.py +++ b/InvenTree/label/apps.py @@ -46,7 +46,7 @@ class LabelConfig(AppConfig): try: from .models import StockLocationLabel assert bool(StockLocationLabel is not None) - except AppRegistryNotReady: + except AppRegistryNotReady: # pragma: no cover # Database might not yet be ready warnings.warn('Database was not ready for creating labels') return diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index c7a38f09eb..07819806bf 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -171,7 +171,7 @@ class LabelTemplate(models.Model): Note: Override this in any subclass """ - return {} + return {} # pragma: no cover def generate_filename(self, request, **kwargs): """ @@ -242,7 +242,7 @@ class StockItemLabel(LabelTemplate): @staticmethod def get_api_url(): - return reverse('api-stockitem-label-list') + return reverse('api-stockitem-label-list') # pragma: no cover SUBDIR = "stockitem" @@ -302,7 +302,7 @@ class StockLocationLabel(LabelTemplate): @staticmethod def get_api_url(): - return reverse('api-stocklocation-label-list') + return reverse('api-stocklocation-label-list') # pragma: no cover SUBDIR = "stocklocation" @@ -349,7 +349,7 @@ class PartLabel(LabelTemplate): @staticmethod def get_api_url(): - return reverse('api-part-label-list') + return reverse('api-part-label-list') # pragma: no cover SUBDIR = 'part' diff --git a/InvenTree/label/test_api.py b/InvenTree/label/test_api.py index 807de5b63e..d9b0bafc9a 100644 --- a/InvenTree/label/test_api.py +++ b/InvenTree/label/test_api.py @@ -63,39 +63,3 @@ class TestReportTests(InvenTreeAPITestCase): 'items': [10, 11, 12], } ) - - -class TestLabels(InvenTreeAPITestCase): - """ - Tests for the label APIs - """ - - fixtures = [ - 'category', - 'part', - 'location', - 'stock', - ] - - roles = [ - 'stock.view', - 'stock_location.view', - ] - - def do_list(self, filters={}): - - response = self.client.get(self.list_url, filters, format='json') - - self.assertEqual(response.status_code, 200) - - return response.data - - def test_lists(self): - self.list_url = reverse('api-stockitem-label-list') - self.do_list() - - self.list_url = reverse('api-stocklocation-label-list') - self.do_list() - - self.list_url = reverse('api-part-label-list') - self.do_list() diff --git a/InvenTree/plugin/base/label/label.py b/InvenTree/plugin/base/label/label.py index daaaaedbe2..d85251dd81 100644 --- a/InvenTree/plugin/base/label/label.py +++ b/InvenTree/plugin/base/label/label.py @@ -26,13 +26,13 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None): plugin = registry.plugins.get(plugin_slug, None) - if plugin is None: + if plugin is None: # pragma: no cover logger.error(f"Could not find matching plugin for '{plugin_slug}'") return try: plugin.print_label(label_image, width=label_instance.width, height=label_instance.height) - except Exception as e: + except Exception as e: # pragma: no cover # Plugin threw an error - notify the user who attempted to print ctx = { diff --git a/InvenTree/plugin/base/label/test_label_mixin.py b/InvenTree/plugin/base/label/test_label_mixin.py new file mode 100644 index 0000000000..29250f76a5 --- /dev/null +++ b/InvenTree/plugin/base/label/test_label_mixin.py @@ -0,0 +1,209 @@ +"""Unit tests for the label printing mixin""" + +from django.apps import apps +from django.urls import reverse + +from common.models import InvenTreeSetting +from InvenTree.api_tester import InvenTreeAPITestCase +from label.models import PartLabel, StockItemLabel, StockLocationLabel +from part.models import Part +from plugin.base.label.mixins import LabelPrintingMixin +from plugin.helpers import MixinNotImplementedError +from plugin.plugin import InvenTreePlugin +from plugin.registry import registry +from stock.models import StockItem, StockLocation + + +class LabelMixinTests(InvenTreeAPITestCase): + """Test that the Label mixin operates correctly""" + + fixtures = [ + 'category', + 'part', + 'location', + 'stock', + ] + + roles = 'all' + + def do_activate_plugin(self): + """Activate the 'samplelabel' plugin""" + + config = registry.get_plugin('samplelabel').plugin_config() + config.active = True + config.save() + + def do_url(self, parts, plugin_ref, label, url_name: str = 'api-part-label-print', url_single: str = 'part', invalid: bool = False): + """Generate an URL to print a label""" + # Construct URL + kwargs = {} + if label: + kwargs["pk"] = label.pk + + url = reverse(url_name, kwargs=kwargs) + + # Append part filters + if not parts: + pass + elif len(parts) == 1: + url += f'?{url_single}={parts[0].pk}' + elif len(parts) > 1: + url += '?' + '&'.join([f'{url_single}s={item.pk}' for item in parts]) + + # Append an invalid item + if invalid: + url += f'&{url_single}{"s" if len(parts) > 1 else ""}=abc' + + # Append plugin reference + if plugin_ref: + url += f'&plugin={plugin_ref}' + + return url + + def test_wrong_implementation(self): + """Test that a wrong implementation raises an error""" + + class WrongPlugin(LabelPrintingMixin, InvenTreePlugin): + pass + + with self.assertRaises(MixinNotImplementedError): + plugin = WrongPlugin() + plugin.print_label('test') + + def test_installed(self): + """Test that the sample printing plugin is installed""" + + # Get all label plugins + plugins = registry.with_mixin('labels') + self.assertEqual(len(plugins), 1) + + # But, it is not 'active' + plugins = registry.with_mixin('labels', active=True) + self.assertEqual(len(plugins), 0) + + def test_api(self): + """Test that we can filter the API endpoint by mixin""" + + url = reverse('api-plugin-list') + + # Try POST (disallowed) + response = self.client.post(url, {}) + self.assertEqual(response.status_code, 405) + + response = self.client.get( + url, + { + 'mixin': 'labels', + 'active': True, + } + ) + + # No results matching this query! + self.assertEqual(len(response.data), 0) + + # What about inactive? + response = self.client.get( + url, + { + 'mixin': 'labels', + 'active': False, + } + ) + + self.assertEqual(len(response.data), 0) + + self.do_activate_plugin() + # Should be available via the API now + response = self.client.get( + url, + { + 'mixin': 'labels', + 'active': True, + } + ) + + self.assertEqual(len(response.data), 1) + data = response.data[0] + self.assertEqual(data['key'], 'samplelabel') + + def test_printing_process(self): + """Test that a label can be printed""" + + # Ensure the labels were created + apps.get_app_config('label').create_labels() + + # Lookup references + part = Part.objects.first() + plugin_ref = 'samplelabel' + label = PartLabel.objects.first() + + url = self.do_url([part], plugin_ref, label) + + # Non-exsisting plugin + response = self.get(f'{url}123', expected_code=404) + self.assertIn(f'Plugin \'{plugin_ref}123\' not found', str(response.content, 'utf8')) + + # Inactive plugin + response = self.get(url, expected_code=400) + self.assertIn(f'Plugin \'{plugin_ref}\' is not enabled', str(response.content, 'utf8')) + + # Active plugin + self.do_activate_plugin() + + # Print one part + self.get(url, expected_code=200) + + # Print multiple parts + self.get(self.do_url(Part.objects.all()[:2], plugin_ref, label), expected_code=200) + + # Print multiple parts without a plugin + self.get(self.do_url(Part.objects.all()[:2], None, label), expected_code=200) + + # Print multiple parts without a plugin in debug mode + InvenTreeSetting.set_setting('REPORT_DEBUG_MODE', True, None) + response = self.get(self.do_url(Part.objects.all()[:2], None, label), expected_code=200) + self.assertIn('@page', str(response.content)) + + # Print no part + self.get(self.do_url(None, plugin_ref, label), expected_code=400) + + def test_printing_endpoints(self): + """Cover the endpoints not covered by `test_printing_process`""" + plugin_ref = 'samplelabel' + + # Activate the label components + apps.get_app_config('label').create_labels() + self.do_activate_plugin() + + def run_print_test(label, qs, url_name, url_single): + """Run tests on single and multiple page printing + + Args: + label (_type_): class of the label + qs (_type_): class of the base queryset + url_name (_type_): url for endpoints + url_single (_type_): item lookup reference + """ + label = label.objects.first() + qs = qs.objects.all() + + # List endpoint + self.get(self.do_url(None, None, None, f'{url_name}-list', url_single), expected_code=200) + + # List endpoint with filter + self.get(self.do_url(qs[:2], None, None, f'{url_name}-list', url_single, invalid=True), expected_code=200) + + # Single page printing + self.get(self.do_url(qs[:1], plugin_ref, label, f'{url_name}-print', url_single), expected_code=200) + + # Multi page printing + self.get(self.do_url(qs[:2], plugin_ref, label, f'{url_name}-print', url_single), expected_code=200) + + # Test StockItemLabels + run_print_test(StockItemLabel, StockItem, 'api-stockitem-label', 'item') + + # Test StockLocationLabels + run_print_test(StockLocationLabel, StockLocation, 'api-stocklocation-label', 'location') + + # Test PartLabels + run_print_test(PartLabel, Part, 'api-part-label', 'part') diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index cb1a938504..3671dc532e 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -120,7 +120,7 @@ class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin): 'icon': 'fa-user', 'content_template': 'panel_demo/childless.html', # Note that the panel content is rendered using a template file! }) - except: + except: # pragma: no cover pass return panels diff --git a/InvenTree/plugin/samples/integration/label_sample.py b/InvenTree/plugin/samples/integration/label_sample.py new file mode 100644 index 0000000000..845e1b7908 --- /dev/null +++ b/InvenTree/plugin/samples/integration/label_sample.py @@ -0,0 +1,18 @@ + +from plugin import InvenTreePlugin +from plugin.mixins import LabelPrintingMixin + + +class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin): + """ + Sample plugin which provides a 'fake' label printer endpoint + """ + + NAME = "Label Printer" + SLUG = "samplelabel" + TITLE = "Sample Label Printer" + DESCRIPTION = "A sample plugin which provides a (fake) label printer interface" + VERSION = "0.1" + + def print_label(self, label, **kwargs): + print("OK PRINTING")