Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Machine integration #4824

Merged
merged 105 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
9708a21
Added initial draft for machines
wolflu05 May 15, 2023
e713c5a
refactor: isPluginRegistryLoaded check into own ready function
wolflu05 May 15, 2023
7861b73
Added suggestions from codereview
wolflu05 May 15, 2023
aee3d57
Refactor: base_drivers -> machine_types
wolflu05 May 18, 2023
e73a062
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 May 19, 2023
9ed334d
Use new BaseInvenTreeSetting unique interface
wolflu05 May 19, 2023
621485b
Fix Django not ready error
wolflu05 May 19, 2023
81389df
Added get_machines function to driver
wolflu05 May 19, 2023
8aeb81e
Added error handeling for driver and machine type
wolflu05 May 19, 2023
fd68eac
Extended get_machines functionality
wolflu05 May 19, 2023
5c73739
Export everything from plugin module
wolflu05 May 19, 2023
67db09f
Fix spelling mistakes
wolflu05 May 26, 2023
4767439
Better states handeling, BaseMachineType is now used instead of Machi…
wolflu05 May 26, 2023
595ccd6
Use uuid as pk
wolflu05 May 26, 2023
2f8ad36
WIP: machine termination hook
wolflu05 May 27, 2023
6bb0ba7
Remove termination hook as this does not work with gunicorn
wolflu05 May 30, 2023
396a4f3
Remove machine from registry after delete
wolflu05 May 30, 2023
8129bd3
Added ClassProviderMixin
wolflu05 May 30, 2023
21aa0fe
Check for slug dupplication
wolflu05 May 30, 2023
72f5934
Added config_type to MachineSettings to define machine/driver settings
wolflu05 May 31, 2023
0bb62a1
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 May 31, 2023
10a9372
Refactor helper mixins into own file in InvenTree app
wolflu05 May 31, 2023
19deddc
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Jun 2, 2023
11febee
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Jul 5, 2023
cd13b2a
Fixed typing and added required_attributes for BaseDriver
wolflu05 Jul 5, 2023
d4ff9f6
fix: generic status import
wolflu05 Jul 6, 2023
758550d
Added first draft for machine states
wolflu05 Jul 7, 2023
cabc01d
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Jul 9, 2023
7d0a495
Added convention for status codes
wolflu05 Jul 9, 2023
50d0695
Added update_machine hook
wolflu05 Jul 9, 2023
f441085
Removed unnecessary _key suffix from machine config model
wolflu05 Jul 9, 2023
1db7825
Initil draft for machine API
wolflu05 Jul 10, 2023
71149da
Refactored BaseInvenTreeSetting all_items and allValues method
wolflu05 Jul 11, 2023
d94a6ea
Added required to InvenTreeBaseSetting and check_settings method
wolflu05 Jul 11, 2023
fa34c8c
check if all required machine settings are defined and refactor: use …
wolflu05 Jul 11, 2023
bffd764
Fix: comment
wolflu05 Jul 11, 2023
083aebd
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Jul 12, 2023
d483cfa
Fix initialize error and python 3.9 compability
wolflu05 Jul 12, 2023
ce99ec9
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Nov 8, 2023
27db67d
Make machine states available through the global states api
wolflu05 Nov 8, 2023
7b6229c
Added basic PUI machine admin implementation that is still in dev
wolflu05 Nov 8, 2023
265b712
Added basic machine setting UI to PUI
wolflu05 Nov 9, 2023
158dcdd
Added machine detail view to PUI admin center
wolflu05 Nov 9, 2023
4b6664f
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Jan 26, 2024
44d277d
Fix merge issues
wolflu05 Jan 26, 2024
a1602e4
Fix style issues
wolflu05 Jan 26, 2024
2430af8
Added machine type,machine driver,error stack tables
wolflu05 Jan 26, 2024
758deb5
Fix style in machine/serializers.py
wolflu05 Jan 26, 2024
c87a9f0
Added pui link from machine to machine type/driver drawer
wolflu05 Jan 26, 2024
8a59c90
Removed only partially working django admin in favor of the PUI admin…
wolflu05 Jan 27, 2024
1c227f8
Added required field to settings item
wolflu05 Jan 27, 2024
0e141c0
Added machine restart function
wolflu05 Jan 27, 2024
b454a48
Added restart requird badge to machine table/drawer
wolflu05 Jan 27, 2024
d321d5e
Added driver init function
wolflu05 Jan 27, 2024
fe2ba8e
handle error functions for machines and registry
wolflu05 Jan 28, 2024
be0abc0
Added driver errors
wolflu05 Jan 28, 2024
0424cff
Added machine table to driver drawer
wolflu05 Jan 28, 2024
badbb1d
Added back button to detail drawer component
wolflu05 Jan 28, 2024
07c89b7
Fix auto formatable pre-commit
wolflu05 Jan 28, 2024
34d730f
fix: style
wolflu05 Jan 28, 2024
e36451c
Fix deepsource
wolflu05 Jan 28, 2024
9bf51fe
Removed slug field from table, added more links between drawers, remo…
wolflu05 Jan 28, 2024
8182a72
Added initial docs
wolflu05 Jan 28, 2024
b922315
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Feb 2, 2024
d27ac8d
Removed description from driver/machine type select and fixed disable…
wolflu05 Feb 2, 2024
c7450e4
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Feb 2, 2024
24b4987
Added basic label printing implementation
wolflu05 Feb 2, 2024
0d0cc76
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Feb 2, 2024
58ad80b
Remove translated column names because they are now retrieved from th…
wolflu05 Feb 2, 2024
d4ed13e
Added printer location setting
wolflu05 Feb 3, 2024
bddfe8a
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Feb 3, 2024
a9808c6
Save last 10 used printer machine per user and sort them in the print…
wolflu05 Feb 3, 2024
d4c1884
Added BasePrintingOptionsSerializer for common options
wolflu05 Feb 3, 2024
8e4ad5c
Fix not printing_options are not properly casted to its internal value
wolflu05 Feb 3, 2024
da1aabb
Fix type
wolflu05 Feb 3, 2024
e4b5569
Improved machine docs
wolflu05 Feb 4, 2024
b8090b9
Fix docs
wolflu05 Feb 4, 2024
ff0f02d
Added UNKNOWN status code to label printer status
wolflu05 Feb 5, 2024
71761cd
Skip machine loading when running migrations
wolflu05 Feb 5, 2024
60fbb85
Fix testing?
wolflu05 Feb 5, 2024
25a147e
Fix: tests?
wolflu05 Feb 5, 2024
568f76a
Fix: tests?
wolflu05 Feb 5, 2024
599e36d
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Feb 5, 2024
dbdd364
Disable docs check precommit
wolflu05 Feb 5, 2024
a3966c2
Disable docs check precommit
wolflu05 Feb 5, 2024
55508b8
First draft for tests
wolflu05 Feb 5, 2024
422a51f
fix test
matmair Feb 5, 2024
138a154
Add type ignore
wolflu05 Feb 5, 2024
8ffd865
Added API tests
wolflu05 Feb 6, 2024
32dc782
Test ci?
wolflu05 Feb 6, 2024
6a35a5c
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Feb 6, 2024
7cfe1e8
Add more tests
wolflu05 Feb 6, 2024
be6eb54
Added more tests
wolflu05 Feb 7, 2024
da308fd
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Feb 7, 2024
17f8bb4
Bump api version
wolflu05 Feb 7, 2024
b4f6758
Changed driver/base driver naming schema
wolflu05 Feb 7, 2024
502863f
Added more tests
wolflu05 Feb 7, 2024
8f7b247
Fix tests
wolflu05 Feb 7, 2024
6571fea
Added setting choice with kwargs and get_machines with initialized=None
wolflu05 Feb 7, 2024
ec6b972
Refetch table after deleting machine
wolflu05 Feb 7, 2024
e6f88a0
Fix test
wolflu05 Feb 7, 2024
8638f3b
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Feb 7, 2024
44d7bfe
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Feb 10, 2024
bb7b8fe
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Feb 14, 2024
bb146ed
Merge remote-tracking branch 'upstream/master' into feature/machines
wolflu05 Feb 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@
'stock.apps.StockConfig',
'users.apps.UsersConfig',
'plugin.apps.PluginAppConfig',
'machine.apps.MachineConfig',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last

