diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index c6efb687f1..b3d81d75a5 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -131,7 +131,7 @@ def round_decimal(value, places): class RoundingDecimalFormField(forms.DecimalField): def to_python(self, value): - value = super(RoundingDecimalFormField, self).to_python(value) + value = super().to_python(value) value = round_decimal(value, self.decimal_places) return value @@ -149,7 +149,7 @@ class RoundingDecimalFormField(forms.DecimalField): class RoundingDecimalField(models.DecimalField): def to_python(self, value): - value = super(RoundingDecimalField, self).to_python(value) + value = super().to_python(value) return round_decimal(value, self.decimal_places) def formfield(self, **kwargs): diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 36ad66de24..8814c571dd 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -58,7 +58,7 @@ class HelperForm(forms.ModelForm): def is_valid(self): - valid = super(HelperForm, self).is_valid() + valid = super().is_valid() return valid diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 5d0aea35e3..3012cdc61f 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -343,7 +343,7 @@ TEMPLATES = [ ], 'loaders': [( 'django.template.loaders.cached.Loader', [ - 'plugin.loader.PluginTemplateLoader', + 'plugin.template.PluginTemplateLoader', 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ]) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index aefed5156b..2b31d7c3b5 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -153,28 +153,24 @@ backendpatterns = [ ] frontendpatterns = [ - re_path(r'^part/', include(part_urls)), - re_path(r'^manufacturer-part/', include(manufacturer_part_urls)), - re_path(r'^supplier-part/', include(supplier_part_urls)), + # Apps + re_path(r'^build/', include(build_urls)), re_path(r'^common/', include(common_urls)), - - re_path(r'^stock/', include(stock_urls)), - re_path(r'^company/', include(company_urls)), re_path(r'^order/', include(order_urls)), - - re_path(r'^build/', include(build_urls)), - - re_path(r'^settings/', include(settings_urls)), - - re_path(r'^notifications/', include(notifications_urls)), + re_path(r'^manufacturer-part/', include(manufacturer_part_urls)), + re_path(r'^part/', include(part_urls)), + re_path(r'^stock/', include(stock_urls)), + re_path(r'^supplier-part/', include(supplier_part_urls)), re_path(r'^edit-user/', EditUserView.as_view(), name='edit-user'), re_path(r'^set-password/', SetPasswordView.as_view(), name='set-password'), re_path(r'^index/', IndexView.as_view(), name='index'), + re_path(r'^notifications/', include(notifications_urls)), re_path(r'^search/', SearchView.as_view(), name='search'), + re_path(r'^settings/', include(settings_urls)), re_path(r'^stats/', DatabaseStatsView.as_view(), name='stats'), # admin sites diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 183e491580..6291b321a8 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -654,7 +654,7 @@ class IndexView(TemplateView): def get_context_data(self, **kwargs): - context = super(TemplateView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) return context @@ -849,7 +849,7 @@ class SettingCategorySelectView(FormView): def get_initial(self): """ Set category selection """ - initial = super(SettingCategorySelectView, self).get_initial() + initial = super().get_initial() category = self.request.GET.get('category', None) if category: diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 80d648f53a..c8ac4509b8 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -14,6 +14,8 @@ from InvenTree.views import AjaxDeleteView from InvenTree.views import InvenTreeRoleMixin from InvenTree.status_codes import BuildStatus +from plugin.views import InvenTreePluginViewMixin + class BuildIndex(InvenTreeRoleMixin, ListView): """ @@ -29,7 +31,7 @@ class BuildIndex(InvenTreeRoleMixin, ListView): def get_context_data(self, **kwargs): - context = super(BuildIndex, self).get_context_data(**kwargs).copy() + context = super().get_context_data(**kwargs) context['BuildStatus'] = BuildStatus @@ -41,7 +43,7 @@ class BuildIndex(InvenTreeRoleMixin, ListView): return context -class BuildDetail(InvenTreeRoleMixin, DetailView): +class BuildDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detail view of a single Build object. """ @@ -52,7 +54,7 @@ class BuildDetail(InvenTreeRoleMixin, DetailView): def get_context_data(self, **kwargs): - ctx = super(DetailView, self).get_context_data(**kwargs) + ctx = super().get_context_data(**kwargs) build = self.get_object() diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 8c23002800..6d8279558c 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -23,9 +23,10 @@ from .models import Company from .models import ManufacturerPart from .models import SupplierPart - from .forms import CompanyImageDownloadForm +from plugin.views import InvenTreePluginViewMixin + class CompanyIndex(InvenTreeRoleMixin, ListView): """ View for displaying list of companies @@ -104,7 +105,7 @@ class CompanyIndex(InvenTreeRoleMixin, ListView): return queryset -class CompanyDetail(DetailView): +class CompanyDetail(InvenTreePluginViewMixin, DetailView): """ Detail view for Company object """ context_obect_name = 'company' template_name = 'company/detail.html' @@ -196,7 +197,7 @@ class CompanyImageDownloadFromURL(AjaxUpdateView): ) -class ManufacturerPartDetail(DetailView): +class ManufacturerPartDetail(InvenTreePluginViewMixin, DetailView): """ Detail view for ManufacturerPart """ model = ManufacturerPart template_name = 'company/manufacturer_part_detail.html' @@ -210,7 +211,7 @@ class ManufacturerPartDetail(DetailView): return ctx -class SupplierPartDetail(DetailView): +class SupplierPartDetail(InvenTreePluginViewMixin, DetailView): """ Detail view for SupplierPart """ model = SupplierPart template_name = 'company/supplier_part_detail.html' diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 81a96ba37e..f8102908e9 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -33,6 +33,8 @@ from part.views import PartPricing from InvenTree.helpers import DownloadFile from InvenTree.views import InvenTreeRoleMixin, AjaxView +from plugin.views import InvenTreePluginViewMixin + logger = logging.getLogger("inventree") @@ -65,7 +67,7 @@ class SalesOrderIndex(InvenTreeRoleMixin, ListView): context_object_name = 'orders' -class PurchaseOrderDetail(InvenTreeRoleMixin, DetailView): +class PurchaseOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detail view for a PurchaseOrder object """ context_object_name = 'order' @@ -78,7 +80,7 @@ class PurchaseOrderDetail(InvenTreeRoleMixin, DetailView): return ctx -class SalesOrderDetail(InvenTreeRoleMixin, DetailView): +class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detail view for a SalesOrder object """ context_object_name = 'order' diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index aa3ad4963a..bcedc95da8 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -397,7 +397,7 @@
- + {% endblock %} diff --git a/InvenTree/part/templates/part/part_sidebar.html b/InvenTree/part/templates/part/part_sidebar.html index e8763fb973..1da07aa0c6 100644 --- a/InvenTree/part/templates/part/part_sidebar.html +++ b/InvenTree/part/templates/part/part_sidebar.html @@ -57,4 +57,4 @@ {% trans "Attachments" as text %} {% include "sidebar_item.html" with label="part-attachments" text=text icon="fa-paperclip" %} {% trans "Notes" as text %} -{% include "sidebar_item.html" with label="part-notes" text=text icon="fa-clipboard" %} +{% include "sidebar_item.html" with label="part-notes" text=text icon="fa-clipboard" %} \ No newline at end of file diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index e024fca35b..b86e0acecc 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -19,7 +19,7 @@ from .templatetags import inventree_extras import part.settings from InvenTree import version -from common.models import InvenTreeSetting, NotificationEntry, NotificationMessage +from common.models import InvenTreeSetting, InvenTreeUserSetting, NotificationEntry, NotificationMessage from common.notifications import storage, UIMessageNotification @@ -87,7 +87,7 @@ class TemplateTagTest(TestCase): def test_user_settings(self): result = inventree_extras.user_settings(self.user) - self.assertEqual(len(result), 35) + self.assertEqual(len(result), len(InvenTreeUserSetting.SETTINGS)) def test_global_settings(self): result = inventree_extras.global_settings() diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index efaf83ae95..f1d587e206 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -53,6 +53,8 @@ from InvenTree.views import InvenTreeRoleMixin from InvenTree.helpers import str2bool +from plugin.views import InvenTreePluginViewMixin + class PartIndex(InvenTreeRoleMixin, ListView): """ View for displaying list of Part objects @@ -67,7 +69,7 @@ class PartIndex(InvenTreeRoleMixin, ListView): def get_context_data(self, **kwargs): - context = super(PartIndex, self).get_context_data(**kwargs).copy() + context = super().get_context_data(**kwargs).copy() # View top-level categories children = PartCategory.objects.filter(parent=None) @@ -365,7 +367,7 @@ class PartImportAjax(FileManagementAjaxView, PartImport): return PartImport.validate(self, self.steps.current, form, **kwargs) -class PartDetail(InvenTreeRoleMixin, DetailView): +class PartDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detail view for Part object """ @@ -969,7 +971,7 @@ class PartParameterTemplateDelete(AjaxDeleteView): ajax_form_title = _("Delete Part Parameter Template") -class CategoryDetail(InvenTreeRoleMixin, DetailView): +class CategoryDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detail view for PartCategory """ model = PartCategory @@ -979,7 +981,7 @@ class CategoryDetail(InvenTreeRoleMixin, DetailView): def get_context_data(self, **kwargs): - context = super(CategoryDetail, self).get_context_data(**kwargs).copy() + context = super().get_context_data(**kwargs).copy() try: context['part_count'] = kwargs['object'].partcount() @@ -1045,7 +1047,7 @@ class CategoryParameterTemplateCreate(AjaxCreateView): - Display parameter templates which are not yet related """ - form = super(AjaxCreateView, self).get_form() + form = super().get_form() form.fields['category'].widget = HiddenInput() @@ -1140,7 +1142,7 @@ class CategoryParameterTemplateEdit(AjaxUpdateView): - Display parameter templates which are not yet related """ - form = super(AjaxUpdateView, self).get_form() + form = super().get_form() form.fields['category'].widget = HiddenInput() form.fields['add_to_all_categories'].widget = HiddenInput() diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index ebe3ebf553..b22efc9415 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -9,9 +9,12 @@ import requests from django.urls import include, re_path from django.db.utils import OperationalError, ProgrammingError -from plugin.models import PluginConfig, PluginSetting -from plugin.urls import PLUGIN_BASE +import InvenTree.helpers + from plugin.helpers import MixinImplementationError, MixinNotImplementedError +from plugin.models import PluginConfig, PluginSetting +from plugin.template import render_template +from plugin.urls import PLUGIN_BASE logger = logging.getLogger('inventree') @@ -542,3 +545,120 @@ class APICallMixin: if simple_response: return response.json() return response + + +class PanelMixin: + """ + Mixin which allows integration of custom 'panels' into a particular page. + + The mixin provides a number of key functionalities: + + - Adds an (initially hidden) panel to the page + - Allows rendering of custom templated content to the panel + - Adds a menu item to the 'navbar' on the left side of the screen + - Allows custom javascript to be run when the panel is initially loaded + + The PanelMixin class allows multiple panels to be returned for any page, + and also allows the plugin to return panels for many different pages. + + Any class implementing this mixin must provide the 'get_custom_panels' method, + which dynamically returns the custom panels for a particular page. + + This method is provided with: + + - view : The View object which is being rendered + - request : The HTTPRequest object + + Note that as this is called dynamically (per request), + then the actual panels returned can vary depending on the particular request or page + + The 'get_custom_panels' method must return a list of dict objects, each with the following keys: + + - title : The title of the panel, to appear in the sidebar menu + - description : Extra descriptive text (optional) + - icon : The icon to appear in the sidebar menu + - content : The HTML content to appear in the panel, OR + - content_template : A template file which will be rendered to produce the panel content + - javascript : The javascript content to be rendered when the panel is loade, OR + - javascript_template : A template file which will be rendered to produce javascript + + e.g. + + { + 'title': "Updates", + 'description': "Latest updates for this part", + 'javascript': 'alert("You just loaded this panel!")', + 'content': 'Hello world', + } + + """ + + class MixinMeta: + MIXIN_NAME = 'Panel' + + def __init__(self): + super().__init__() + self.add_mixin('panel', True, __class__) + + def get_custom_panels(self, view, request): + """ This method *must* be implemented by the plugin class """ + raise NotImplementedError(f"{__class__} is missing the 'get_custom_panels' method") + + def get_panel_context(self, view, request, context): + """ + Build the context data to be used for template rendering. + Custom class can override this to provide any custom context data. + + (See the example in "custom_panel_sample.py") + """ + + # Provide some standard context items to the template for rendering + context['plugin'] = self + context['request'] = request + context['user'] = getattr(request, 'user', None) + context['view'] = view + + try: + context['object'] = view.get_object() + except AttributeError: + pass + + return context + + def render_panels(self, view, request, context): + + panels = [] + + # Construct an updated context object for template rendering + ctx = self.get_panel_context(view, request, context) + + for panel in self.get_custom_panels(view, request): + + content_template = panel.get('content_template', None) + javascript_template = panel.get('javascript_template', None) + + if content_template: + # Render content template to HTML + panel['content'] = render_template(self, content_template, ctx) + + if javascript_template: + # Render javascript template to HTML + panel['javascript'] = render_template(self, javascript_template, ctx) + + # Check for required keys + required_keys = ['title', 'content'] + + if any([key not in panel for key in required_keys]): + logger.warning(f"Custom panel for plugin {__class__} is missing a required parameter") + continue + + # Add some information on this plugin + panel['plugin'] = self + panel['slug'] = self.slug + + # Add a 'key' for the panel, which is mostly guaranteed to be unique + panel['key'] = InvenTree.helpers.generateTestKey(self.slug + panel.get('title', 'panel')) + + panels.append(panel) + + return panels diff --git a/InvenTree/plugin/loader.py b/InvenTree/plugin/loader.py deleted file mode 100644 index 538bd2358b..0000000000 --- a/InvenTree/plugin/loader.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -load templates for loaded plugins -""" -from django.template.loaders.filesystem import Loader as FilesystemLoader -from pathlib import Path - -from plugin import registry - - -class PluginTemplateLoader(FilesystemLoader): - - def get_dirs(self): - dirname = 'templates' - template_dirs = [] - for plugin in registry.plugins.values(): - new_path = Path(plugin.path) / dirname - if Path(new_path).is_dir(): - template_dirs.append(new_path) # pragma: no cover - return tuple(template_dirs) diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index 900289ae37..fdbe863e19 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -2,7 +2,8 @@ Utility class to enable simpler imports """ -from ..builtin.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin +from ..builtin.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin, PanelMixin + from common.notifications import SingleNotificationMethod, BulkNotificationMethod from ..builtin.action.mixins import ActionMixin @@ -17,6 +18,7 @@ __all__ = [ 'ScheduleMixin', 'SettingsMixin', 'UrlsMixin', + 'PanelMixin', 'ActionMixin', 'BarcodeMixin', 'SingleNotificationMethod', diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 1249d95aa3..45961d7a8b 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -307,14 +307,17 @@ class PluginsRegistry: # 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}') + try: plugin = plugin() except Exception as error: # log error and raise it -> disable plugin handle_error(error, log_name='init') - logger.info(f'Loaded integration plugin {plugin.slug}') + logger.debug(f'Loaded integration plugin {plugin.PLUGIN_NAME}') + plugin.is_package = was_packaged + if plugin_db_setting: plugin.pk = plugin_db_setting.pk diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py new file mode 100644 index 0000000000..73ca863576 --- /dev/null +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -0,0 +1,94 @@ +""" +Sample plugin which renders custom panels on certain pages +""" + +from plugin import IntegrationPluginBase +from plugin.mixins import PanelMixin, SettingsMixin + +from part.views import PartDetail +from stock.views import StockLocationDetail + + +class CustomPanelSample(PanelMixin, SettingsMixin, IntegrationPluginBase): + """ + A sample plugin which renders some custom panels. + """ + + PLUGIN_NAME = "CustomPanelExample" + PLUGIN_SLUG = "panel" + PLUGIN_TITLE = "Custom Panel Example" + DESCRIPTION = "An example plugin demonstrating how custom panels can be added to the user interface" + VERSION = "0.1" + + SETTINGS = { + 'ENABLE_HELLO_WORLD': { + 'name': 'Hello World', + 'description': 'Enable a custom hello world panel on every page', + 'default': False, + 'validator': bool, + } + } + + def get_panel_context(self, view, request, context): + + ctx = super().get_panel_context(view, request, context) + + # If we are looking at a StockLocationDetail view, add location context object + if isinstance(view, StockLocationDetail): + ctx['location'] = view.get_object() + + return ctx + + def get_custom_panels(self, view, request): + + """ + You can decide at run-time which custom panels you want to display! + + - Display on every page + - Only on a single page or set of pages + - Only for a specific instance (e.g. part) + - Based on the user viewing the page! + """ + + panels = [ + { + # This panel will not be displayed, as it is missing the 'content' key + 'title': 'No Content', + } + ] + + if self.get_setting('ENABLE_HELLO_WORLD'): + panels.append({ + # This 'hello world' panel will be displayed on any view which implements custom panels + 'title': 'Hello World', + 'icon': 'fas fa-boxes', + 'content': 'Hello world!', + 'description': 'A simple panel which renders hello world', + 'javascript': 'console.log("Hello world, from a custom panel!");', + }) + + # This panel will *only* display on the PartDetail view + if isinstance(view, PartDetail): + panels.append({ + 'title': 'Custom Part Panel', + 'icon': 'fas fa-shapes', + 'content': 'This content only appears on the PartDetail page, you know!', + }) + + # This panel will *only* display on the StockLocation view, + # and *only* if the StockLocation has *no* child locations + if isinstance(view, StockLocationDetail): + + try: + loc = view.get_object() + + if not loc.get_descendants(include_self=False).exists(): + panels.append({ + 'title': 'Childless Location', + 'icon': 'fa-user', + 'content_template': 'panel_demo/childless.html', # Note that the panel content is rendered using a template file! + }) + except: + pass + + return panels diff --git a/InvenTree/plugin/samples/integration/templates/panel_demo/childless.html b/InvenTree/plugin/samples/integration/templates/panel_demo/childless.html new file mode 100644 index 0000000000..061dcc514d --- /dev/null +++ b/InvenTree/plugin/samples/integration/templates/panel_demo/childless.html @@ -0,0 +1,11 @@ +

Template Rendering

+ +
+ This panel has been rendered using a template file! +
+ +This location has no sublocations! + diff --git a/InvenTree/plugin/template.py b/InvenTree/plugin/template.py new file mode 100644 index 0000000000..53ee7bb6db --- /dev/null +++ b/InvenTree/plugin/template.py @@ -0,0 +1,62 @@ +""" +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. + + Each plugin can register templates simply by providing a 'templates' directory in its root path. + + The convention is that each 'templates' directory contains a subdirectory with the same name as the plugin, + e.g. templates/myplugin/my_template.html + + In this case, the template can then be loaded (from any plugin!) by loading "myplugin/my_template.html". + + The separate plugin-named directories help keep the templates separated and uniquely identifiable. + """ + + def get_dirs(self): + dirname = 'templates' + template_dirs = [] + + for plugin in registry.plugins.values(): + new_path = Path(plugin.path) / dirname + if Path(new_path).is_dir(): + 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""" +
+ Template file {template_file} does not exist. +
+ """ + + # Render with the provided context + html = tmp.render(context) + + return html diff --git a/InvenTree/plugin/views.py b/InvenTree/plugin/views.py new file mode 100644 index 0000000000..1b45fefbb1 --- /dev/null +++ b/InvenTree/plugin/views.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.conf import settings + +from plugin.registry import registry + + +class InvenTreePluginViewMixin: + """ + Custom view mixin which adds context data to the view, + based on loaded plugins. + + This allows rendered pages to be augmented by loaded plugins. + + """ + + def get_plugin_panels(self, ctx): + """ + Return a list of extra 'plugin panels' associated with this view + """ + + panels = [] + + for plug in registry.with_mixin('panel'): + panels += plug.render_panels(self, self.request, ctx) + + return panels + + def get_context_data(self, **kwargs): + """ + Add plugin context data to the view + """ + + ctx = super().get_context_data(**kwargs) + + if settings.PLUGINS_ENABLED: + ctx['plugin_panels'] = self.get_plugin_panels(ctx) + + return ctx diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 01d2b67c73..168403d692 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -26,8 +26,10 @@ import common.settings from . import forms as StockForms +from plugin.views import InvenTreePluginViewMixin -class StockIndex(InvenTreeRoleMixin, ListView): + +class StockIndex(InvenTreeRoleMixin, InvenTreePluginViewMixin, ListView): """ StockIndex view loads all StockLocation and StockItem object """ model = StockItem @@ -35,7 +37,7 @@ class StockIndex(InvenTreeRoleMixin, ListView): context_obect_name = 'locations' def get_context_data(self, **kwargs): - context = super(StockIndex, self).get_context_data(**kwargs).copy() + context = super().get_context_data(**kwargs).copy() # Return all top-level locations locations = StockLocation.objects.filter(parent=None) @@ -54,7 +56,7 @@ class StockIndex(InvenTreeRoleMixin, ListView): return context -class StockLocationDetail(InvenTreeRoleMixin, DetailView): +class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detailed view of a single StockLocation object """ @@ -75,7 +77,7 @@ class StockLocationDetail(InvenTreeRoleMixin, DetailView): return context -class StockItemDetail(InvenTreeRoleMixin, DetailView): +class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detailed view of a single StockItem object """ diff --git a/InvenTree/templates/InvenTree/settings/plugin.html b/InvenTree/templates/InvenTree/settings/plugin.html index 94a166c84a..a451fb6544 100644 --- a/InvenTree/templates/InvenTree/settings/plugin.html +++ b/InvenTree/templates/InvenTree/settings/plugin.html @@ -69,6 +69,12 @@ {{ plugin.human_name }} - {{plugin_key}} {% define plugin.registered_mixins as mixin_list %} + {% if plugin.is_sample %} + + {% trans "Sample" %} + + {% endif %} + {% if mixin_list %} {% for mixin in mixin_list %} @@ -77,12 +83,6 @@ {% endfor %} {% endif %} - {% if plugin.is_sample %} - - {% trans "code sample" %} - - {% endif %} - {% if plugin.website %} {% endif %} diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 483a8ca6ad..0d8272892a 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -83,6 +83,7 @@ {% block sidebar %} {% endblock %} + {% include "plugin/panel_menu.html" %} {% include "sidebar_toggle.html" with target='sidebar' %} @@ -209,6 +210,9 @@