From b80ff5e460304e78511e718d92be60c1500fc19e Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> 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 @@ <td>{{ plugin.human_name }}<span class="text-muted"> - {{plugin_key}}</span> {% define plugin.registered_mixins as mixin_list %} + {% if plugin.is_sample %} + <a class='sidebar-selector' id='select-plugin-{{plugin_key}}' data-bs-parent="#sidebar"> + <span class='badge bg-info rounded-pill badge-right'>{% trans "Sample" %}</span> + </a> + {% endif %} + {% if mixin_list %} {% for mixin in mixin_list %} <a class='sidebar-selector' id='select-plugin-{{plugin_key}}' data-bs-parent="#sidebar"> @@ -77,12 +83,6 @@ {% endfor %} {% endif %} - {% if plugin.is_sample %} - <a class='sidebar-selector' id='select-plugin-{{plugin_key}}' data-bs-parent="#sidebar"> - <span class='badge bg-info rounded-pill'>{% trans "code sample" %}</span> - </a> - {% endif %} - {% if plugin.website %} <a href="{{ plugin.website }}"><span class="fas fa-globe"></span></a> {% endif %} From 170cb544907ecc22d1a99ea3fe71ce684fa218fd Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> 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 <oliver.henry.walters@gmail.com> 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 <oliver.henry.walters@gmail.com> 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': '<b>Hello world</b>', + } + + """ 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': '<b>Hello world!</b>', + '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': '<em>This content only appears on the PartDetail page, you know!</em>', + }) + + # 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': '<h4>I have no children!</h4>' + }) + 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 <oliver.henry.walters@gmail.com> 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 @@ </div> <table class='table table-condensed table-striped' id='manufacturer-part-table' data-toolbar='#manufacturer-button-toolbar'></table> </div> - </div> + </div> </div> +{% 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': '<h4>I have no children!</h4>' }) - 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 %} + +<div class='panel panel-hidden' id='panel-{{ panel.key }}'> + <div class='panel-heading'> + <div class='d-flex flex-wrap'> + <h4>{{ panel.title }}</h4> + {% include "spacer.html" %} + <div class='btn-group' role='group'> + <!-- TODO: Implement custom action buttons for plugin panels --> + </div> + </div> + </div> + <div class='panel-content'> + {{ panel.content | safe }} + </div> +</div> + +{% endfor %} \ No newline at end of file From 71128a1c8e643c046f75a8dc88e72113603ed0c1 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> 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 @@ <script type='text/javascript'> $(document).ready(function () { + + {% include "panel/plugin_javascript.html" %} + {% block js_ready %} {% endblock %} From 0797e9ebf02986d214ae897d9ed4b896c6c26f89 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 6 May 2022 23:19:21 +1000 Subject: [PATCH 07/21] Simplify the new template rendering - No extra template code is required for any new page - All loaded in base.html or page_base.html - Oh, so clean! --- InvenTree/part/templates/part/detail.html | 2 -- InvenTree/part/templates/part/part_sidebar.html | 4 +--- InvenTree/templates/base.html | 3 ++- InvenTree/templates/page_base.html | 2 ++ .../plugin_panels.html => plugin/panel_content.html} | 7 ++++--- .../{panel/plugin_javascript.html => plugin/panel_js.html} | 0 .../plugin_menu_items.html => plugin/panel_menu.html} | 5 ++++- 7 files changed, 13 insertions(+), 10 deletions(-) rename InvenTree/templates/{panel/plugin_panels.html => plugin/panel_content.html} (84%) rename InvenTree/templates/{panel/plugin_javascript.html => plugin/panel_js.html} (100%) rename InvenTree/templates/{panel/plugin_menu_items.html => plugin/panel_menu.html} (54%) diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index e2e3d27be0..bcedc95da8 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -400,8 +400,6 @@ </div> </div> -{% include "panel/plugin_panels.html" %} - {% endblock %} {% block js_load %} diff --git a/InvenTree/part/templates/part/part_sidebar.html b/InvenTree/part/templates/part/part_sidebar.html index 18890b82af..1da07aa0c6 100644 --- a/InvenTree/part/templates/part/part_sidebar.html +++ b/InvenTree/part/templates/part/part_sidebar.html @@ -57,6 +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 "panel/plugin_menu_items.html" %} \ No newline at end of file +{% include "sidebar_item.html" with label="part-notes" text=text icon="fa-clipboard" %} \ No newline at end of file diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 7dbb17c7d8..0d8272892a 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -83,6 +83,7 @@ {% block sidebar %} <!-- Sidebar goes here --> {% endblock %} + {% include "plugin/panel_menu.html" %} {% include "sidebar_toggle.html" with target='sidebar' %} </ul> </div> @@ -210,7 +211,7 @@ $(document).ready(function () { - {% include "panel/plugin_javascript.html" %} + {% include "plugin/panel_js.html" %} {% block js_ready %} {% endblock %} diff --git a/InvenTree/templates/page_base.html b/InvenTree/templates/page_base.html index 17077700a2..9dcd33912d 100644 --- a/InvenTree/templates/page_base.html +++ b/InvenTree/templates/page_base.html @@ -61,6 +61,8 @@ </div> {% block page_content %} +<!-- Custom page content goes here--> {% endblock %} +{% include "plugin/panel_content.html" %} {% endblock %} diff --git a/InvenTree/templates/panel/plugin_panels.html b/InvenTree/templates/plugin/panel_content.html similarity index 84% rename from InvenTree/templates/panel/plugin_panels.html rename to InvenTree/templates/plugin/panel_content.html index ddbdbeee45..6b5c4ed90b 100644 --- a/InvenTree/templates/panel/plugin_panels.html +++ b/InvenTree/templates/plugin/panel_content.html @@ -1,5 +1,6 @@ +{% if plugin_panels %} +<!-- Custom panel items, loaded via plugins --> {% for panel in plugin_panels %} - <div class='panel panel-hidden' id='panel-{{ panel.key }}'> <div class='panel-heading'> <div class='d-flex flex-wrap'> @@ -14,5 +15,5 @@ {{ panel.content | safe }} </div> </div> - -{% endfor %} \ No newline at end of file +{% endfor %} +{% endif %} \ No newline at end of file diff --git a/InvenTree/templates/panel/plugin_javascript.html b/InvenTree/templates/plugin/panel_js.html similarity index 100% rename from InvenTree/templates/panel/plugin_javascript.html rename to InvenTree/templates/plugin/panel_js.html diff --git a/InvenTree/templates/panel/plugin_menu_items.html b/InvenTree/templates/plugin/panel_menu.html similarity index 54% rename from InvenTree/templates/panel/plugin_menu_items.html rename to InvenTree/templates/plugin/panel_menu.html index 2c084a021e..bbbadfdc64 100644 --- a/InvenTree/templates/panel/plugin_menu_items.html +++ b/InvenTree/templates/plugin/panel_menu.html @@ -1,3 +1,6 @@ +{% if plugin_panels %} +<!-- Custom sidebar menu items, loaded via plugins --> {% 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 +{% endfor %} +{% endif %} \ No newline at end of file From 12c58b14d6d40fefdfccf688e84fb690962dbec7 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 6 May 2022 23:19:47 +1000 Subject: [PATCH 08/21] Improvements for panel mixin sample --- InvenTree/InvenTree/views.py | 1 - InvenTree/plugin/builtin/integration/mixins.py | 6 +++--- .../samples/integration/custom_panel_sample.py | 18 ++++++++++++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 629ee1f31a..d9a9495bbd 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -75,7 +75,6 @@ class InvenTreePluginMixin: panels = [] for plug in registry.with_mixin('panel'): - panels += plug.render_panels(self, self.request) return panels diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 57c9fb3f0a..fbe7e383a9 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -606,18 +606,18 @@ class PanelMixin: for panel in self.get_custom_panels(view, request): if 'content_template' in panel: - # TODO: Render the actual content + # TODO: Render the actual HTML content from a template file ... if 'javascript_template' in panel: - # TODO: Render the actual content + # TODO: Render the actual javascript content from a template file ... # 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") + logger.warning(f"Custom panel for plugin {__class__} is missing a required parameter") continue # Add some information on this plugin diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index 3b999cce27..be6567a5fb 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -18,6 +18,19 @@ class CustomPanelSample(PanelMixin, IntegrationPluginBase): PLUGIN_SLUG = "panel" PLUGIN_TITLE = "Custom Panel Example" + def render_location_info(self, loc): + """ + Demonstrate that we can render information particular to a page + """ + return f""" + <h5>Location Information</h5> + <em>This location has no sublocations!</em> + <ul> + <li><b>Name</b>: {loc.name}</li> + <li><b>Path</b>: {loc.pathstring}</li> + </ul> + """ + def get_custom_panels(self, view, request): panels = [ @@ -46,14 +59,15 @@ 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): + try: loc = view.get_object() if not loc.get_descendants(include_self=False).exists(): panels.append({ - 'title': 'Childless', + 'title': 'Childless Location', 'icon': 'fa-user', - 'content': '<h4>I have no children!</h4>' + 'content': self.render_location_info(loc), }) except: pass From 243e3ff37d62c50bd839df1bf8eb2211de2cef12 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 6 May 2022 23:32:09 +1000 Subject: [PATCH 09/21] Fix calls to super() --- InvenTree/InvenTree/fields.py | 4 ++-- InvenTree/InvenTree/forms.py | 2 +- InvenTree/InvenTree/serializers.py | 2 +- InvenTree/InvenTree/views.py | 4 ++-- InvenTree/build/views.py | 4 ++-- InvenTree/part/views.py | 6 +++--- 6 files changed, 11 insertions(+), 11 deletions(-) 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/serializers.py b/InvenTree/InvenTree/serializers.py index 3e57883875..88132ad606 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -51,7 +51,7 @@ class InvenTreeMoneySerializer(MoneyField): Test that the returned amount is a valid Decimal """ - amount = super(DecimalField, self).get_value(data) + amount = super().get_value(data) # Convert an empty string to None if len(str(amount).strip()) == 0: diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index d9a9495bbd..23ca44cc83 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -687,7 +687,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 @@ -882,7 +882,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..d594a1ce1a 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -29,7 +29,7 @@ class BuildIndex(InvenTreeRoleMixin, ListView): def get_context_data(self, **kwargs): - context = super(BuildIndex, self).get_context_data(**kwargs).copy() + context = super(self).get_context_data(**kwargs).copy() context['BuildStatus'] = BuildStatus @@ -52,7 +52,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/part/views.py b/InvenTree/part/views.py index cb9725f94e..c43d604808 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -979,7 +979,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 +1045,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 +1140,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() From 60f799c90a486ed763ff6899a5e679476a098013 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 6 May 2022 23:32:42 +1000 Subject: [PATCH 10/21] Add plugin view support for most of the remaining views --- InvenTree/build/views.py | 4 ++-- InvenTree/company/views.py | 8 ++++---- InvenTree/order/views.py | 6 +++--- InvenTree/part/views.py | 4 ++-- InvenTree/stock/views.py | 10 +++++----- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index d594a1ce1a..ed240763f7 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -11,7 +11,7 @@ from django.views.generic import DetailView, ListView from .models import Build from InvenTree.views import AjaxDeleteView -from InvenTree.views import InvenTreeRoleMixin +from InvenTree.views import InvenTreeRoleMixin, InvenTreePluginMixin from InvenTree.status_codes import BuildStatus @@ -41,7 +41,7 @@ class BuildIndex(InvenTreeRoleMixin, ListView): return context -class BuildDetail(InvenTreeRoleMixin, DetailView): +class BuildDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): """ Detail view of a single Build object. """ diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 8c23002800..4dff4377b9 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -17,7 +17,7 @@ import requests import io from InvenTree.views import AjaxUpdateView -from InvenTree.views import InvenTreeRoleMixin +from InvenTree.views import InvenTreeRoleMixin, InvenTreePluginMixin from .models import Company from .models import ManufacturerPart @@ -104,7 +104,7 @@ class CompanyIndex(InvenTreeRoleMixin, ListView): return queryset -class CompanyDetail(DetailView): +class CompanyDetail(InvenTreePluginMixin, DetailView): """ Detail view for Company object """ context_obect_name = 'company' template_name = 'company/detail.html' @@ -196,7 +196,7 @@ class CompanyImageDownloadFromURL(AjaxUpdateView): ) -class ManufacturerPartDetail(DetailView): +class ManufacturerPartDetail(InvenTreePluginMixin, DetailView): """ Detail view for ManufacturerPart """ model = ManufacturerPart template_name = 'company/manufacturer_part_detail.html' @@ -210,7 +210,7 @@ class ManufacturerPartDetail(DetailView): return ctx -class SupplierPartDetail(DetailView): +class SupplierPartDetail(InvenTreePluginMixin, 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..532b7b244e 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -31,7 +31,7 @@ from . import forms as order_forms from part.views import PartPricing from InvenTree.helpers import DownloadFile -from InvenTree.views import InvenTreeRoleMixin, AjaxView +from InvenTree.views import InvenTreeRoleMixin, InvenTreePluginMixin, AjaxView logger = logging.getLogger("inventree") @@ -65,7 +65,7 @@ class SalesOrderIndex(InvenTreeRoleMixin, ListView): context_object_name = 'orders' -class PurchaseOrderDetail(InvenTreeRoleMixin, DetailView): +class PurchaseOrderDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): """ Detail view for a PurchaseOrder object """ context_object_name = 'order' @@ -78,7 +78,7 @@ class PurchaseOrderDetail(InvenTreeRoleMixin, DetailView): return ctx -class SalesOrderDetail(InvenTreeRoleMixin, DetailView): +class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): """ Detail view for a SalesOrder object """ context_object_name = 'order' diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index c43d604808..1d3c3b8f19 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -67,7 +67,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) @@ -969,7 +969,7 @@ class PartParameterTemplateDelete(AjaxDeleteView): ajax_form_title = _("Delete Part Parameter Template") -class CategoryDetail(InvenTreeRoleMixin, DetailView): +class CategoryDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): """ Detail view for PartCategory """ model = PartCategory diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 01d2b67c73..03429d4b8c 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -15,7 +15,7 @@ from django.utils.translation import gettext_lazy as _ from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import QRCodeView -from InvenTree.views import InvenTreeRoleMixin +from InvenTree.views import InvenTreeRoleMixin, InvenTreePluginMixin from InvenTree.forms import ConfirmForm from InvenTree.helpers import str2bool @@ -27,7 +27,7 @@ import common.settings from . import forms as StockForms -class StockIndex(InvenTreeRoleMixin, ListView): +class StockIndex(InvenTreeRoleMixin, InvenTreePluginMixin, ListView): """ StockIndex view loads all StockLocation and StockItem object """ model = StockItem @@ -35,7 +35,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 +54,7 @@ class StockIndex(InvenTreeRoleMixin, ListView): return context -class StockLocationDetail(InvenTreeRoleMixin, DetailView): +class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): """ Detailed view of a single StockLocation object """ @@ -75,7 +75,7 @@ class StockLocationDetail(InvenTreeRoleMixin, DetailView): return context -class StockItemDetail(InvenTreeRoleMixin, DetailView): +class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): """ Detailed view of a single StockItem object """ From 5ed0128435b67dc260e9091ef4a65d74014bf855 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 6 May 2022 23:34:34 +1000 Subject: [PATCH 11/21] PEP style fixes --- InvenTree/InvenTree/serializers.py | 1 - InvenTree/plugin/builtin/integration/mixins.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 88132ad606..243594c77e 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -26,7 +26,6 @@ from rest_framework import serializers from rest_framework.utils import model_meta from rest_framework.fields import empty from rest_framework.exceptions import ValidationError -from rest_framework.serializers import DecimalField from .models import extract_int diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index fbe7e383a9..b5040de78f 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -612,7 +612,7 @@ class PanelMixin: if 'javascript_template' in panel: # TODO: Render the actual javascript content from a template file ... - + # Check for required keys required_keys = ['title', 'content'] @@ -623,9 +623,9 @@ class PanelMixin: # 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')) + panel['key'] = InvenTree.helpers.generateTestKey(self.slug + panel.get('title', 'panel')) panels.append(panel) From bcf6e41b489b5447186c063193d32714856bdfc7 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 6 May 2022 23:43:54 +1000 Subject: [PATCH 12/21] Add some example docs --- .../plugin/samples/integration/custom_panel_sample.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index be6567a5fb..8b73f793d1 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -33,6 +33,15 @@ class CustomPanelSample(PanelMixin, IntegrationPluginBase): 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 'hello world' panel will be displayed on any view which implements custom panels From 96f61dfcdbf156b89b11ce55feb4333184758498 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 6 May 2022 23:48:24 +1000 Subject: [PATCH 13/21] Add plugin description --- InvenTree/plugin/samples/integration/custom_panel_sample.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index 8b73f793d1..e5b027fdac 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -17,6 +17,8 @@ class CustomPanelSample(PanelMixin, IntegrationPluginBase): 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" def render_location_info(self, loc): """ From 44c4e8864690fc0b1da13f0356146a048f9c5ac3 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sat, 7 May 2022 02:17:20 +1000 Subject: [PATCH 14/21] Add a configurable setting to the demo plugin --- .../integration/custom_panel_sample.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index e5b027fdac..0eada9c8ab 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -3,13 +3,13 @@ Sample plugin which renders custom panels on certain pages """ from plugin import IntegrationPluginBase -from plugin.mixins import PanelMixin +from plugin.mixins import PanelMixin, SettingsMixin from part.views import PartDetail from stock.views import StockLocationDetail -class CustomPanelSample(PanelMixin, IntegrationPluginBase): +class CustomPanelSample(PanelMixin, SettingsMixin, IntegrationPluginBase): """ A sample plugin which renders some custom panels. """ @@ -20,6 +20,15 @@ class CustomPanelSample(PanelMixin, IntegrationPluginBase): 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 render_location_info(self, loc): """ Demonstrate that we can render information particular to a page @@ -46,18 +55,20 @@ class CustomPanelSample(PanelMixin, IntegrationPluginBase): 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': '<b>Hello world!</b>', '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', - } - ] + }) # This panel will *only* display on the PartDetail view if isinstance(view, PartDetail): From 103921f5c47e00d7a4da8c0a77b9ca4084789355 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sat, 7 May 2022 19:59:59 +1000 Subject: [PATCH 15/21] Rename plugin.loader to plugin.template - Add helper function for rendering a template --- InvenTree/InvenTree/settings.py | 2 +- InvenTree/plugin/loader.py | 19 ---------- InvenTree/plugin/template.py | 66 +++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 20 deletions(-) delete mode 100644 InvenTree/plugin/loader.py create mode 100644 InvenTree/plugin/template.py 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/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/template.py b/InvenTree/plugin/template.py new file mode 100644 index 0000000000..7e1da81609 --- /dev/null +++ b/InvenTree/plugin/template.py @@ -0,0 +1,66 @@ +""" +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) # pragma: no cover + + return tuple(template_dirs) + + +def render_template(plugin, template_file, context=None): + """ + Locate and render a template file, available in the global template context. + """ + + print("render_template", "->", template_file) + print("Context:") + print(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 From 9f15dd8e2a6a9a4592a05e707fb9a561ec9e0dfc Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sat, 7 May 2022 22:32:26 +1000 Subject: [PATCH 16/21] Custom panels can now be rendered from a template --- InvenTree/InvenTree/views.py | 6 +-- .../plugin/builtin/integration/mixins.py | 54 ++++++++++++++----- .../integration/custom_panel_sample.py | 23 ++++---- .../templates/panel_demo/childless.html | 11 ++++ InvenTree/plugin/template.py | 4 -- 5 files changed, 65 insertions(+), 33 deletions(-) create mode 100644 InvenTree/plugin/samples/integration/templates/panel_demo/childless.html diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 23ca44cc83..f98a1b1cdc 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -67,7 +67,7 @@ class InvenTreePluginMixin: """ - def get_plugin_panels(self): + def get_plugin_panels(self, ctx): """ Return a list of extra 'plugin panels' associated with this view """ @@ -75,7 +75,7 @@ class InvenTreePluginMixin: panels = [] for plug in registry.with_mixin('panel'): - panels += plug.render_panels(self, self.request) + panels += plug.render_panels(self, self.request, ctx) return panels @@ -84,7 +84,7 @@ class InvenTreePluginMixin: ctx = super().get_context_data(**kwargs) if settings.PLUGINS_ENABLED: - ctx['plugin_panels'] = self.get_plugin_panels() + ctx['plugin_panels'] = self.get_plugin_panels(ctx) return ctx diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index b5040de78f..d50a9ce729 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -11,9 +11,10 @@ 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 +from plugin.models import PluginConfig, PluginSetting +from plugin.template import render_template +from plugin.urls import PLUGIN_BASE logger = logging.getLogger('inventree') @@ -599,19 +600,50 @@ class PanelMixin: super().__init__() self.add_mixin('panel', True, __class__) - def render_panels(self, view, request): + 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): - if 'content_template' in panel: - # TODO: Render the actual HTML content from a template file - ... + content_template = panel.get('content_template', None) + javascript_template = panel.get('javascript_template', None) - if 'javascript_template' in panel: - # TODO: Render the actual javascript content from a template file - ... + 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'] @@ -630,7 +662,3 @@ class PanelMixin: 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/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index 0eada9c8ab..e0b84fe01a 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -29,18 +29,15 @@ class CustomPanelSample(PanelMixin, SettingsMixin, IntegrationPluginBase): } } - def render_location_info(self, loc): - """ - Demonstrate that we can render information particular to a page - """ - return f""" - <h5>Location Information</h5> - <em>This location has no sublocations!</em> - <ul> - <li><b>Name</b>: {loc.name}</li> - <li><b>Path</b>: {loc.pathstring}</li> - </ul> - """ + 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): @@ -89,7 +86,7 @@ class CustomPanelSample(PanelMixin, SettingsMixin, IntegrationPluginBase): panels.append({ 'title': 'Childless Location', 'icon': 'fa-user', - 'content': self.render_location_info(loc), + 'content_template': 'panel_demo/childless.html', # Note that the panel content is rendered using a template file! }) except: pass 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 @@ +<h4>Template Rendering</h4> + +<div class='alert alert-block alert-info'> + This panel has been rendered using a template file! +</div> + +<em>This location has no sublocations!</em> +<ul> + <li><b>Location Name</b>: {{ location.name }}</li> + <li><b>Location Path</b>: {{ location.pathstring }}</li> +</ul> diff --git a/InvenTree/plugin/template.py b/InvenTree/plugin/template.py index 7e1da81609..0f580a8023 100644 --- a/InvenTree/plugin/template.py +++ b/InvenTree/plugin/template.py @@ -45,10 +45,6 @@ def render_template(plugin, template_file, context=None): Locate and render a template file, available in the global template context. """ - print("render_template", "->", template_file) - print("Context:") - print(context) - try: tmp = template.loader.get_template(template_file) except template.TemplateDoesNotExist: From 06e79ee91bc9927bb79a3bf632a10ccaffe01b3c Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sat, 7 May 2022 22:33:30 +1000 Subject: [PATCH 17/21] Move view mixin to plugin.views --- InvenTree/InvenTree/views.py | 33 ----------------------------- InvenTree/build/views.py | 6 ++++-- InvenTree/company/views.py | 11 +++++----- InvenTree/order/views.py | 8 +++++--- InvenTree/part/views.py | 8 +++++--- InvenTree/plugin/views.py | 40 ++++++++++++++++++++++++++++++++++++ InvenTree/stock/views.py | 10 +++++---- 7 files changed, 66 insertions(+), 50 deletions(-) create mode 100644 InvenTree/plugin/views.py diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index f98a1b1cdc..6291b321a8 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -38,8 +38,6 @@ 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 @@ -58,37 +56,6 @@ 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, 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): - - ctx = super().get_context_data(**kwargs) - - if settings.PLUGINS_ENABLED: - ctx['plugin_panels'] = self.get_plugin_panels(ctx) - - return ctx - - class InvenTreeRoleMixin(PermissionRequiredMixin): """ Permission class based on user roles, not user 'permissions'. diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index ed240763f7..6b44ae3dfc 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -11,9 +11,11 @@ from django.views.generic import DetailView, ListView from .models import Build from InvenTree.views import AjaxDeleteView -from InvenTree.views import InvenTreeRoleMixin, InvenTreePluginMixin +from InvenTree.views import InvenTreeRoleMixin from InvenTree.status_codes import BuildStatus +from plugin.views import InvenTreePluginViewMixin + class BuildIndex(InvenTreeRoleMixin, ListView): """ @@ -41,7 +43,7 @@ class BuildIndex(InvenTreeRoleMixin, ListView): return context -class BuildDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): +class BuildDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detail view of a single Build object. """ diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 4dff4377b9..6d8279558c 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -17,15 +17,16 @@ import requests import io from InvenTree.views import AjaxUpdateView -from InvenTree.views import InvenTreeRoleMixin, InvenTreePluginMixin +from InvenTree.views import InvenTreeRoleMixin 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(InvenTreePluginMixin, 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(InvenTreePluginMixin, DetailView): +class ManufacturerPartDetail(InvenTreePluginViewMixin, DetailView): """ Detail view for ManufacturerPart """ model = ManufacturerPart template_name = 'company/manufacturer_part_detail.html' @@ -210,7 +211,7 @@ class ManufacturerPartDetail(InvenTreePluginMixin, DetailView): return ctx -class SupplierPartDetail(InvenTreePluginMixin, 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 532b7b244e..f8102908e9 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -31,7 +31,9 @@ from . import forms as order_forms from part.views import PartPricing from InvenTree.helpers import DownloadFile -from InvenTree.views import InvenTreeRoleMixin, InvenTreePluginMixin, AjaxView +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, InvenTreePluginMixin, DetailView): +class PurchaseOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detail view for a PurchaseOrder object """ context_object_name = 'order' @@ -78,7 +80,7 @@ class PurchaseOrderDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): return ctx -class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): +class SalesOrderDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detail view for a SalesOrder object """ context_object_name = 'order' diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 1d3c3b8f19..f1d587e206 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -49,10 +49,12 @@ from order.models import PurchaseOrderLineItem from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import QRCodeView -from InvenTree.views import InvenTreeRoleMixin, InvenTreePluginMixin +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 @@ -365,7 +367,7 @@ class PartImportAjax(FileManagementAjaxView, PartImport): return PartImport.validate(self, self.steps.current, form, **kwargs) -class PartDetail(InvenTreeRoleMixin, InvenTreePluginMixin, 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, InvenTreePluginMixin, DetailView): +class CategoryDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detail view for PartCategory """ model = PartCategory 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 03429d4b8c..168403d692 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -15,7 +15,7 @@ from django.utils.translation import gettext_lazy as _ from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import QRCodeView -from InvenTree.views import InvenTreeRoleMixin, InvenTreePluginMixin +from InvenTree.views import InvenTreeRoleMixin from InvenTree.forms import ConfirmForm from InvenTree.helpers import str2bool @@ -26,8 +26,10 @@ import common.settings from . import forms as StockForms +from plugin.views import InvenTreePluginViewMixin -class StockIndex(InvenTreeRoleMixin, InvenTreePluginMixin, ListView): + +class StockIndex(InvenTreeRoleMixin, InvenTreePluginViewMixin, ListView): """ StockIndex view loads all StockLocation and StockItem object """ model = StockItem @@ -54,7 +56,7 @@ class StockIndex(InvenTreeRoleMixin, InvenTreePluginMixin, ListView): return context -class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): +class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detailed view of a single StockLocation object """ @@ -75,7 +77,7 @@ class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): return context -class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginMixin, DetailView): +class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): """ Detailed view of a single StockItem object """ From e57a3870c6af815ba786c855fbd5ec7dda7eecb4 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sat, 7 May 2022 22:47:48 +1000 Subject: [PATCH 18/21] Fix build index template --- InvenTree/build/views.py | 2 +- InvenTree/plugin/builtin/integration/mixins.py | 2 +- InvenTree/plugin/samples/integration/custom_panel_sample.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 6b44ae3dfc..c8ac4509b8 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -31,7 +31,7 @@ class BuildIndex(InvenTreeRoleMixin, ListView): def get_context_data(self, **kwargs): - context = super(self).get_context_data(**kwargs).copy() + context = super().get_context_data(**kwargs) context['BuildStatus'] = BuildStatus diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index d50a9ce729..b22efc9415 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -622,7 +622,7 @@ class PanelMixin: context['object'] = view.get_object() except AttributeError: pass - + return context def render_panels(self, view, request, context): diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index e0b84fe01a..73ca863576 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -64,7 +64,7 @@ class CustomPanelSample(PanelMixin, SettingsMixin, IntegrationPluginBase): 'icon': 'fas fa-boxes', 'content': '<b>Hello world!</b>', 'description': 'A simple panel which renders hello world', - 'javascript': 'alert("Hello world");', + 'javascript': 'console.log("Hello world, from a custom panel!");', }) # This panel will *only* display on the PartDetail view From b2689b943ebe9d246f821c7c6796c9ab1f513733 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sat, 7 May 2022 23:22:54 +1000 Subject: [PATCH 19/21] Specific call to super() was actually needed --- InvenTree/InvenTree/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 243594c77e..3e57883875 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -26,6 +26,7 @@ from rest_framework import serializers from rest_framework.utils import model_meta from rest_framework.fields import empty from rest_framework.exceptions import ValidationError +from rest_framework.serializers import DecimalField from .models import extract_int @@ -50,7 +51,7 @@ class InvenTreeMoneySerializer(MoneyField): Test that the returned amount is a valid Decimal """ - amount = super().get_value(data) + amount = super(DecimalField, self).get_value(data) # Convert an empty string to None if len(str(amount).strip()) == 0: From 50d8f242bb35ffdd74bd62c45a6b4a9d816ed098 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sat, 7 May 2022 23:50:41 +1000 Subject: [PATCH 20/21] Fix for unit test --- InvenTree/part/test_part.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index f3e1d4490d..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), 36) + self.assertEqual(len(result), len(InvenTreeUserSetting.SETTINGS)) def test_global_settings(self): result = inventree_extras.global_settings() From ada1eeeb3554ffc3ce0f6af92c12a10eda3e34b3 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Sun, 8 May 2022 07:53:21 +1000 Subject: [PATCH 21/21] Remove 'no cover' --- InvenTree/plugin/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/template.py b/InvenTree/plugin/template.py index 0f580a8023..53ee7bb6db 100644 --- a/InvenTree/plugin/template.py +++ b/InvenTree/plugin/template.py @@ -35,7 +35,7 @@ class PluginTemplateLoader(FilesystemLoader): 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 + template_dirs.append(new_path) return tuple(template_dirs)