# Core django modules
Expand Down
14 changes: 14 additions & 0 deletions InvenTree/common/models.py
wolflu05 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,12 @@ def get_setting_object(cls, key, **kwargs):
filters['plugin'] = plugin
kwargs['plugin'] = plugin

# Filter by machine
machine = kwargs.get('machine', None)

if machine is not None:
filters['machine'] = machine

# Filter by method
method = kwargs.get('method', None)

Expand Down Expand Up @@ -497,6 +503,7 @@ def set_setting(cls, key, value, change_user, create=True, **kwargs):

user = kwargs.get('user', None)
plugin = kwargs.get('plugin', None)
machine = kwargs.get('machine', None)

if user is not None:
filters['user'] = user
Expand All @@ -509,6 +516,9 @@ def set_setting(cls, key, value, change_user, create=True, **kwargs):
else:
filters['plugin'] = plugin

if machine is not None:
filters['machine'] = machine

try:
setting = cls.objects.get(**filters)
except cls.DoesNotExist:
Expand Down Expand Up @@ -629,13 +639,17 @@ def validate_unique(self, exclude=None, **kwargs):

user = getattr(self, 'user', None)
plugin = getattr(self, 'plugin', None)
machine = getattr(self, 'machine', None)

if user is not None:
filters['user'] = user

