From b80ff5e460304e78511e718d92be60c1500fc19e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 6 May 2022 21:03:30 +1000 Subject: [PATCH 01/21] Tweak display of plugin badges --- InvenTree/templates/InvenTree/settings/plugin.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 %} From 170cb544907ecc22d1a99ea3fe71ce684fa218fd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 6 May 2022 21:30:27 +1000 Subject: [PATCH 02/21] Sort urls.py --- InvenTree/InvenTree/urls.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) 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 From 28e16616e5d8b574e8f24714bcac29a6c5bb2c5c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 6 May 2022 21:32:11 +1000 Subject: [PATCH 03/21] Adds a PanelMixin plugin mixin class Intended to allow rendering of custom panels on pages --- .../plugin/builtin/integration/mixins.py | 44 +++++++++++++++++++ InvenTree/plugin/mixins/__init__.py | 4 +- .../integration/custom_panel_sample.py | 22 ++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 InvenTree/plugin/samples/integration/custom_panel_sample.py diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index ebe3ebf553..dce48304ec 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -542,3 +542,47 @@ 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: + + - page: The name of the page e.g. 'part-detail' + - instance: The model instance specific to the page + - request: The request object responsible for the page load + + It must return a list of CustomPanel class instances (see below). + + Note that as this is called dynamically (per request), + then the actual panels returned can vary depending on the particular request or page + + """ + + class CustomPanel: + ... + + class MixinMeta: + MIXIN_NAME = 'Panel' + + def __init__(self): + super().__init__() + self.add_mixin('panel', True, __class__) + + def get_custom_panels(self, page, instance, request): + raise NotImplementedError(f"{__class__} is missing the 'get_custom_panels' method") 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/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py new file mode 100644 index 0000000000..c2bae15548 --- /dev/null +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -0,0 +1,22 @@ +""" +Sample plugin which renders custom panels on certain pages +""" + +from plugin import IntegrationPluginBase +from plugin.mixins import PanelMixin + + +class CustomPanelSample(PanelMixin, IntegrationPluginBase): + """ + A sample plugin which renders some custom panels. + """ + + PLUGIN_NAME = "CustomPanelExample" + PLUGIN_SLUG = "panel" + PLUGIN_TITLE = "Custom Panel Example" + + def get_custom_panels(self, page, instance, request): + + print("get_custom_panels:") + + return [] \ No newline at end of file From 7b8a10173dcb3c3d651dd3372c2da37e39554c62 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 6 May 2022 22:49:51 +1000 Subject: [PATCH 04/21] Adds a new "Panel" mixin which can render custom panels on given pages - Adds item to sidebar menu - Adds panel content - Runs custom javascript when the page is loaded --- .../plugin/builtin/integration/mixins.py | 70 ++++++++++++++++--- InvenTree/plugin/registry.py | 5 +- .../integration/custom_panel_sample.py | 52 +++++++++++++- 3 files changed, 112 insertions(+), 15 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index dce48304ec..57c9fb3f0a 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -9,6 +9,8 @@ import requests from django.urls import include, re_path from django.db.utils import OperationalError, ProgrammingError +import InvenTree.helpers + from plugin.models import PluginConfig, PluginSetting from plugin.urls import PLUGIN_BASE from plugin.helpers import MixinImplementationError, MixinNotImplementedError @@ -563,26 +565,72 @@ class PanelMixin: This method is provided with: - - page: The name of the page e.g. 'part-detail' - - instance: The model instance specific to the page - - request: The request object responsible for the page load - - It must return a list of CustomPanel class instances (see below). + - 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: - class CustomPanel: - ... + - 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, page, instance, request): + + def render_panels(self, view, request): + + panels = [] + + for panel in self.get_custom_panels(view, request): + + if 'content_template' in panel: + # TODO: Render the actual content + ... + + if 'javascript_template' in panel: + # TODO: Render the actual content + ... + + # 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 key") + 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 + + 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") 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 index c2bae15548..5cca44f524 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -5,6 +5,9 @@ Sample plugin which renders custom panels on certain pages from plugin import IntegrationPluginBase from plugin.mixins import PanelMixin +from part.views import PartDetail +from stock.views import StockLocationDetail + class CustomPanelSample(PanelMixin, IntegrationPluginBase): """ @@ -15,8 +18,51 @@ class CustomPanelSample(PanelMixin, IntegrationPluginBase): PLUGIN_SLUG = "panel" PLUGIN_TITLE = "Custom Panel Example" - def get_custom_panels(self, page, instance, request): + def get_custom_panels(self, view, request): - print("get_custom_panels:") + panels = [ + { + # 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': 'alert("Hello world");', + }, + { + # This panel will not be displayed, as it is missing the 'content' key + 'title': 'No Content', + } + ] - return [] \ No newline at end of file + # 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): + + print("yep, stocklocation view!") + + try: + loc = view.get_object() + + if not loc.get_descendants(include_self=False).exists(): + panels.append({ + 'title': 'Childless', + 'icon': 'fa-user', + 'content': '

I have no children!

' + }) + else: + print("abcdefgh") + + except: + print("error could not get object!") + pass + + return panels From c80b36fc2fa30bfe6e981b078142a90621f21675 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 6 May 2022 22:52:52 +1000 Subject: [PATCH 05/21] Adds a new InvenTreePluginMixin mixin class for enabling custom plugin rendering on a page - Any view which needs custom plugin code must implement this mixin - Initially implement for the PartDetail page --- InvenTree/InvenTree/views.py | 34 +++++++++++++++++++ InvenTree/part/templates/part/detail.html | 6 +++- .../part/templates/part/part_sidebar.html | 2 ++ InvenTree/part/views.py | 4 +-- .../integration/custom_panel_sample.py | 7 ---- .../templates/panel/plugin_javascript.html | 10 ++++++ .../templates/panel/plugin_menu_items.html | 3 ++ InvenTree/templates/panel/plugin_panels.html | 18 ++++++++++ 8 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 InvenTree/templates/panel/plugin_javascript.html create mode 100644 InvenTree/templates/panel/plugin_menu_items.html create mode 100644 InvenTree/templates/panel/plugin_panels.html diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 183e491580..629ee1f31a 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -38,6 +38,8 @@ from part.models import PartCategory from common.models import InvenTreeSetting, ColorTheme from users.models import check_user_role, RuleSet +from plugin.registry import registry + from .forms import DeleteForm, EditUserForm, SetPasswordForm from .forms import SettingCategorySelectForm from .helpers import str2bool @@ -56,6 +58,38 @@ def auth_request(request): return HttpResponse(status=403) +class InvenTreePluginMixin: + """ + 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): + """ + 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) + + return panels + + def get_context_data(self, **kwargs): + + ctx = super().get_context_data(**kwargs) + + if settings.PLUGINS_ENABLED: + ctx['plugin_panels'] = self.get_plugin_panels() + + return ctx + + class InvenTreeRoleMixin(PermissionRequiredMixin): """ Permission class based on user roles, not user 'permissions'. diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index aa3ad4963a..cd8404be5b 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -397,9 +397,11 @@
- + +{% include "panel/plugin_panels.html" %} + {% endblock %} {% block js_load %} @@ -1083,4 +1085,6 @@ } }); + {% include "panel/plugin_javascript.html" %} + {% endblock %} diff --git a/InvenTree/part/templates/part/part_sidebar.html b/InvenTree/part/templates/part/part_sidebar.html index e8763fb973..18890b82af 100644 --- a/InvenTree/part/templates/part/part_sidebar.html +++ b/InvenTree/part/templates/part/part_sidebar.html @@ -58,3 +58,5 @@ {% 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 "panel/plugin_menu_items.html" %} \ No newline at end of file diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index efaf83ae95..cb9725f94e 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -49,7 +49,7 @@ from order.models import PurchaseOrderLineItem from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import QRCodeView -from InvenTree.views import InvenTreeRoleMixin +from InvenTree.views import InvenTreeRoleMixin, InvenTreePluginMixin from InvenTree.helpers import str2bool @@ -365,7 +365,7 @@ class PartImportAjax(FileManagementAjaxView, PartImport): return PartImport.validate(self, self.steps.current, form, **kwargs) -class PartDetail(InvenTreeRoleMixin, DetailView): +class PartDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): """ Detail view for Part object """ diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index 5cca44f524..3b999cce27 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -46,9 +46,6 @@ class CustomPanelSample(PanelMixin, IntegrationPluginBase): # This panel will *only* display on the StockLocation view, # and *only* if the StockLocation has *no* child locations if isinstance(view, StockLocationDetail): - - print("yep, stocklocation view!") - try: loc = view.get_object() @@ -58,11 +55,7 @@ class CustomPanelSample(PanelMixin, IntegrationPluginBase): 'icon': 'fa-user', 'content': '

