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:
Matthias Mair 2022-05-23 00:54:44 +02:00 committed by GitHub
parent 6247eecf69
commit 840ade25cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 252 additions and 59 deletions

View File

@ -4,12 +4,11 @@ from django.conf import settings
from django.core.exceptions import FieldError, ValidationError from django.core.exceptions import FieldError, ValidationError
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
from django.urls import include, re_path from django.urls import include, re_path
from django.utils.translation import gettext_lazy as _
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from PIL import Image from PIL import Image
from rest_framework import filters, generics from rest_framework import filters, generics
from rest_framework.response import Response from rest_framework.exceptions import NotFound
import common.models import common.models
import InvenTree.helpers import InvenTree.helpers
@ -62,10 +61,14 @@ class LabelPrintMixin:
""" """
if not settings.PLUGINS_ENABLED: if not settings.PLUGINS_ENABLED:
return None return None # pragma: no cover
plugin_key = request.query_params.get('plugin', None) 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) plugin = registry.get_plugin(plugin_key)
if plugin: if plugin:
@ -74,9 +77,10 @@ class LabelPrintMixin:
if config and config.active: if config and config.active:
# Only return the plugin if it is enabled! # Only return the plugin if it is enabled!
return plugin return plugin
else:
# No matches found raise ValidationError(f"Plugin '{plugin_key}' is not enabled")
return None else:
raise NotFound(f"Plugin '{plugin_key}' not found")
def print(self, request, items_to_print): 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 # Check the request to determine if the user has selected a label printing plugin
plugin = self.get_plugin(request) plugin = self.get_plugin(request)
if len(items_to_print) == 0: if len(items_to_print) == 0:
# No valid items provided, return an error message # 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 = [] outputs = []
@ -281,7 +283,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin):
# Filter string defined for the StockItemLabel object # Filter string defined for the StockItemLabel object
try: try:
filters = InvenTree.helpers.validateFilterString(label.filters) filters = InvenTree.helpers.validateFilterString(label.filters)
except ValidationError: except ValidationError: # pragma: no cover
continue continue
for item in items: for item in items:
@ -300,7 +302,7 @@ class StockItemLabelList(LabelListView, StockItemLabelMixin):
if matches: if matches:
valid_label_ids.add(label.pk) valid_label_ids.add(label.pk)
else: else:
continue continue # pragma: no cover
# Reduce queryset to only valid matches # Reduce queryset to only valid matches
queryset = queryset.filter(pk__in=[pk for pk in valid_label_ids]) 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 # Filter string defined for the StockLocationLabel object
try: try:
filters = InvenTree.helpers.validateFilterString(label.filters) filters = InvenTree.helpers.validateFilterString(label.filters)
except: except: # pragma: no cover
# Skip if there was an error validating the filters... # Skip if there was an error validating the filters...
continue continue
@ -432,7 +434,7 @@ class StockLocationLabelList(LabelListView, StockLocationLabelMixin):
if matches: if matches:
valid_label_ids.add(label.pk) valid_label_ids.add(label.pk)
else: else:
continue continue # pragma: no cover
# Reduce queryset to only valid matches # Reduce queryset to only valid matches
queryset = queryset.filter(pk__in=[pk for pk in valid_label_ids]) queryset = queryset.filter(pk__in=[pk for pk in valid_label_ids])
@ -519,7 +521,7 @@ class PartLabelList(LabelListView, PartLabelMixin):
try: try:
filters = InvenTree.helpers.validateFilterString(label.filters) filters = InvenTree.helpers.validateFilterString(label.filters)
except ValidationError: except ValidationError: # pragma: no cover
continue continue
for part in parts: for part in parts:

View File

@ -46,7 +46,7 @@ class LabelConfig(AppConfig):
try: try:
from .models import StockLocationLabel from .models import StockLocationLabel
assert bool(StockLocationLabel is not None) assert bool(StockLocationLabel is not None)
except AppRegistryNotReady: except AppRegistryNotReady: # pragma: no cover
# Database might not yet be ready # Database might not yet be ready
warnings.warn('Database was not ready for creating labels') warnings.warn('Database was not ready for creating labels')
return return

View File

@ -171,7 +171,7 @@ class LabelTemplate(models.Model):
Note: Override this in any subclass Note: Override this in any subclass
""" """
return {} return {} # pragma: no cover
def generate_filename(self, request, **kwargs): def generate_filename(self, request, **kwargs):
""" """
@ -242,7 +242,7 @@ class StockItemLabel(LabelTemplate):
@staticmethod @staticmethod
def get_api_url(): def get_api_url():
return reverse('api-stockitem-label-list') return reverse('api-stockitem-label-list') # pragma: no cover
SUBDIR = "stockitem" SUBDIR = "stockitem"
@ -302,7 +302,7 @@ class StockLocationLabel(LabelTemplate):
@staticmethod @staticmethod
def get_api_url(): def get_api_url():
return reverse('api-stocklocation-label-list') return reverse('api-stocklocation-label-list') # pragma: no cover
SUBDIR = "stocklocation" SUBDIR = "stocklocation"
@ -349,7 +349,7 @@ class PartLabel(LabelTemplate):
@staticmethod @staticmethod
def get_api_url(): def get_api_url():
return reverse('api-part-label-list') return reverse('api-part-label-list') # pragma: no cover
SUBDIR = 'part' SUBDIR = 'part'

View File

@ -63,39 +63,3 @@ class TestReportTests(InvenTreeAPITestCase):
'items': [10, 11, 12], '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()

View File

@ -26,13 +26,13 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None):
plugin = registry.plugins.get(plugin_slug, 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}'") logger.error(f"Could not find matching plugin for '{plugin_slug}'")
return return
try: try:
plugin.print_label(label_image, width=label_instance.width, height=label_instance.height) 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 # Plugin threw an error - notify the user who attempted to print
ctx = { ctx = {

View 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')

View File

@ -120,7 +120,7 @@ class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin):
'icon': 'fa-user', 'icon': 'fa-user',
'content_template': 'panel_demo/childless.html', # Note that the panel content is rendered using a template file! 'content_template': 'panel_demo/childless.html', # Note that the panel content is rendered using a template file!
}) })
except: except: # pragma: no cover
pass pass
return panels return panels

View 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")