mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 21:15:41 +00:00
Docker improvements (#3042)
* Simplified dockerfile - Changed from alpine to python:slim - Removed some database libs (because we *connect* to a db, not host it) * - Add gettext as required package - Only create inventree user as part of production build (leave admin access for dev build) * Tweaks for tasks.py * Fix user permissions (drop to inventree user) * Drop to the 'inventree' user level as part of init.sh - As we have mounted volumes at 'run time' we need to ensure that the inventree user has correct permissions! - Ref: https://stackoverflow.com/questions/39397548/how-to-give-non-root-user-in-docker-container-access-to-a-volume-mounted-on-the * Adjust user setup - Only drop to non-root user as part of "production" build - Mounted external volumes make it tricky when in the dev build - Might want to revisit this later on * More dockerfile changes - reduce required system packages - * Add new docker github workflow * Print some more debug * GITHUB_BASE_REF * Add gnupg to base requirements * Improve debug output during testing * Refactoring updates for label printing API - Update weasyprint version to 55.0 - Generate labels as pdf files - Provide filename to label printing plugin - Additional unit testing - Improve extraction of some hidden debug data during TESTING - Fix a spelling mistake (notifaction -> notification) * Working on github action * More testing * Add requirement for pdf2image * Fix label printing plugin and update unit testing * Add required packages for CI * Move docker files to the top level directory - This allows us to build the production image directly from soure - Don't need to re-download the source code from github - Note: The docker install guide will need to be updated! * Fix for docker ci file * Print GIT SHA * Bake git information into the production image * Add some exta docstrings to dockerfile * Simplify version check script * Extract git commit info * Extract docker tag from check_version.py * Newline * More work on the docker workflow * Dockerfile fixes - Directory / path issues * Dockerfile fixes - Directory / path issues * Ignore certain steps on a pull request * Add poppler-utils to CI * Consolidate version check into existing CI file * Don't run docker workflow on pull request * Pass docker image tag through to the build Also check .j2k files * Add supervisord.conf example file back in * Remove --no-cache-dir option from pip install
This commit is contained in:
@ -117,6 +117,11 @@ class InvenTreeAPITestCase(UserMixin, APITestCase):
|
||||
response = self.client.get(url, data, format='json')
|
||||
|
||||
if expected_code is not None:
|
||||
|
||||
if response.status_code != expected_code:
|
||||
print(f"Unexpected response at '{url}':")
|
||||
print(response.data)
|
||||
|
||||
self.assertEqual(response.status_code, expected_code)
|
||||
|
||||
return response
|
||||
|
@ -40,7 +40,11 @@ def exception_handler(exc, context):
|
||||
if response is None:
|
||||
# DRF handler did not provide a default response for this exception
|
||||
|
||||
if settings.DEBUG:
|
||||
if settings.TESTING:
|
||||
# If in TESTING mode, re-throw the exception for traceback
|
||||
raise exc
|
||||
elif settings.DEBUG:
|
||||
# If in DEBUG mode, provide error information in the response
|
||||
error_detail = str(exc)
|
||||
else:
|
||||
error_detail = _("Error details can be found in the admin panel")
|
||||
|
@ -129,7 +129,7 @@ def TestIfImageURL(url):
|
||||
Simply tests the extension against a set of allowed values
|
||||
"""
|
||||
return os.path.splitext(os.path.basename(url))[-1].lower() in [
|
||||
'.jpg', '.jpeg',
|
||||
'.jpg', '.jpeg', '.j2k',
|
||||
'.png', '.bmp',
|
||||
'.tif', '.tiff',
|
||||
'.webp', '.gif',
|
||||
|
@ -380,6 +380,30 @@ class TestVersionNumber(TestCase):
|
||||
self.assertTrue(v_d > v_c)
|
||||
self.assertTrue(v_d > v_a)
|
||||
|
||||
def test_commit_info(self):
|
||||
"""Test that the git commit information is extracted successfully"""
|
||||
|
||||
envs = {
|
||||
'INVENTREE_COMMIT_HASH': 'abcdef',
|
||||
'INVENTREE_COMMIT_DATE': '2022-12-31'
|
||||
}
|
||||
|
||||
# Check that the environment variables take priority
|
||||
|
||||
with mock.patch.dict(os.environ, envs):
|
||||
self.assertEqual(version.inventreeCommitHash(), 'abcdef')
|
||||
self.assertEqual(version.inventreeCommitDate(), '2022-12-31')
|
||||
|
||||
import subprocess
|
||||
|
||||
# Check that the current .git values work too
|
||||
|
||||
hash = str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
|
||||
self.assertEqual(hash, version.inventreeCommitHash())
|
||||
|
||||
d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip().split(' ')[0]
|
||||
self.assertEqual(d, version.inventreeCommitDate())
|
||||
|
||||
|
||||
class CurrencyTests(TestCase):
|
||||
"""
|
||||
|
@ -3,6 +3,7 @@ Version information for InvenTree.
|
||||
Provides information on the current InvenTree version
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
@ -99,6 +100,12 @@ def inventreeDjangoVersion():
|
||||
def inventreeCommitHash():
|
||||
""" Returns the git commit hash for the running codebase """
|
||||
|
||||
# First look in the environment variables, i.e. if running in docker
|
||||
commit_hash = os.environ.get('INVENTREE_COMMIT_HASH', '')
|
||||
|
||||
if commit_hash:
|
||||
return commit_hash
|
||||
|
||||
try:
|
||||
return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
|
||||
except: # pragma: no cover
|
||||
@ -108,6 +115,12 @@ def inventreeCommitHash():
|
||||
def inventreeCommitDate():
|
||||
""" Returns the git commit date for the running codebase """
|
||||
|
||||
# First look in the environment variables, e.g. if running in docker
|
||||
commit_date = os.environ.get('INVENTREE_COMMIT_DATE', '')
|
||||
|
||||
if commit_date:
|
||||
return commit_date.split(' ')[0]
|
||||
|
||||
try:
|
||||
d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip()
|
||||
return d.split(' ')[0]
|
||||
|
@ -203,7 +203,7 @@ class UIMessageNotification(SingleNotificationMethod):
|
||||
return True
|
||||
|
||||
|
||||
def trigger_notifaction(obj, category=None, obj_ref='pk', **kwargs):
|
||||
def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
||||
"""
|
||||
Send out a notification
|
||||
"""
|
||||
|
@ -1,12 +1,9 @@
|
||||
from io import BytesIO
|
||||
|
||||
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_filters.rest_framework import DjangoFilterBackend
|
||||
from PIL import Image
|
||||
from rest_framework import filters, generics
|
||||
from rest_framework.exceptions import NotFound
|
||||
|
||||
@ -137,25 +134,21 @@ class LabelPrintMixin:
|
||||
# Label instance
|
||||
label_instance = self.get_object()
|
||||
|
||||
for output in outputs:
|
||||
for idx, output in enumerate(outputs):
|
||||
"""
|
||||
For each output, we generate a temporary image file,
|
||||
which will then get sent to the printer
|
||||
"""
|
||||
|
||||
# Generate a png image at 300dpi
|
||||
(img_data, w, h) = output.get_document().write_png(resolution=300)
|
||||
|
||||
# Construct a BytesIO object, which can be read by pillow
|
||||
img_bytes = BytesIO(img_data)
|
||||
|
||||
image = Image.open(img_bytes)
|
||||
# Generate PDF data for the label
|
||||
pdf = output.get_document().write_pdf()
|
||||
|
||||
# Offload a background task to print the provided label
|
||||
offload_task(
|
||||
plugin_label.print_label,
|
||||
plugin.plugin_slug(),
|
||||
image,
|
||||
pdf,
|
||||
filename=label_names[idx],
|
||||
label_instance=label_instance,
|
||||
user=request.user,
|
||||
)
|
||||
|
@ -24,7 +24,7 @@ def notify_low_stock(part: part.models.Part):
|
||||
},
|
||||
}
|
||||
|
||||
common.notifications.trigger_notifaction(
|
||||
common.notifications.trigger_notification(
|
||||
part,
|
||||
'part.notify_low_stock',
|
||||
target_fnc=part.get_subscribers,
|
||||
|
@ -1098,7 +1098,7 @@ class PartDetailTests(InvenTreeAPITestCase):
|
||||
self.assertIn('Upload a valid image', str(response.data))
|
||||
|
||||
# Now try to upload a valid image file, in multiple formats
|
||||
for fmt in ['jpg', 'png', 'bmp', 'webp']:
|
||||
for fmt in ['jpg', 'j2k', 'png', 'bmp', 'webp']:
|
||||
fn = f'dummy_image.{fmt}'
|
||||
|
||||
img = PIL.Image.new('RGB', (128, 128), color='red')
|
||||
|
@ -1,7 +1,14 @@
|
||||
"""Functions to print a label to a mixin printer"""
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.debug import ExceptionReporter
|
||||
|
||||
import pdf2image
|
||||
from error_report.models import Error
|
||||
|
||||
import common.notifications
|
||||
from plugin.registry import registry
|
||||
@ -9,7 +16,7 @@ from plugin.registry import registry
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def print_label(plugin_slug, label_image, label_instance=None, user=None):
|
||||
def print_label(plugin_slug, pdf_data, filename=None, label_instance=None, user=None):
|
||||
"""
|
||||
Print label with the provided plugin.
|
||||
|
||||
@ -19,10 +26,11 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None):
|
||||
|
||||
Arguments:
|
||||
plugin_slug: The unique slug (key) of the plugin
|
||||
label_image: A PIL.Image image object to be printed
|
||||
pdf_data: Binary PDF data
|
||||
filename: The intended name of the printed label
|
||||
"""
|
||||
|
||||
logger.info(f"Plugin '{plugin_slug}' is printing a label")
|
||||
logger.info(f"Plugin '{plugin_slug}' is printing a label '{filename}'")
|
||||
|
||||
plugin = registry.plugins.get(plugin_slug, None)
|
||||
|
||||
@ -30,8 +38,22 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None):
|
||||
logger.error(f"Could not find matching plugin for '{plugin_slug}'")
|
||||
return
|
||||
|
||||
# In addition to providing a .pdf image, we'll also provide a .png file
|
||||
png_file = pdf2image.convert_from_bytes(
|
||||
pdf_data,
|
||||
dpi=300,
|
||||
)[0]
|
||||
|
||||
try:
|
||||
plugin.print_label(label_image, width=label_instance.width, height=label_instance.height)
|
||||
plugin.print_label(
|
||||
pdf_data=pdf_data,
|
||||
png_file=png_file,
|
||||
filename=filename,
|
||||
label_instance=label_instance,
|
||||
width=label_instance.width,
|
||||
height=label_instance.height,
|
||||
user=user
|
||||
)
|
||||
except Exception as e: # pragma: no cover
|
||||
# Plugin threw an error - notify the user who attempted to print
|
||||
|
||||
@ -40,13 +62,28 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None):
|
||||
'message': str(e),
|
||||
}
|
||||
|
||||
logger.error(f"Label printing failed: Sending notification to user '{user}'")
|
||||
# Log an error message to the database
|
||||
kind, info, data = sys.exc_info()
|
||||
|
||||
Error.objects.create(
|
||||
kind=kind.__name__,
|
||||
info=info,
|
||||
data='\n'.join(traceback.format_exception(kind, info, data)),
|
||||
path='print_label',
|
||||
html=ExceptionReporter(None, kind, info, data).get_traceback_html(),
|
||||
)
|
||||
|
||||
logger.error(f"Label printing failed: Sending notification to user '{user}'") # pragma: no cover
|
||||
|
||||
# Throw an error against the plugin instance
|
||||
common.notifications.trigger_notifaction(
|
||||
common.notifications.trigger_notification(
|
||||
plugin.plugin_config(),
|
||||
'label.printing_failed',
|
||||
targets=[user],
|
||||
context=ctx,
|
||||
delivery_methods=[common.notifications.UIMessageNotification]
|
||||
delivery_methods=set([common.notifications.UIMessageNotification])
|
||||
)
|
||||
|
||||
if settings.TESTING:
|
||||
# If we are in testing mode, we want to know about this exception
|
||||
raise e
|
||||
|
@ -22,17 +22,18 @@ class LabelPrintingMixin:
|
||||
super().__init__()
|
||||
self.add_mixin('labels', True, __class__)
|
||||
|
||||
def print_label(self, label, **kwargs):
|
||||
def print_label(self, **kwargs):
|
||||
"""
|
||||
Callback to print a single label
|
||||
|
||||
Arguments:
|
||||
label: A black-and-white pillow Image object
|
||||
|
||||
kwargs:
|
||||
length: The length of the label (in mm)
|
||||
width: The width of the label (in mm)
|
||||
|
||||
pdf_data: Raw PDF data of the rendered label
|
||||
png_file: An in-memory PIL image file, rendered at 300dpi
|
||||
label_instance: The instance of the label model which triggered the print_label() method
|
||||
width: The expected width of the label (in mm)
|
||||
height: The expected height of the label (in mm)
|
||||
filename: The filename of this PDF label
|
||||
user: The user who printed this label
|
||||
"""
|
||||
|
||||
# Unimplemented (to be implemented by the particular plugin class)
|
||||
|
@ -1,8 +1,11 @@
|
||||
"""Unit tests for the label printing mixin"""
|
||||
import os
|
||||
|
||||
from django.apps import apps
|
||||
from django.urls import reverse
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
from label.models import PartLabel, StockItemLabel, StockLocationLabel
|
||||
@ -68,7 +71,7 @@ class LabelMixinTests(InvenTreeAPITestCase):
|
||||
|
||||
with self.assertRaises(MixinNotImplementedError):
|
||||
plugin = WrongPlugin()
|
||||
plugin.print_label('test')
|
||||
plugin.print_label(filename='test')
|
||||
|
||||
def test_installed(self):
|
||||
"""Test that the sample printing plugin is installed"""
|
||||
@ -167,6 +170,21 @@ class LabelMixinTests(InvenTreeAPITestCase):
|
||||
# Print no part
|
||||
self.get(self.do_url(None, plugin_ref, label), expected_code=400)
|
||||
|
||||
# Test that the labels have been printed
|
||||
# The sample labelling plugin simply prints to file
|
||||
self.assertTrue(os.path.exists('label.pdf'))
|
||||
|
||||
# Read the raw .pdf data - ensure it contains some sensible information
|
||||
with open('label.pdf', 'rb') as f:
|
||||
pdf_data = str(f.read())
|
||||
self.assertIn('WeasyPrint', pdf_data)
|
||||
|
||||
# Check that the .png file has already been created
|
||||
self.assertTrue(os.path.exists('label.png'))
|
||||
|
||||
# And that it is a valid image file
|
||||
Image.open('label.png')
|
||||
|
||||
def test_printing_endpoints(self):
|
||||
"""Cover the endpoints not covered by `test_printing_process`"""
|
||||
plugin_ref = 'samplelabel'
|
||||
|
@ -12,7 +12,22 @@ class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
|
||||
SLUG = "samplelabel"
|
||||
TITLE = "Sample Label Printer"
|
||||
DESCRIPTION = "A sample plugin which provides a (fake) label printer interface"
|
||||
VERSION = "0.1"
|
||||
VERSION = "0.2"
|
||||
|
||||
def print_label(self, label, **kwargs):
|
||||
print("OK PRINTING")
|
||||
def print_label(self, **kwargs):
|
||||
|
||||
# Test that the expected kwargs are present
|
||||
print(f"Printing Label: {kwargs['filename']} (User: {kwargs['user']})")
|
||||
print(f"Width: {kwargs['width']} x Height: {kwargs['height']}")
|
||||
|
||||
pdf_data = kwargs['pdf_data']
|
||||
png_file = kwargs['png_file']
|
||||
|
||||
filename = kwargs['filename']
|
||||
|
||||
# Dump the PDF to a local file
|
||||
with open(filename, 'wb') as pdf_out:
|
||||
pdf_out.write(pdf_data)
|
||||
|
||||
# Save the PNG to disk
|
||||
png_file.save(filename.replace('.pdf', '.png'))
|
||||
|
Reference in New Issue
Block a user