I have no children!

' }) - else: - print("abcdefgh") - except: - print("error could not get object!") pass return panels diff --git a/InvenTree/templates/panel/plugin_javascript.html b/InvenTree/templates/panel/plugin_javascript.html new file mode 100644 index 0000000000..bf8b7fea34 --- /dev/null +++ b/InvenTree/templates/panel/plugin_javascript.html @@ -0,0 +1,10 @@ +{% if plugin_panels %} +// Run custom javascript when plugin panels are loaded +{% for panel in plugin_panels %} +{% if panel.javascript %} +onPanelLoad('{{ panel.key }}', function() { +{{ panel.javascript | safe }} +}); +{% endif %} +{% endfor %} +{% endif %} \ No newline at end of file diff --git a/InvenTree/templates/panel/plugin_menu_items.html b/InvenTree/templates/panel/plugin_menu_items.html new file mode 100644 index 0000000000..2c084a021e --- /dev/null +++ b/InvenTree/templates/panel/plugin_menu_items.html @@ -0,0 +1,3 @@ +{% for panel in plugin_panels %} +{% include "sidebar_item.html" with label=panel.key text=panel.title icon=panel.icon %} +{% endfor %} \ No newline at end of file diff --git a/InvenTree/templates/panel/plugin_panels.html b/InvenTree/templates/panel/plugin_panels.html new file mode 100644 index 0000000000..ddbdbeee45 --- /dev/null +++ b/InvenTree/templates/panel/plugin_panels.html @@ -0,0 +1,18 @@ +{% for panel in plugin_panels %} + +
+
+
+

{{ panel.title }}

+ {% include "spacer.html" %} +
+ +
+
+
+
+ {{ panel.content | safe }} +
+
+ +{% endfor %} \ No newline at end of file From 71128a1c8e643c046f75a8dc88e72113603ed0c1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 6 May 2022 22:57:15 +1000 Subject: [PATCH 06/21] Refactor the plugin javascript template - Can appear in "base.html" - Only renders anything if there are actually plugins available for the page --- InvenTree/part/templates/part/detail.html | 2 -- InvenTree/templates/base.html | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index cd8404be5b..e2e3d27be0 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -1085,6 +1085,4 @@ } }); - {% include "panel/plugin_javascript.html" %} - {% endblock %} diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 483a8ca6ad..7dbb17c7d8 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -209,6 +209,9 @@