mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
add more docstrings for plugin app
This commit is contained in:
parent
6c25872f81
commit
e01918e607
@ -1,3 +1,4 @@
|
||||
"""Admin for plugin app."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
@ -43,6 +44,7 @@ class PluginSettingInline(admin.TabularInline):
|
||||
]
|
||||
|
||||
def has_add_permission(self, request, obj):
|
||||
"""The plugin settings should not be meddled with manually."""
|
||||
return False
|
||||
|
||||
|
||||
@ -66,6 +68,7 @@ class NotificationUserSettingAdmin(admin.ModelAdmin):
|
||||
]
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Notifications should not be changed."""
|
||||
return False
|
||||
|
||||
|
||||
|
@ -31,6 +31,10 @@ class PluginList(generics.ListAPIView):
|
||||
queryset = PluginConfig.objects.all()
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Filter for API requests.
|
||||
|
||||
Filter by mixin with the `mixin` flag
|
||||
"""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
|
@ -1,3 +1,8 @@
|
||||
"""Apps file for plugin app.
|
||||
|
||||
This initializes the plugin mechanisms and handles reloading throught the lifecycle.
|
||||
The main code for plugin special sauce is in the plugin registry in `InvenTree/plugin/registry.py`.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
@ -15,9 +20,12 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class PluginAppConfig(AppConfig):
|
||||
"""AppConfig for plugins."""
|
||||
|
||||
name = 'plugin'
|
||||
|
||||
def ready(self):
|
||||
"""The ready method is extended to initialize plugins."""
|
||||
if settings.PLUGINS_ENABLED:
|
||||
if not canAppAccessDatabase(allow_test=True):
|
||||
logger.info("Skipping plugin loading sequence") # pragma: no cover
|
||||
|
@ -17,7 +17,7 @@ class ActionPluginView(APIView):
|
||||
]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
"""This function checks if all required info was submitted and then performs a plugin_action or returns an error."""
|
||||
action = request.data.get('action', None)
|
||||
|
||||
data = request.data.get('data', None)
|
||||
|
@ -13,6 +13,10 @@ class ActionMixinTests(TestCase):
|
||||
ACTION_RETURN = 'a action was performed'
|
||||
|
||||
def setUp(self):
|
||||
"""Setup enviroment for tests.
|
||||
|
||||
Contains multiple sample plugins that are used in the tests
|
||||
"""
|
||||
class SimplePlugin(ActionMixin, InvenTreePlugin):
|
||||
pass
|
||||
self.plugin = SimplePlugin()
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""API endpoints for barcode plugins."""
|
||||
|
||||
|
||||
from django.urls import path, re_path, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -40,7 +42,10 @@ class BarcodeScan(APIView):
|
||||
]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Respond to a barcode POST request."""
|
||||
"""Respond to a barcode POST request.
|
||||
|
||||
Check if required info was provided and then run though the plugin steps or try to match up-
|
||||
"""
|
||||
data = request.data
|
||||
|
||||
if 'barcode' not in data:
|
||||
@ -139,7 +144,10 @@ class BarcodeAssign(APIView):
|
||||
]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Respond to a barcode assign POST request.
|
||||
|
||||
Checks inputs and assign barcode (hash) to StockItem.
|
||||
"""
|
||||
data = request.data
|
||||
|
||||
if 'barcode' not in data:
|
||||
|
@ -9,6 +9,7 @@ from stock.models import StockItem
|
||||
|
||||
|
||||
class BarcodeAPITest(InvenTreeAPITestCase):
|
||||
"""Tests for barcode api."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -18,17 +19,18 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
super().setUp()
|
||||
|
||||
self.scan_url = reverse('api-barcode-scan')
|
||||
self.assign_url = reverse('api-barcode-link')
|
||||
|
||||
def postBarcode(self, url, barcode):
|
||||
|
||||
"""Post barcode and return results."""
|
||||
return self.client.post(url, format='json', data={'barcode': str(barcode)})
|
||||
|
||||
def test_invalid(self):
|
||||
|
||||
"""Test that invalid requests fail."""
|
||||
# test scan url
|
||||
response = self.client.post(self.scan_url, format='json', data={})
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
@ -44,7 +46,10 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_empty(self):
|
||||
"""Test an empty barcode scan.
|
||||
|
||||
Ensure that all required data is in teh respomse.
|
||||
"""
|
||||
response = self.postBarcode(self.scan_url, '')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@ -157,7 +162,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
||||
self.assertEqual(response.data['stocklocation'], 'Stock location does not exist')
|
||||
|
||||
def test_integer_barcode(self):
|
||||
|
||||
"""Test scan of an integer barcode."""
|
||||
response = self.postBarcode(self.scan_url, '123456789')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@ -171,7 +176,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
||||
self.assertIsNone(data['plugin'])
|
||||
|
||||
def test_array_barcode(self):
|
||||
|
||||
"""Test scan of barcode with string encoded array."""
|
||||
response = self.postBarcode(self.scan_url, "['foo', 'bar']")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@ -185,7 +190,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
||||
self.assertIsNone(data['plugin'])
|
||||
|
||||
def test_barcode_generation(self):
|
||||
|
||||
"""Test that a barcode is generated with a scan."""
|
||||
item = StockItem.objects.get(pk=522)
|
||||
|
||||
response = self.postBarcode(self.scan_url, item.format_barcode())
|
||||
|
@ -23,5 +23,6 @@ class EventMixin:
|
||||
MIXIN_NAME = 'Events'
|
||||
|
||||
def __init__(self):
|
||||
"""Register the mixin."""
|
||||
super().__init__()
|
||||
self.add_mixin('events', True, __class__)
|
||||
|
@ -22,9 +22,11 @@ class SettingsMixin:
|
||||
"""Mixin that enables global settings for the plugin."""
|
||||
|
||||
class MixinMeta:
|
||||
"""Meta for mixin."""
|
||||
MIXIN_NAME = 'Settings'
|
||||
|
||||
def __init__(self):
|
||||
"""Register mixin."""
|
||||
super().__init__()
|
||||
self.add_mixin('settings', 'has_settings', __class__)
|
||||
self.settings = getattr(self, 'SETTINGS', {})
|
||||
@ -91,6 +93,7 @@ class ScheduleMixin:
|
||||
MIXIN_NAME = 'Schedule'
|
||||
|
||||
def __init__(self):
|
||||
"""Register mixin."""
|
||||
super().__init__()
|
||||
self.scheduled_tasks = self.get_scheduled_tasks()
|
||||
self.validate_scheduled_tasks()
|
||||
@ -98,6 +101,10 @@ class ScheduleMixin:
|
||||
self.add_mixin('schedule', 'has_scheduled_tasks', __class__)
|
||||
|
||||
def get_scheduled_tasks(self):
|
||||
"""Returns `SCHEDULED_TASKS` context.
|
||||
|
||||
Override if you want the scheduled tasks to be dynamic (influenced by settings for example).
|
||||
"""
|
||||
return getattr(self, 'SCHEDULED_TASKS', {})
|
||||
|
||||
@property
|
||||
@ -216,6 +223,7 @@ class UrlsMixin:
|
||||
MIXIN_NAME = 'URLs'
|
||||
|
||||
def __init__(self):
|
||||
"""Register mixin."""
|
||||
super().__init__()
|
||||
self.add_mixin('urls', 'has_urls', __class__)
|
||||
self.urls = self.setup_urls()
|
||||
@ -259,6 +267,7 @@ class NavigationMixin:
|
||||
MIXIN_NAME = 'Navigation Links'
|
||||
|
||||
def __init__(self):
|
||||
"""Register mixin."""
|
||||
super().__init__()
|
||||
self.add_mixin('navigation', 'has_naviation', __class__)
|
||||
self.navigation = self.setup_navigation()
|
||||
@ -301,6 +310,7 @@ class AppMixin:
|
||||
MIXIN_NAME = 'App registration'
|
||||
|
||||
def __init__(self):
|
||||
"""Register mixin."""
|
||||
super().__init__()
|
||||
self.add_mixin('app', 'has_app', __class__)
|
||||
|
||||
@ -366,6 +376,7 @@ class APICallMixin:
|
||||
MIXIN_NAME = 'API calls'
|
||||
|
||||
def __init__(self):
|
||||
"""Register mixin."""
|
||||
super().__init__()
|
||||
self.add_mixin('api_call', 'has_api_call', __class__)
|
||||
|
||||
@ -380,22 +391,49 @@ class APICallMixin:
|
||||
|
||||
@property
|
||||
def api_url(self):
|
||||
"""Base url path."""
|
||||
return f'{self.API_METHOD}://{self.get_setting(self.API_URL_SETTING)}'
|
||||
|
||||
@property
|
||||
def api_headers(self):
|
||||
"""Returns the default headers for requests with api_call.
|
||||
|
||||
Contains a header with the key set in `API_TOKEN` for the plugin it `API_TOKEN_SETTING` is defined.
|
||||
Check the mixin class docstring for a full example.
|
||||
"""
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
if getattr(self, 'API_TOKEN_SETTING'):
|
||||
headers[self.API_TOKEN] = self.get_setting(self.API_TOKEN_SETTING)
|
||||
return headers
|
||||
|
||||
def api_build_url_args(self, arguments):
|
||||
def api_build_url_args(self, arguments: dict) -> str:
|
||||
"""Returns an encoded path for the provided dict."""
|
||||
groups = []
|
||||
for key, val in arguments.items():
|
||||
groups.append(f'{key}={",".join([str(a) for a in val])}')
|
||||
return f'?{"&".join(groups)}'
|
||||
|
||||
def api_call(self, endpoint, method: str = 'GET', url_args=None, data=None, headers=None, simple_response: bool = True, endpoint_is_url: bool = False):
|
||||
def api_call(self, endpoint: str, method: str = 'GET', url_args: dict = None, data=None, headers: dict = None, simple_response: bool = True, endpoint_is_url: bool = False):
|
||||
"""Do an API call.
|
||||
|
||||
Simplest call example:
|
||||
```python
|
||||
self.api_call('hello')
|
||||
```
|
||||
Will call the `{base_url}/hello` with a GET request and - if set - the token for this plugin.
|
||||
|
||||
Args:
|
||||
endpoint (str): Path to current endpoint. Either the endpoint or the full or if the flag is set
|
||||
method (str, optional): HTTP method that should be uses - capitalized. Defaults to 'GET'.
|
||||
url_args (dict, optional): arguments that should be appended to the url. Defaults to None.
|
||||
data (Any, optional): Data that should be transmitted in the body - must be JSON serializable. Defaults to None.
|
||||
headers (dict, optional): Headers that should be used for the request. Defaults to self.api_headers.
|
||||
simple_response (bool, optional): Return the response as JSON. Defaults to True.
|
||||
endpoint_is_url (bool, optional): The provided endpoint is the full url - do not use self.api_url as base. Defaults to False.
|
||||
|
||||
Returns:
|
||||
Response
|
||||
"""
|
||||
if url_args:
|
||||
endpoint += self.api_build_url_args(url_args)
|
||||
|
||||
@ -469,9 +507,12 @@ class PanelMixin:
|
||||
"""
|
||||
|
||||
class MixinMeta:
|
||||
"""Meta for mixin."""
|
||||
|
||||
MIXIN_NAME = 'Panel'
|
||||
|
||||
def __init__(self):
|
||||
"""Register mixin."""
|
||||
super().__init__()
|
||||
self.add_mixin('panel', True, __class__)
|
||||
|
||||
@ -500,7 +541,16 @@ class PanelMixin:
|
||||
return context
|
||||
|
||||
def render_panels(self, view, request, context):
|
||||
"""Get panels for a view.
|
||||
|
||||
Args:
|
||||
view: Current view context
|
||||
request: Current request for passthrough
|
||||
context: Rendering context
|
||||
|
||||
Returns:
|
||||
Array of panels
|
||||
"""
|
||||
panels = []
|
||||
|
||||
# Construct an updated context object for template rendering
|
||||
|
@ -17,7 +17,10 @@ from plugin.urls import PLUGIN_BASE
|
||||
|
||||
|
||||
class BaseMixinDefinition:
|
||||
"""Mixin to test the meta functions of all mixins."""
|
||||
|
||||
def test_mixin_name(self):
|
||||
"""Test that the mixin registers itseld correctly."""
|
||||
# mixin name
|
||||
self.assertIn(self.MIXIN_NAME, [item['key'] for item in self.mixin.registered_mixins])
|
||||
# human name
|
||||
@ -25,6 +28,8 @@ class BaseMixinDefinition:
|
||||
|
||||
|
||||
class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase):
|
||||
"""Tests for SettingsMixin."""
|
||||
|
||||
MIXIN_HUMAN_NAME = 'Settings'
|
||||
MIXIN_NAME = 'settings'
|
||||
MIXIN_ENABLE_CHECK = 'has_settings'
|
||||
@ -32,6 +37,7 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase):
|
||||
TEST_SETTINGS = {'SETTING1': {'default': '123', }}
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
class SettingsCls(SettingsMixin, InvenTreePlugin):
|
||||
SETTINGS = self.TEST_SETTINGS
|
||||
self.mixin = SettingsCls()
|
||||
@ -43,6 +49,7 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase):
|
||||
super().setUp()
|
||||
|
||||
def test_function(self):
|
||||
"""Test that the mixin functions."""
|
||||
# settings variable
|
||||
self.assertEqual(self.mixin.settings, self.TEST_SETTINGS)
|
||||
|
||||
@ -60,11 +67,14 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase):
|
||||
|
||||
|
||||
class UrlsMixinTest(BaseMixinDefinition, TestCase):
|
||||
"""Tests for UrlsMixin."""
|
||||
|
||||
MIXIN_HUMAN_NAME = 'URLs'
|
||||
MIXIN_NAME = 'urls'
|
||||
MIXIN_ENABLE_CHECK = 'has_urls'
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
class UrlsCls(UrlsMixin, InvenTreePlugin):
|
||||
def test():
|
||||
return 'ccc'
|
||||
@ -76,6 +86,7 @@ class UrlsMixinTest(BaseMixinDefinition, TestCase):
|
||||
self.mixin_nothing = NoUrlsCls()
|
||||
|
||||
def test_function(self):
|
||||
"""Test that the mixin functions."""
|
||||
plg_name = self.mixin.plugin_name()
|
||||
|
||||
# base_url
|
||||
@ -99,26 +110,32 @@ class UrlsMixinTest(BaseMixinDefinition, TestCase):
|
||||
|
||||
|
||||
class AppMixinTest(BaseMixinDefinition, TestCase):
|
||||
"""Tests for AppMixin."""
|
||||
|
||||
MIXIN_HUMAN_NAME = 'App registration'
|
||||
MIXIN_NAME = 'app'
|
||||
MIXIN_ENABLE_CHECK = 'has_app'
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
class TestCls(AppMixin, InvenTreePlugin):
|
||||
pass
|
||||
self.mixin = TestCls()
|
||||
|
||||
def test_function(self):
|
||||
# test that this plugin is in settings
|
||||
"""Test that the sample plugin registers in settings."""
|
||||
self.assertIn('plugin.samples.integration', settings.INSTALLED_APPS)
|
||||
|
||||
|
||||
class NavigationMixinTest(BaseMixinDefinition, TestCase):
|
||||
"""Tests for NavigationMixin."""
|
||||
|
||||
MIXIN_HUMAN_NAME = 'Navigation Links'
|
||||
MIXIN_NAME = 'navigation'
|
||||
MIXIN_ENABLE_CHECK = 'has_naviation'
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
class NavigationCls(NavigationMixin, InvenTreePlugin):
|
||||
NAVIGATION = [
|
||||
{'name': 'aa', 'link': 'plugin:test:test_view'},
|
||||
@ -131,6 +148,7 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase):
|
||||
self.nothing_mixin = NothingNavigationCls()
|
||||
|
||||
def test_function(self):
|
||||
"""Test that a correct configuration functions."""
|
||||
# check right configuration
|
||||
self.assertEqual(self.mixin.navigation, [{'name': 'aa', 'link': 'plugin:test:test_view'}, ])
|
||||
|
||||
@ -139,7 +157,7 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase):
|
||||
self.assertEqual(self.nothing_mixin.navigation_name, '')
|
||||
|
||||
def test_fail(self):
|
||||
# check wrong links fails
|
||||
"""Test that wrong links fail."""
|
||||
with self.assertRaises(NotImplementedError):
|
||||
class NavigationCls(NavigationMixin, InvenTreePlugin):
|
||||
NAVIGATION = ['aa', 'aa']
|
||||
@ -147,11 +165,14 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase):
|
||||
|
||||
|
||||
class APICallMixinTest(BaseMixinDefinition, TestCase):
|
||||
"""Tests for APICallMixin."""
|
||||
|
||||
MIXIN_HUMAN_NAME = 'API calls'
|
||||
MIXIN_NAME = 'api_call'
|
||||
MIXIN_ENABLE_CHECK = 'has_api_call'
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
class MixinCls(APICallMixin, SettingsMixin, InvenTreePlugin):
|
||||
NAME = "Sample API Caller"
|
||||
|
||||
|
@ -163,7 +163,7 @@ class LabelMixinTests(InvenTreeAPITestCase):
|
||||
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`"""
|
||||
"""Cover the endpoints not covered by `test_printing_process`."""
|
||||
plugin_ref = 'samplelabel'
|
||||
|
||||
# Activate the label components
|
||||
|
@ -18,7 +18,7 @@ class LocatePluginView(APIView):
|
||||
]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
"""Check inputs and offload the task to the plugin."""
|
||||
# Which plugin to we wish to use?
|
||||
plugin = request.data.get('plugin', None)
|
||||
|
||||
|
@ -24,9 +24,11 @@ class LocateMixin:
|
||||
"""
|
||||
|
||||
class MixinMeta:
|
||||
"""Meta for mixin."""
|
||||
MIXIN_NAME = "Locate"
|
||||
|
||||
def __init__(self):
|
||||
"""Register the mixin."""
|
||||
super().__init__()
|
||||
self.add_mixin('locate', True, __class__)
|
||||
|
||||
|
@ -9,6 +9,7 @@ from stock.models import StockItem, StockLocation
|
||||
|
||||
|
||||
class LocatePluginTests(InvenTreeAPITestCase):
|
||||
"""Tests for LocateMixin."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
|
@ -8,6 +8,7 @@ class SimpleActionPluginTests(InvenTreeTestCase):
|
||||
"""Tests for SampleIntegrationPlugin."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for tests."""
|
||||
super().setUp()
|
||||
|
||||
self.plugin = SimpleActionPlugin()
|
||||
|
@ -8,6 +8,7 @@ from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
|
||||
|
||||
class TestInvenTreeBarcode(InvenTreeAPITestCase):
|
||||
"""Tests for the integrated InvenTreeBarcode barcode plugin."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""Tests for core_notifications."""
|
||||
|
||||
from part.test_part import BaseNotificationIntegrationTest
|
||||
from plugin import registry
|
||||
from plugin.builtin.integration.core_notifications import \
|
||||
@ -6,6 +8,7 @@ from plugin.models import NotificationUserSetting
|
||||
|
||||
|
||||
class CoreNotificationTestTests(BaseNotificationIntegrationTest):
|
||||
"""Tests for CoreNotificationsPlugin."""
|
||||
|
||||
def test_email(self):
|
||||
"""Ensure that the email notifications run."""
|
||||
|
@ -22,10 +22,17 @@ class IntegrationPluginError(Exception):
|
||||
"""Error that encapsulates another error and adds the path / reference of the raising plugin."""
|
||||
|
||||
def __init__(self, path, message):
|
||||
"""Init a plugin error.
|
||||
|
||||
Args:
|
||||
path: Path on which the error occured - used to find out which plugin it was
|
||||
message: The original error message
|
||||
"""
|
||||
self.path = path
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
"""Returns the error message."""
|
||||
return self.message # pragma: no cover
|
||||
|
||||
|
||||
@ -142,6 +149,7 @@ class GitStatus:
|
||||
msg: str = ''
|
||||
|
||||
def __init__(self, key: str = 'N', status: int = 2, msg: str = '') -> None:
|
||||
"""Define a git Status -> needed for lookup."""
|
||||
self.key = key
|
||||
self.status = status
|
||||
self.msg = msg
|
||||
|
@ -24,6 +24,7 @@ class MetadataMixin(models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Meta for MetadataMixin."""
|
||||
abstract = True
|
||||
|
||||
metadata = models.JSONField(
|
||||
@ -74,6 +75,7 @@ class PluginConfig(models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Meta for PluginConfig."""
|
||||
verbose_name = _("Plugin Configuration")
|
||||
verbose_name_plural = _("Plugin Configurations")
|
||||
|
||||
@ -99,6 +101,7 @@ class PluginConfig(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Nice name for printing."""
|
||||
name = f'{self.name} - {self.key}'
|
||||
if not self.active:
|
||||
name += '(not active)'
|
||||
@ -106,7 +109,7 @@ class PluginConfig(models.Model):
|
||||
|
||||
# extra attributes from the registry
|
||||
def mixins(self):
|
||||
|
||||
"""Returns all registered mixins."""
|
||||
try:
|
||||
return self.plugin._mixinreg
|
||||
except (AttributeError, ValueError): # pragma: no cover
|
||||
@ -153,6 +156,7 @@ class PluginSetting(common.models.BaseInvenTreeSetting):
|
||||
"""This model represents settings for individual plugins."""
|
||||
|
||||
class Meta:
|
||||
"""Meta for PluginSetting."""
|
||||
unique_together = [
|
||||
('plugin', 'key'),
|
||||
]
|
||||
@ -201,12 +205,14 @@ class NotificationUserSetting(common.models.BaseInvenTreeSetting):
|
||||
"""This model represents notification settings for a user."""
|
||||
|
||||
class Meta:
|
||||
"""Meta for NotificationUserSetting."""
|
||||
unique_together = [
|
||||
('method', 'user', 'key'),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_setting_definition(cls, key, **kwargs):
|
||||
"""Override setting_definition to use notification settings."""
|
||||
from common.notifications import storage
|
||||
|
||||
kwargs['settings'] = storage.user_settings
|
||||
@ -234,4 +240,5 @@ class NotificationUserSetting(common.models.BaseInvenTreeSetting):
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Nice name of printing."""
|
||||
return f'{self.key} (for {self.user}): {self.value}'
|
||||
|
@ -117,6 +117,10 @@ class MixinBase:
|
||||
"""Base set of mixin functions and mechanisms."""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
"""Init sup-parts.
|
||||
|
||||
Adds state dicts.
|
||||
"""
|
||||
self._mixinreg = {}
|
||||
self._mixins = {}
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -180,6 +184,10 @@ class InvenTreePlugin(MixinBase, MetaBase):
|
||||
LICENSE = None
|
||||
|
||||
def __init__(self):
|
||||
"""Init a plugin.
|
||||
|
||||
Set paths and load metadata.
|
||||
"""
|
||||
super().__init__()
|
||||
self.add_mixin('base')
|
||||
self.def_path = inspect.getfile(self.__class__)
|
||||
@ -198,7 +206,7 @@ class InvenTreePlugin(MixinBase, MetaBase):
|
||||
|
||||
@property
|
||||
def author(self):
|
||||
"""Author of plugin - either from plugin settings or git"""
|
||||
"""Author of plugin - either from plugin settings or git."""
|
||||
author = getattr(self, 'AUTHOR', None)
|
||||
if not author:
|
||||
author = self.package.get('author')
|
||||
@ -208,7 +216,7 @@ class InvenTreePlugin(MixinBase, MetaBase):
|
||||
|
||||
@property
|
||||
def pub_date(self):
|
||||
"""Publishing date of plugin - either from plugin settings or git"""
|
||||
"""Publishing date of plugin - either from plugin settings or git."""
|
||||
pub_date = getattr(self, 'PUBLISH_DATE', None)
|
||||
if not pub_date:
|
||||
pub_date = self.package.get('date')
|
||||
@ -226,7 +234,7 @@ class InvenTreePlugin(MixinBase, MetaBase):
|
||||
|
||||
@property
|
||||
def website(self):
|
||||
"""Website of plugin - if set else None"""
|
||||
"""Website of plugin - if set else None."""
|
||||
website = getattr(self, 'WEBSITE', None)
|
||||
return website
|
||||
|
||||
@ -293,6 +301,11 @@ class InvenTreePlugin(MixinBase, MetaBase):
|
||||
|
||||
|
||||
class IntegrationPluginBase(InvenTreePlugin):
|
||||
"""Legacy base class for plugins.
|
||||
|
||||
Do not use!
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Send warning about using this reference."""
|
||||
# TODO remove in 0.8.0
|
||||
|
@ -33,6 +33,10 @@ class PluginsRegistry:
|
||||
"""The PluginsRegistry class."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize registry.
|
||||
|
||||
Set up all needed references for internal and external states.
|
||||
"""
|
||||
# plugin registry
|
||||
self.plugins = {}
|
||||
self.plugins_inactive = {}
|
||||
@ -334,7 +338,11 @@ class PluginsRegistry:
|
||||
|
||||
# region mixin specific loading ...
|
||||
def activate_plugin_settings(self, plugins):
|
||||
"""Activate plugin settings.
|
||||
|
||||
Add all defined settings form the plugins to a unified dict in the registry.
|
||||
This dict is referenced by the PluginSettings for settings definitions.
|
||||
"""
|
||||
logger.info('Activating plugin settings')
|
||||
|
||||
self.mixins_settings = {}
|
||||
@ -356,7 +364,7 @@ class PluginsRegistry:
|
||||
self.mixins_settings = {}
|
||||
|
||||
def activate_plugin_schedule(self, plugins):
|
||||
|
||||
"""Activate scheudles from plugins with the ScheduleMixin."""
|
||||
logger.info('Activating plugin tasks')
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
@ -407,7 +415,7 @@ class PluginsRegistry:
|
||||
pass
|
||||
|
||||
def activate_plugin_app(self, plugins, force_reload=False):
|
||||
"""Activate AppMixin plugins - add custom apps and reload
|
||||
"""Activate AppMixin plugins - add custom apps and reload.
|
||||
|
||||
:param plugins: list of IntegrationPlugins that should be installed
|
||||
:type plugins: dict
|
||||
@ -445,7 +453,7 @@ class PluginsRegistry:
|
||||
self._update_urls()
|
||||
|
||||
def _reregister_contrib_apps(self):
|
||||
"""Fix reloading of contrib apps - models and admin
|
||||
"""Fix reloading of contrib apps - models and admin.
|
||||
|
||||
This is needed if plugins were loaded earlier and then reloaded as models and admins rely on imports.
|
||||
Those register models and admin in their respective objects (e.g. admin.site for admin).
|
||||
@ -493,7 +501,7 @@ class PluginsRegistry:
|
||||
return plugin_path
|
||||
|
||||
def deactivate_plugin_app(self):
|
||||
"""Deactivate AppMixin plugins - some magic required"""
|
||||
"""Deactivate AppMixin plugins - some magic required."""
|
||||
# unregister models from admin
|
||||
for plugin_path in self.installed_apps:
|
||||
models = [] # the modelrefs need to be collected as poping an item in a iter is not welcomed
|
||||
|
@ -10,6 +10,7 @@ class BrokenIntegrationPlugin(InvenTreePlugin):
|
||||
SLUG = 'broken'
|
||||
|
||||
def __init__(self):
|
||||
"""Raise a KeyError to provoke a range of unit tests and safety mechanisms in the plugin loading mechanism."""
|
||||
super().__init__()
|
||||
|
||||
raise KeyError('This is a dummy error')
|
||||
|
@ -31,7 +31,7 @@ class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin):
|
||||
}
|
||||
|
||||
def get_panel_context(self, view, request, context):
|
||||
|
||||
"""Returns enriched context."""
|
||||
ctx = super().get_panel_context(view, request, context)
|
||||
|
||||
# If we are looking at a StockLocationDetail view, add location context object
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""Simple sample for a plugin with the LabelPrintingMixin.
|
||||
|
||||
This does not function in real usage and is more to show the required components and for unit tests.
|
||||
"""
|
||||
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import LabelPrintingMixin
|
||||
@ -13,4 +17,8 @@ class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
|
||||
VERSION = "0.1"
|
||||
|
||||
def print_label(self, label, **kwargs):
|
||||
"""Sample printing step.
|
||||
|
||||
Normally here the connection to the printer and transfer of the label would take place.
|
||||
"""
|
||||
print("OK PRINTING")
|
||||
|
@ -23,6 +23,7 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi
|
||||
return HttpResponse(f'Hi there {request.user.username} this works')
|
||||
|
||||
def setup_urls(self):
|
||||
"""Urls that are exposed by this plugin."""
|
||||
he_urls = [
|
||||
re_path(r'^he/', self.view_test, name='he'),
|
||||
re_path(r'^ha/', self.view_test, name='ha'),
|
||||
|
@ -6,10 +6,18 @@ from plugin.mixins import ScheduleMixin, SettingsMixin
|
||||
|
||||
# Define some simple tasks to perform
|
||||
def print_hello():
|
||||
"""Sample function that can be called on schedule.
|
||||
|
||||
Contents do not matter - therefore no coverage.
|
||||
"""
|
||||
print("Hello") # pragma: no cover
|
||||
|
||||
|
||||
def print_world():
|
||||
"""Sample function that can be called on schedule.
|
||||
|
||||
Contents do not matter - therefore no coverage.
|
||||
"""
|
||||
print("World") # pragma: no cover
|
||||
|
||||
|
||||
|
@ -24,7 +24,11 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin):
|
||||
VERSION = "0.2"
|
||||
|
||||
def locate_stock_item(self, item_pk):
|
||||
"""Locate a StockItem.
|
||||
|
||||
Args:
|
||||
item_pk: primary key for item
|
||||
"""
|
||||
from stock.models import StockItem
|
||||
|
||||
logger.info(f"SampleLocatePlugin attempting to locate item ID {item_pk}")
|
||||
@ -40,7 +44,11 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin):
|
||||
logger.error(f"StockItem ID {item_pk} does not exist!")
|
||||
|
||||
def locate_stock_location(self, location_pk):
|
||||
"""Locate a StockLocation.
|
||||
|
||||
Args:
|
||||
location_pk: primary key for location
|
||||
"""
|
||||
from stock.models import StockLocation
|
||||
|
||||
logger.info(f"SampleLocatePlugin attempting to locate location ID {location_pk}")
|
||||
|
@ -41,12 +41,13 @@ class MetadataSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class PluginConfigSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for a PluginConfig:"""
|
||||
"""Serializer for a PluginConfig."""
|
||||
|
||||
meta = serializers.DictField(read_only=True)
|
||||
mixins = serializers.DictField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
"""Meta for serializer."""
|
||||
model = PluginConfig
|
||||
fields = [
|
||||
'key',
|
||||
@ -78,6 +79,7 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
"""Meta for serializer."""
|
||||
fields = [
|
||||
'url',
|
||||
'packagename',
|
||||
@ -85,6 +87,10 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
"""Validate inputs.
|
||||
|
||||
Make sure both confirm and url are provided.
|
||||
"""
|
||||
super().validate(data)
|
||||
|
||||
# check the base requirements are met
|
||||
@ -97,6 +103,7 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""Install a plugin from a package registry and set operational results as instance data."""
|
||||
data = self.validated_data
|
||||
|
||||
packagename = data.get('packagename', '')
|
||||
|
@ -21,6 +21,7 @@ class PluginTemplateLoader(FilesystemLoader):
|
||||
"""
|
||||
|
||||
def get_dirs(self):
|
||||
"""Returns all template dir paths in plugins."""
|
||||
dirname = 'templates'
|
||||
template_dirs = []
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
"""Tests for general API tests for the plugin app."""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
@ -15,6 +16,7 @@ class PluginDetailAPITest(InvenTreeAPITestCase):
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
self.MSG_NO_PKG = 'Either packagename of URL must be provided'
|
||||
|
||||
self.PKG_NAME = 'minimal'
|
||||
|
@ -15,6 +15,7 @@ class PluginTagTests(TestCase):
|
||||
"""Tests for the plugin extras."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
self.sample = SampleIntegrationPlugin()
|
||||
self.plugin_no = NoIntegrationPlugin()
|
||||
self.plugin_wrong = WrongIntegrationPlugin()
|
||||
@ -60,6 +61,7 @@ class InvenTreePluginTests(TestCase):
|
||||
"""Tests for InvenTreePlugin."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
self.plugin = InvenTreePlugin()
|
||||
|
||||
class NamedPlugin(InvenTreePlugin):
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""Views for plugin app."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
Loading…
Reference in New Issue
Block a user