if plugin is not None:
filters['plugin'] = plugin

if machine is not None:
filters['machine'] = machine

try:
# Check if a duplicate setting already exists
setting = self.__class__.objects.filter(**filters).exclude(id=self.id)
Expand Down
Empty file added InvenTree/machine/__init__.py
wolflu05 marked this conversation as resolved.
Show resolved Hide resolved
Empty file.
44 changes: 44 additions & 0 deletions InvenTree/machine/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from django import forms
from django.contrib import admin
from django.utils.translation import gettext_lazy as _

from machine import models
from machine.registry import registry


class MachineAdminForm(forms.ModelForm):
def get_machine_type_choices():
return [(machine_type.SLUG, machine_type.NAME) for machine_type in registry.machine_types.values()]

def get_driver_choices():
return [(driver.SLUG, driver.NAME) for driver in registry.drivers.values()]

# TODO: add conditional choices like shown here
# Ref: https://www.reddit.com/r/django/comments/18cj55/conditional_choices_for_model_field_based_on/
# Ref: https://gist.github.com/blackrobot/4956070
driver_key = forms.ChoiceField(label=_("Driver"), choices=get_driver_choices)
machine_type_key = forms.ChoiceField(label=_("Machine Type"), choices=get_machine_type_choices)


class MachineSettingInline(admin.TabularInline):
"""Inline admin class for MachineSetting."""

model = models.MachineSetting

read_only_fields = [
'key',
]

def has_add_permission(self, request, obj):
"""The machine settings should not be meddled with manually."""
return False


@admin.register(models.Machine)
class MachineAdmin(admin.ModelAdmin):
"""Custom admin with restricted id fields."""

form = MachineAdminForm
list_filter = ["active"]
list_display = ["name", "machine_type_key", "driver_key", "active", "is_driver_available", "no_errors"]
inlines = [MachineSettingInline]
23 changes: 23 additions & 0 deletions InvenTree/machine/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import logging

from django.apps import AppConfig

