mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Label printing unit test (#3047)
* Adds a very simple sample plugin for label printing * Test mixin install status and API query * Better error reporting for label printing API * pep fixes * fix assertation * remove broken assertation * igonre for coverage * test the base process of printing * refactor tests * clean up basic test * refactor url * fix url creation * test printing multiples * test all printing endpoints * test all list options - move api tests * test for invalid filters * refactor * test with no part * these should not happen checks are in place upstream * fix assertation * do not cover continue parts * test for wrong implementation * ignore DB not ready * remove covage from default parts * fix url generation * test debug mode * fix url assertation * check that nothing was rendered Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
		| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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' | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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 = { | ||||
|   | ||||
							
								
								
									
										209
									
								
								InvenTree/plugin/base/label/test_label_mixin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								InvenTree/plugin/base/label/test_label_mixin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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') | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										18
									
								
								InvenTree/plugin/samples/integration/label_sample.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								InvenTree/plugin/samples/integration/label_sample.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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") | ||||
		Reference in New Issue
	
	Block a user