mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 19:46:46 +00:00
Merge pull request #3034 from SchrodingersGat/plugin-panels-test
Plugin panels test
This commit is contained in:
commit
1bff1868fd
1
.github/workflows/qc_checks.yaml
vendored
1
.github/workflows/qc_checks.yaml
vendored
@ -153,6 +153,7 @@ jobs:
|
|||||||
invoke delete-data -f
|
invoke delete-data -f
|
||||||
invoke import-fixtures
|
invoke import-fixtures
|
||||||
invoke server -a 127.0.0.1:12345 &
|
invoke server -a 127.0.0.1:12345 &
|
||||||
|
invoke wait
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
cd ${{ env.wrapper_name }}
|
cd ${{ env.wrapper_name }}
|
||||||
|
@ -39,7 +39,8 @@ def canAppAccessDatabase(allow_test=False):
|
|||||||
'createsuperuser',
|
'createsuperuser',
|
||||||
'wait_for_db',
|
'wait_for_db',
|
||||||
'prerender',
|
'prerender',
|
||||||
'rebuild',
|
'rebuild_models',
|
||||||
|
'rebuild_thumbnails',
|
||||||
'collectstatic',
|
'collectstatic',
|
||||||
'makemessages',
|
'makemessages',
|
||||||
'compilemessages',
|
'compilemessages',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
@ -406,11 +407,23 @@ class CurrencyTests(TestCase):
|
|||||||
with self.assertRaises(MissingRate):
|
with self.assertRaises(MissingRate):
|
||||||
convert_money(Money(100, 'AUD'), 'USD')
|
convert_money(Money(100, 'AUD'), 'USD')
|
||||||
|
|
||||||
|
update_successful = False
|
||||||
|
|
||||||
|
# Note: the update sometimes fails in CI, let's give it a few chances
|
||||||
|
for idx in range(10):
|
||||||
InvenTree.tasks.update_exchange_rates()
|
InvenTree.tasks.update_exchange_rates()
|
||||||
|
|
||||||
rates = Rate.objects.all()
|
rates = Rate.objects.all()
|
||||||
|
|
||||||
self.assertEqual(rates.count(), len(currency_codes()))
|
if rates.count() == len(currency_codes()):
|
||||||
|
update_successful = True
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Exchange rate update failed - retrying")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
self.assertTrue(update_successful)
|
||||||
|
|
||||||
# Now that we have some exchange rate information, we can perform conversions
|
# Now that we have some exchange rate information, we can perform conversions
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from maintenance_mode.core import set_maintenance_mode
|
from maintenance_mode.core import set_maintenance_mode
|
||||||
|
|
||||||
from InvenTree.ready import isImportingData
|
from InvenTree.ready import canAppAccessDatabase
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
from plugin.helpers import check_git_version, log_error
|
from plugin.helpers import check_git_version, log_error
|
||||||
|
|
||||||
@ -20,9 +20,8 @@ class PluginAppConfig(AppConfig):
|
|||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
if settings.PLUGINS_ENABLED:
|
if settings.PLUGINS_ENABLED:
|
||||||
|
if not canAppAccessDatabase(allow_test=True):
|
||||||
if isImportingData(): # pragma: no cover
|
logger.info("Skipping plugin loading sequence")
|
||||||
logger.info('Skipping plugin loading for data import')
|
|
||||||
else:
|
else:
|
||||||
logger.info('Loading InvenTree plugins')
|
logger.info('Loading InvenTree plugins')
|
||||||
|
|
||||||
@ -48,3 +47,6 @@ class PluginAppConfig(AppConfig):
|
|||||||
registry.git_is_modern = check_git_version()
|
registry.git_is_modern = check_git_version()
|
||||||
if not registry.git_is_modern: # pragma: no cover # simulating old git seems not worth it for coverage
|
if not registry.git_is_modern: # pragma: no cover # simulating old git seems not worth it for coverage
|
||||||
log_error(_('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load')
|
log_error(_('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load')
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.info("Plugins not enabled - skipping loading sequence")
|
||||||
|
@ -11,7 +11,8 @@ from django.db.utils import OperationalError, ProgrammingError
|
|||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
|
||||||
from plugin.helpers import MixinImplementationError, MixinNotImplementedError, render_template
|
from plugin.helpers import MixinImplementationError, MixinNotImplementedError
|
||||||
|
from plugin.helpers import render_template, render_text
|
||||||
from plugin.models import PluginConfig, PluginSetting
|
from plugin.models import PluginConfig, PluginSetting
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
from plugin.urls import PLUGIN_BASE
|
from plugin.urls import PLUGIN_BASE
|
||||||
@ -59,6 +60,7 @@ class SettingsMixin:
|
|||||||
|
|
||||||
if not plugin:
|
if not plugin:
|
||||||
# Cannot find associated plugin model, return
|
# Cannot find associated plugin model, return
|
||||||
|
logger.error(f"Plugin configuration not found for plugin '{self.slug}'")
|
||||||
return # pragma: no cover
|
return # pragma: no cover
|
||||||
|
|
||||||
PluginSetting.set_setting(key, value, user, plugin=plugin)
|
PluginSetting.set_setting(key, value, user, plugin=plugin)
|
||||||
@ -578,10 +580,16 @@ class PanelMixin:
|
|||||||
if content_template:
|
if content_template:
|
||||||
# Render content template to HTML
|
# Render content template to HTML
|
||||||
panel['content'] = render_template(self, content_template, ctx)
|
panel['content'] = render_template(self, content_template, ctx)
|
||||||
|
else:
|
||||||
|
# Render content string to HTML
|
||||||
|
panel['content'] = render_text(panel.get('content', ''), ctx)
|
||||||
|
|
||||||
if javascript_template:
|
if javascript_template:
|
||||||
# Render javascript template to HTML
|
# Render javascript template to HTML
|
||||||
panel['javascript'] = render_template(self, javascript_template, ctx)
|
panel['javascript'] = render_template(self, javascript_template, ctx)
|
||||||
|
else:
|
||||||
|
# Render javascript string to HTML
|
||||||
|
panel['javascript'] = render_text(panel.get('javascript', ''), ctx)
|
||||||
|
|
||||||
# Check for required keys
|
# Check for required keys
|
||||||
required_keys = ['title', 'content']
|
required_keys = ['title', 'content']
|
||||||
|
@ -2,14 +2,19 @@
|
|||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import include, re_path
|
from django.urls import include, re_path, reverse
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
from error_report.models import Error
|
||||||
|
|
||||||
from plugin import InvenTreePlugin
|
from plugin import InvenTreePlugin
|
||||||
from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, APICallMixin
|
from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, APICallMixin
|
||||||
from plugin.urls import PLUGIN_BASE
|
from plugin.urls import PLUGIN_BASE
|
||||||
from plugin.helpers import MixinNotImplementedError
|
from plugin.helpers import MixinNotImplementedError
|
||||||
|
|
||||||
|
from plugin.registry import registry
|
||||||
|
|
||||||
|
|
||||||
class BaseMixinDefinition:
|
class BaseMixinDefinition:
|
||||||
def test_mixin_name(self):
|
def test_mixin_name(self):
|
||||||
@ -244,3 +249,161 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
|
|||||||
# cover wrong token setting
|
# cover wrong token setting
|
||||||
with self.assertRaises(MixinNotImplementedError):
|
with self.assertRaises(MixinNotImplementedError):
|
||||||
self.mixin_wrong2.has_api_call()
|
self.mixin_wrong2.has_api_call()
|
||||||
|
|
||||||
|
|
||||||
|
class PanelMixinTests(TestCase):
|
||||||
|
"""Test that the PanelMixin plugin operates correctly"""
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
'category',
|
||||||
|
'part',
|
||||||
|
'location',
|
||||||
|
'stock',
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
# Create a user which has all the privelages
|
||||||
|
user = get_user_model()
|
||||||
|
|
||||||
|
self.user = user.objects.create_user(
|
||||||
|
username='username',
|
||||||
|
email='user@email.com',
|
||||||
|
password='password'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Put the user into a group with the correct permissions
|
||||||
|
group = Group.objects.create(name='mygroup')
|
||||||
|
self.user.groups.add(group)
|
||||||
|
|
||||||
|
# Give the group *all* the permissions!
|
||||||
|
for rule in group.rule_sets.all():
|
||||||
|
rule.can_view = True
|
||||||
|
rule.can_change = True
|
||||||
|
rule.can_add = True
|
||||||
|
rule.can_delete = True
|
||||||
|
|
||||||
|
rule.save()
|
||||||
|
|
||||||
|
self.client.login(username='username', password='password')
|
||||||
|
|
||||||
|
def test_installed(self):
|
||||||
|
"""Test that the sample panel plugin is installed"""
|
||||||
|
|
||||||
|
plugins = registry.with_mixin('panel')
|
||||||
|
|
||||||
|
self.assertTrue(len(plugins) > 0)
|
||||||
|
|
||||||
|
self.assertIn('samplepanel', [p.slug for p in plugins])
|
||||||
|
|
||||||
|
plugins = registry.with_mixin('panel', active=True)
|
||||||
|
|
||||||
|
self.assertEqual(len(plugins), 0)
|
||||||
|
|
||||||
|
def test_disabled(self):
|
||||||
|
"""Test that the panels *do not load* if the plugin is not enabled"""
|
||||||
|
|
||||||
|
plugin = registry.get_plugin('samplepanel')
|
||||||
|
|
||||||
|
plugin.set_setting('ENABLE_HELLO_WORLD', True)
|
||||||
|
plugin.set_setting('ENABLE_BROKEN_PANEL', True)
|
||||||
|
|
||||||
|
# Ensure that the plugin is *not* enabled
|
||||||
|
config = plugin.plugin_config()
|
||||||
|
|
||||||
|
self.assertFalse(config.active)
|
||||||
|
|
||||||
|
# Load some pages, ensure that the panel content is *not* loaded
|
||||||
|
for url in [
|
||||||
|
reverse('part-detail', kwargs={'pk': 1}),
|
||||||
|
reverse('stock-item-detail', kwargs={'pk': 2}),
|
||||||
|
reverse('stock-location-detail', kwargs={'pk': 1}),
|
||||||
|
]:
|
||||||
|
response = self.client.get(
|
||||||
|
url
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Test that these panels have *not* been loaded
|
||||||
|
self.assertNotIn('No Content', str(response.content))
|
||||||
|
self.assertNotIn('Hello world', str(response.content))
|
||||||
|
self.assertNotIn('Custom Part Panel', str(response.content))
|
||||||
|
|
||||||
|
def test_enabled(self):
|
||||||
|
"""
|
||||||
|
Test that the panels *do* load if the plugin is enabled
|
||||||
|
"""
|
||||||
|
|
||||||
|
plugin = registry.get_plugin('samplepanel')
|
||||||
|
|
||||||
|
self.assertEqual(len(registry.with_mixin('panel', active=True)), 0)
|
||||||
|
|
||||||
|
# Ensure that the plugin is enabled
|
||||||
|
config = plugin.plugin_config()
|
||||||
|
config.active = True
|
||||||
|
config.save()
|
||||||
|
|
||||||
|
self.assertTrue(config.active)
|
||||||
|
self.assertEqual(len(registry.with_mixin('panel', active=True)), 1)
|
||||||
|
|
||||||
|
# Load some pages, ensure that the panel content is *not* loaded
|
||||||
|
urls = [
|
||||||
|
reverse('part-detail', kwargs={'pk': 1}),
|
||||||
|
reverse('stock-item-detail', kwargs={'pk': 2}),
|
||||||
|
reverse('stock-location-detail', kwargs={'pk': 1}),
|
||||||
|
]
|
||||||
|
|
||||||
|
plugin.set_setting('ENABLE_HELLO_WORLD', False)
|
||||||
|
plugin.set_setting('ENABLE_BROKEN_PANEL', False)
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
response = self.client.get(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertIn('No Content', str(response.content))
|
||||||
|
|
||||||
|
# This panel is disabled by plugin setting
|
||||||
|
self.assertNotIn('Hello world!', str(response.content))
|
||||||
|
|
||||||
|
# This panel is only active for the "Part" view
|
||||||
|
if url == urls[0]:
|
||||||
|
self.assertIn('Custom Part Panel', str(response.content))
|
||||||
|
else:
|
||||||
|
self.assertNotIn('Custom Part Panel', str(response.content))
|
||||||
|
|
||||||
|
# Enable the 'Hello World' panel
|
||||||
|
plugin.set_setting('ENABLE_HELLO_WORLD', True)
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
response = self.client.get(url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertIn('Hello world!', str(response.content))
|
||||||
|
|
||||||
|
# The 'Custom Part' panel should still be there, too
|
||||||
|
if url == urls[0]:
|
||||||
|
self.assertIn('Custom Part Panel', str(response.content))
|
||||||
|
else:
|
||||||
|
self.assertNotIn('Custom Part Panel', str(response.content))
|
||||||
|
|
||||||
|
# Enable the 'broken panel' setting - this will cause all panels to not render
|
||||||
|
plugin.set_setting('ENABLE_BROKEN_PANEL', True)
|
||||||
|
|
||||||
|
n_errors = Error.objects.count()
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# No custom panels should have been loaded
|
||||||
|
self.assertNotIn('No Content', str(response.content))
|
||||||
|
self.assertNotIn('Hello world!', str(response.content))
|
||||||
|
self.assertNotIn('Broken Panel', str(response.content))
|
||||||
|
self.assertNotIn('Custom Part Panel', str(response.content))
|
||||||
|
|
||||||
|
# Assert that each request threw an error
|
||||||
|
self.assertEqual(Error.objects.count(), n_errors + len(urls))
|
||||||
|
@ -245,4 +245,15 @@ def render_template(plugin, template_file, context=None):
|
|||||||
html = tmp.render(context)
|
html = tmp.render(context)
|
||||||
|
|
||||||
return html
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
def render_text(text, context=None):
|
||||||
|
"""
|
||||||
|
Locate a raw string with provided context
|
||||||
|
"""
|
||||||
|
|
||||||
|
ctx = template.Context(context)
|
||||||
|
|
||||||
|
return template.Template(text).render(ctx)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
@ -243,7 +243,7 @@ class PluginsRegistry:
|
|||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region registry functions
|
# region registry functions
|
||||||
def with_mixin(self, mixin: str):
|
def with_mixin(self, mixin: str, active=None):
|
||||||
"""
|
"""
|
||||||
Returns reference to all plugins that have a specified mixin enabled
|
Returns reference to all plugins that have a specified mixin enabled
|
||||||
"""
|
"""
|
||||||
@ -251,6 +251,14 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
for plugin in self.plugins.values():
|
for plugin in self.plugins.values():
|
||||||
if plugin.mixin_enabled(mixin):
|
if plugin.mixin_enabled(mixin):
|
||||||
|
|
||||||
|
if active is not None:
|
||||||
|
# Filter by 'enabled' status
|
||||||
|
config = plugin.plugin_config()
|
||||||
|
|
||||||
|
if config.active != active:
|
||||||
|
continue
|
||||||
|
|
||||||
result.append(plugin)
|
result.append(plugin)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -12,7 +12,7 @@ class EventPluginSample(EventMixin, InvenTreePlugin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
NAME = "EventPlugin"
|
NAME = "EventPlugin"
|
||||||
SLUG = "event"
|
SLUG = "sampleevent"
|
||||||
TITLE = "Triggered Events"
|
TITLE = "Triggered Events"
|
||||||
|
|
||||||
def process_event(self, event, *args, **kwargs):
|
def process_event(self, event, *args, **kwargs):
|
||||||
|
@ -15,17 +15,23 @@ class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
NAME = "CustomPanelExample"
|
NAME = "CustomPanelExample"
|
||||||
SLUG = "panel"
|
SLUG = "samplepanel"
|
||||||
TITLE = "Custom Panel Example"
|
TITLE = "Custom Panel Example"
|
||||||
DESCRIPTION = "An example plugin demonstrating how custom panels can be added to the user interface"
|
DESCRIPTION = "An example plugin demonstrating how custom panels can be added to the user interface"
|
||||||
VERSION = "0.1"
|
VERSION = "0.1"
|
||||||
|
|
||||||
SETTINGS = {
|
SETTINGS = {
|
||||||
'ENABLE_HELLO_WORLD': {
|
'ENABLE_HELLO_WORLD': {
|
||||||
'name': 'Hello World',
|
'name': 'Enable Hello World',
|
||||||
'description': 'Enable a custom hello world panel on every page',
|
'description': 'Enable a custom hello world panel on every page',
|
||||||
'default': False,
|
'default': False,
|
||||||
'validator': bool,
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'ENABLE_BROKEN_PANEL': {
|
||||||
|
'name': 'Enable Broken Panel',
|
||||||
|
'description': 'Enable a panel with rendering issues',
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,21 +58,48 @@ class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin):
|
|||||||
|
|
||||||
panels = [
|
panels = [
|
||||||
{
|
{
|
||||||
# This panel will not be displayed, as it is missing the 'content' key
|
# Simple panel without any actual content
|
||||||
'title': 'No Content',
|
'title': 'No Content',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.get_setting('ENABLE_HELLO_WORLD'):
|
if self.get_setting('ENABLE_HELLO_WORLD'):
|
||||||
|
|
||||||
|
# We can use template rendering in the raw content
|
||||||
|
content = """
|
||||||
|
<strong>Hello world!</strong>
|
||||||
|
<hr>
|
||||||
|
<div class='alert-alert-block alert-info'>
|
||||||
|
<em>We can render custom content using the templating system!</em>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<table class='table table-striped'>
|
||||||
|
<tr><td><strong>Path</strong></td><td>{{ request.path }}</tr>
|
||||||
|
<tr><td><strong>User</strong></td><td>{{ user.username }}</tr>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
|
||||||
panels.append({
|
panels.append({
|
||||||
# This 'hello world' panel will be displayed on any view which implements custom panels
|
# This 'hello world' panel will be displayed on any view which implements custom panels
|
||||||
'title': 'Hello World',
|
'title': 'Hello World',
|
||||||
'icon': 'fas fa-boxes',
|
'icon': 'fas fa-boxes',
|
||||||
'content': '<b>Hello world!</b>',
|
'content': content,
|
||||||
'description': 'A simple panel which renders hello world',
|
'description': 'A simple panel which renders hello world',
|
||||||
'javascript': 'console.log("Hello world, from a custom panel!");',
|
'javascript': 'console.log("Hello world, from a custom panel!");',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if self.get_setting('ENABLE_BROKEN_PANEL'):
|
||||||
|
|
||||||
|
# Enabling this panel will cause panel rendering to break,
|
||||||
|
# due to the invalid tags
|
||||||
|
panels.append({
|
||||||
|
'title': 'Broken Panel',
|
||||||
|
'icon': 'fas fa-times-circle',
|
||||||
|
'content': '{% tag_not_loaded %}',
|
||||||
|
'description': 'This panel is broken',
|
||||||
|
'javascript': '{% another_bad_tag %}',
|
||||||
|
})
|
||||||
|
|
||||||
# This panel will *only* display on the PartDetail view
|
# This panel will *only* display on the PartDetail view
|
||||||
if isinstance(view, PartDetail):
|
if isinstance(view, PartDetail):
|
||||||
panels.append({
|
panels.append({
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@ -9,6 +10,9 @@ from error_report.models import Error
|
|||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
class InvenTreePluginViewMixin:
|
class InvenTreePluginViewMixin:
|
||||||
"""
|
"""
|
||||||
Custom view mixin which adds context data to the view,
|
Custom view mixin which adds context data to the view,
|
||||||
@ -25,7 +29,7 @@ class InvenTreePluginViewMixin:
|
|||||||
|
|
||||||
panels = []
|
panels = []
|
||||||
|
|
||||||
for plug in registry.with_mixin('panel'):
|
for plug in registry.with_mixin('panel', active=True):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
panels += plug.render_panels(self, self.request, ctx)
|
panels += plug.render_panels(self, self.request, ctx)
|
||||||
@ -42,6 +46,8 @@ class InvenTreePluginViewMixin:
|
|||||||
html=ExceptionReporter(self.request, kind, info, data).get_traceback_html(),
|
html=ExceptionReporter(self.request, kind, info, data).get_traceback_html(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.error(f"Plugin '{plug.slug}' could not render custom panels at '{self.request.path}'")
|
||||||
|
|
||||||
return panels
|
return panels
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -15,7 +15,7 @@ ignore =
|
|||||||
N806,
|
N806,
|
||||||
# - N812 - lowercase imported as non-lowercase
|
# - N812 - lowercase imported as non-lowercase
|
||||||
N812,
|
N812,
|
||||||
exclude = .git,__pycache__,*/migrations/*,*/lib/*,*/bin/*,*/media/*,*/static/*
|
exclude = .git,__pycache__,*/migrations/*,*/lib/*,*/bin/*,*/media/*,*/static/*,InvenTree/plugins/*
|
||||||
max-complexity = 20
|
max-complexity = 20
|
||||||
|
|
||||||
[coverage:run]
|
[coverage:run]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user