from InvenTree.ready import canAppAccessDatabase
from plugin import registry as plg_registry

logger = logging.getLogger('inventree')


class MachineConfig(AppConfig):
name = 'machine'

def ready(self) -> None:
"""Initialization method for the Machine app."""
if not canAppAccessDatabase(allow_test=True) or plg_registry.is_loading:
wolflu05 marked this conversation as resolved.
Show resolved Hide resolved
logger.info("Skipping machine loading sequence")
wolflu05 marked this conversation as resolved.
Show resolved Hide resolved
return

from machine.registry import registry

logger.info("Loading InvenTree machines")
registry.initialize()
23 changes: 23 additions & 0 deletions InvenTree/machine/base_drivers/BaseLabelPrintingDriver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.utils.translation import gettext_lazy as _

from machine.driver import BaseDriver, BaseMachineType


class BaseLabelPrintingDriver(BaseDriver):
"""Base label printing driver."""

def print_label():
"""This function must be overriden."""
raise NotImplementedError("The `print_label` function must be overriden!")

def print_labels():
"""This function must be overriden."""
raise NotImplementedError("The `print_labels` function must be overriden!")


class LabelPrintingMachineType(BaseMachineType):
SLUG = "label_printer"
NAME = _("Label Printer")
DESCRIPTION = _("Label printer used to print labels")
wolflu05 marked this conversation as resolved.
Show resolved Hide resolved

base_driver = BaseLabelPrintingDriver
6 changes: 6 additions & 0 deletions InvenTree/machine/base_drivers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from machine.base_drivers.BaseLabelPrintingDriver import \
BaseLabelPrintingDriver

__all__ = [
"BaseLabelPrintingDriver",
]
52 changes: 52 additions & 0 deletions InvenTree/machine/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import Dict


class BaseDriver:
wolflu05 marked this conversation as resolved.
Show resolved Hide resolved
"""Base class for machine drivers

Attributes:
SLUG: Slug string for identifying a machine
NAME: User friendly name for displaying
DESCRIPTION: Description of what this driver does (default: "")
"""

SLUG: str
NAME: str
DESCRIPTION: str = ""

# Import only for typechecking
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from machine.models import Machine
else: # pragma: no cover
class Machine:
pass

# TODO: add better typing
matmair marked this conversation as resolved.
Show resolved Hide resolved
MACHINE_SETTINGS: Dict[str, dict]

def init_machine(self, machine: Machine):
"""This method get called for each active machine using that driver while initialization

Arguments:
machine: Machine instance
"""
pass


class BaseMachineType:
wolflu05 marked this conversation as resolved.
Show resolved Hide resolved
"""Base class for machine types

Attributes:
SLUG: Slug string for identifying a machine type
NAME: User friendly name for displaying
DESCRIPTION: Description of what this machine type can do (default: "")

base_driver: Reference to the base driver for this machine type
"""

SLUG: str
NAME: str
DESCRIPTION: str = ""

base_driver: BaseDriver
37 changes: 37 additions & 0 deletions InvenTree/machine/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 3.2.19 on 2023-05-14 18:39

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Machine',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Name of machine', max_length=255, unique=True, verbose_name='Name')),
('machine_type_key', models.CharField(help_text='Type of machine', max_length=255, verbose_name='Machine Type')),
('driver_key', models.CharField(help_text='Driver used for the machine', max_length=255, verbose_name='Driver')),
('active', models.BooleanField(default=True, help_text='Machines can be disabled', verbose_name='Active')),
],
),
migrations.CreateModel(
name='MachineSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(help_text='Settings key (must be unique - case insensitive)', max_length=50)),
('value', models.CharField(blank=True, help_text='Settings value', max_length=200)),
('machine', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='machine.machine', verbose_name='Machine')),
],
options={
'unique_together': {('machine', 'key')},
},
),
]
Empty file.
146 changes: 146 additions & 0 deletions InvenTree/machine/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
from typing import Any

