mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into not-working-tests
This commit is contained in:
commit
bfd8b1e3b5
2
.github/workflows/qc_checks.yaml
vendored
2
.github/workflows/qc_checks.yaml
vendored
@ -12,7 +12,7 @@ on:
|
||||
- l10*
|
||||
|
||||
env:
|
||||
python_version: 3.8
|
||||
python_version: 3.9
|
||||
node_version: 16
|
||||
|
||||
server_start_sleep: 60
|
||||
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue seems stale. Please react to show this is still important.'
|
||||
stale-pr-message: 'This PR seems stale. Please react to show this is still important.'
|
||||
stale-issue-label: 'no-activity'
|
||||
stale-pr-label: 'no-activity'
|
||||
stale-issue-label: 'inactive'
|
||||
stale-pr-label: 'inactive'
|
||||
start-date: '2022-01-01'
|
||||
exempt-all-milestones: true
|
||||
|
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@ -21,10 +21,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python 3.7
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: 3.9
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
9
.github/workflows/welcome.yml
vendored
9
.github/workflows/welcome.yml
vendored
@ -16,5 +16,10 @@ jobs:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: 'Welcome to InvenTree! Please check the [contributing docs](https://inventree.readthedocs.io/en/latest/contribute/) on how to help.\nIf you experience setup / install issues please read all [install docs]( https://inventree.readthedocs.io/en/latest/start/intro/).'
|
||||
pr-message: 'This is your first PR, welcome!\nPlease check [Contributing](https://github.com/inventree/InvenTree/blob/master/CONTRIBUTING.md) to make sure your submission fits our general code-style and workflow.\nMake sure to document why this PR is needed and to link connected issues so we can review it faster.'
|
||||
issue-message: |
|
||||
Welcome to InvenTree! Please check the [contributing docs](https://inventree.readthedocs.io/en/latest/contribute/) on how to help.
|
||||
If you experience setup / install issues please read all [install docs]( https://inventree.readthedocs.io/en/latest/start/intro/).
|
||||
pr-message: |
|
||||
This is your first PR, welcome!
|
||||
Please check [Contributing](https://github.com/inventree/InvenTree/blob/master/CONTRIBUTING.md) to make sure your submission fits our general code-style and workflow.
|
||||
Make sure to document why this PR is needed and to link connected issues so we can review it faster.
|
||||
|
@ -13,15 +13,11 @@ from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import filters
|
||||
|
||||
from rest_framework import permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .views import AjaxView
|
||||
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
|
||||
from .status import is_worker_running
|
||||
|
||||
from plugin import registry
|
||||
|
||||
|
||||
class InfoView(AjaxView):
|
||||
""" Simple JSON endpoint for InvenTree information.
|
||||
@ -119,40 +115,3 @@ class AttachmentMixin:
|
||||
attachment = serializer.save()
|
||||
attachment.user = self.request.user
|
||||
attachment.save()
|
||||
|
||||
|
||||
class ActionPluginView(APIView):
|
||||
"""
|
||||
Endpoint for running custom action plugins.
|
||||
"""
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
action = request.data.get('action', None)
|
||||
|
||||
data = request.data.get('data', None)
|
||||
|
||||
if action is None:
|
||||
return Response({
|
||||
'error': _("No action specified")
|
||||
})
|
||||
|
||||
action_plugins = registry.with_mixin('action')
|
||||
for plugin in action_plugins:
|
||||
if plugin.action_name() == action:
|
||||
# TODO @matmair use easier syntax once InvenTree 0.7.0 is released
|
||||
plugin.init(request.user, data=data)
|
||||
|
||||
plugin.perform_action()
|
||||
|
||||
return Response(plugin.get_response())
|
||||
|
||||
# If we got to here, no matching action was found
|
||||
return Response({
|
||||
'error': _("No matching action found"),
|
||||
"action": action,
|
||||
})
|
||||
|
@ -197,8 +197,6 @@ class InvenTreeConfig(AppConfig):
|
||||
logger.info(f'User {str(new_user)} was created!')
|
||||
except IntegrityError as _e:
|
||||
logger.warning(f'The user "{add_user}" could not be created due to the following error:\n{str(_e)}')
|
||||
if settings.TESTING_ENV:
|
||||
raise _e
|
||||
|
||||
# do not try again
|
||||
settings.USER_ADDED = True
|
||||
|
@ -96,6 +96,12 @@ class HTMLAPITests(TestCase):
|
||||
response = self.client.get(url, HTTP_ACCEPT='text/html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_not_found(self):
|
||||
"""Test that the NotFoundView is working"""
|
||||
|
||||
response = self.client.get('/api/anc')
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
class APITests(InvenTreeAPITestCase):
|
||||
""" Tests for the InvenTree API """
|
||||
|
@ -32,9 +32,6 @@ class MiddlewareTests(TestCase):
|
||||
# logout
|
||||
self.client.logout()
|
||||
|
||||
# check that static files go through
|
||||
self.check_path('/static/css/inventree.css')
|
||||
|
||||
# check that account things go through
|
||||
self.check_path(reverse('account_login'))
|
||||
|
||||
@ -62,5 +59,8 @@ class MiddlewareTests(TestCase):
|
||||
# request with token
|
||||
self.check_path(reverse('settings.js'), HTTP_Authorization=f'Token {token}')
|
||||
|
||||
# Request with broken token
|
||||
self.check_path(reverse('settings.js'), 401, HTTP_Authorization='Token abcd123')
|
||||
|
||||
# should still fail without token
|
||||
self.check_path(reverse('settings.js'), 401)
|
||||
|
@ -451,6 +451,11 @@ class TestSettings(TestCase):
|
||||
self.user_mdl = get_user_model()
|
||||
self.env = EnvironmentVarGuard()
|
||||
|
||||
# Create a user for auth
|
||||
user = get_user_model()
|
||||
self.user = user.objects.create_superuser('testuser1', 'test1@testing.com', 'password1')
|
||||
self.client.login(username='testuser1', password='password1')
|
||||
|
||||
def run_reload(self):
|
||||
from plugin import registry
|
||||
|
||||
@ -467,23 +472,49 @@ class TestSettings(TestCase):
|
||||
|
||||
# nothing set
|
||||
self.run_reload()
|
||||
self.assertEqual(user_count(), 0)
|
||||
self.assertEqual(user_count(), 1)
|
||||
|
||||
# not enough set
|
||||
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
|
||||
self.run_reload()
|
||||
self.assertEqual(user_count(), 0)
|
||||
self.assertEqual(user_count(), 1)
|
||||
|
||||
# enough set
|
||||
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
|
||||
self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email
|
||||
self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password
|
||||
self.run_reload()
|
||||
self.assertEqual(user_count(), 1)
|
||||
self.assertEqual(user_count(), 2)
|
||||
|
||||
# create user manually
|
||||
self.user_mdl.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
self.assertEqual(user_count(), 3)
|
||||
# check it will not be created again
|
||||
self.env.set('INVENTREE_ADMIN_USER', 'testuser')
|
||||
self.env.set('INVENTREE_ADMIN_EMAIL', 'test@testing.com')
|
||||
self.env.set('INVENTREE_ADMIN_PASSWORD', 'password')
|
||||
self.run_reload()
|
||||
self.assertEqual(user_count(), 3)
|
||||
|
||||
# make sure to clean up
|
||||
settings.TESTING_ENV = False
|
||||
|
||||
def test_initial_install(self):
|
||||
"""Test if install of plugins on startup works"""
|
||||
from plugin import registry
|
||||
|
||||
# Check an install run
|
||||
response = registry.install_plugin_file()
|
||||
self.assertEqual(response, 'first_run')
|
||||
|
||||
# Set dynamic setting to True and rerun to launch install
|
||||
InvenTreeSetting.set_setting('PLUGIN_ON_STARTUP', True, self.user)
|
||||
registry.reload_plugins()
|
||||
|
||||
# Check that there was anotehr run
|
||||
response = registry.install_plugin_file()
|
||||
self.assertEqual(response, True)
|
||||
|
||||
def test_helpers_cfg_file(self):
|
||||
# normal run - not configured
|
||||
self.assertIn('InvenTree/InvenTree/config.yaml', config.get_config_file())
|
||||
|
@ -27,7 +27,7 @@ from order.api import order_api_urls
|
||||
from label.api import label_api_urls
|
||||
from report.api import report_api_urls
|
||||
from plugin.api import plugin_api_urls
|
||||
from plugin.barcode import barcode_api_urls
|
||||
from users.api import user_urls
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
@ -45,20 +45,11 @@ from .views import DynamicJsView
|
||||
from .views import NotificationsView
|
||||
|
||||
from .api import InfoView, NotFoundView
|
||||
from .api import ActionPluginView
|
||||
|
||||
from users.api import user_urls
|
||||
|
||||
admin.site.site_header = "InvenTree Admin"
|
||||
|
||||
apipatterns = []
|
||||
|
||||
if settings.PLUGINS_ENABLED:
|
||||
apipatterns.append(
|
||||
re_path(r'^plugin/', include(plugin_api_urls))
|
||||
)
|
||||
|
||||
apipatterns += [
|
||||
apipatterns = [
|
||||
re_path(r'^settings/', include(settings_api_urls)),
|
||||
re_path(r'^part/', include(part_api_urls)),
|
||||
re_path(r'^bom/', include(bom_api_urls)),
|
||||
@ -68,13 +59,10 @@ apipatterns += [
|
||||
re_path(r'^order/', include(order_api_urls)),
|
||||
re_path(r'^label/', include(label_api_urls)),
|
||||
re_path(r'^report/', include(report_api_urls)),
|
||||
|
||||
# User URLs
|
||||
re_path(r'^user/', include(user_urls)),
|
||||
|
||||
# Plugin endpoints
|
||||
re_path(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
|
||||
re_path(r'^barcode/', include(barcode_api_urls)),
|
||||
path('', include(plugin_api_urls)),
|
||||
|
||||
# Webhook enpoint
|
||||
path('', include(common_api_urls)),
|
||||
|
@ -271,9 +271,9 @@ class BaseInvenTreeSetting(models.Model):
|
||||
plugin = kwargs.get('plugin', None)
|
||||
|
||||
if plugin is not None:
|
||||
from plugin import InvenTreePluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
|
||||
if issubclass(plugin.__class__, InvenTreePluginBase):
|
||||
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||
plugin = plugin.plugin_config()
|
||||
|
||||
filters['plugin'] = plugin
|
||||
@ -375,9 +375,9 @@ class BaseInvenTreeSetting(models.Model):
|
||||
filters['user'] = user
|
||||
|
||||
if plugin is not None:
|
||||
from plugin import InvenTreePluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
|
||||
if issubclass(plugin.__class__, InvenTreePluginBase):
|
||||
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||
filters['plugin'] = plugin.plugin_config()
|
||||
else:
|
||||
filters['plugin'] = plugin
|
||||
|
@ -108,7 +108,7 @@ class NotificationMethod:
|
||||
return False
|
||||
|
||||
# Check if method globally enabled
|
||||
plg_instance = registry.plugins.get(plg_cls.PLUGIN_NAME.lower())
|
||||
plg_instance = registry.plugins.get(plg_cls.NAME.lower())
|
||||
if plg_instance and not plg_instance.get_setting(self.GLOBAL_SETTING):
|
||||
return True
|
||||
|
||||
|
@ -10,7 +10,8 @@ from django.urls import reverse
|
||||
|
||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
from InvenTree.helpers import str2bool
|
||||
from plugin.models import NotificationUserSetting
|
||||
from plugin.models import NotificationUserSetting, PluginConfig
|
||||
from plugin import registry
|
||||
|
||||
from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint, WebhookMessage, NotificationEntry, ColorTheme
|
||||
from .api import WebhookView
|
||||
@ -477,15 +478,36 @@ class PluginSettingsApiTest(InvenTreeAPITestCase):
|
||||
|
||||
self.get(url, expected_code=200)
|
||||
|
||||
def test_invalid_plugin_slug(self):
|
||||
"""Test that an invalid plugin slug returns a 404"""
|
||||
def test_valid_plugin_slug(self):
|
||||
"""Test that an valid plugin slug runs through"""
|
||||
# load plugin configs
|
||||
fixtures = PluginConfig.objects.all()
|
||||
if not fixtures:
|
||||
registry.reload_plugins()
|
||||
fixtures = PluginConfig.objects.all()
|
||||
|
||||
# get data
|
||||
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'API_KEY'})
|
||||
response = self.get(url, expected_code=200)
|
||||
|
||||
# check the right setting came through
|
||||
self.assertTrue(response.data['key'], 'API_KEY')
|
||||
self.assertTrue(response.data['plugin'], 'sample')
|
||||
self.assertTrue(response.data['type'], 'string')
|
||||
self.assertTrue(response.data['description'], 'Key required for accessing external API')
|
||||
|
||||
# Failure mode tests
|
||||
|
||||
# Non - exsistant plugin
|
||||
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'doesnotexist', 'key': 'doesnotmatter'})
|
||||
|
||||
response = self.get(url, expected_code=404)
|
||||
|
||||
self.assertIn("Plugin 'doesnotexist' not installed", str(response.data))
|
||||
|
||||
# Wrong key
|
||||
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'doesnotexsist'})
|
||||
response = self.get(url, expected_code=404)
|
||||
self.assertIn("Plugin 'sample' has no setting matching 'doesnotexsist'", str(response.data))
|
||||
|
||||
def test_invalid_setting_key(self):
|
||||
"""Test that an invalid setting key returns a 404"""
|
||||
...
|
||||
|
@ -156,7 +156,7 @@ class LabelPrintMixin:
|
||||
|
||||
# Offload a background task to print the provided label
|
||||
offload_task(
|
||||
'plugin.events.print_label',
|
||||
'plugin.base.label.label.print_label',
|
||||
plugin.plugin_slug(),
|
||||
image,
|
||||
label_instance=label_instance,
|
||||
|
@ -7,13 +7,12 @@ import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.apps import apps
|
||||
from django.urls import reverse
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from InvenTree.helpers import validateFilterString
|
||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
|
||||
from .models import StockItemLabel, StockLocationLabel, PartLabel
|
||||
from .models import StockItemLabel, StockLocationLabel
|
||||
from stock.models import StockItem
|
||||
|
||||
|
||||
@ -86,13 +85,3 @@ class LabelTest(InvenTreeAPITestCase):
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
validateFilterString(bad_filter_string, model=StockItem)
|
||||
|
||||
def test_label_rendering(self):
|
||||
"""Test label rendering"""
|
||||
|
||||
labels = PartLabel.objects.all()
|
||||
part = PartLabel.objects.first()
|
||||
|
||||
for label in labels:
|
||||
url = reverse('api-part-label-print', kwargs={'pk': label.pk})
|
||||
self.get(f'{url}?parts={part.pk}', expected_code=200)
|
||||
|
@ -3,17 +3,14 @@ Utility file to enable simper imports
|
||||
"""
|
||||
|
||||
from .registry import registry
|
||||
from .plugin import InvenTreePluginBase
|
||||
from .integration import IntegrationPluginBase
|
||||
from .action import ActionPlugin
|
||||
|
||||
from .plugin import InvenTreePlugin, IntegrationPluginBase
|
||||
from .helpers import MixinNotImplementedError, MixinImplementationError
|
||||
|
||||
__all__ = [
|
||||
'ActionPlugin',
|
||||
'IntegrationPluginBase',
|
||||
'InvenTreePluginBase',
|
||||
'registry',
|
||||
|
||||
'InvenTreePlugin',
|
||||
IntegrationPluginBase,
|
||||
'MixinNotImplementedError',
|
||||
'MixinImplementationError',
|
||||
]
|
||||
|
@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Class for ActionPlugin"""
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from plugin.builtin.action.mixins import ActionMixin
|
||||
import plugin.integration
|
||||
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class ActionPlugin(ActionMixin, plugin.integration.IntegrationPluginBase):
|
||||
"""
|
||||
Legacy action definition - will be replaced
|
||||
Please use the new Integration Plugin API and the Action mixin
|
||||
"""
|
||||
# TODO @matmair remove this with InvenTree 0.7.0
|
||||
def __init__(self, user=None, data=None):
|
||||
warnings.warn("using the ActionPlugin is depreceated", DeprecationWarning)
|
||||
super().__init__()
|
||||
self.init(user, data)
|
@ -5,6 +5,7 @@ JSON API for the plugin app
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import include, re_path
|
||||
|
||||
from rest_framework import generics
|
||||
@ -16,6 +17,8 @@ from rest_framework.response import Response
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
from common.api import GlobalSettingsPermissions
|
||||
from plugin.base.barcodes.api import barcode_api_urls
|
||||
from plugin.base.action.api import ActionPluginView
|
||||
from plugin.models import PluginConfig, PluginSetting
|
||||
import plugin.serializers as PluginSerializers
|
||||
from plugin.registry import registry
|
||||
@ -141,7 +144,8 @@ class PluginSettingDetail(generics.RetrieveUpdateAPIView):
|
||||
plugin = registry.get_plugin(plugin_slug)
|
||||
|
||||
if plugin is None:
|
||||
raise NotFound(detail=f"Plugin '{plugin_slug}' not found")
|
||||
# This only occurs if the plugin mechanism broke
|
||||
raise NotFound(detail=f"Plugin '{plugin_slug}' not found") # pragma: no cover
|
||||
|
||||
settings = getattr(plugin, 'SETTINGS', {})
|
||||
|
||||
@ -157,6 +161,11 @@ class PluginSettingDetail(generics.RetrieveUpdateAPIView):
|
||||
|
||||
|
||||
plugin_api_urls = [
|
||||
re_path(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
|
||||
re_path(r'^barcode/', include(barcode_api_urls)),
|
||||
]
|
||||
|
||||
general_plugin_api_urls = [
|
||||
|
||||
# Plugin settings URLs
|
||||
re_path(r'^settings/', include([
|
||||
@ -174,3 +183,8 @@ plugin_api_urls = [
|
||||
# Anything else
|
||||
re_path(r'^.*$', PluginList.as_view(), name='api-plugin-list'),
|
||||
]
|
||||
|
||||
if settings.PLUGINS_ENABLED:
|
||||
plugin_api_urls.append(
|
||||
re_path(r'^plugin/', include(general_plugin_api_urls))
|
||||
)
|
||||
|
0
InvenTree/plugin/base/__init__.py
Normal file
0
InvenTree/plugin/base/__init__.py
Normal file
0
InvenTree/plugin/base/action/__init__.py
Normal file
0
InvenTree/plugin/base/action/__init__.py
Normal file
45
InvenTree/plugin/base/action/api.py
Normal file
45
InvenTree/plugin/base/action/api.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""APIs for action plugins"""
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from plugin import registry
|
||||
|
||||
|
||||
class ActionPluginView(APIView):
|
||||
"""
|
||||
Endpoint for running custom action plugins.
|
||||
"""
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
action = request.data.get('action', None)
|
||||
|
||||
data = request.data.get('data', None)
|
||||
|
||||
if action is None:
|
||||
return Response({
|
||||
'error': _("No action specified")
|
||||
})
|
||||
|
||||
action_plugins = registry.with_mixin('action')
|
||||
for plugin in action_plugins:
|
||||
if plugin.action_name() == action:
|
||||
# TODO @matmair use easier syntax once InvenTree 0.7.0 is released
|
||||
plugin.init(request.user, data=data)
|
||||
|
||||
plugin.perform_action()
|
||||
|
||||
return Response(plugin.get_response())
|
||||
|
||||
# If we got to here, no matching action was found
|
||||
return Response({
|
||||
'error': _("No matching action found"),
|
||||
"action": action,
|
||||
})
|
@ -15,16 +15,17 @@ class ActionMixin:
|
||||
"""
|
||||
MIXIN_NAME = 'Actions'
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, user=None, data=None):
|
||||
super().__init__()
|
||||
self.add_mixin('action', True, __class__)
|
||||
self.init(user, data)
|
||||
|
||||
def action_name(self):
|
||||
"""
|
||||
Action name for this plugin.
|
||||
|
||||
If the ACTION_NAME parameter is empty,
|
||||
uses the PLUGIN_NAME instead.
|
||||
uses the NAME instead.
|
||||
"""
|
||||
if self.ACTION_NAME:
|
||||
return self.ACTION_NAME
|
@ -1,34 +1,38 @@
|
||||
""" Unit tests for action plugins """
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from plugin.action import ActionPlugin
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import ActionMixin
|
||||
|
||||
|
||||
class ActionPluginTests(TestCase):
|
||||
""" Tests for ActionPlugin """
|
||||
class ActionMixinTests(TestCase):
|
||||
""" Tests for ActionMixin """
|
||||
ACTION_RETURN = 'a action was performed'
|
||||
|
||||
def setUp(self):
|
||||
self.plugin = ActionPlugin('user')
|
||||
class SimplePlugin(ActionMixin, InvenTreePlugin):
|
||||
pass
|
||||
self.plugin = SimplePlugin('user')
|
||||
|
||||
class TestActionPlugin(ActionPlugin):
|
||||
class TestActionPlugin(ActionMixin, InvenTreePlugin):
|
||||
"""a action plugin"""
|
||||
ACTION_NAME = 'abc123'
|
||||
|
||||
def perform_action(self):
|
||||
return ActionPluginTests.ACTION_RETURN + 'action'
|
||||
return ActionMixinTests.ACTION_RETURN + 'action'
|
||||
|
||||
def get_result(self):
|
||||
return ActionPluginTests.ACTION_RETURN + 'result'
|
||||
return ActionMixinTests.ACTION_RETURN + 'result'
|
||||
|
||||
def get_info(self):
|
||||
return ActionPluginTests.ACTION_RETURN + 'info'
|
||||
return ActionMixinTests.ACTION_RETURN + 'info'
|
||||
|
||||
self.action_plugin = TestActionPlugin('user')
|
||||
|
||||
class NameActionPlugin(ActionPlugin):
|
||||
PLUGIN_NAME = 'Aplugin'
|
||||
class NameActionPlugin(ActionMixin, InvenTreePlugin):
|
||||
NAME = 'Aplugin'
|
||||
|
||||
self.action_name = NameActionPlugin('user')
|
||||
|
||||
@ -59,3 +63,32 @@ class ActionPluginTests(TestCase):
|
||||
"result": self.ACTION_RETURN + 'result',
|
||||
"info": self.ACTION_RETURN + 'info',
|
||||
})
|
||||
|
||||
|
||||
class APITests(TestCase):
|
||||
""" Tests for action api """
|
||||
|
||||
def setUp(self):
|
||||
# Create a user for auth
|
||||
user = get_user_model()
|
||||
self.test_user = user.objects.create_user('testuser', 'test@testing.com', 'password')
|
||||
self.client.login(username='testuser', password='password')
|
||||
|
||||
def test_post_errors(self):
|
||||
"""Check the possible errors with post"""
|
||||
|
||||
# Test empty request
|
||||
response = self.client.post('/api/action/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
{'error': 'No action specified'}
|
||||
)
|
||||
|
||||
# Test non-exsisting action
|
||||
response = self.client.post('/api/action/', data={'action': "nonexsisting"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
{'error': 'No matching action found', 'action': 'nonexsisting'}
|
||||
)
|
0
InvenTree/plugin/base/barcodes/__init__.py
Normal file
0
InvenTree/plugin/base/barcodes/__init__.py
Normal file
@ -11,7 +11,7 @@ from stock.models import StockItem
|
||||
from stock.serializers import StockItemSerializer
|
||||
|
||||
from plugin.builtin.barcodes.inventree_barcode import InvenTreeBarcodePlugin
|
||||
from plugin.builtin.barcodes.mixins import hash_barcode
|
||||
from plugin.base.barcodes.mixins import hash_barcode
|
||||
from plugin import registry
|
||||
|
||||
|
||||
@ -237,7 +237,7 @@ class BarcodeAssign(APIView):
|
||||
|
||||
|
||||
barcode_api_urls = [
|
||||
|
||||
# Link a barcode to a part
|
||||
path('link/', BarcodeAssign.as_view(), name='api-barcode-link'),
|
||||
|
||||
# Catch-all performs barcode 'scan'
|
0
InvenTree/plugin/base/event/__init__.py
Normal file
0
InvenTree/plugin/base/event/__init__.py
Normal file
192
InvenTree/plugin/base/event/events.py
Normal file
192
InvenTree/plugin/base/event/events.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""
|
||||
Functions for triggering and responding to server side events
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch.dispatcher import receiver
|
||||
|
||||
from InvenTree.ready import canAppAccessDatabase, isImportingData
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
from plugin.registry import registry
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def trigger_event(event, *args, **kwargs):
|
||||
"""
|
||||
Trigger an event with optional arguments.
|
||||
|
||||
This event will be stored in the database,
|
||||
and the worker will respond to it later on.
|
||||
"""
|
||||
|
||||
if not settings.PLUGINS_ENABLED:
|
||||
# Do nothing if plugins are not enabled
|
||||
return
|
||||
|
||||
if not canAppAccessDatabase():
|
||||
logger.debug(f"Ignoring triggered event '{event}' - database not ready")
|
||||
return
|
||||
|
||||
logger.debug(f"Event triggered: '{event}'")
|
||||
|
||||
offload_task(
|
||||
'plugin.events.register_event',
|
||||
event,
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def register_event(event, *args, **kwargs):
|
||||
"""
|
||||
Register the event with any interested plugins.
|
||||
|
||||
Note: This function is processed by the background worker,
|
||||
as it performs multiple database access operations.
|
||||
"""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
logger.debug(f"Registering triggered event: '{event}'")
|
||||
|
||||
# Determine if there are any plugins which are interested in responding
|
||||
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'):
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
for slug, plugin in registry.plugins.items():
|
||||
|
||||
if plugin.mixin_enabled('events'):
|
||||
|
||||
config = plugin.plugin_config()
|
||||
|
||||
if config and config.active:
|
||||
|
||||
logger.debug(f"Registering callback for plugin '{slug}'")
|
||||
|
||||
# Offload a separate task for each plugin
|
||||
offload_task(
|
||||
'plugin.events.process_event',
|
||||
slug,
|
||||
event,
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def process_event(plugin_slug, event, *args, **kwargs):
|
||||
"""
|
||||
Respond to a triggered event.
|
||||
|
||||
This function is run by the background worker process.
|
||||
|
||||
This function may queue multiple functions to be handled by the background worker.
|
||||
"""
|
||||
|
||||
logger.info(f"Plugin '{plugin_slug}' is processing triggered event '{event}'")
|
||||
|
||||
plugin = registry.plugins.get(plugin_slug, None)
|
||||
|
||||
if plugin is None:
|
||||
logger.error(f"Could not find matching plugin for '{plugin_slug}'")
|
||||
return
|
||||
|
||||
plugin.process_event(event, *args, **kwargs)
|
||||
|
||||
|
||||
def allow_table_event(table_name):
|
||||
"""
|
||||
Determine if an automatic event should be fired for a given table.
|
||||
We *do not* want events to be fired for some tables!
|
||||
"""
|
||||
|
||||
if isImportingData():
|
||||
# Prevent table events during the data import process
|
||||
return False
|
||||
|
||||
table_name = table_name.lower().strip()
|
||||
|
||||
# Ignore any tables which start with these prefixes
|
||||
ignore_prefixes = [
|
||||
'account_',
|
||||
'auth_',
|
||||
'authtoken_',
|
||||
'django_',
|
||||
'error_',
|
||||
'exchange_',
|
||||
'otp_',
|
||||
'plugin_',
|
||||
'socialaccount_',
|
||||
'user_',
|
||||
'users_',
|
||||
]
|
||||
|
||||
if any([table_name.startswith(prefix) for prefix in ignore_prefixes]):
|
||||
return False
|
||||
|
||||
ignore_tables = [
|
||||
'common_notificationentry',
|
||||
'common_webhookendpoint',
|
||||
'common_webhookmessage',
|
||||
]
|
||||
|
||||
if table_name in ignore_tables:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
def after_save(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Trigger an event whenever a database entry is saved
|
||||
"""
|
||||
|
||||
table = sender.objects.model._meta.db_table
|
||||
|
||||
instance_id = getattr(instance, 'id', None)
|
||||
|
||||
if instance_id is None:
|
||||
return
|
||||
|
||||
if not allow_table_event(table):
|
||||
return
|
||||
|
||||
if created:
|
||||
trigger_event(
|
||||
f'{table}.created',
|
||||
id=instance.id,
|
||||
model=sender.__name__,
|
||||
)
|
||||
else:
|
||||
trigger_event(
|
||||
f'{table}.saved',
|
||||
id=instance.id,
|
||||
model=sender.__name__,
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete)
|
||||
def after_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Trigger an event whenever a database entry is deleted
|
||||
"""
|
||||
|
||||
table = sender.objects.model._meta.db_table
|
||||
|
||||
if not allow_table_event(table):
|
||||
return
|
||||
|
||||
trigger_event(
|
||||
f'{table}.deleted',
|
||||
model=sender.__name__,
|
||||
)
|
29
InvenTree/plugin/base/event/mixins.py
Normal file
29
InvenTree/plugin/base/event/mixins.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""Plugin mixin class for events"""
|
||||
|
||||
from plugin.helpers import MixinNotImplementedError
|
||||
|
||||
|
||||
class EventMixin:
|
||||
"""
|
||||
Mixin that provides support for responding to triggered events.
|
||||
|
||||
Implementing classes must provide a "process_event" function:
|
||||
"""
|
||||
|
||||
def process_event(self, event, *args, **kwargs):
|
||||
"""
|
||||
Function to handle events
|
||||
Must be overridden by plugin
|
||||
"""
|
||||
# Default implementation does not do anything
|
||||
raise MixinNotImplementedError
|
||||
|
||||
class MixinMeta:
|
||||
"""
|
||||
Meta options for this mixin
|
||||
"""
|
||||
MIXIN_NAME = 'Events'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.add_mixin('events', True, __class__)
|
0
InvenTree/plugin/base/integration/__init__.py
Normal file
0
InvenTree/plugin/base/integration/__init__.py
Normal file
@ -11,9 +11,8 @@ from django.db.utils import OperationalError, ProgrammingError
|
||||
|
||||
import InvenTree.helpers
|
||||
|
||||
from plugin.helpers import MixinImplementationError, MixinNotImplementedError
|
||||
from plugin.helpers import MixinImplementationError, MixinNotImplementedError, render_template
|
||||
from plugin.models import PluginConfig, PluginSetting
|
||||
from plugin.template import render_template
|
||||
from plugin.urls import PLUGIN_BASE
|
||||
|
||||
|
||||
@ -238,32 +237,6 @@ class ScheduleMixin:
|
||||
logger.warning("unregister_tasks failed, database not ready")
|
||||
|
||||
|
||||
class EventMixin:
|
||||
"""
|
||||
Mixin that provides support for responding to triggered events.
|
||||
|
||||
Implementing classes must provide a "process_event" function:
|
||||
"""
|
||||
|
||||
def process_event(self, event, *args, **kwargs):
|
||||
"""
|
||||
Function to handle events
|
||||
Must be overridden by plugin
|
||||
"""
|
||||
# Default implementation does not do anything
|
||||
raise MixinNotImplementedError
|
||||
|
||||
class MixinMeta:
|
||||
"""
|
||||
Meta options for this mixin
|
||||
"""
|
||||
MIXIN_NAME = 'Events'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.add_mixin('events', True, __class__)
|
||||
|
||||
|
||||
class UrlsMixin:
|
||||
"""
|
||||
Mixin that enables custom URLs for the plugin
|
||||
@ -396,42 +369,6 @@ class AppMixin:
|
||||
return True
|
||||
|
||||
|
||||
class LabelPrintingMixin:
|
||||
"""
|
||||
Mixin which enables direct printing of stock labels.
|
||||
|
||||
Each plugin must provide a PLUGIN_NAME attribute, which is used to uniquely identify the printer.
|
||||
|
||||
The plugin must also implement the print_label() function
|
||||
"""
|
||||
|
||||
class MixinMeta:
|
||||
"""
|
||||
Meta options for this mixin
|
||||
"""
|
||||
MIXIN_NAME = 'Label printing'
|
||||
|
||||
def __init__(self): # pragma: no cover
|
||||
super().__init__()
|
||||
self.add_mixin('labels', True, __class__)
|
||||
|
||||
def print_label(self, label, **kwargs):
|
||||
"""
|
||||
Callback to print a single label
|
||||
|
||||
Arguments:
|
||||
label: A black-and-white pillow Image object
|
||||
|
||||
kwargs:
|
||||
length: The length of the label (in mm)
|
||||
width: The width of the label (in mm)
|
||||
|
||||
"""
|
||||
|
||||
# Unimplemented (to be implemented by the particular plugin class)
|
||||
... # pragma: no cover
|
||||
|
||||
|
||||
class APICallMixin:
|
||||
"""
|
||||
Mixin that enables easier API calls for a plugin
|
||||
@ -447,15 +384,15 @@ class APICallMixin:
|
||||
|
||||
Example:
|
||||
```
|
||||
from plugin import IntegrationPluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import APICallMixin, SettingsMixin
|
||||
|
||||
|
||||
class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase):
|
||||
class SampleApiCallerPlugin(APICallMixin, SettingsMixin, InvenTreePlugin):
|
||||
'''
|
||||
A small api call sample
|
||||
'''
|
||||
PLUGIN_NAME = "Sample API Caller"
|
||||
NAME = "Sample API Caller"
|
||||
|
||||
SETTINGS = {
|
||||
'API_TOKEN': {
|
||||
@ -496,9 +433,9 @@ class APICallMixin:
|
||||
def has_api_call(self):
|
||||
"""Is the mixin ready to call external APIs?"""
|
||||
if not bool(self.API_URL_SETTING):
|
||||
raise ValueError("API_URL_SETTING must be defined")
|
||||
raise MixinNotImplementedError("API_URL_SETTING must be defined")
|
||||
if not bool(self.API_TOKEN_SETTING):
|
||||
raise ValueError("API_TOKEN_SETTING must be defined")
|
||||
raise MixinNotImplementedError("API_TOKEN_SETTING must be defined")
|
||||
return True
|
||||
|
||||
@property
|
@ -1,17 +1,14 @@
|
||||
""" Unit tests for integration plugins """
|
||||
""" Unit tests for base mixins for plugins """
|
||||
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
from django.urls import include, re_path
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from plugin import IntegrationPluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, APICallMixin
|
||||
from plugin.urls import PLUGIN_BASE
|
||||
|
||||
from plugin.samples.integration.sample import SampleIntegrationPlugin
|
||||
from plugin.helpers import MixinNotImplementedError
|
||||
|
||||
|
||||
class BaseMixinDefinition:
|
||||
@ -30,11 +27,11 @@ class SettingsMixinTest(BaseMixinDefinition, TestCase):
|
||||
TEST_SETTINGS = {'SETTING1': {'default': '123', }}
|
||||
|
||||
def setUp(self):
|
||||
class SettingsCls(SettingsMixin, IntegrationPluginBase):
|
||||
class SettingsCls(SettingsMixin, InvenTreePlugin):
|
||||
SETTINGS = self.TEST_SETTINGS
|
||||
self.mixin = SettingsCls()
|
||||
|
||||
class NoSettingsCls(SettingsMixin, IntegrationPluginBase):
|
||||
class NoSettingsCls(SettingsMixin, InvenTreePlugin):
|
||||
pass
|
||||
self.mixin_nothing = NoSettingsCls()
|
||||
|
||||
@ -65,13 +62,13 @@ class UrlsMixinTest(BaseMixinDefinition, TestCase):
|
||||
MIXIN_ENABLE_CHECK = 'has_urls'
|
||||
|
||||
def setUp(self):
|
||||
class UrlsCls(UrlsMixin, IntegrationPluginBase):
|
||||
class UrlsCls(UrlsMixin, InvenTreePlugin):
|
||||
def test():
|
||||
return 'ccc'
|
||||
URLS = [re_path('testpath', test, name='test'), ]
|
||||
self.mixin = UrlsCls()
|
||||
|
||||
class NoUrlsCls(UrlsMixin, IntegrationPluginBase):
|
||||
class NoUrlsCls(UrlsMixin, InvenTreePlugin):
|
||||
pass
|
||||
self.mixin_nothing = NoUrlsCls()
|
||||
|
||||
@ -104,7 +101,7 @@ class AppMixinTest(BaseMixinDefinition, TestCase):
|
||||
MIXIN_ENABLE_CHECK = 'has_app'
|
||||
|
||||
def setUp(self):
|
||||
class TestCls(AppMixin, IntegrationPluginBase):
|
||||
class TestCls(AppMixin, InvenTreePlugin):
|
||||
pass
|
||||
self.mixin = TestCls()
|
||||
|
||||
@ -119,30 +116,32 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase):
|
||||
MIXIN_ENABLE_CHECK = 'has_naviation'
|
||||
|
||||
def setUp(self):
|
||||
class NavigationCls(NavigationMixin, IntegrationPluginBase):
|
||||
class NavigationCls(NavigationMixin, InvenTreePlugin):
|
||||
NAVIGATION = [
|
||||
{'name': 'aa', 'link': 'plugin:test:test_view'},
|
||||
]
|
||||
NAVIGATION_TAB_NAME = 'abcd1'
|
||||
self.mixin = NavigationCls()
|
||||
|
||||
class NothingNavigationCls(NavigationMixin, IntegrationPluginBase):
|
||||
class NothingNavigationCls(NavigationMixin, InvenTreePlugin):
|
||||
pass
|
||||
self.nothing_mixin = NothingNavigationCls()
|
||||
|
||||
def test_function(self):
|
||||
# check right configuration
|
||||
self.assertEqual(self.mixin.navigation, [{'name': 'aa', 'link': 'plugin:test:test_view'}, ])
|
||||
# check wrong links fails
|
||||
with self.assertRaises(NotImplementedError):
|
||||
class NavigationCls(NavigationMixin, IntegrationPluginBase):
|
||||
NAVIGATION = ['aa', 'aa']
|
||||
NavigationCls()
|
||||
|
||||
# navigation name
|
||||
self.assertEqual(self.mixin.navigation_name, 'abcd1')
|
||||
self.assertEqual(self.nothing_mixin.navigation_name, '')
|
||||
|
||||
def test_fail(self):
|
||||
# check wrong links fails
|
||||
with self.assertRaises(NotImplementedError):
|
||||
class NavigationCls(NavigationMixin, InvenTreePlugin):
|
||||
NAVIGATION = ['aa', 'aa']
|
||||
NavigationCls()
|
||||
|
||||
|
||||
class APICallMixinTest(BaseMixinDefinition, TestCase):
|
||||
MIXIN_HUMAN_NAME = 'API calls'
|
||||
@ -150,8 +149,8 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
|
||||
MIXIN_ENABLE_CHECK = 'has_api_call'
|
||||
|
||||
def setUp(self):
|
||||
class MixinCls(APICallMixin, SettingsMixin, IntegrationPluginBase):
|
||||
PLUGIN_NAME = "Sample API Caller"
|
||||
class MixinCls(APICallMixin, SettingsMixin, InvenTreePlugin):
|
||||
NAME = "Sample API Caller"
|
||||
|
||||
SETTINGS = {
|
||||
'API_TOKEN': {
|
||||
@ -167,22 +166,23 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
|
||||
API_URL_SETTING = 'API_URL'
|
||||
API_TOKEN_SETTING = 'API_TOKEN'
|
||||
|
||||
def get_external_url(self):
|
||||
def get_external_url(self, simple: bool = True):
|
||||
'''
|
||||
returns data from the sample endpoint
|
||||
'''
|
||||
return self.api_call('api/users/2')
|
||||
return self.api_call('api/users/2', simple_response=simple)
|
||||
self.mixin = MixinCls()
|
||||
|
||||
class WrongCLS(APICallMixin, IntegrationPluginBase):
|
||||
class WrongCLS(APICallMixin, InvenTreePlugin):
|
||||
pass
|
||||
self.mixin_wrong = WrongCLS()
|
||||
|
||||
class WrongCLS2(APICallMixin, IntegrationPluginBase):
|
||||
class WrongCLS2(APICallMixin, InvenTreePlugin):
|
||||
API_URL_SETTING = 'test'
|
||||
self.mixin_wrong2 = WrongCLS2()
|
||||
|
||||
def test_function(self):
|
||||
def test_base_setup(self):
|
||||
"""Test that the base settings work"""
|
||||
# check init
|
||||
self.assertTrue(self.mixin.has_api_call)
|
||||
# api_url
|
||||
@ -192,6 +192,8 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
|
||||
headers = self.mixin.api_headers
|
||||
self.assertEqual(headers, {'Bearer': '', 'Content-Type': 'application/json'})
|
||||
|
||||
def test_args(self):
|
||||
"""Test that building up args work"""
|
||||
# api_build_url_args
|
||||
# 1 arg
|
||||
result = self.mixin.api_build_url_args({'a': 'b'})
|
||||
@ -203,88 +205,42 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
|
||||
result = self.mixin.api_build_url_args({'a': 'b', 'c': ['d', 'e', 'f', ]})
|
||||
self.assertEqual(result, '?a=b&c=d,e,f')
|
||||
|
||||
def test_api_call(self):
|
||||
"""Test that api calls work"""
|
||||
# api_call
|
||||
result = self.mixin.get_external_url()
|
||||
self.assertTrue(result)
|
||||
self.assertIn('data', result,)
|
||||
|
||||
# api_call without json conversion
|
||||
result = self.mixin.get_external_url(False)
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result.reason, 'OK')
|
||||
|
||||
# api_call with full url
|
||||
result = self.mixin.api_call('https://reqres.in/api/users/2', endpoint_is_url=True)
|
||||
self.assertTrue(result)
|
||||
|
||||
# api_call with post and data
|
||||
result = self.mixin.api_call(
|
||||
'api/users/',
|
||||
data={"name": "morpheus", "job": "leader"},
|
||||
method='POST'
|
||||
)
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result['name'], 'morpheus')
|
||||
|
||||
# api_call with filter
|
||||
result = self.mixin.api_call('api/users', url_args={'page': '2'})
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result['page'], 2)
|
||||
|
||||
def test_function_errors(self):
|
||||
"""Test function errors"""
|
||||
# wrongly defined plugins should not load
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(MixinNotImplementedError):
|
||||
self.mixin_wrong.has_api_call()
|
||||
|
||||
# cover wrong token setting
|
||||
with self.assertRaises(ValueError):
|
||||
self.mixin_wrong.has_api_call()
|
||||
|
||||
|
||||
class IntegrationPluginBaseTests(TestCase):
|
||||
""" Tests for IntegrationPluginBase """
|
||||
|
||||
def setUp(self):
|
||||
self.plugin = IntegrationPluginBase()
|
||||
|
||||
class SimpeIntegrationPluginBase(IntegrationPluginBase):
|
||||
PLUGIN_NAME = 'SimplePlugin'
|
||||
|
||||
self.plugin_simple = SimpeIntegrationPluginBase()
|
||||
|
||||
class NameIntegrationPluginBase(IntegrationPluginBase):
|
||||
PLUGIN_NAME = 'Aplugin'
|
||||
PLUGIN_SLUG = 'a'
|
||||
PLUGIN_TITLE = 'a titel'
|
||||
PUBLISH_DATE = "1111-11-11"
|
||||
AUTHOR = 'AA BB'
|
||||
DESCRIPTION = 'A description'
|
||||
VERSION = '1.2.3a'
|
||||
WEBSITE = 'http://aa.bb/cc'
|
||||
LICENSE = 'MIT'
|
||||
|
||||
self.plugin_name = NameIntegrationPluginBase()
|
||||
self.plugin_sample = SampleIntegrationPlugin()
|
||||
|
||||
def test_action_name(self):
|
||||
"""check the name definition possibilities"""
|
||||
# plugin_name
|
||||
self.assertEqual(self.plugin.plugin_name(), '')
|
||||
self.assertEqual(self.plugin_simple.plugin_name(), 'SimplePlugin')
|
||||
self.assertEqual(self.plugin_name.plugin_name(), 'Aplugin')
|
||||
|
||||
# is_sampe
|
||||
self.assertEqual(self.plugin.is_sample, False)
|
||||
self.assertEqual(self.plugin_sample.is_sample, True)
|
||||
|
||||
# slug
|
||||
self.assertEqual(self.plugin.slug, '')
|
||||
self.assertEqual(self.plugin_simple.slug, 'simpleplugin')
|
||||
self.assertEqual(self.plugin_name.slug, 'a')
|
||||
|
||||
# human_name
|
||||
self.assertEqual(self.plugin.human_name, '')
|
||||
self.assertEqual(self.plugin_simple.human_name, 'SimplePlugin')
|
||||
self.assertEqual(self.plugin_name.human_name, 'a titel')
|
||||
|
||||
# description
|
||||
self.assertEqual(self.plugin.description, '')
|
||||
self.assertEqual(self.plugin_simple.description, 'SimplePlugin')
|
||||
self.assertEqual(self.plugin_name.description, 'A description')
|
||||
|
||||
# author
|
||||
self.assertEqual(self.plugin_name.author, 'AA BB')
|
||||
|
||||
# pub_date
|
||||
self.assertEqual(self.plugin_name.pub_date, datetime(1111, 11, 11, 0, 0))
|
||||
|
||||
# version
|
||||
self.assertEqual(self.plugin.version, None)
|
||||
self.assertEqual(self.plugin_simple.version, None)
|
||||
self.assertEqual(self.plugin_name.version, '1.2.3a')
|
||||
|
||||
# website
|
||||
self.assertEqual(self.plugin.website, None)
|
||||
self.assertEqual(self.plugin_simple.website, None)
|
||||
self.assertEqual(self.plugin_name.website, 'http://aa.bb/cc')
|
||||
|
||||
# license
|
||||
self.assertEqual(self.plugin.license, None)
|
||||
self.assertEqual(self.plugin_simple.license, None)
|
||||
self.assertEqual(self.plugin_name.license, 'MIT')
|
||||
with self.assertRaises(MixinNotImplementedError):
|
||||
self.mixin_wrong2.has_api_call()
|
0
InvenTree/plugin/base/label/__init__.py
Normal file
0
InvenTree/plugin/base/label/__init__.py
Normal file
53
InvenTree/plugin/base/label/label.py
Normal file
53
InvenTree/plugin/base/label/label.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""Functions to print a label to a mixin printer"""
|
||||
import logging
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plugin.registry import registry
|
||||
import common.notifications
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def print_label(plugin_slug, label_image, label_instance=None, user=None):
|
||||
"""
|
||||
Print label with the provided plugin.
|
||||
|
||||
This task is nominally handled by the background worker.
|
||||
|
||||
If the printing fails (throws an exception) then the user is notified.
|
||||
|
||||
Arguments:
|
||||
plugin_slug: The unique slug (key) of the plugin
|
||||
label_image: A PIL.Image image object to be printed
|
||||
"""
|
||||
|
||||
logger.info(f"Plugin '{plugin_slug}' is printing a label")
|
||||
|
||||
plugin = registry.plugins.get(plugin_slug, None)
|
||||
|
||||
if plugin is None:
|
||||
logger.error(f"Could not find matching plugin for '{plugin_slug}'")
|
||||
return
|
||||
|
||||
try:
|
||||
plugin.print_label(label_image, width=label_instance.width, height=label_instance.height)
|
||||
except Exception as e:
|
||||
# Plugin threw an error - notify the user who attempted to print
|
||||
|
||||
ctx = {
|
||||
'name': _('Label printing failed'),
|
||||
'message': str(e),
|
||||
}
|
||||
|
||||
logger.error(f"Label printing failed: Sending notification to user '{user}'")
|
||||
|
||||
# Throw an error against the plugin instance
|
||||
common.notifications.trigger_notifaction(
|
||||
plugin.plugin_config(),
|
||||
'label.printing_failed',
|
||||
targets=[user],
|
||||
context=ctx,
|
||||
delivery_methods=[common.notifications.UIMessageNotification]
|
||||
)
|
39
InvenTree/plugin/base/label/mixins.py
Normal file
39
InvenTree/plugin/base/label/mixins.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""Plugin mixin classes for label plugins"""
|
||||
|
||||
from plugin.helpers import MixinNotImplementedError
|
||||
|
||||
|
||||
class LabelPrintingMixin:
|
||||
"""
|
||||
Mixin which enables direct printing of stock labels.
|
||||
|
||||
Each plugin must provide a NAME attribute, which is used to uniquely identify the printer.
|
||||
|
||||
The plugin must also implement the print_label() function
|
||||
"""
|
||||
|
||||
class MixinMeta:
|
||||
"""
|
||||
Meta options for this mixin
|
||||
"""
|
||||
MIXIN_NAME = 'Label printing'
|
||||
|
||||
def __init__(self): # pragma: no cover
|
||||
super().__init__()
|
||||
self.add_mixin('labels', True, __class__)
|
||||
|
||||
def print_label(self, label, **kwargs):
|
||||
"""
|
||||
Callback to print a single label
|
||||
|
||||
Arguments:
|
||||
label: A black-and-white pillow Image object
|
||||
|
||||
kwargs:
|
||||
length: The length of the label (in mm)
|
||||
width: The width of the label (in mm)
|
||||
|
||||
"""
|
||||
|
||||
# Unimplemented (to be implemented by the particular plugin class)
|
||||
MixinNotImplementedError('This Plugin must implement a `print_label` method')
|
@ -1,15 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""sample implementation for ActionPlugin"""
|
||||
from plugin.action import ActionPlugin
|
||||
"""sample implementation for ActionMixin"""
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import ActionMixin
|
||||
|
||||
|
||||
class SimpleActionPlugin(ActionPlugin):
|
||||
class SimpleActionPlugin(ActionMixin, InvenTreePlugin):
|
||||
"""
|
||||
An EXTREMELY simple action plugin which demonstrates
|
||||
the capability of the ActionPlugin class
|
||||
the capability of the ActionMixin class
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = "SimpleActionPlugin"
|
||||
NAME = "SimpleActionPlugin"
|
||||
ACTION_NAME = "simple"
|
||||
|
||||
def perform_action(self):
|
||||
|
@ -13,7 +13,7 @@ references model objects actually exist in the database.
|
||||
|
||||
import json
|
||||
|
||||
from plugin import IntegrationPluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import BarcodeMixin
|
||||
|
||||
from stock.models import StockItem, StockLocation
|
||||
@ -22,9 +22,9 @@ from part.models import Part
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
class InvenTreeBarcodePlugin(BarcodeMixin, IntegrationPluginBase):
|
||||
class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
||||
|
||||
PLUGIN_NAME = "InvenTreeBarcode"
|
||||
NAME = "InvenTreeBarcode"
|
||||
|
||||
def validate(self):
|
||||
"""
|
||||
|
@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from allauth.account.models import EmailAddress
|
||||
|
||||
from plugin import IntegrationPluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import BulkNotificationMethod, SettingsMixin
|
||||
import InvenTree.tasks
|
||||
|
||||
@ -15,12 +15,12 @@ class PlgMixin:
|
||||
return CoreNotificationsPlugin
|
||||
|
||||
|
||||
class CoreNotificationsPlugin(SettingsMixin, IntegrationPluginBase):
|
||||
class CoreNotificationsPlugin(SettingsMixin, InvenTreePlugin):
|
||||
"""
|
||||
Core notification methods for InvenTree
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = "CoreNotificationsPlugin"
|
||||
NAME = "CoreNotificationsPlugin"
|
||||
AUTHOR = _('InvenTree contributors')
|
||||
DESCRIPTION = _('Integrated outgoing notificaton methods')
|
||||
|
||||
|
@ -1,239 +1,9 @@
|
||||
"""
|
||||
Functions for triggering and responding to server side events
|
||||
Import helper for events
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
from plugin.base.event.events import trigger_event
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch.dispatcher import receiver
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
import common.notifications
|
||||
|
||||
from InvenTree.ready import canAppAccessDatabase, isImportingData
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
from plugin.registry import registry
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def trigger_event(event, *args, **kwargs):
|
||||
"""
|
||||
Trigger an event with optional arguments.
|
||||
|
||||
This event will be stored in the database,
|
||||
and the worker will respond to it later on.
|
||||
"""
|
||||
|
||||
if not settings.PLUGINS_ENABLED:
|
||||
# Do nothing if plugins are not enabled
|
||||
return
|
||||
|
||||
if not canAppAccessDatabase():
|
||||
logger.debug(f"Ignoring triggered event '{event}' - database not ready")
|
||||
return
|
||||
|
||||
logger.debug(f"Event triggered: '{event}'")
|
||||
|
||||
offload_task(
|
||||
'plugin.events.register_event',
|
||||
event,
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def register_event(event, *args, **kwargs):
|
||||
"""
|
||||
Register the event with any interested plugins.
|
||||
|
||||
Note: This function is processed by the background worker,
|
||||
as it performs multiple database access operations.
|
||||
"""
|
||||
|
||||
logger.debug(f"Registering triggered event: '{event}'")
|
||||
|
||||
# Determine if there are any plugins which are interested in responding
|
||||
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'):
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
for slug, plugin in registry.plugins.items():
|
||||
|
||||
if plugin.mixin_enabled('events'):
|
||||
|
||||
config = plugin.plugin_config()
|
||||
|
||||
if config and config.active:
|
||||
|
||||
logger.debug(f"Registering callback for plugin '{slug}'")
|
||||
|
||||
# Offload a separate task for each plugin
|
||||
offload_task(
|
||||
'plugin.events.process_event',
|
||||
slug,
|
||||
event,
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def process_event(plugin_slug, event, *args, **kwargs):
|
||||
"""
|
||||
Respond to a triggered event.
|
||||
|
||||
This function is run by the background worker process.
|
||||
|
||||
This function may queue multiple functions to be handled by the background worker.
|
||||
"""
|
||||
|
||||
logger.info(f"Plugin '{plugin_slug}' is processing triggered event '{event}'")
|
||||
|
||||
plugin = registry.plugins.get(plugin_slug, None)
|
||||
|
||||
if plugin is None:
|
||||
logger.error(f"Could not find matching plugin for '{plugin_slug}'")
|
||||
return
|
||||
|
||||
plugin.process_event(event, *args, **kwargs)
|
||||
|
||||
|
||||
def allow_table_event(table_name):
|
||||
"""
|
||||
Determine if an automatic event should be fired for a given table.
|
||||
We *do not* want events to be fired for some tables!
|
||||
"""
|
||||
|
||||
if isImportingData():
|
||||
# Prevent table events during the data import process
|
||||
return False
|
||||
|
||||
table_name = table_name.lower().strip()
|
||||
|
||||
# Ignore any tables which start with these prefixes
|
||||
ignore_prefixes = [
|
||||
'account_',
|
||||
'auth_',
|
||||
'authtoken_',
|
||||
'django_',
|
||||
'error_',
|
||||
'exchange_',
|
||||
'otp_',
|
||||
'plugin_',
|
||||
'socialaccount_',
|
||||
'user_',
|
||||
'users_',
|
||||
]
|
||||
|
||||
if any([table_name.startswith(prefix) for prefix in ignore_prefixes]):
|
||||
return False
|
||||
|
||||
ignore_tables = [
|
||||
'common_notificationentry',
|
||||
'common_webhookendpoint',
|
||||
'common_webhookmessage',
|
||||
]
|
||||
|
||||
if table_name in ignore_tables:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
def after_save(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Trigger an event whenever a database entry is saved
|
||||
"""
|
||||
|
||||
table = sender.objects.model._meta.db_table
|
||||
|
||||
instance_id = getattr(instance, 'id', None)
|
||||
|
||||
if instance_id is None:
|
||||
return
|
||||
|
||||
if not allow_table_event(table):
|
||||
return
|
||||
|
||||
if created:
|
||||
trigger_event(
|
||||
f'{table}.created',
|
||||
id=instance.id,
|
||||
model=sender.__name__,
|
||||
)
|
||||
else:
|
||||
trigger_event(
|
||||
f'{table}.saved',
|
||||
id=instance.id,
|
||||
model=sender.__name__,
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete)
|
||||
def after_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Trigger an event whenever a database entry is deleted
|
||||
"""
|
||||
|
||||
table = sender.objects.model._meta.db_table
|
||||
|
||||
if not allow_table_event(table):
|
||||
return
|
||||
|
||||
trigger_event(
|
||||
f'{table}.deleted',
|
||||
model=sender.__name__,
|
||||
)
|
||||
|
||||
|
||||
def print_label(plugin_slug, label_image, label_instance=None, user=None):
|
||||
"""
|
||||
Print label with the provided plugin.
|
||||
|
||||
This task is nominally handled by the background worker.
|
||||
|
||||
If the printing fails (throws an exception) then the user is notified.
|
||||
|
||||
Arguments:
|
||||
plugin_slug: The unique slug (key) of the plugin
|
||||
label_image: A PIL.Image image object to be printed
|
||||
"""
|
||||
|
||||
logger.info(f"Plugin '{plugin_slug}' is printing a label")
|
||||
|
||||
plugin = registry.plugins.get(plugin_slug, None)
|
||||
|
||||
if plugin is None:
|
||||
logger.error(f"Could not find matching plugin for '{plugin_slug}'")
|
||||
return
|
||||
|
||||
try:
|
||||
plugin.print_label(label_image, width=label_instance.width, height=label_instance.height)
|
||||
except Exception as e:
|
||||
# Plugin threw an error - notify the user who attempted to print
|
||||
|
||||
ctx = {
|
||||
'name': _('Label printing failed'),
|
||||
'message': str(e),
|
||||
}
|
||||
|
||||
logger.error(f"Label printing failed: Sending notification to user '{user}'")
|
||||
|
||||
# Throw an error against the plugin instance
|
||||
common.notifications.trigger_notifaction(
|
||||
plugin.plugin_config(),
|
||||
'label.printing_failed',
|
||||
targets=[user],
|
||||
context=ctx,
|
||||
delivery_methods=[common.notifications.UIMessageNotification]
|
||||
)
|
||||
__all__ = [
|
||||
trigger_event,
|
||||
]
|
||||
|
@ -8,12 +8,17 @@ import sysconfig
|
||||
import traceback
|
||||
import inspect
|
||||
import pkgutil
|
||||
import logging
|
||||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
# region logging / errors
|
||||
class IntegrationPluginError(Exception):
|
||||
"""
|
||||
@ -200,7 +205,7 @@ def get_plugins(pkg, baseclass):
|
||||
Return a list of all modules under a given package.
|
||||
|
||||
- Modules must be a subclass of the provided 'baseclass'
|
||||
- Modules must have a non-empty PLUGIN_NAME parameter
|
||||
- Modules must have a non-empty NAME parameter
|
||||
"""
|
||||
|
||||
plugins = []
|
||||
@ -212,8 +217,32 @@ def get_plugins(pkg, baseclass):
|
||||
# Iterate through each class in the module
|
||||
for item in get_classes(mod):
|
||||
plugin = item[1]
|
||||
if issubclass(plugin, baseclass) and plugin.PLUGIN_NAME:
|
||||
if issubclass(plugin, baseclass) and plugin.NAME:
|
||||
plugins.append(plugin)
|
||||
|
||||
return plugins
|
||||
# endregion
|
||||
|
||||
|
||||
# region templates
|
||||
def render_template(plugin, template_file, context=None):
|
||||
"""
|
||||
Locate and render a template file, available in the global template context.
|
||||
"""
|
||||
|
||||
try:
|
||||
tmp = template.loader.get_template(template_file)
|
||||
except template.TemplateDoesNotExist:
|
||||
logger.error(f"Plugin {plugin.slug} could not locate template '{template_file}'")
|
||||
|
||||
return f"""
|
||||
<div class='alert alert-block alert-danger'>
|
||||
Template file <em>{template_file}</em> does not exist.
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Render with the provided context
|
||||
html = tmp.render(context)
|
||||
|
||||
return html
|
||||
# endregion
|
||||
|
@ -1,261 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Class for IntegrationPluginBase and Mixin Base
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import inspect
|
||||
from datetime import datetime
|
||||
import pathlib
|
||||
|
||||
from django.urls.base import reverse
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import plugin.plugin as plugin_base
|
||||
from plugin.helpers import get_git_log, GitStatus
|
||||
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class MixinBase:
|
||||
"""
|
||||
Base set of mixin functions and mechanisms
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._mixinreg = {}
|
||||
self._mixins = {}
|
||||
|
||||
def add_mixin(self, key: str, fnc_enabled=True, cls=None):
|
||||
"""
|
||||
Add a mixin to the plugins registry
|
||||
"""
|
||||
|
||||
self._mixins[key] = fnc_enabled
|
||||
self.setup_mixin(key, cls=cls)
|
||||
|
||||
def setup_mixin(self, key, cls=None):
|
||||
"""
|
||||
Define mixin details for the current mixin -> provides meta details for all active mixins
|
||||
"""
|
||||
|
||||
# get human name
|
||||
human_name = getattr(cls.MixinMeta, 'MIXIN_NAME', key) if cls and hasattr(cls, 'MixinMeta') else key
|
||||
|
||||
# register
|
||||
self._mixinreg[key] = {
|
||||
'key': key,
|
||||
'human_name': human_name,
|
||||
}
|
||||
|
||||
@property
|
||||
def registered_mixins(self, with_base: bool = False):
|
||||
"""
|
||||
Get all registered mixins for the plugin
|
||||
"""
|
||||
|
||||
mixins = getattr(self, '_mixinreg', None)
|
||||
if mixins:
|
||||
# filter out base
|
||||
if not with_base and 'base' in mixins:
|
||||
del mixins['base']
|
||||
# only return dict
|
||||
mixins = [a for a in mixins.values()]
|
||||
return mixins
|
||||
|
||||
|
||||
class IntegrationPluginBase(MixinBase, plugin_base.InvenTreePluginBase):
|
||||
"""
|
||||
The IntegrationPluginBase class is used to integrate with 3rd party software
|
||||
"""
|
||||
|
||||
AUTHOR = None
|
||||
DESCRIPTION = None
|
||||
PUBLISH_DATE = None
|
||||
VERSION = None
|
||||
WEBSITE = None
|
||||
LICENSE = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.add_mixin('base')
|
||||
self.def_path = inspect.getfile(self.__class__)
|
||||
self.path = os.path.dirname(self.def_path)
|
||||
|
||||
self.define_package()
|
||||
|
||||
@property
|
||||
def _is_package(self):
|
||||
"""
|
||||
Is the plugin delivered as a package
|
||||
"""
|
||||
return getattr(self, 'is_package', False)
|
||||
|
||||
@property
|
||||
def is_sample(self):
|
||||
"""
|
||||
Is this plugin part of the samples?
|
||||
"""
|
||||
path = str(self.package_path)
|
||||
return path.startswith('plugin/samples/')
|
||||
|
||||
# region properties
|
||||
@property
|
||||
def slug(self):
|
||||
"""
|
||||
Slug of plugin
|
||||
"""
|
||||
return self.plugin_slug()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Name of plugin
|
||||
"""
|
||||
return self.plugin_name()
|
||||
|
||||
@property
|
||||
def human_name(self):
|
||||
"""
|
||||
Human readable name of plugin
|
||||
"""
|
||||
return self.plugin_title()
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""
|
||||
Description of plugin
|
||||
"""
|
||||
description = getattr(self, 'DESCRIPTION', None)
|
||||
if not description:
|
||||
description = self.plugin_name()
|
||||
return description
|
||||
|
||||
@property
|
||||
def author(self):
|
||||
"""
|
||||
Author of plugin - either from plugin settings or git
|
||||
"""
|
||||
author = getattr(self, 'AUTHOR', None)
|
||||
if not author:
|
||||
author = self.package.get('author')
|
||||
if not author:
|
||||
author = _('No author found') # pragma: no cover
|
||||
return author
|
||||
|
||||
@property
|
||||
def pub_date(self):
|
||||
"""
|
||||
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')
|
||||
else:
|
||||
pub_date = datetime.fromisoformat(str(pub_date))
|
||||
if not pub_date:
|
||||
pub_date = _('No date found') # pragma: no cover
|
||||
return pub_date
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""
|
||||
Version of plugin
|
||||
"""
|
||||
version = getattr(self, 'VERSION', None)
|
||||
return version
|
||||
|
||||
@property
|
||||
def website(self):
|
||||
"""
|
||||
Website of plugin - if set else None
|
||||
"""
|
||||
website = getattr(self, 'WEBSITE', None)
|
||||
return website
|
||||
|
||||
@property
|
||||
def license(self):
|
||||
"""
|
||||
License of plugin
|
||||
"""
|
||||
lic = getattr(self, 'LICENSE', None)
|
||||
return lic
|
||||
# endregion
|
||||
|
||||
@property
|
||||
def package_path(self):
|
||||
"""
|
||||
Path to the plugin
|
||||
"""
|
||||
if self._is_package:
|
||||
return self.__module__ # pragma: no cover
|
||||
return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR)
|
||||
|
||||
@property
|
||||
def settings_url(self):
|
||||
"""
|
||||
URL to the settings panel for this plugin
|
||||
"""
|
||||
return f'{reverse("settings")}#select-plugin-{self.slug}'
|
||||
|
||||
# region mixins
|
||||
def mixin(self, key):
|
||||
"""
|
||||
Check if mixin is registered
|
||||
"""
|
||||
return key in self._mixins
|
||||
|
||||
def mixin_enabled(self, key):
|
||||
"""
|
||||
Check if mixin is registered, enabled and ready
|
||||
"""
|
||||
if self.mixin(key):
|
||||
fnc_name = self._mixins.get(key)
|
||||
|
||||
# Allow for simple case where the mixin is "always" ready
|
||||
if fnc_name is True:
|
||||
return True
|
||||
|
||||
return getattr(self, fnc_name, True)
|
||||
return False
|
||||
# endregion
|
||||
|
||||
# region package info
|
||||
def _get_package_commit(self):
|
||||
"""
|
||||
Get last git commit for the plugin
|
||||
"""
|
||||
return get_git_log(self.def_path)
|
||||
|
||||
def _get_package_metadata(self):
|
||||
"""
|
||||
Get package metadata for plugin
|
||||
"""
|
||||
return {} # pragma: no cover # TODO add usage for package metadata
|
||||
|
||||
def define_package(self):
|
||||
"""
|
||||
Add package info of the plugin into plugins context
|
||||
"""
|
||||
package = self._get_package_metadata() if self._is_package else self._get_package_commit()
|
||||
|
||||
# process date
|
||||
if package.get('date'):
|
||||
package['date'] = datetime.fromisoformat(package.get('date'))
|
||||
|
||||
# process sign state
|
||||
sign_state = getattr(GitStatus, str(package.get('verified')), GitStatus.N)
|
||||
if sign_state.status == 0:
|
||||
self.sign_color = 'success' # pragma: no cover
|
||||
elif sign_state.status == 1:
|
||||
self.sign_color = 'warning'
|
||||
else:
|
||||
self.sign_color = 'danger' # pragma: no cover
|
||||
|
||||
# set variables
|
||||
self.package = package
|
||||
self.sign_state = sign_state
|
||||
# endregion
|
@ -2,12 +2,14 @@
|
||||
Utility class to enable simpler imports
|
||||
"""
|
||||
|
||||
from ..builtin.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin, PanelMixin
|
||||
from ..base.integration.mixins import APICallMixin, AppMixin, SettingsMixin, ScheduleMixin, UrlsMixin, NavigationMixin, PanelMixin
|
||||
|
||||
from common.notifications import SingleNotificationMethod, BulkNotificationMethod
|
||||
|
||||
from ..builtin.action.mixins import ActionMixin
|
||||
from ..builtin.barcodes.mixins import BarcodeMixin
|
||||
from ..base.action.mixins import ActionMixin
|
||||
from ..base.barcodes.mixins import BarcodeMixin
|
||||
from ..base.event.mixins import EventMixin
|
||||
from ..base.label.mixins import LabelPrintingMixin
|
||||
|
||||
__all__ = [
|
||||
'APICallMixin',
|
||||
|
@ -4,14 +4,16 @@ Plugin model definitions
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import warnings
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
|
||||
import common.models
|
||||
|
||||
from plugin import InvenTreePluginBase, registry
|
||||
from plugin import InvenTreePlugin, registry
|
||||
|
||||
|
||||
class PluginConfig(models.Model):
|
||||
@ -59,7 +61,7 @@ class PluginConfig(models.Model):
|
||||
|
||||
try:
|
||||
return self.plugin._mixinreg
|
||||
except (AttributeError, ValueError):
|
||||
except (AttributeError, ValueError): # pragma: no cover
|
||||
return {}
|
||||
|
||||
# functions
|
||||
@ -97,6 +99,8 @@ class PluginConfig(models.Model):
|
||||
if not reload:
|
||||
if (self.active is False and self.__org_active is True) or \
|
||||
(self.active is True and self.__org_active is False):
|
||||
if settings.PLUGIN_TESTING:
|
||||
warnings.warn('A reload was triggered')
|
||||
registry.reload_plugins()
|
||||
|
||||
return ret
|
||||
@ -141,7 +145,7 @@ class PluginSetting(common.models.BaseInvenTreeSetting):
|
||||
|
||||
if plugin:
|
||||
|
||||
if issubclass(plugin.__class__, InvenTreePluginBase):
|
||||
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||
plugin = plugin.plugin_config()
|
||||
|
||||
kwargs['settings'] = registry.mixins_settings.get(plugin.key, {})
|
||||
|
@ -2,33 +2,71 @@
|
||||
"""
|
||||
Base Class for InvenTree plugins
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import inspect
|
||||
from datetime import datetime
|
||||
import pathlib
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.urls.base import reverse
|
||||
|
||||
from plugin.helpers import get_git_log, GitStatus
|
||||
|
||||
|
||||
class InvenTreePluginBase():
|
||||
"""
|
||||
Base class for a plugin
|
||||
DO NOT USE THIS DIRECTLY, USE plugin.IntegrationPluginBase
|
||||
"""
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
class MetaBase:
|
||||
"""Base class for a plugins metadata"""
|
||||
|
||||
# Override the plugin name for each concrete plugin instance
|
||||
PLUGIN_NAME = ''
|
||||
NAME = ''
|
||||
SLUG = None
|
||||
TITLE = None
|
||||
|
||||
PLUGIN_SLUG = None
|
||||
def get_meta_value(self, key: str, old_key: str = None, __default=None):
|
||||
"""Reference a meta item with a key
|
||||
|
||||
PLUGIN_TITLE = None
|
||||
Args:
|
||||
key (str): key for the value
|
||||
old_key (str, optional): depreceated key - will throw warning
|
||||
__default (optional): Value if nothing with key can be found. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Value referenced with key, old_key or __default if set and not value found
|
||||
"""
|
||||
value = getattr(self, key, None)
|
||||
|
||||
# The key was not used
|
||||
if old_key and value is None:
|
||||
value = getattr(self, old_key, None)
|
||||
|
||||
# Sound of a warning if old_key worked
|
||||
if value:
|
||||
warnings.warn(f'Usage of {old_key} was depreciated in 0.7.0 in favour of {key}', DeprecationWarning)
|
||||
|
||||
# Use __default if still nothing set
|
||||
if (value is None) and __default:
|
||||
return __default
|
||||
return value
|
||||
|
||||
def plugin_name(self):
|
||||
"""
|
||||
Name of plugin
|
||||
"""
|
||||
return self.PLUGIN_NAME
|
||||
return self.get_meta_value('NAME', 'PLUGIN_NAME')
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Name of plugin
|
||||
"""
|
||||
return self.plugin_name()
|
||||
|
||||
def plugin_slug(self):
|
||||
"""
|
||||
@ -36,22 +74,35 @@ class InvenTreePluginBase():
|
||||
If not set plugin name slugified
|
||||
"""
|
||||
|
||||
slug = getattr(self, 'PLUGIN_SLUG', None)
|
||||
|
||||
if slug is None:
|
||||
slug = self.get_meta_value('SLUG', 'PLUGIN_SLUG', None)
|
||||
if not slug:
|
||||
slug = self.plugin_name()
|
||||
|
||||
return slugify(slug.lower())
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
"""
|
||||
Slug of plugin
|
||||
"""
|
||||
return self.plugin_slug()
|
||||
|
||||
def plugin_title(self):
|
||||
"""
|
||||
Title of plugin
|
||||
"""
|
||||
|
||||
if self.PLUGIN_TITLE:
|
||||
return self.PLUGIN_TITLE
|
||||
else:
|
||||
return self.plugin_name()
|
||||
title = self.get_meta_value('TITLE', 'PLUGIN_TITLE', None)
|
||||
if title:
|
||||
return title
|
||||
return self.plugin_name()
|
||||
|
||||
@property
|
||||
def human_name(self):
|
||||
"""
|
||||
Human readable name of plugin
|
||||
"""
|
||||
return self.plugin_title()
|
||||
|
||||
def plugin_config(self):
|
||||
"""
|
||||
@ -83,11 +134,230 @@ class InvenTreePluginBase():
|
||||
return False # pragma: no cover
|
||||
|
||||
|
||||
# TODO @matmair remove after InvenTree 0.7.0 release
|
||||
class InvenTreePlugin(InvenTreePluginBase):
|
||||
class MixinBase:
|
||||
"""
|
||||
This is here for leagcy reasons and will be removed in the next major release
|
||||
Base set of mixin functions and mechanisms
|
||||
"""
|
||||
def __init__(self): # pragma: no cover
|
||||
warnings.warn("Using the InvenTreePlugin is depreceated", DeprecationWarning)
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._mixinreg = {}
|
||||
self._mixins = {}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def mixin(self, key):
|
||||
"""
|
||||
Check if mixin is registered
|
||||
"""
|
||||
return key in self._mixins
|
||||
|
||||
def mixin_enabled(self, key):
|
||||
"""
|
||||
Check if mixin is registered, enabled and ready
|
||||
"""
|
||||
if self.mixin(key):
|
||||
fnc_name = self._mixins.get(key)
|
||||
|
||||
# Allow for simple case where the mixin is "always" ready
|
||||
if fnc_name is True:
|
||||
return True
|
||||
|
||||
return getattr(self, fnc_name, True)
|
||||
return False
|
||||
|
||||
def add_mixin(self, key: str, fnc_enabled=True, cls=None):
|
||||
"""
|
||||
Add a mixin to the plugins registry
|
||||
"""
|
||||
|
||||
self._mixins[key] = fnc_enabled
|
||||
self.setup_mixin(key, cls=cls)
|
||||
|
||||
def setup_mixin(self, key, cls=None):
|
||||
"""
|
||||
Define mixin details for the current mixin -> provides meta details for all active mixins
|
||||
"""
|
||||
|
||||
# get human name
|
||||
human_name = getattr(cls.MixinMeta, 'MIXIN_NAME', key) if cls and hasattr(cls, 'MixinMeta') else key
|
||||
|
||||
# register
|
||||
self._mixinreg[key] = {
|
||||
'key': key,
|
||||
'human_name': human_name,
|
||||
}
|
||||
|
||||
@property
|
||||
def registered_mixins(self, with_base: bool = False):
|
||||
"""
|
||||
Get all registered mixins for the plugin
|
||||
"""
|
||||
|
||||
mixins = getattr(self, '_mixinreg', None)
|
||||
if mixins:
|
||||
# filter out base
|
||||
if not with_base and 'base' in mixins:
|
||||
del mixins['base']
|
||||
# only return dict
|
||||
mixins = [a for a in mixins.values()]
|
||||
return mixins
|
||||
|
||||
|
||||
class InvenTreePlugin(MixinBase, MetaBase):
|
||||
"""
|
||||
The InvenTreePlugin class is used to integrate with 3rd party software
|
||||
|
||||
DO NOT USE THIS DIRECTLY, USE plugin.InvenTreePlugin
|
||||
"""
|
||||
|
||||
AUTHOR = None
|
||||
DESCRIPTION = None
|
||||
PUBLISH_DATE = None
|
||||
VERSION = None
|
||||
WEBSITE = None
|
||||
LICENSE = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.add_mixin('base')
|
||||
self.def_path = inspect.getfile(self.__class__)
|
||||
self.path = os.path.dirname(self.def_path)
|
||||
|
||||
self.define_package()
|
||||
|
||||
# region properties
|
||||
@property
|
||||
def description(self):
|
||||
"""
|
||||
Description of plugin
|
||||
"""
|
||||
description = getattr(self, 'DESCRIPTION', None)
|
||||
if not description:
|
||||
description = self.plugin_name()
|
||||
return description
|
||||
|
||||
@property
|
||||
def author(self):
|
||||
"""
|
||||
Author of plugin - either from plugin settings or git
|
||||
"""
|
||||
author = getattr(self, 'AUTHOR', None)
|
||||
if not author:
|
||||
author = self.package.get('author')
|
||||
if not author:
|
||||
author = _('No author found') # pragma: no cover
|
||||
return author
|
||||
|
||||
@property
|
||||
def pub_date(self):
|
||||
"""
|
||||
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')
|
||||
else:
|
||||
pub_date = datetime.fromisoformat(str(pub_date))
|
||||
if not pub_date:
|
||||
pub_date = _('No date found') # pragma: no cover
|
||||
return pub_date
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""
|
||||
Version of plugin
|
||||
"""
|
||||
version = getattr(self, 'VERSION', None)
|
||||
return version
|
||||
|
||||
@property
|
||||
def website(self):
|
||||
"""
|
||||
Website of plugin - if set else None
|
||||
"""
|
||||
website = getattr(self, 'WEBSITE', None)
|
||||
return website
|
||||
|
||||
@property
|
||||
def license(self):
|
||||
"""
|
||||
License of plugin
|
||||
"""
|
||||
lic = getattr(self, 'LICENSE', None)
|
||||
return lic
|
||||
# endregion
|
||||
|
||||
@property
|
||||
def _is_package(self):
|
||||
"""
|
||||
Is the plugin delivered as a package
|
||||
"""
|
||||
return getattr(self, 'is_package', False)
|
||||
|
||||
@property
|
||||
def is_sample(self):
|
||||
"""
|
||||
Is this plugin part of the samples?
|
||||
"""
|
||||
path = str(self.package_path)
|
||||
return path.startswith('plugin/samples/')
|
||||
|
||||
@property
|
||||
def package_path(self):
|
||||
"""
|
||||
Path to the plugin
|
||||
"""
|
||||
if self._is_package:
|
||||
return self.__module__ # pragma: no cover
|
||||
return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR)
|
||||
|
||||
@property
|
||||
def settings_url(self):
|
||||
"""
|
||||
URL to the settings panel for this plugin
|
||||
"""
|
||||
return f'{reverse("settings")}#select-plugin-{self.slug}'
|
||||
|
||||
# region package info
|
||||
def _get_package_commit(self):
|
||||
"""
|
||||
Get last git commit for the plugin
|
||||
"""
|
||||
return get_git_log(self.def_path)
|
||||
|
||||
def _get_package_metadata(self):
|
||||
"""
|
||||
Get package metadata for plugin
|
||||
"""
|
||||
return {} # pragma: no cover # TODO add usage for package metadata
|
||||
|
||||
def define_package(self):
|
||||
"""
|
||||
Add package info of the plugin into plugins context
|
||||
"""
|
||||
package = self._get_package_metadata() if self._is_package else self._get_package_commit()
|
||||
|
||||
# process date
|
||||
if package.get('date'):
|
||||
package['date'] = datetime.fromisoformat(package.get('date'))
|
||||
|
||||
# process sign state
|
||||
sign_state = getattr(GitStatus, str(package.get('verified')), GitStatus.N)
|
||||
if sign_state.status == 0:
|
||||
self.sign_color = 'success' # pragma: no cover
|
||||
elif sign_state.status == 1:
|
||||
self.sign_color = 'warning'
|
||||
else:
|
||||
self.sign_color = 'danger' # pragma: no cover
|
||||
|
||||
# set variables
|
||||
self.package = package
|
||||
self.sign_state = sign_state
|
||||
# endregion
|
||||
|
||||
|
||||
class IntegrationPluginBase(InvenTreePlugin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Send warning about using this reference"""
|
||||
# TODO remove in 0.8.0
|
||||
warnings.warn("This import is deprecated - use InvenTreePlugin", DeprecationWarning)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -12,7 +12,7 @@ import os
|
||||
import subprocess
|
||||
|
||||
from typing import OrderedDict
|
||||
from importlib import reload
|
||||
from importlib import reload, metadata
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
@ -22,16 +22,10 @@ from django.urls import clear_url_caches
|
||||
from django.contrib import admin
|
||||
from django.utils.text import slugify
|
||||
|
||||
try:
|
||||
from importlib import metadata
|
||||
except: # pragma: no cover
|
||||
import importlib_metadata as metadata
|
||||
# TODO remove when python minimum is 3.8
|
||||
|
||||
from maintenance_mode.core import maintenance_mode_on
|
||||
from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode
|
||||
|
||||
from .integration import IntegrationPluginBase
|
||||
from .plugin import InvenTreePlugin
|
||||
from .helpers import handle_error, log_error, get_plugins, IntegrationPluginError
|
||||
|
||||
|
||||
@ -57,7 +51,6 @@ class PluginsRegistry:
|
||||
self.apps_loading = True # Marks if apps were reloaded yet
|
||||
self.git_is_modern = True # Is a modern version of git available
|
||||
|
||||
# integration specific
|
||||
self.installed_apps = [] # Holds all added plugin_paths
|
||||
|
||||
# mixins
|
||||
@ -129,7 +122,7 @@ class PluginsRegistry:
|
||||
log_error({error.path: error.message}, 'load')
|
||||
blocked_plugin = error.path # we will not try to load this app again
|
||||
|
||||
# Initialize apps without any integration plugins
|
||||
# Initialize apps without any plugins
|
||||
self._clean_registry()
|
||||
self._clean_installed_apps()
|
||||
self._activate_plugins(force_reload=True)
|
||||
@ -198,9 +191,7 @@ class PluginsRegistry:
|
||||
logger.info('Finished reloading plugins')
|
||||
|
||||
def collect_plugins(self):
|
||||
"""
|
||||
Collect integration plugins from all possible ways of loading
|
||||
"""
|
||||
"""Collect plugins from all possible ways of loading"""
|
||||
|
||||
if not settings.PLUGINS_ENABLED:
|
||||
# Plugins not enabled, do nothing
|
||||
@ -210,7 +201,7 @@ class PluginsRegistry:
|
||||
|
||||
# Collect plugins from paths
|
||||
for plugin in settings.PLUGIN_DIRS:
|
||||
modules = get_plugins(importlib.import_module(plugin), IntegrationPluginBase)
|
||||
modules = get_plugins(importlib.import_module(plugin), InvenTreePlugin)
|
||||
if modules:
|
||||
[self.plugin_modules.append(item) for item in modules]
|
||||
|
||||
@ -236,7 +227,7 @@ class PluginsRegistry:
|
||||
|
||||
if settings.PLUGIN_FILE_CHECKED:
|
||||
logger.info('Plugin file was already checked')
|
||||
return
|
||||
return True
|
||||
|
||||
try:
|
||||
output = str(subprocess.check_output(['pip', 'install', '-U', '-r', settings.PLUGIN_FILE], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')
|
||||
@ -248,6 +239,7 @@ class PluginsRegistry:
|
||||
|
||||
# do not run again
|
||||
settings.PLUGIN_FILE_CHECKED = True
|
||||
return 'first_run'
|
||||
|
||||
# endregion
|
||||
|
||||
@ -280,15 +272,15 @@ class PluginsRegistry:
|
||||
|
||||
logger.info('Starting plugin initialisation')
|
||||
|
||||
# Initialize integration plugins
|
||||
# Initialize plugins
|
||||
for plugin in self.plugin_modules:
|
||||
# Check if package
|
||||
was_packaged = getattr(plugin, 'is_package', False)
|
||||
|
||||
# Check if activated
|
||||
# These checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!!
|
||||
plug_name = plugin.PLUGIN_NAME
|
||||
plug_key = plugin.PLUGIN_SLUG if getattr(plugin, 'PLUGIN_SLUG', None) else plug_name
|
||||
plug_name = plugin.NAME
|
||||
plug_key = plugin.SLUG if getattr(plugin, 'SLUG', None) else plug_name
|
||||
plug_key = slugify(plug_key) # keys are slugs!
|
||||
try:
|
||||
plugin_db_setting, _ = PluginConfig.objects.get_or_create(key=plug_key, name=plug_name)
|
||||
@ -320,7 +312,7 @@ class PluginsRegistry:
|
||||
# now we can be sure that an admin has activated the plugin
|
||||
# TODO check more stuff -> as of Nov 2021 there are not many checks in place
|
||||
# but we could enhance those to check signatures, run the plugin against a whitelist etc.
|
||||
logger.info(f'Loading integration plugin {plugin.PLUGIN_NAME}')
|
||||
logger.info(f'Loading plugin {plug_name}')
|
||||
|
||||
try:
|
||||
plugin = plugin()
|
||||
@ -328,7 +320,7 @@ class PluginsRegistry:
|
||||
# log error and raise it -> disable plugin
|
||||
handle_error(error, log_name='init')
|
||||
|
||||
logger.debug(f'Loaded integration plugin {plugin.PLUGIN_NAME}')
|
||||
logger.debug(f'Loaded plugin {plug_name}')
|
||||
|
||||
plugin.is_package = was_packaged
|
||||
|
||||
@ -343,7 +335,7 @@ class PluginsRegistry:
|
||||
|
||||
def _activate_plugins(self, force_reload=False):
|
||||
"""
|
||||
Run integration functions for all plugins
|
||||
Run activation functions for all plugins
|
||||
|
||||
:param force_reload: force reload base apps, defaults to False
|
||||
:type force_reload: bool, optional
|
||||
@ -352,22 +344,20 @@ class PluginsRegistry:
|
||||
plugins = self.plugins.items()
|
||||
logger.info(f'Found {len(plugins)} active plugins')
|
||||
|
||||
self.activate_integration_settings(plugins)
|
||||
self.activate_integration_schedule(plugins)
|
||||
self.activate_integration_app(plugins, force_reload=force_reload)
|
||||
self.activate_plugin_settings(plugins)
|
||||
self.activate_plugin_schedule(plugins)
|
||||
self.activate_plugin_app(plugins, force_reload=force_reload)
|
||||
|
||||
def _deactivate_plugins(self):
|
||||
"""
|
||||
Run integration deactivation functions for all plugins
|
||||
"""
|
||||
"""Run deactivation functions for all plugins"""
|
||||
|
||||
self.deactivate_integration_app()
|
||||
self.deactivate_integration_schedule()
|
||||
self.deactivate_integration_settings()
|
||||
self.deactivate_plugin_app()
|
||||
self.deactivate_plugin_schedule()
|
||||
self.deactivate_plugin_settings()
|
||||
# endregion
|
||||
|
||||
# region mixin specific loading ...
|
||||
def activate_integration_settings(self, plugins):
|
||||
def activate_plugin_settings(self, plugins):
|
||||
|
||||
logger.info('Activating plugin settings')
|
||||
|
||||
@ -378,7 +368,7 @@ class PluginsRegistry:
|
||||
plugin_setting = plugin.settings
|
||||
self.mixins_settings[slug] = plugin_setting
|
||||
|
||||
def deactivate_integration_settings(self):
|
||||
def deactivate_plugin_settings(self):
|
||||
|
||||
# collect all settings
|
||||
plugin_settings = {}
|
||||
@ -389,7 +379,7 @@ class PluginsRegistry:
|
||||
# clear cache
|
||||
self.mixins_settings = {}
|
||||
|
||||
def activate_integration_schedule(self, plugins):
|
||||
def activate_plugin_schedule(self, plugins):
|
||||
|
||||
logger.info('Activating plugin tasks')
|
||||
|
||||
@ -433,14 +423,14 @@ class PluginsRegistry:
|
||||
# Database might not yet be ready
|
||||
logger.warning("activate_integration_schedule failed, database not ready")
|
||||
|
||||
def deactivate_integration_schedule(self):
|
||||
def deactivate_plugin_schedule(self):
|
||||
"""
|
||||
Deactivate ScheduleMixin
|
||||
currently nothing is done
|
||||
"""
|
||||
pass
|
||||
|
||||
def activate_integration_app(self, plugins, force_reload=False):
|
||||
def activate_plugin_app(self, plugins, force_reload=False):
|
||||
"""
|
||||
Activate AppMixin plugins - add custom apps and reload
|
||||
|
||||
@ -522,13 +512,11 @@ class PluginsRegistry:
|
||||
plugin_path = '.'.join(pathlib.Path(plugin.path).relative_to(settings.BASE_DIR).parts)
|
||||
except ValueError: # pragma: no cover
|
||||
# plugin is shipped as package
|
||||
plugin_path = plugin.PLUGIN_NAME
|
||||
plugin_path = plugin.NAME
|
||||
return plugin_path
|
||||
|
||||
def deactivate_integration_app(self):
|
||||
"""
|
||||
Deactivate integration app - some magic required
|
||||
"""
|
||||
def deactivate_plugin_app(self):
|
||||
"""Deactivate AppMixin plugins - some magic required"""
|
||||
|
||||
# unregister models from admin
|
||||
for plugin_path in self.installed_apps:
|
||||
|
0
InvenTree/plugin/samples/event/__init__.py
Normal file
0
InvenTree/plugin/samples/event/__init__.py
Normal file
@ -2,18 +2,18 @@
|
||||
Sample plugin which responds to events
|
||||
"""
|
||||
|
||||
from plugin import IntegrationPluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import EventMixin
|
||||
|
||||
|
||||
class EventPluginSample(EventMixin, IntegrationPluginBase):
|
||||
class EventPluginSample(EventMixin, InvenTreePlugin):
|
||||
"""
|
||||
A sample plugin which provides supports for triggered events
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = "EventPlugin"
|
||||
PLUGIN_SLUG = "event"
|
||||
PLUGIN_TITLE = "Triggered Events"
|
||||
NAME = "EventPlugin"
|
||||
SLUG = "event"
|
||||
TITLE = "Triggered Events"
|
||||
|
||||
def process_event(self, event, *args, **kwargs):
|
||||
""" Custom event processing """
|
@ -1,19 +1,19 @@
|
||||
"""sample implementation for IntegrationPlugin"""
|
||||
from plugin import IntegrationPluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import UrlsMixin
|
||||
|
||||
|
||||
class NoIntegrationPlugin(IntegrationPluginBase):
|
||||
class NoIntegrationPlugin(InvenTreePlugin):
|
||||
"""
|
||||
An basic integration plugin
|
||||
An basic plugin
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = "NoIntegrationPlugin"
|
||||
NAME = "NoIntegrationPlugin"
|
||||
|
||||
|
||||
class WrongIntegrationPlugin(UrlsMixin, IntegrationPluginBase):
|
||||
class WrongIntegrationPlugin(UrlsMixin, InvenTreePlugin):
|
||||
"""
|
||||
An basic integration plugin
|
||||
An basic wron plugin with urls
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = "WrongIntegrationPlugin"
|
||||
NAME = "WrongIntegrationPlugin"
|
||||
|
@ -1,15 +1,15 @@
|
||||
"""
|
||||
Sample plugin for calling an external API
|
||||
"""
|
||||
from plugin import IntegrationPluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import APICallMixin, SettingsMixin
|
||||
|
||||
|
||||
class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase):
|
||||
class SampleApiCallerPlugin(APICallMixin, SettingsMixin, InvenTreePlugin):
|
||||
"""
|
||||
A small api call sample
|
||||
"""
|
||||
PLUGIN_NAME = "Sample API Caller"
|
||||
NAME = "Sample API Caller"
|
||||
|
||||
SETTINGS = {
|
||||
'API_TOKEN': {
|
||||
|
@ -1,10 +1,10 @@
|
||||
"""sample of a broken python file that will be ignored on import"""
|
||||
from plugin import IntegrationPluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
|
||||
|
||||
class BrokenFileIntegrationPlugin(IntegrationPluginBase):
|
||||
class BrokenFileIntegrationPlugin(InvenTreePlugin):
|
||||
"""
|
||||
An very broken integration plugin
|
||||
An very broken plugin
|
||||
"""
|
||||
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
"""sample of a broken integration plugin"""
|
||||
from plugin import IntegrationPluginBase
|
||||
"""sample of a broken plugin"""
|
||||
from plugin import InvenTreePlugin
|
||||
|
||||
|
||||
class BrokenIntegrationPlugin(IntegrationPluginBase):
|
||||
class BrokenIntegrationPlugin(InvenTreePlugin):
|
||||
"""
|
||||
An very broken integration plugin
|
||||
An very broken plugin
|
||||
"""
|
||||
PLUGIN_NAME = 'Test'
|
||||
PLUGIN_TITLE = 'Broken Plugin'
|
||||
PLUGIN_SLUG = 'broken'
|
||||
NAME = 'Test'
|
||||
TITLE = 'Broken Plugin'
|
||||
SLUG = 'broken'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
@ -2,21 +2,21 @@
|
||||
Sample plugin which renders custom panels on certain pages
|
||||
"""
|
||||
|
||||
from plugin import IntegrationPluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import PanelMixin, SettingsMixin
|
||||
|
||||
from part.views import PartDetail
|
||||
from stock.views import StockLocationDetail
|
||||
|
||||
|
||||
class CustomPanelSample(PanelMixin, SettingsMixin, IntegrationPluginBase):
|
||||
class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin):
|
||||
"""
|
||||
A sample plugin which renders some custom panels.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = "CustomPanelExample"
|
||||
PLUGIN_SLUG = "panel"
|
||||
PLUGIN_TITLE = "Custom Panel Example"
|
||||
NAME = "CustomPanelExample"
|
||||
SLUG = "panel"
|
||||
TITLE = "Custom Panel Example"
|
||||
DESCRIPTION = "An example plugin demonstrating how custom panels can be added to the user interface"
|
||||
VERSION = "0.1"
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
Sample implementations for IntegrationPlugin
|
||||
"""
|
||||
|
||||
from plugin import IntegrationPluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin
|
||||
|
||||
from django.http import HttpResponse
|
||||
@ -10,14 +10,14 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.urls import include, re_path
|
||||
|
||||
|
||||
class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, IntegrationPluginBase):
|
||||
class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, InvenTreePlugin):
|
||||
"""
|
||||
A full integration plugin example
|
||||
A full plugin example
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = "SampleIntegrationPlugin"
|
||||
PLUGIN_SLUG = "sample"
|
||||
PLUGIN_TITLE = "Sample Plugin"
|
||||
NAME = "SampleIntegrationPlugin"
|
||||
SLUG = "sample"
|
||||
TITLE = "Sample Plugin"
|
||||
|
||||
NAVIGATION_TAB_NAME = "Sample Nav"
|
||||
NAVIGATION_TAB_ICON = 'fas fa-plus'
|
||||
|
@ -2,7 +2,7 @@
|
||||
Sample plugin which supports task scheduling
|
||||
"""
|
||||
|
||||
from plugin import IntegrationPluginBase
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import ScheduleMixin, SettingsMixin
|
||||
|
||||
|
||||
@ -15,14 +15,14 @@ def print_world():
|
||||
print("World") # pragma: no cover
|
||||
|
||||
|
||||
class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, IntegrationPluginBase):
|
||||
class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, InvenTreePlugin):
|
||||
"""
|
||||
A sample plugin which provides support for scheduled tasks
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = "ScheduledTasksPlugin"
|
||||
PLUGIN_SLUG = "schedule"
|
||||
PLUGIN_TITLE = "Scheduled Tasks"
|
||||
NAME = "ScheduledTasksPlugin"
|
||||
SLUG = "schedule"
|
||||
TITLE = "Scheduled Tasks"
|
||||
|
||||
SCHEDULED_TASKS = {
|
||||
'member': {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from plugin import registry, IntegrationPluginBase
|
||||
from plugin import registry, InvenTreePlugin
|
||||
from plugin.helpers import MixinImplementationError
|
||||
from plugin.registry import call_function
|
||||
from plugin.mixins import ScheduleMixin
|
||||
@ -45,16 +45,20 @@ class ExampleScheduledTaskPluginTests(TestCase):
|
||||
|
||||
def test_calling(self):
|
||||
"""check if a function can be called without errors"""
|
||||
# Check with right parameters
|
||||
self.assertEqual(call_function('schedule', 'member_func'), False)
|
||||
|
||||
# Check with wrong key
|
||||
self.assertEqual(call_function('does_not_exsist', 'member_func'), None)
|
||||
|
||||
|
||||
class ScheduledTaskPluginTests(TestCase):
|
||||
""" Tests for ScheduledTaskPluginTests mixin base """
|
||||
|
||||
def test_init(self):
|
||||
"""Check that all MixinImplementationErrors raise"""
|
||||
class Base(ScheduleMixin, IntegrationPluginBase):
|
||||
PLUGIN_NAME = 'APlugin'
|
||||
class Base(ScheduleMixin, InvenTreePlugin):
|
||||
NAME = 'APlugin'
|
||||
|
||||
class NoSchedules(Base):
|
||||
"""Plugin without schedules"""
|
||||
|
@ -96,8 +96,10 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
||||
install_name.append(f'{packagename}@{url}')
|
||||
else:
|
||||
install_name.append(url)
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
# using a custom package repositories
|
||||
# This is only for pypa compliant directory services (all current are tested above)
|
||||
# and not covered by tests.
|
||||
install_name.append('-i')
|
||||
install_name.append(url)
|
||||
install_name.append(packagename)
|
||||
|
@ -1,19 +1,12 @@
|
||||
"""
|
||||
load templates for loaded plugins
|
||||
"""
|
||||
"""Load templates for loaded plugins"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from django import template
|
||||
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
||||
|
||||
from plugin import registry
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class PluginTemplateLoader(FilesystemLoader):
|
||||
"""
|
||||
A custom template loader which allows loading of templates from installed plugins.
|
||||
@ -38,25 +31,3 @@ class PluginTemplateLoader(FilesystemLoader):
|
||||
template_dirs.append(new_path)
|
||||
|
||||
return tuple(template_dirs)
|
||||
|
||||
|
||||
def render_template(plugin, template_file, context=None):
|
||||
"""
|
||||
Locate and render a template file, available in the global template context.
|
||||
"""
|
||||
|
||||
try:
|
||||
tmp = template.loader.get_template(template_file)
|
||||
except template.TemplateDoesNotExist:
|
||||
logger.error(f"Plugin {plugin.slug} could not locate template '{template_file}'")
|
||||
|
||||
return f"""
|
||||
<div class='alert alert-block alert-danger'>
|
||||
Template file <em>{template_file}</em> does not exist.
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Render with the provided context
|
||||
html = tmp.render(context)
|
||||
|
||||
return html
|
||||
|
@ -16,7 +16,7 @@ register = template.Library()
|
||||
@register.simple_tag()
|
||||
def plugin_list(*args, **kwargs):
|
||||
"""
|
||||
List of all installed integration plugins
|
||||
List of all installed plugins
|
||||
"""
|
||||
return registry.plugins
|
||||
|
||||
@ -24,7 +24,7 @@ def plugin_list(*args, **kwargs):
|
||||
@register.simple_tag()
|
||||
def inactive_plugin_list(*args, **kwargs):
|
||||
"""
|
||||
List of all inactive integration plugins
|
||||
List of all inactive plugins
|
||||
"""
|
||||
return registry.plugins_inactive
|
||||
|
||||
|
@ -45,6 +45,14 @@ class PluginDetailAPITest(InvenTreeAPITestCase):
|
||||
}, expected_code=201).data
|
||||
self.assertEqual(data['success'], True)
|
||||
|
||||
# valid - github url and packagename
|
||||
data = self.post(url, {
|
||||
'confirm': True,
|
||||
'url': self.PKG_URL,
|
||||
'packagename': 'minimal',
|
||||
}, expected_code=201).data
|
||||
self.assertEqual(data['success'], True)
|
||||
|
||||
# invalid tries
|
||||
# no input
|
||||
self.post(url, {}, expected_code=400)
|
||||
@ -124,3 +132,30 @@ class PluginDetailAPITest(InvenTreeAPITestCase):
|
||||
'_save': 'Save',
|
||||
}, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_model(self):
|
||||
"""
|
||||
Test the PluginConfig model
|
||||
"""
|
||||
from plugin.models import PluginConfig
|
||||
from plugin import registry
|
||||
|
||||
fixtures = PluginConfig.objects.all()
|
||||
|
||||
# check if plugins were registered
|
||||
if not fixtures:
|
||||
registry.reload_plugins()
|
||||
fixtures = PluginConfig.objects.all()
|
||||
|
||||
# check mixin registry
|
||||
plg = fixtures.first()
|
||||
mixin_dict = plg.mixins()
|
||||
self.assertIn('base', mixin_dict)
|
||||
self.assertDictContainsSubset({'base': {'key': 'base', 'human_name': 'base'}}, mixin_dict)
|
||||
|
||||
# check reload on save
|
||||
with self.assertWarns(Warning) as cm:
|
||||
plg_inactive = fixtures.filter(active=False).first()
|
||||
plg_inactive.active = True
|
||||
plg_inactive.save()
|
||||
self.assertEqual(cm.warning.args[0], 'A reload was triggered')
|
||||
|
23
InvenTree/plugin/test_helpers.py
Normal file
23
InvenTree/plugin/test_helpers.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""Unit tests for helpers.py"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from .helpers import render_template
|
||||
|
||||
|
||||
class HelperTests(TestCase):
|
||||
"""Tests for helpers"""
|
||||
|
||||
def test_render_template(self):
|
||||
"""Check if render_template helper works"""
|
||||
class ErrorSource:
|
||||
slug = 'sampleplg'
|
||||
|
||||
# working sample
|
||||
response = render_template(ErrorSource(), 'sample/sample.html', {'abc': 123})
|
||||
self.assertEqual(response, '<h1>123</h1>')
|
||||
|
||||
# Wrong sample
|
||||
response = render_template(ErrorSource(), 'sample/wrongsample.html', {'abc': 123})
|
||||
self.assertTrue('lert alert-block alert-danger' in response)
|
||||
self.assertTrue('Template file <em>sample/wrongsample.html</em>' in response)
|
@ -2,38 +2,14 @@
|
||||
Unit tests for plugins
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from plugin.samples.integration.sample import SampleIntegrationPlugin
|
||||
from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin
|
||||
import plugin.templatetags.plugin_extras as plugin_tags
|
||||
from plugin import registry, InvenTreePluginBase
|
||||
|
||||
|
||||
class InvenTreePluginTests(TestCase):
|
||||
""" Tests for InvenTreePlugin """
|
||||
def setUp(self):
|
||||
self.plugin = InvenTreePluginBase()
|
||||
|
||||
class NamedPlugin(InvenTreePluginBase):
|
||||
"""a named plugin"""
|
||||
PLUGIN_NAME = 'abc123'
|
||||
|
||||
self.named_plugin = NamedPlugin()
|
||||
|
||||
def test_basic_plugin_init(self):
|
||||
"""check if a basic plugin intis"""
|
||||
self.assertEqual(self.plugin.PLUGIN_NAME, '')
|
||||
self.assertEqual(self.plugin.plugin_name(), '')
|
||||
|
||||
def test_basic_plugin_name(self):
|
||||
"""check if the name of a basic plugin can be set"""
|
||||
self.assertEqual(self.named_plugin.PLUGIN_NAME, 'abc123')
|
||||
self.assertEqual(self.named_plugin.plugin_name(), 'abc123')
|
||||
|
||||
def test_basic_is_active(self):
|
||||
"""check if a basic plugin is active"""
|
||||
self.assertEqual(self.plugin.is_active(), False)
|
||||
from plugin import registry, InvenTreePlugin, IntegrationPluginBase
|
||||
|
||||
|
||||
class PluginTagTests(TestCase):
|
||||
@ -79,3 +55,118 @@ class PluginTagTests(TestCase):
|
||||
def test_tag_plugin_errors(self):
|
||||
"""test that all errors are listed"""
|
||||
self.assertEqual(plugin_tags.plugin_errors(), registry.errors)
|
||||
|
||||
|
||||
class InvenTreePluginTests(TestCase):
|
||||
""" Tests for InvenTreePlugin """
|
||||
|
||||
def setUp(self):
|
||||
self.plugin = InvenTreePlugin()
|
||||
|
||||
class NamedPlugin(InvenTreePlugin):
|
||||
"""a named plugin"""
|
||||
NAME = 'abc123'
|
||||
|
||||
self.named_plugin = NamedPlugin()
|
||||
|
||||
class SimpleInvenTreePlugin(InvenTreePlugin):
|
||||
NAME = 'SimplePlugin'
|
||||
|
||||
self.plugin_simple = SimpleInvenTreePlugin()
|
||||
|
||||
class OldInvenTreePlugin(InvenTreePlugin):
|
||||
PLUGIN_SLUG = 'old'
|
||||
|
||||
self.plugin_old = OldInvenTreePlugin()
|
||||
|
||||
class NameInvenTreePlugin(InvenTreePlugin):
|
||||
NAME = 'Aplugin'
|
||||
SLUG = 'a'
|
||||
TITLE = 'a titel'
|
||||
PUBLISH_DATE = "1111-11-11"
|
||||
AUTHOR = 'AA BB'
|
||||
DESCRIPTION = 'A description'
|
||||
VERSION = '1.2.3a'
|
||||
WEBSITE = 'http://aa.bb/cc'
|
||||
LICENSE = 'MIT'
|
||||
|
||||
self.plugin_name = NameInvenTreePlugin()
|
||||
self.plugin_sample = SampleIntegrationPlugin()
|
||||
|
||||
def test_basic_plugin_init(self):
|
||||
"""check if a basic plugin intis"""
|
||||
self.assertEqual(self.plugin.NAME, '')
|
||||
self.assertEqual(self.plugin.plugin_name(), '')
|
||||
|
||||
def test_basic_plugin_name(self):
|
||||
"""check if the name of a basic plugin can be set"""
|
||||
self.assertEqual(self.named_plugin.NAME, 'abc123')
|
||||
self.assertEqual(self.named_plugin.plugin_name(), 'abc123')
|
||||
|
||||
def test_basic_is_active(self):
|
||||
"""check if a basic plugin is active"""
|
||||
self.assertEqual(self.plugin.is_active(), False)
|
||||
|
||||
def test_action_name(self):
|
||||
"""check the name definition possibilities"""
|
||||
# plugin_name
|
||||
self.assertEqual(self.plugin.plugin_name(), '')
|
||||
self.assertEqual(self.plugin_simple.plugin_name(), 'SimplePlugin')
|
||||
self.assertEqual(self.plugin_name.plugin_name(), 'Aplugin')
|
||||
|
||||
# is_sampe
|
||||
self.assertEqual(self.plugin.is_sample, False)
|
||||
self.assertEqual(self.plugin_sample.is_sample, True)
|
||||
|
||||
# slug
|
||||
self.assertEqual(self.plugin.slug, '')
|
||||
self.assertEqual(self.plugin_simple.slug, 'simpleplugin')
|
||||
self.assertEqual(self.plugin_name.slug, 'a')
|
||||
|
||||
# human_name
|
||||
self.assertEqual(self.plugin.human_name, '')
|
||||
self.assertEqual(self.plugin_simple.human_name, 'SimplePlugin')
|
||||
self.assertEqual(self.plugin_name.human_name, 'a titel')
|
||||
|
||||
# description
|
||||
self.assertEqual(self.plugin.description, '')
|
||||
self.assertEqual(self.plugin_simple.description, 'SimplePlugin')
|
||||
self.assertEqual(self.plugin_name.description, 'A description')
|
||||
|
||||
# author
|
||||
self.assertEqual(self.plugin_name.author, 'AA BB')
|
||||
|
||||
# pub_date
|
||||
self.assertEqual(self.plugin_name.pub_date, datetime(1111, 11, 11, 0, 0))
|
||||
|
||||
# version
|
||||
self.assertEqual(self.plugin.version, None)
|
||||
self.assertEqual(self.plugin_simple.version, None)
|
||||
self.assertEqual(self.plugin_name.version, '1.2.3a')
|
||||
|
||||
# website
|
||||
self.assertEqual(self.plugin.website, None)
|
||||
self.assertEqual(self.plugin_simple.website, None)
|
||||
self.assertEqual(self.plugin_name.website, 'http://aa.bb/cc')
|
||||
|
||||
# license
|
||||
self.assertEqual(self.plugin.license, None)
|
||||
self.assertEqual(self.plugin_simple.license, None)
|
||||
self.assertEqual(self.plugin_name.license, 'MIT')
|
||||
|
||||
def test_depreciation(self):
|
||||
"""Check if depreciations raise as expected"""
|
||||
|
||||
# check deprecation warning is firing
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
self.assertEqual(self.plugin_old.slug, 'old')
|
||||
# check default value is used
|
||||
self.assertEqual(self.plugin_old.get_meta_value('ABC', 'ABCD', '123'), '123')
|
||||
|
||||
# check usage of the old class fires
|
||||
class OldPlugin(IntegrationPluginBase):
|
||||
pass
|
||||
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
plg = OldPlugin()
|
||||
self.assertIsInstance(plg, InvenTreePlugin)
|
||||
|
@ -1,2 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals # pragma: no cover
|
||||
"""
|
||||
Directory for custom plugin development
|
||||
|
||||
Please read the docs for more information https://inventree.readthedocs.io/en/latest/extend/plugins/#local-directory
|
||||
"""
|
||||
|
1
InvenTree/templates/sample/sample.html
Normal file
1
InvenTree/templates/sample/sample.html
Normal file
@ -0,0 +1 @@
|
||||
<h1>{{abc}}</h1>
|
@ -648,36 +648,6 @@ class Owner(models.Model):
|
||||
owner_type=content_type_id)
|
||||
except Owner.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
# Check whether user_or_group is a Group instance
|
||||
try:
|
||||
group = Group.objects.get(pk=user_or_group.id)
|
||||
except Group.DoesNotExist:
|
||||
group = None
|
||||
|
||||
if group:
|
||||
try:
|
||||
owner = Owner.objects.get(owner_id=user_or_group.id,
|
||||
owner_type=content_type_id_list[0])
|
||||
except Owner.DoesNotExist:
|
||||
pass
|
||||
|
||||
return owner
|
||||
|
||||
# Check whether user_or_group is a User instance
|
||||
try:
|
||||
user = user_model.objects.get(pk=user_or_group.id)
|
||||
except user_model.DoesNotExist:
|
||||
user = None
|
||||
|
||||
if user:
|
||||
try:
|
||||
owner = Owner.objects.get(owner_id=user_or_group.id,
|
||||
owner_type=content_type_id_list[1])
|
||||
except Owner.DoesNotExist:
|
||||
pass
|
||||
|
||||
return owner
|
||||
|
||||
return owner
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user