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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,5 +23,6 @@ class EventMixin:
MIXIN_NAME = 'Events'
def __init__(self):
"""Register the mixin."""
super().__init__()
self.add_mixin('events', True, __class__)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ class SimpleActionPluginTests(InvenTreeTestCase):
"""Tests for SampleIntegrationPlugin."""
def setUp(self):
"""Setup for tests."""
super().setUp()
self.plugin = SimpleActionPlugin()

View File

@ -8,6 +8,7 @@ from InvenTree.api_tester import InvenTreeAPITestCase
class TestInvenTreeBarcode(InvenTreeAPITestCase):
"""Tests for the integrated InvenTreeBarcode barcode plugin."""
fixtures = [
'category',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@ class PluginTemplateLoader(FilesystemLoader):
"""
def get_dirs(self):
"""Returns all template dir paths in plugins."""
dirname = 'templates'
template_dirs = []

View File

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

View File

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

View File

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