from django.contrib import admin
from django.db import models
from django.utils.translation import gettext_lazy as _

import common.models
from machine.registry import registry


class Machine(models.Model):
wolflu05 marked this conversation as resolved.
Show resolved Hide resolved
"""A Machine objects represents a physical machine."""

name = models.CharField(
unique=True,
max_length=255,
verbose_name=_("Name"),
help_text=_("Name of machine")
)

machine_type_key = models.CharField(
max_length=255,
verbose_name=_("Machine Type"),
help_text=_("Type of machine"),
)

driver_key = models.CharField(
max_length=255,
verbose_name=_("Driver"),
help_text=_("Driver used for the machine")
)

active = models.BooleanField(
default=True,
verbose_name=_("Active"),
help_text=_("Machines can be disabled")
)

def __str__(self) -> str:
"""String representation of a machine."""
return f"{self.name}"

def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Override to set origignal state of the machine instance."""
super().__init__(*args, **kwargs)

self.errors = []

self.driver = registry.get_driver_instance(self.driver_key)
self.machine_type = registry.machine_types.get(self.machine_type_key, None)

if not self.driver:
self.errors.append(f"Driver '{self.driver_key}' not found")
if not self.machine_type:
self.errors.append(f"Machine type '{self.machine_type_key}' not found")
if self.machine_type and not isinstance(self.driver, self.machine_type.base_driver):
self.errors.append(f"'{self.driver.NAME}' is incompatibe with machine type '{self.machine_type.NAME}'")

if len(self.errors) > 0:
return

# TODO: add other init stuff here

@admin.display(boolean=True, description=_("Driver available"))
def is_driver_available(self) -> bool:
"""Status if driver for machine is available"""
return self.driver is not None

@admin.display(boolean=True, description=_("Machine has no errors"))
def no_errors(self) -> bool:
"""Status if machine has errors"""
return len(self.errors) == 0

def initialize(self):
"""Machine initialitation function, gets called after all machines are loaded"""
if self.driver is None:
return

self.driver.init_machine(self)

def get_setting(self, key, cache=False):
"""Return the 'value' of the setting associated with this machine.

Arguments:
key: The 'name' of the setting value to be retrieved
cache: Whether to use RAM cached value (default = False)
"""
from machine.models import MachineSetting

return MachineSetting.get_setting(key, machine=self, cache=cache)

def set_setting(self, key, value, user=None):
"""Set plugin setting value by key.

Arguments:
key: The 'name' of the setting to set
value: The 'value' of the setting
user: TODO: what is this doing?
"""
from machine.models import MachineSetting

MachineSetting.set_setting(key, value, user, machine=self)


class MachineSetting(common.models.BaseInvenTreeSetting):
"""This models represents settings for individial machines."""

typ = "machine"

class Meta:
"""Meta for MachineSetting."""
unique_together = [
("machine", "key")
]

machine = models.ForeignKey(
Machine,
related_name="settings",
verbose_name=_("Machine"),
on_delete=models.CASCADE
)

@classmethod
def get_setting_definition(cls, key, **kwargs):
"""In the BaseInvenTreeSetting class, we have a class attribute named 'SETTINGS', which
is a dict object that fully defines all the setting parameters.

Here, unlike the BaseInvenTreeSetting, we do not know the definitions of all settings
'ahead of time' (as they are defined externally in the machine).

Settings can be provided by the caller, as kwargs['settings'].

If not provided, we'll look at the machine registry to see what settings this machine requires
"""
if 'settings' not in kwargs:
machine: Machine = kwargs.pop('machine', None)
if machine:
kwargs['settings'] = getattr(machine.driver, "MACHINE_SETTINGS", {})

return super().get_setting_definition(key, **kwargs)

def get_kwargs(self):
"""Explicit kwargs required to uniquely identify a particular setting object, in addition to the 'key' parameter."""
return {
'machine': self.machine,
}
Loading