2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00
Lukas aa7eaaab3a
Machine integration (#4824)
* Added initial draft for machines

* refactor: isPluginRegistryLoaded check into own ready function

* Added suggestions from codereview

* Refactor: base_drivers -> machine_types

* Use new BaseInvenTreeSetting unique interface

* Fix Django not ready error

* Added get_machines function to driver

- get_machines function on driver
- get_machine function on driver
- initialized attribute on machine

* Added error handeling for driver and machine type

* Extended get_machines functionality

* Export everything from plugin module

* Fix spelling mistakes

* Better states handeling, BaseMachineType is now used instead of Machine Model

* Use uuid as pk

* WIP: machine termination hook

* Remove termination hook as this does not work with gunicorn

* Remove machine from registry after delete

* Added ClassProviderMixin

* Check for slug dupplication

* Added config_type to MachineSettings to define machine/driver settings

* Refactor helper mixins into own file in InvenTree app

* Fixed typing and added required_attributes for BaseDriver

* fix: generic status import

* Added first draft for machine states

* Added convention for status codes

* Added update_machine hook

* Removed unnecessary _key suffix from machine config model

* Initil draft for machine API

* Refactored BaseInvenTreeSetting all_items and allValues method

* Added required to InvenTreeBaseSetting and check_settings method

* check if all required machine settings are defined and refactor: use getattr

* Fix: comment

* Fix initialize error and python 3.9 compability

* Make machine states available through the global states api

* Added basic PUI machine admin implementation that is still in dev

* Added basic machine setting UI to PUI

* Added machine detail view to PUI admin center

* Fix merge issues

* Fix style issues

* Added machine type,machine driver,error stack tables

* Fix style in machine/serializers.py

* Added pui link from machine to machine type/driver drawer

* Removed only partially working django admin in favor of the PUI admin center implementation

* Added required field to settings item

* Added machine restart function

* Added restart requird badge to machine table/drawer

* Added driver init function

* handle error functions for machines and registry

* Added driver errors

* Added machine table to driver drawer

* Added back button to detail drawer component

* Fix auto formatable pre-commit

* fix: style

* Fix deepsource

* Removed slug field from table, added more links between drawers, remove detail drawer blur

* Added initial docs

* Removed description from driver/machine type select and fixed disabled driver select if no machine type is selected

* Added basic label printing implementation

* Remove translated column names because they are now retrieved from the api

* Added printer location setting

* Save last 10 used printer machine per user and sort them in the printing dialog

* Added BasePrintingOptionsSerializer for common options

* Fix not printing_options are not properly casted to its internal value

* Fix type

* Improved machine docs

* Fix docs

* Added UNKNOWN status code to label printer status

* Skip machine loading when running migrations

* Fix testing?

* Fix: tests?

* Fix: tests?

* Disable docs check precommit

* Disable docs check precommit

* First draft for tests

* fix test

* Add type ignore

* Added API tests

* Test ci?

* Add more tests

* Added more tests

* Bump api version

* Changed driver/base driver naming schema

* Added more tests

* Fix tests

* Added setting choice with kwargs and get_machines with initialized=None

* Refetch table after deleting machine

* Fix test

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
2024-02-14 14:13:47 +00:00

505 lines
16 KiB
Python

"""API functionality for the 'label' app."""
from django.core.exceptions import FieldError, ValidationError
from django.http import JsonResponse
from django.urls import include, path, re_path
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import cache_page, never_cache
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import serializers
from rest_framework.exceptions import NotFound
from rest_framework.request import clone_request
import build.models
import common.models
import InvenTree.exceptions
import InvenTree.helpers
import label.models
import label.serializers
from InvenTree.api import MetadataView
from InvenTree.filters import InvenTreeSearchFilter
from InvenTree.mixins import ListCreateAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
from part.models import Part
from plugin.builtin.labels.inventree_label import InvenTreeLabelPlugin
from plugin.registry import registry
from stock.models import StockItem, StockLocation
class LabelFilterMixin:
"""Mixin for filtering a queryset by a list of object ID values.
Each implementing class defines a database model to lookup,
and a "key" (query parameter) for providing a list of ID (PK) values.
This mixin defines a 'get_items' method which provides a generic
implementation to return a list of matching database model instances.
"""
# Database model for instances to actually be "printed" against this label template
ITEM_MODEL = None
# Default key for looking up database model instances
ITEM_KEY = 'item'
def get_items(self):
"""Return a list of database objects from query parameter."""
ids = []
# Construct a list of possible query parameter value options
# e.g. if self.ITEM_KEY = 'part' -> ['part', 'part[]', 'parts', parts[]']
for k in [self.ITEM_KEY + x for x in ['', '[]', 's', 's[]']]:
if ids := self.request.query_params.getlist(k, []):
# Return the first list of matches
break
# Next we must validate each provided object ID
valid_ids = []
for id in ids:
try:
valid_ids.append(int(id))
except ValueError:
pass
# Filter queryset by matching ID values
return self.ITEM_MODEL.objects.filter(pk__in=valid_ids)
class LabelListView(LabelFilterMixin, ListCreateAPI):
"""Generic API class for label templates."""
def filter_queryset(self, queryset):
"""Filter the queryset based on the provided label ID values.
As each 'label' instance may optionally define its own filters,
the resulting queryset is the 'union' of the two.
"""
queryset = super().filter_queryset(queryset)
items = self.get_items()
if len(items) > 0:
"""
At this point, we are basically forced to be inefficient,
as we need to compare the 'filters' string of each label,
and see if it matches against each of the requested items.
TODO: In the future, if this becomes excessively slow, it
will need to be readdressed.
"""
valid_label_ids = set()
for lbl in queryset.all():
matches = True
try:
filters = InvenTree.helpers.validateFilterString(lbl.filters)
except ValidationError:
continue
for item in items:
item_query = self.ITEM_MODEL.objects.filter(pk=item.pk)
try:
if not item_query.filter(**filters).exists():
matches = False
break
except FieldError:
matches = False
break
# Matched all items
if matches:
valid_label_ids.add(lbl.pk)
else:
continue
# Reduce queryset to only valid matches
queryset = queryset.filter(pk__in=list(valid_label_ids))
return queryset
filter_backends = [DjangoFilterBackend, InvenTreeSearchFilter]
filterset_fields = ['enabled']
search_fields = ['name', 'description']
@method_decorator(cache_page(5), name='dispatch')
class LabelPrintMixin(LabelFilterMixin):
"""Mixin for printing labels."""
rolemap = {'GET': 'view', 'POST': 'view'}
def check_permissions(self, request):
"""Override request method to GET so that also non superusers can print using a post request."""
if request.method == 'POST':
request = clone_request(request, 'GET')
return super().check_permissions(request)
@method_decorator(never_cache)
def dispatch(self, *args, **kwargs):
"""Prevent caching when printing report templates."""
return super().dispatch(*args, **kwargs)
def get_serializer(self, *args, **kwargs):
"""Define a get_serializer method to be discoverable by the OPTIONS request."""
# Check the request to determine if the user has selected a label printing plugin
plugin = self.get_plugin(self.request)
kwargs.setdefault('context', self.get_serializer_context())
serializer = plugin.get_printing_options_serializer(
self.request, *args, **kwargs
)
# if no serializer is defined, return an empty serializer
if not serializer:
return serializers.Serializer()
return serializer
def get(self, request, *args, **kwargs):
"""Perform a GET request against this endpoint to print labels."""
common.models.InvenTreeUserSetting.set_setting(
'DEFAULT_' + self.ITEM_KEY.upper() + '_LABEL_TEMPLATE',
self.get_object().pk,
None,
user=request.user,
)
return self.print(request, self.get_items())
def post(self, request, *args, **kwargs):
"""Perform a GET request against this endpoint to print labels."""
return self.get(request, *args, **kwargs)
def get_plugin(self, request):
"""Return the label printing plugin associated with this request.
This is provided in the url, e.g. ?plugin=myprinter
Requires:
- settings.PLUGINS_ENABLED is True
- matching plugin can be found
- matching plugin implements the 'labels' mixin
- matching plugin is enabled
"""
plugin_key = request.query_params.get('plugin', None)
# No plugin provided!
if plugin_key is None:
# Default to the builtin label printing plugin
plugin_key = InvenTreeLabelPlugin.NAME.lower()
plugin = registry.get_plugin(plugin_key)
if not plugin:
raise NotFound(f"Plugin '{plugin_key}' not found")
if not plugin.is_active():
raise ValidationError(f"Plugin '{plugin_key}' is not enabled")
if not plugin.mixin_enabled('labels'):
raise ValidationError(
f"Plugin '{plugin_key}' is not a label printing plugin"
)
# Only return the plugin if it is enabled and has the label printing mixin
return plugin
def print(self, request, items_to_print):
"""Print this label template against a number of pre-validated items."""
# 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
raise ValidationError('No valid objects provided to label template')
# Label template
label = self.get_object()
# Check the label dimensions
if label.width <= 0 or label.height <= 0:
raise ValidationError('Label has invalid dimensions')
# if the plugin returns a serializer, validate the data
if serializer := plugin.get_printing_options_serializer(
request, data=request.data, context=self.get_serializer_context()
):
serializer.is_valid(raise_exception=True)
# At this point, we offload the label(s) to the selected plugin.
# The plugin is responsible for handling the request and returning a response.
try:
result = plugin.print_labels(
label,
items_to_print,
request,
printing_options=(serializer.data if serializer else {}),
)
except ValidationError as e:
raise (e)
except Exception as e:
raise ValidationError([_('Error printing label'), str(e)])
if isinstance(result, JsonResponse):
result['plugin'] = plugin.plugin_slug()
return result
raise ValidationError(
f"Plugin '{plugin.plugin_slug()}' returned invalid response type '{type(result)}'"
)
class StockItemLabelMixin:
"""Mixin for StockItemLabel endpoints."""
queryset = label.models.StockItemLabel.objects.all()
serializer_class = label.serializers.StockItemLabelSerializer
ITEM_MODEL = StockItem
ITEM_KEY = 'item'
class StockItemLabelList(StockItemLabelMixin, LabelListView):
"""API endpoint for viewing list of StockItemLabel objects.
Filterable by:
- enabled: Filter by enabled / disabled status
- item: Filter by single stock item
- items: Filter by list of stock items
"""
pass
class StockItemLabelDetail(StockItemLabelMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for a single StockItemLabel object."""
pass
class StockItemLabelPrint(StockItemLabelMixin, LabelPrintMixin, RetrieveAPI):
"""API endpoint for printing a StockItemLabel object."""
pass
class StockLocationLabelMixin:
"""Mixin for StockLocationLabel endpoints."""
queryset = label.models.StockLocationLabel.objects.all()
serializer_class = label.serializers.StockLocationLabelSerializer
ITEM_MODEL = StockLocation
ITEM_KEY = 'location'
class StockLocationLabelList(StockLocationLabelMixin, LabelListView):
"""API endpoint for viewiing list of StockLocationLabel objects.
Filterable by:
- enabled: Filter by enabled / disabled status
- location: Filter by a single stock location
- locations: Filter by list of stock locations
"""
pass
class StockLocationLabelDetail(StockLocationLabelMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for a single StockLocationLabel object."""
pass
class StockLocationLabelPrint(StockLocationLabelMixin, LabelPrintMixin, RetrieveAPI):
"""API endpoint for printing a StockLocationLabel object."""
pass
class PartLabelMixin:
"""Mixin for PartLabel endpoints."""
queryset = label.models.PartLabel.objects.all()
serializer_class = label.serializers.PartLabelSerializer
ITEM_MODEL = Part
ITEM_KEY = 'part'
class PartLabelList(PartLabelMixin, LabelListView):
"""API endpoint for viewing list of PartLabel objects."""
pass
class PartLabelDetail(PartLabelMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for a single PartLabel object."""
pass
class PartLabelPrint(PartLabelMixin, LabelPrintMixin, RetrieveAPI):
"""API endpoint for printing a PartLabel object."""
pass
class BuildLineLabelMixin:
"""Mixin class for BuildLineLabel endpoints."""
queryset = label.models.BuildLineLabel.objects.all()
serializer_class = label.serializers.BuildLineLabelSerializer
ITEM_MODEL = build.models.BuildLine
ITEM_KEY = 'line'
class BuildLineLabelList(BuildLineLabelMixin, LabelListView):
"""API endpoint for viewing a list of BuildLineLabel objects."""
pass
class BuildLineLabelDetail(BuildLineLabelMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for a single BuildLineLabel object."""
pass
class BuildLineLabelPrint(BuildLineLabelMixin, LabelPrintMixin, RetrieveAPI):
"""API endpoint for printing a BuildLineLabel object."""
pass
label_api_urls = [
# Stock item labels
path(
'stock/',
include([
# Detail views
path(
'<int:pk>/',
include([
re_path(
r'print/?',
StockItemLabelPrint.as_view(),
name='api-stockitem-label-print',
),
path(
'metadata/',
MetadataView.as_view(),
{'model': label.models.StockItemLabel},
name='api-stockitem-label-metadata',
),
path(
'',
StockItemLabelDetail.as_view(),
name='api-stockitem-label-detail',
),
]),
),
# List view
path('', StockItemLabelList.as_view(), name='api-stockitem-label-list'),
]),
),
# Stock location labels
path(
'location/',
include([
# Detail views
path(
'<int:pk>/',
include([
re_path(
r'print/?',
StockLocationLabelPrint.as_view(),
name='api-stocklocation-label-print',
),
path(
'metadata/',
MetadataView.as_view(),
{'model': label.models.StockLocationLabel},
name='api-stocklocation-label-metadata',
),
path(
'',
StockLocationLabelDetail.as_view(),
name='api-stocklocation-label-detail',
),
]),
),
# List view
path(
'',
StockLocationLabelList.as_view(),
name='api-stocklocation-label-list',
),
]),
),
# Part labels
path(
'part/',
include([
# Detail views
path(
'<int:pk>/',
include([
re_path(
r'print/?',
PartLabelPrint.as_view(),
name='api-part-label-print',
),
path(
'metadata/',
MetadataView.as_view(),
{'model': label.models.PartLabel},
name='api-part-label-metadata',
),
path('', PartLabelDetail.as_view(), name='api-part-label-detail'),
]),
),
# List view
path('', PartLabelList.as_view(), name='api-part-label-list'),
]),
),
# BuildLine labels
path(
'buildline/',
include([
# Detail views
path(
'<int:pk>/',
include([
re_path(
r'print/?',
BuildLineLabelPrint.as_view(),
name='api-buildline-label-print',
),
path(
'metadata/',
MetadataView.as_view(),
{'model': label.models.BuildLineLabel},
name='api-buildline-label-metadata',
),
path(
'',
BuildLineLabelDetail.as_view(),
name='api-buildline-label-detail',
),
]),
),
# List view
path('', BuildLineLabelList.as_view(), name='api-buildline-label-list'),
]),
),
]