add more docstrings for plugin app

This commit is contained in:
Matthias 2022-05-29 03:02:54 +02:00
parent 6c25872f81
commit e01918e607
No known key found for this signature in database
GPG Key ID: AB6D0E6C4CB65093
32 changed files with 211 additions and 23 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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:

View File

@ -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())

View File

@ -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__)

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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__)

View File

@ -9,6 +9,7 @@ from stock.models import StockItem, StockLocation
class LocatePluginTests(InvenTreeAPITestCase): class LocatePluginTests(InvenTreeAPITestCase):
"""Tests for LocateMixin."""
fixtures = [ fixtures = [
'category', 'category',

View File

@ -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()

View File

@ -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',

View File

@ -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."""

View File

@ -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

View File

@ -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}'

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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")

View File

@ -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'),

View File

@ -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

View File

@ -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}")

View File

@ -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', '')

View File

@ -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 = []

View File

@ -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'

View File

@ -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):

View File

@ -1,3 +1,5 @@
"""Views for plugin app."""
import logging import logging
import sys import sys
import traceback import traceback