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