From f600083deec98997f3fa66d030c8bb634800cf30 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Sep 2021 16:14:06 +0200 Subject: [PATCH 001/487] initial webhook view #2036 --- InvenTree/InvenTree/urls.py | 4 +- InvenTree/common/admin.py | 8 +- InvenTree/common/api.py | 117 ++++++++++++++++++ .../common/migrations/0012_webhookendpoint.py | 28 +++++ InvenTree/common/models.py | 50 ++++++++ 5 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 InvenTree/common/migrations/0012_webhookendpoint.py diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 7d51c6a4cf..b586bd6a65 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -54,7 +54,6 @@ admin.site.site_header = "InvenTree Admin" apipatterns = [ url(r'^barcode/', include(barcode_api_urls)), - url(r'^common/', include(common_api_urls)), url(r'^part/', include(part_api_urls)), url(r'^bom/', include(bom_api_urls)), url(r'^company/', include(company_api_urls)), @@ -70,6 +69,9 @@ apipatterns = [ # Plugin endpoints url(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'), + # Webhook enpoint + path('', include(common_api_urls)), + # InvenTree information endpoint url(r'^$', InfoView.as_view(), name='api-inventree-info'), diff --git a/InvenTree/common/admin.py b/InvenTree/common/admin.py index 1eda18e869..50954924ff 100644 --- a/InvenTree/common/admin.py +++ b/InvenTree/common/admin.py @@ -5,7 +5,7 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin -from .models import InvenTreeSetting, InvenTreeUserSetting +from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint class SettingsAdmin(ImportExportModelAdmin): @@ -18,5 +18,11 @@ class UserSettingsAdmin(ImportExportModelAdmin): list_display = ('key', 'value', 'user', ) +class WebhookAdmin(ImportExportModelAdmin): + + list_display = ('endpoint_id', 'name', 'active', 'user') + + admin.site.register(InvenTreeSetting, SettingsAdmin) admin.site.register(InvenTreeUserSetting, UserSettingsAdmin) +admin.site.register(WebhookEndpoint, WebhookAdmin) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 8a2dfbd6a7..efde6e863e 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -5,5 +5,122 @@ Provides a JSON API for common components. # -*- coding: utf-8 -*- from __future__ import unicode_literals +import json +from secrets import compare_digest + +from django.utils.decorators import method_decorator +from django.urls import path +from django.views.decorators.csrf import csrf_exempt + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.exceptions import PermissionDenied, NotFound, NotAcceptable + +from .models import WebhookEndpoint + + +class CsrfExemptMixin(object): + """ + Exempts the view from CSRF requirements. + """ + + @method_decorator(csrf_exempt) + def dispatch(self, *args, **kwargs): + return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) + + +class VerificationMethod: + NONE = 0 + TOKEN = 1 + HMAC = 2 + + +class WebhookView(CsrfExemptMixin, APIView): + """ + Endpoint for receiving webhoks. + """ + authentication_classes = [] + permission_classes = [] + model_class = WebhookEndpoint + + # Token + TOKEN_NAME = "Token" + VERIFICATION_METHOD = VerificationMethod.NONE + + MESSAGE_OK = "Message was received." + MESSAGE_TOKEN_ERROR = "Incorrect token in header." + + def post(self, request, endpoint, *args, **kwargs): + self.init(request, *args, **kwargs) + # get webhook definition + self.get_webhook(endpoint, *args, **kwargs) + # check headers + headers = request.headers + self.validate_token(headers) + + # process data + try: + payload = json.loads(request.body) + except json.decoder.JSONDecodeError as error: + raise NotAcceptable(error.msg) + self.save_data(payload, headers, request) + self.process_payload(payload, headers, request) + + # return results + return_kwargs = self.get_result(payload, headers, request) + return Response(**return_kwargs) + + # To be overridden + def init(self, request, *args, **kwargs): + self.token = '' + self.verify = self.VERIFICATION_METHOD + + def get_webhook(self, endpoint): + try: + webhook = self.model_class.objects.get(endpoint_id=endpoint) + self.webhook = webhook + return self.process_webhook() + except self.model_class.DoesNotExist: + raise NotFound() + + def process_webhook(self): + if self.webhook.token: + self.token = self.webhook.token + self.verify = VerificationMethod.TOKEN + return True + + def validate_token(self, headers): + token = headers.get(self.TOKEN_NAME, "") + + # no token + if self.verify == VerificationMethod.NONE: + return True + + # static token + elif self.verify == VerificationMethod.TOKEN: + if not compare_digest(token, self.token): + raise PermissionDenied(self.MESSAGE_TOKEN_ERROR) + return True + + # hmac token + elif self.verify == VerificationMethod.HMAC: + # TODO write check + return True + + def save_data(self, payload, headers=None, request=None): + # TODO safe data + return + + def process_payload(self, payload, headers=None, request=None): + return + + def get_result(self, payload, headers=None, request=None): + context = {} + context['data'] = {'message': self.MESSAGE_OK} + context['status'] = 200 + return context + + common_api_urls = [ + path('webhook//', WebhookView.as_view(), name='api-webhook'), ] diff --git a/InvenTree/common/migrations/0012_webhookendpoint.py b/InvenTree/common/migrations/0012_webhookendpoint.py new file mode 100644 index 0000000000..b3cf6bce2f --- /dev/null +++ b/InvenTree/common/migrations/0012_webhookendpoint.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.4 on 2021-09-12 12:42 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('common', '0011_auto_20210722_2114'), + ] + + operations = [ + migrations.CreateModel( + name='WebhookEndpoint', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('endpoint_id', models.CharField(default=uuid.uuid4, editable=False, help_text='Endpoint at which this webhook is received', max_length=255, verbose_name='Endpoint')), + ('name', models.CharField(blank=True, help_text='Name for this webhook', max_length=255, null=True, verbose_name='Name')), + ('active', models.BooleanField(default=True, help_text='Is this webhook active', verbose_name='Active')), + ('token', models.CharField(blank=True, default=uuid.uuid4, help_text='Token for access', max_length=255, null=True, verbose_name='Token')), + ('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + ), + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index aed6f2bf14..df64cd5cbc 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -9,6 +9,7 @@ from __future__ import unicode_literals import os import decimal import math +import uuid from django.db import models, transaction from django.contrib.auth.models import User @@ -1165,3 +1166,52 @@ class ColorTheme(models.Model): return True return False + + +class WebhookEndpoint(models.Model): + """ Defines a Webhook entdpoint + + Attributes: + endpoint_id: Path to the webhook, + name: Name of the webhook, + active: Is this webhook active?, + user: User associated with webhook, + token: Token for sending a webhook, + """ + + endpoint_id = models.CharField( + max_length=255, + verbose_name=_('Endpoint'), + help_text=_('Endpoint at which this webhook is received'), + default=uuid.uuid4, + editable=False, + ) + + name = models.CharField( + max_length=255, + blank=True, null=True, + verbose_name=_('Name'), + help_text=_('Name for this webhook') + ) + + active = models.BooleanField( + default=True, + verbose_name=_('Active'), + help_text=_('Is this webhook active') + ) + + user = models.ForeignKey( + User, + on_delete=models.SET_NULL, + blank=True, null=True, + verbose_name=_('User'), + help_text=_('User'), + ) + + token = models.CharField( + max_length=255, + blank=True, null=True, + verbose_name=_('Token'), + help_text=_('Token for access'), + default=uuid.uuid4, + ) From 5bf9561984a3af80b9c41f447c987a7d0379589a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Sep 2021 16:32:40 +0200 Subject: [PATCH 002/487] refactor --- InvenTree/common/api.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index efde6e863e..ea363fa394 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -54,15 +54,17 @@ class WebhookView(CsrfExemptMixin, APIView): self.init(request, *args, **kwargs) # get webhook definition self.get_webhook(endpoint, *args, **kwargs) + # check headers headers = request.headers - self.validate_token(headers) - - # process data try: payload = json.loads(request.body) except json.decoder.JSONDecodeError as error: raise NotAcceptable(error.msg) + + # validate + self.validate_token(payload, headers) + # process data self.save_data(payload, headers, request) self.process_payload(payload, headers, request) @@ -87,25 +89,25 @@ class WebhookView(CsrfExemptMixin, APIView): if self.webhook.token: self.token = self.webhook.token self.verify = VerificationMethod.TOKEN + # TODO make a object-setting return True - def validate_token(self, headers): + def validate_token(self, payload, headers): token = headers.get(self.TOKEN_NAME, "") # no token if self.verify == VerificationMethod.NONE: - return True + pass # static token elif self.verify == VerificationMethod.TOKEN: if not compare_digest(token, self.token): raise PermissionDenied(self.MESSAGE_TOKEN_ERROR) - return True # hmac token elif self.verify == VerificationMethod.HMAC: - # TODO write check - return True + + return True def save_data(self, payload, headers=None, request=None): # TODO safe data From 68ca6729376f56fff2ef59fa541242e3fffe3550 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Sep 2021 16:49:47 +0200 Subject: [PATCH 003/487] hmac verification --- InvenTree/common/api.py | 12 ++++++++++++ .../migrations/0013_auto_20210912_1443.py | 18 ++++++++++++++++++ InvenTree/common/models.py | 8 ++++++++ 3 files changed, 38 insertions(+) create mode 100644 InvenTree/common/migrations/0013_auto_20210912_1443.py diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index ea363fa394..e307d5485f 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -6,6 +6,9 @@ Provides a JSON API for common components. from __future__ import unicode_literals import json +import hmac +import hashlib +import base64 from secrets import compare_digest from django.utils.decorators import method_decorator @@ -75,6 +78,7 @@ class WebhookView(CsrfExemptMixin, APIView): # To be overridden def init(self, request, *args, **kwargs): self.token = '' + self.secret = '' self.verify = self.VERIFICATION_METHOD def get_webhook(self, endpoint): @@ -90,6 +94,10 @@ class WebhookView(CsrfExemptMixin, APIView): self.token = self.webhook.token self.verify = VerificationMethod.TOKEN # TODO make a object-setting + if self.webhook.secret: + self.secret = self.webhook.secret + self.verify = VerificationMethod.HMAC + # TODO make a object-setting return True def validate_token(self, payload, headers): @@ -106,6 +114,10 @@ class WebhookView(CsrfExemptMixin, APIView): # hmac token elif self.verify == VerificationMethod.HMAC: + digest = hmac.new(self.secret, payload.encode('utf-8'), hashlib.sha256).digest() + computed_hmac = base64.b64encode(digest) + if not hmac.compare_digest(computed_hmac, token.encode('utf-8')): + raise PermissionDenied(self.MESSAGE_TOKEN_ERROR) return True diff --git a/InvenTree/common/migrations/0013_auto_20210912_1443.py b/InvenTree/common/migrations/0013_auto_20210912_1443.py new file mode 100644 index 0000000000..f9c05fe05f --- /dev/null +++ b/InvenTree/common/migrations/0013_auto_20210912_1443.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.4 on 2021-09-12 14:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0012_webhookendpoint'), + ] + + operations = [ + migrations.AddField( + model_name='webhookendpoint', + name='secret', + field=models.CharField(blank=True, help_text='Shared secret for HMAC', max_length=255, null=True, verbose_name='Secret'), + ), + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index df64cd5cbc..ca13e84357 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -1177,6 +1177,7 @@ class WebhookEndpoint(models.Model): active: Is this webhook active?, user: User associated with webhook, token: Token for sending a webhook, + secret: Shared secret for HMAC verification, """ endpoint_id = models.CharField( @@ -1215,3 +1216,10 @@ class WebhookEndpoint(models.Model): help_text=_('Token for access'), default=uuid.uuid4, ) + + secret = models.CharField( + max_length=255, + blank=True, null=True, + verbose_name=_('Secret'), + help_text=_('Shared secret for HMAC'), + ) From 440311cddb4ab0b3cbb9ce44776c3008a7b67b47 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Sep 2021 17:36:57 +0200 Subject: [PATCH 004/487] ruleset --- InvenTree/users/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 179a70ed74..3ebaefe5a4 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -142,6 +142,7 @@ class RuleSet(models.Model): 'common_colortheme', 'common_inventreesetting', 'common_inventreeusersetting', + 'common_webhookendpoint', 'company_contact', 'users_owner', From e2bb5e978bb962c5a41498b79879680fd4f961db Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Sep 2021 19:25:36 +0200 Subject: [PATCH 005/487] fix hmac --- InvenTree/common/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index e307d5485f..a0d69bd2ab 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -66,7 +66,7 @@ class WebhookView(CsrfExemptMixin, APIView): raise NotAcceptable(error.msg) # validate - self.validate_token(payload, headers) + self.validate_token(payload, headers, request) # process data self.save_data(payload, headers, request) self.process_payload(payload, headers, request) @@ -100,7 +100,7 @@ class WebhookView(CsrfExemptMixin, APIView): # TODO make a object-setting return True - def validate_token(self, payload, headers): + def validate_token(self, payload, headers, request): token = headers.get(self.TOKEN_NAME, "") # no token @@ -114,7 +114,7 @@ class WebhookView(CsrfExemptMixin, APIView): # hmac token elif self.verify == VerificationMethod.HMAC: - digest = hmac.new(self.secret, payload.encode('utf-8'), hashlib.sha256).digest() + digest = hmac.new(self.secret.encode('utf-8'), request.body, hashlib.sha256).digest() computed_hmac = base64.b64encode(digest) if not hmac.compare_digest(computed_hmac, token.encode('utf-8')): raise PermissionDenied(self.MESSAGE_TOKEN_ERROR) From 0e2db232ae4441f575b11762e506dc5f33d712b9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Sep 2021 22:24:25 +0200 Subject: [PATCH 006/487] save messages --- InvenTree/common/admin.py | 3 +- InvenTree/common/api.py | 18 +++--- .../migrations/0014_auto_20210912_1804.py | 26 +++++++++ InvenTree/common/models.py | 57 +++++++++++++++++++ 4 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 InvenTree/common/migrations/0014_auto_20210912_1804.py diff --git a/InvenTree/common/admin.py b/InvenTree/common/admin.py index 50954924ff..088f53dc39 100644 --- a/InvenTree/common/admin.py +++ b/InvenTree/common/admin.py @@ -5,7 +5,7 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin -from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint +from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint, WebhookMessage class SettingsAdmin(ImportExportModelAdmin): @@ -26,3 +26,4 @@ class WebhookAdmin(ImportExportModelAdmin): admin.site.register(InvenTreeSetting, SettingsAdmin) admin.site.register(InvenTreeUserSetting, UserSettingsAdmin) admin.site.register(WebhookEndpoint, WebhookAdmin) +admin.site.register(WebhookMessage, ImportExportModelAdmin) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index a0d69bd2ab..bfd7163710 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -19,7 +19,7 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.exceptions import PermissionDenied, NotFound, NotAcceptable -from .models import WebhookEndpoint +from .models import WebhookEndpoint, WebhookMessage class CsrfExemptMixin(object): @@ -68,8 +68,8 @@ class WebhookView(CsrfExemptMixin, APIView): # validate self.validate_token(payload, headers, request) # process data - self.save_data(payload, headers, request) - self.process_payload(payload, headers, request) + message = self.save_data(payload, headers, request) + self.process_payload(message, payload, headers) # return results return_kwargs = self.get_result(payload, headers, request) @@ -122,11 +122,15 @@ class WebhookView(CsrfExemptMixin, APIView): return True def save_data(self, payload, headers=None, request=None): - # TODO safe data - return + return WebhookMessage.objects.create( + host=request.host, + header=headers, + body=payload, + endpoint=self.webhook, + ) - def process_payload(self, payload, headers=None, request=None): - return + def process_payload(self, message, payload=None, headers=None): + return True def get_result(self, payload, headers=None, request=None): context = {} diff --git a/InvenTree/common/migrations/0014_auto_20210912_1804.py b/InvenTree/common/migrations/0014_auto_20210912_1804.py new file mode 100644 index 0000000000..18feb0d4a4 --- /dev/null +++ b/InvenTree/common/migrations/0014_auto_20210912_1804.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.4 on 2021-09-12 18:04 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0013_auto_20210912_1443'), + ] + + operations = [ + migrations.CreateModel( + name='WebhookMessage', + fields=[ + ('message_id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unique identifier for this message', primary_key=True, serialize=False, verbose_name='Message ID')), + ('host', models.CharField(editable=False, help_text='Host from which this message was received', max_length=255, verbose_name='Host')), + ('header', models.CharField(blank=True, editable=False, help_text='Header of this message', max_length=255, null=True, verbose_name='Header')), + ('body', models.JSONField(blank=True, editable=False, help_text='Body of this message', null=True, verbose_name='Body')), + ('worked_on', models.BooleanField(default=False, help_text='Was the work on this message finished?', verbose_name='Worked on')), + ('endpoint', models.ForeignKey(blank=True, help_text='Endpoint on which this message was received', null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.webhookendpoint', verbose_name='Endpoint')), + ], + ), + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index ca13e84357..4bd24e878e 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -1223,3 +1223,60 @@ class WebhookEndpoint(models.Model): verbose_name=_('Secret'), help_text=_('Shared secret for HMAC'), ) + + +class WebhookMessage(models.Model): + """ Defines a webhook message + + Attributes: + message_id: Unique identifier for this message, + host: Host from which this message was received, + header: Header of this message, + body: Body of this message, + endpoint: Endpoint on which this message was received, + worked_on: Was the work on this message finished? + """ + + message_id = models.UUIDField( + verbose_name=_('Message ID'), + help_text=_('Unique identifier for this message'), + primary_key=True, + default=uuid.uuid4, + editable=False, + ) + + host = models.CharField( + max_length=255, + verbose_name=_('Host'), + help_text=_('Host from which this message was received'), + editable=False, + ) + + header = models.CharField( + max_length=255, + blank=True, null=True, + verbose_name=_('Header'), + help_text=_('Header of this message'), + editable=False, + ) + + body = models.JSONField( + blank=True, null=True, + verbose_name=_('Body'), + help_text=_('Body of this message'), + editable=False, + ) + + endpoint = models.ForeignKey( + WebhookEndpoint, + on_delete=models.SET_NULL, + blank=True, null=True, + verbose_name=_('Endpoint'), + help_text=_('Endpoint on which this message was received'), + ) + + worked_on = models.BooleanField( + default=False, + verbose_name=_('Worked on'), + help_text=_('Was the work on this message finished?'), + ) From 4baf714c8598a0253b4677ac30eb940ae6597598 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Sep 2021 22:26:20 +0200 Subject: [PATCH 007/487] let tasks run async --- InvenTree/common/api.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index bfd7163710..68a27b9475 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -18,6 +18,7 @@ from django.views.decorators.csrf import csrf_exempt from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.exceptions import PermissionDenied, NotFound, NotAcceptable +from django_q.tasks import async_task from .models import WebhookEndpoint, WebhookMessage @@ -45,6 +46,7 @@ class WebhookView(CsrfExemptMixin, APIView): authentication_classes = [] permission_classes = [] model_class = WebhookEndpoint + run_async = False # Token TOKEN_NAME = "Token" @@ -69,12 +71,22 @@ class WebhookView(CsrfExemptMixin, APIView): self.validate_token(payload, headers, request) # process data message = self.save_data(payload, headers, request) - self.process_payload(message, payload, headers) + if self.run_async: + async_task(self._process_payload, message.id) + else: + message.worked_on = self.process_payload(message, payload, headers) + message.save() # return results return_kwargs = self.get_result(payload, headers, request) return Response(**return_kwargs) + def _process_payload(self, message_id): + message = WebhookMessage.objects.get(message_id=message_id) + process_result = self.process_payload(message, message.body, message.header) + message.worked_on = process_result + message.save() + # To be overridden def init(self, request, *args, **kwargs): self.token = '' From ca3e9709e18eb0f80aba43b67ee1698ab3dbd376 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Sep 2021 22:46:41 +0200 Subject: [PATCH 008/487] rueset for messafe --- InvenTree/users/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 3ebaefe5a4..031933e590 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -143,6 +143,7 @@ class RuleSet(models.Model): 'common_inventreesetting', 'common_inventreeusersetting', 'common_webhookendpoint', + 'common_webhookmessage', 'company_contact', 'users_owner', From e73bf7be23af07a2cc4c467ffd2bc4c9d34e7557 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Sep 2021 00:16:41 +0200 Subject: [PATCH 009/487] tests for base functions --- InvenTree/common/api.py | 3 ++- InvenTree/common/tests.py | 55 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 68a27b9475..16b4199a0b 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -135,7 +135,8 @@ class WebhookView(CsrfExemptMixin, APIView): def save_data(self, payload, headers=None, request=None): return WebhookMessage.objects.create( - host=request.host, + # host=request.host, + # TODO fix header=headers, body=payload, endpoint=self.webhook, diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index d20f76baa0..30fcd9cb41 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from http import HTTPStatus +import json -from django.test import TestCase +from django.test import TestCase, Client from django.contrib.auth import get_user_model -from .models import InvenTreeSetting +from .models import InvenTreeSetting, WebhookEndpoint, WebhookMessage +from .api import WebhookView class SettingsTest(TestCase): @@ -85,3 +88,51 @@ class SettingsTest(TestCase): if setting.default_value not in [True, False]: raise ValueError(f'Non-boolean default value specified for {key}') + + +class WebhookMessageTests(TestCase): + def setUp(self): + self.endpoint_def = WebhookEndpoint.objects.create() + self.url = f'/api/webhook/{self.endpoint_def.endpoint_id}/' + self.client = Client(enforce_csrf_checks=True) + + def test_bad_method(self): + response = self.client.get(self.url) + + assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED + + def test_missing_token(self): + response = self.client.post( + self.url, + content_type='application/json', + ) + + assert response.status_code == HTTPStatus.FORBIDDEN + assert ( + json.loads(response.content)['detail'] == WebhookView.MESSAGE_TOKEN_ERROR + ) + + def test_bad_token(self): + response = self.client.post( + self.url, + content_type='application/json', + **{'HTTP_TOKEN': '1234567fghj'}, + ) + + assert response.status_code == HTTPStatus.FORBIDDEN + assert ( + json.loads(response.content)['detail'] == WebhookView.MESSAGE_TOKEN_ERROR + ) + + def test_success(self): + response = self.client.post( + self.url, + data={"this": "is a message"}, + content_type='application/json', + **{'HTTP_TOKEN': str(self.endpoint_def.token)}, + ) + + assert response.status_code == HTTPStatus.OK + assert json.loads(response.content)['message'] == WebhookView.MESSAGE_OK + message = WebhookMessage.objects.get() + assert message.body == {"this": "is a message"} From 4736c3187c3f6ce3e8b27f6f783c467233d9268e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Sep 2021 00:55:21 +0200 Subject: [PATCH 010/487] more coverage --- InvenTree/common/tests.py | 68 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 30fcd9cb41..dbc7fc6ed1 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -120,10 +120,74 @@ class WebhookMessageTests(TestCase): ) assert response.status_code == HTTPStatus.FORBIDDEN - assert ( - json.loads(response.content)['detail'] == WebhookView.MESSAGE_TOKEN_ERROR + assert (json.loads(response.content)['detail'] == WebhookView.MESSAGE_TOKEN_ERROR) + + def test_bad_url(self): + response = self.client.post( + f'/api/webhook/1234/', + content_type='application/json', ) + assert response.status_code == HTTPStatus.NOT_FOUND + + def test_bad_json(self): + response = self.client.post( + self.url, + data="{'this': 123}", + content_type='application/json', + **{'HTTP_TOKEN': str(self.endpoint_def.token)}, + ) + + assert response.status_code == HTTPStatus.NOT_ACCEPTABLE + assert ( + json.loads(response.content)['detail'] == 'Expecting property name enclosed in double quotes' + ) + + def test_success_no_token_check(self): + # delete token + self.endpoint_def.token = '' + self.endpoint_def.save() + + # check + response = self.client.post( + self.url, + content_type='application/json', + ) + + assert response.status_code == HTTPStatus.OK + assert json.loads(response.content)['message'] == WebhookView.MESSAGE_OK + + def test_bad_hmac(self): + # delete token + self.endpoint_def.token = '' + self.endpoint_def.secret = '123abc' + self.endpoint_def.save() + + # check + response = self.client.post( + self.url, + content_type='application/json', + ) + + assert response.status_code == HTTPStatus.FORBIDDEN + assert (json.loads(response.content)['detail'] == WebhookView.MESSAGE_TOKEN_ERROR) + + def test_success_hmac(self): + # delete token + self.endpoint_def.token = '' + self.endpoint_def.secret = '123abc' + self.endpoint_def.save() + + # check + response = self.client.post( + self.url, + content_type='application/json', + **{'HTTP_TOKEN': str('68MXtc/OiXdA5e2Nq9hATEVrZFpLb3Zb0oau7n8s31I=')}, + ) + + assert response.status_code == HTTPStatus.OK + assert json.loads(response.content)['message'] == WebhookView.MESSAGE_OK + def test_success(self): response = self.client.post( self.url, From 0d6828f4a859199fa664dad1f94375f4db7566b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Sep 2021 00:57:49 +0200 Subject: [PATCH 011/487] PEP fix --- InvenTree/common/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index dbc7fc6ed1..8121b46a6a 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -124,7 +124,7 @@ class WebhookMessageTests(TestCase): def test_bad_url(self): response = self.client.post( - f'/api/webhook/1234/', + '/api/webhook/1234/', content_type='application/json', ) From 76c28e60d372007ac46d7f1c8e36f6d1409cea56 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Sep 2021 20:50:26 +0200 Subject: [PATCH 012/487] fixing typo --- InvenTree/common/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 16b4199a0b..f2479b4a2a 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -41,7 +41,7 @@ class VerificationMethod: class WebhookView(CsrfExemptMixin, APIView): """ - Endpoint for receiving webhoks. + Endpoint for receiving webhooks. """ authentication_classes = [] permission_classes = [] From 7319150e7c55ef758a78d29390fafd4e413e743c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Sep 2021 07:26:30 +0200 Subject: [PATCH 013/487] refactor of load_plugin --- InvenTree/plugins/plugins.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py index f6b68112bc..8ebfcd7586 100644 --- a/InvenTree/plugins/plugins.py +++ b/InvenTree/plugins/plugins.py @@ -51,19 +51,30 @@ def get_plugins(pkg, baseclass): return plugins -def load_action_plugins(): - """ - Return a list of all registered action plugins +def load_plugins(name:str, module, cls): + """general function to load a plugin class + + :param name: name of the plugin for logs + :type name: str + :param module: module from which the plugins should be loaded + :return: class of the to-be-loaded plugin """ - logger.debug("Loading action plugins") + logger.debug(f"Loading {name} plugins") - plugins = get_plugins(action, ActionPlugin) + plugins = get_plugins(module, cls) if len(plugins) > 0: - logger.info("Discovered {n} action plugins:".format(n=len(plugins))) + logger.info(f"Discovered {len(plugins)} {name} plugins:") for ap in plugins: logger.debug(" - {ap}".format(ap=ap.PLUGIN_NAME)) return plugins + + +def load_action_plugins(): + """ + Return a list of all registered action plugins + """ + return load_plugins('action', action, ActionPlugin) From 9f3862ab2720d204c55cdc603a5e6cf798a949aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Sep 2021 07:40:19 +0200 Subject: [PATCH 014/487] basic integration plugin --- InvenTree/plugins/integration/integration.py | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 InvenTree/plugins/integration/integration.py diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py new file mode 100644 index 0000000000..fe6d96744d --- /dev/null +++ b/InvenTree/plugins/integration/integration.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +import logging + +import plugins.plugin as plugin + + +logger = logging.getLogger("inventree") + + +class IntegrationPlugin(plugin.InvenTreePlugin): + """ + The IntegrationPlugin class is used to integrate with 3rd party software + """ + + def __init__(self): + """ + """ + plugin.InvenTreePlugin.__init__(self) + + self.urls = self.setup_urls() + + def setup_urls(self): + """ + setup url endpoints for this plugin + """ + if self.urlpatterns: + return self.urlpatterns + return None + + @property + def has_urls(self): + """ + does this plugin use custom urls + """ + return bool(self.urls) + From d5f022f2cb580cf2a101d4d04b33d0b3540f98c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Sep 2021 07:53:19 +0200 Subject: [PATCH 015/487] url integration --- InvenTree/InvenTree/urls.py | 14 ++++++++++++++ InvenTree/plugins/integration/integration.py | 10 ++++++++++ InvenTree/plugins/plugins.py | 10 ++++++++++ 3 files changed, 34 insertions(+) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index b586bd6a65..11fa976869 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -48,6 +48,8 @@ from common.views import SettingEdit, UserSettingEdit from .api import InfoView, NotFoundView from .api import ActionPluginView +from plugins import plugins as inventree_plugins + from users.api import user_urls admin.site.site_header = "InvenTree Admin" @@ -125,6 +127,15 @@ translated_javascript_urls = [ url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'), ] +# Integration plugin urls +integration_plugins = inventree_plugins.load_integration_plugins() +interation_urls = [] +for plugin in integration_plugins: + # initialize + plugin = plugin() + if plugin.has_urls: + interation_urls.append(plugin.urlpatterns) + urlpatterns = [ url(r'^part/', include(part_urls)), url(r'^manufacturer-part/', include(manufacturer_part_urls)), @@ -167,6 +178,9 @@ urlpatterns = [ url(r'^api/', include(apipatterns)), url(r'^api-doc/', include_docs_urls(title='InvenTree API')), + # plugins + url(r'^plugin/', include(interation_urls)), + url(r'^markdownx/', include('markdownx.urls')), ] diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index fe6d96744d..d4ae0d302a 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -2,6 +2,7 @@ import logging +from django.conf.urls import url, include import plugins.plugin as plugin @@ -28,6 +29,15 @@ class IntegrationPlugin(plugin.InvenTreePlugin): return self.urlpatterns return None + @property + def urlpatterns(self): + """ + retruns the urlpatterns for this plugin + """ + if self.has_urls: + return url(f'^{self.plugin_name()}/', include(self.urls), name=self.plugin_name()) + return None + @property def has_urls(self): """ diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py index 8ebfcd7586..15b5a94c51 100644 --- a/InvenTree/plugins/plugins.py +++ b/InvenTree/plugins/plugins.py @@ -9,6 +9,9 @@ import logging import plugins.action as action from plugins.action.action import ActionPlugin +import plugins.integration as integration +from plugins.integration.integration import IntegrationPlugin + logger = logging.getLogger("inventree") @@ -78,3 +81,10 @@ def load_action_plugins(): Return a list of all registered action plugins """ return load_plugins('action', action, ActionPlugin) + + +def load_integration_plugins(): + """ + Return a list of all registered integration plugins + """ + return load_plugins('integration', integration, IntegrationPlugin) From 682ee87267f637b21ffd8e3ffc2df447aa52c3b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 17 Sep 2021 22:44:11 +0200 Subject: [PATCH 016/487] settings per plugin --- InvenTree/InvenTree/settings.py | 16 +++++++++++++++ InvenTree/common/models.py | 2 ++ InvenTree/plugins/integration/integration.py | 21 ++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index f3c166df88..b1bf4ab05f 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -26,6 +26,8 @@ import yaml from django.utils.translation import gettext_lazy as _ from django.contrib.messages import constants as messages +from plugins import plugins as inventree_plugins + def _is_true(x): # Shortcut function to determine if a value "looks" like a boolean @@ -646,3 +648,17 @@ MESSAGE_TAGS = { messages.ERROR: 'alert alert-block alert-danger', messages.INFO: 'alert alert-block alert-info', } + +# Plugins +INTEGRATION_PLUGINS = inventree_plugins.load_integration_plugins() + +INTEGRATION_PLUGIN_SETTINGS = {} +INTEGRATION_PLUGIN_SETTING = {} +INTEGRATION_PLUGIN_LIST = {} + +for plugin in INTEGRATION_PLUGINS: + plugin = plugin() + if plugin.has_settings: + INTEGRATION_PLUGIN_LIST[plugin.plugin_name()] = plugin + INTEGRATION_PLUGIN_SETTING[plugin.plugin_name()] = plugin.settingspatterns + INTEGRATION_PLUGIN_SETTINGS.update(plugin.settingspatterns) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 4bd24e878e..700463849c 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -827,6 +827,8 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'default': True, 'validator': bool, }, + + **settings.INTEGRATION_PLUGIN_SETTINGS } class Meta: diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index d4ae0d302a..81d64393b3 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -20,6 +20,7 @@ class IntegrationPlugin(plugin.InvenTreePlugin): plugin.InvenTreePlugin.__init__(self) self.urls = self.setup_urls() + self.settings = self.setup_settings() def setup_urls(self): """ @@ -45,3 +46,23 @@ class IntegrationPlugin(plugin.InvenTreePlugin): """ return bool(self.urls) + def setup_settings(self): + """ + setup settings for this plugin + """ + if self.SETTINGS: + return self.SETTINGS + return None + + @property + def has_settings(self): + """ + does this plugin use custom settings + """ + return bool(self.settings) + + @property + def settingspatterns(self): + if self.has_settings: + return {f'PLUGIN_{self.plugin_name().upper()}_{key}': value for key, value in self.settings.items()} + return None From 771c453c404dfe189d312df3a99568d75b4d3f00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 17 Sep 2021 22:47:49 +0200 Subject: [PATCH 017/487] settings UI integration --- InvenTree/part/templatetags/plugin_extras.py | 22 ++++++++++++++++ .../templates/InvenTree/settings/navbar.html | 17 +++++++++++++ .../InvenTree/settings/plugin_settings.html | 25 +++++++++++++++++++ .../InvenTree/settings/settings.html | 8 ++++++ 4 files changed, 72 insertions(+) create mode 100644 InvenTree/part/templatetags/plugin_extras.py create mode 100644 InvenTree/templates/InvenTree/settings/plugin_settings.html diff --git a/InvenTree/part/templatetags/plugin_extras.py b/InvenTree/part/templatetags/plugin_extras.py new file mode 100644 index 0000000000..af80c46ccb --- /dev/null +++ b/InvenTree/part/templatetags/plugin_extras.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +""" This module provides template tags for handeling plugins +""" +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings as djangosettings + +from django import template + + +register = template.Library() + + +@register.simple_tag() +def plugin_list(*args, **kwargs): + """ Return a list of all installed integration plugins """ + return djangosettings.INTEGRATION_PLUGIN_LIST + +@register.simple_tag() +def plugin_settings(plugin, *args, **kwargs): + """ Return a list of all settings for a plugin """ + return djangosettings.INTEGRATION_PLUGIN_SETTING.get(plugin) diff --git a/InvenTree/templates/InvenTree/settings/navbar.html b/InvenTree/templates/InvenTree/settings/navbar.html index ebf24bffb1..1b4137a40d 100644 --- a/InvenTree/templates/InvenTree/settings/navbar.html +++ b/InvenTree/templates/InvenTree/settings/navbar.html @@ -1,4 +1,5 @@ {% load i18n %} +{% load plugin_extras %}
    @@ -116,6 +117,22 @@ + +
  • + {% trans "Plugin Settings" %} +
  • + + {% plugin_list as pl_list %} + {% for plugin_key, plugin in pl_list.items %} + {% if plugin.has_settings %} +
  • + + {{ plugin.plugin_name}} + +
  • + {% endif %} + {% endfor %} + {% endif %}
\ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/plugin_settings.html b/InvenTree/templates/InvenTree/settings/plugin_settings.html new file mode 100644 index 0000000000..7f4deeace8 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/plugin_settings.html @@ -0,0 +1,25 @@ +{% extends "panel.html" %} +{% load i18n %} +{% load inventree_extras %} +{% load plugin_extras %} + +{% block label %}plugin-{{plugin_key}}{% endblock %} + + +{% block heading %} +{% blocktrans with name=plugin.plugin_name %}Plugin Settings for {{name}}{% endblocktrans %} +{% endblock %} + +{% block content %} +{% plugin_settings plugin_key as plugin_settings %} + + + {% include "InvenTree/settings/header.html" %} + + {% for setting in plugin_settings %} + {% include "InvenTree/settings/setting.html" with key=setting%} + {% endfor %} + +
+ +{% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html index beb7f5eb04..96b7c35723 100644 --- a/InvenTree/templates/InvenTree/settings/settings.html +++ b/InvenTree/templates/InvenTree/settings/settings.html @@ -3,6 +3,7 @@ {% load i18n %} {% load static %} {% load inventree_extras %} +{% load plugin_extras %} {% block page_title %} {% inventree_title %} | {% trans "Settings" %} @@ -34,6 +35,13 @@ {% include "InvenTree/settings/po.html" %} {% include "InvenTree/settings/so.html" %} +{% plugin_list as pl_list %} +{% for plugin_key, plugin in pl_list.items %} + {% if plugin.has_settings %} + {% include "InvenTree/settings/plugin_settings.html" %} + {% endif %} +{% endfor %} + {% endif %} {% endblock %} From 733303fb66947395dd92a4874bc6b3a4a1b48e50 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 17 Sep 2021 22:49:43 +0200 Subject: [PATCH 018/487] Plugin overview --- .../templates/InvenTree/settings/navbar.html | 6 ++++ .../templates/InvenTree/settings/plugin.html | 34 +++++++++++++++++++ .../InvenTree/settings/settings.html | 1 + 3 files changed, 41 insertions(+) create mode 100644 InvenTree/templates/InvenTree/settings/plugin.html diff --git a/InvenTree/templates/InvenTree/settings/navbar.html b/InvenTree/templates/InvenTree/settings/navbar.html index 1b4137a40d..5592f63cd9 100644 --- a/InvenTree/templates/InvenTree/settings/navbar.html +++ b/InvenTree/templates/InvenTree/settings/navbar.html @@ -122,6 +122,12 @@ {% trans "Plugin Settings" %} +
  • + + {% trans "Plugin" %} + +
  • + {% plugin_list as pl_list %} {% for plugin_key, plugin in pl_list.items %} {% if plugin.has_settings %} diff --git a/InvenTree/templates/InvenTree/settings/plugin.html b/InvenTree/templates/InvenTree/settings/plugin.html new file mode 100644 index 0000000000..37b47c2766 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/plugin.html @@ -0,0 +1,34 @@ +{% extends "panel.html" %} +{% load i18n %} +{% load inventree_extras %} +{% load plugin_extras %} + +{% block label %}plugin{% endblock %} + + +{% block heading %} +{% trans "Plugin Settings" %} +{% endblock %} + +{% block content %} + +

    {% trans "Plugin list" %}

    + + + + + + + + + + {% plugin_list as pl_list %} + {% for plugin_key, plugin in pl_list.items %} + + + + {% endfor %} + +
    {% trans "Name" %}
    {{plugin_key}} - {{ plugin.plugin_name}}
    + +{% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html index 96b7c35723..a764b82a39 100644 --- a/InvenTree/templates/InvenTree/settings/settings.html +++ b/InvenTree/templates/InvenTree/settings/settings.html @@ -34,6 +34,7 @@ {% include "InvenTree/settings/build.html" %} {% include "InvenTree/settings/po.html" %} {% include "InvenTree/settings/so.html" %} +{% include "InvenTree/settings/plugin.html" %} {% plugin_list as pl_list %} {% for plugin_key, plugin in pl_list.items %} From 4c7d1e665816e3b8c24e0cda3778cc0e855619ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 17 Sep 2021 22:55:02 +0200 Subject: [PATCH 019/487] show enabled functionality in plugin overview --- InvenTree/templates/InvenTree/settings/plugin.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/InvenTree/settings/plugin.html b/InvenTree/templates/InvenTree/settings/plugin.html index 37b47c2766..5502a3b716 100644 --- a/InvenTree/templates/InvenTree/settings/plugin.html +++ b/InvenTree/templates/InvenTree/settings/plugin.html @@ -25,7 +25,14 @@ {% plugin_list as pl_list %} {% for plugin_key, plugin in pl_list.items %} - {{plugin_key}} - {{ plugin.plugin_name}} + {{plugin_key}} - {{ plugin.plugin_name}} + {% if plugin.has_urls %} + {% trans 'Has urls' %} + {% endif %} + {% if plugin.has_settings %} + {% trans 'Has settings' %} + {% endif %} + {% endfor %} From 2498cbde797667823d3aa6a488491eefff9a5971 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 17 Sep 2021 22:59:29 +0200 Subject: [PATCH 020/487] PEP fixes --- InvenTree/part/templatetags/plugin_extras.py | 3 +-- InvenTree/plugins/plugins.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/templatetags/plugin_extras.py b/InvenTree/part/templatetags/plugin_extras.py index af80c46ccb..f400ab94ad 100644 --- a/InvenTree/part/templatetags/plugin_extras.py +++ b/InvenTree/part/templatetags/plugin_extras.py @@ -2,9 +2,7 @@ """ This module provides template tags for handeling plugins """ -from django.utils.translation import ugettext_lazy as _ from django.conf import settings as djangosettings - from django import template @@ -16,6 +14,7 @@ def plugin_list(*args, **kwargs): """ Return a list of all installed integration plugins """ return djangosettings.INTEGRATION_PLUGIN_LIST + @register.simple_tag() def plugin_settings(plugin, *args, **kwargs): """ Return a list of all settings for a plugin """ diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py index 15b5a94c51..0b484b05d0 100644 --- a/InvenTree/plugins/plugins.py +++ b/InvenTree/plugins/plugins.py @@ -54,7 +54,7 @@ def get_plugins(pkg, baseclass): return plugins -def load_plugins(name:str, module, cls): +def load_plugins(name: str, module, cls): """general function to load a plugin class :param name: name of the plugin for logs From fc3188513b97ec0e564f9627330c34abcc8ab77f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 01:37:53 +0200 Subject: [PATCH 021/487] Links in plugin badges --- InvenTree/templates/InvenTree/settings/plugin.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/InvenTree/settings/plugin.html b/InvenTree/templates/InvenTree/settings/plugin.html index 5502a3b716..515bd41132 100644 --- a/InvenTree/templates/InvenTree/settings/plugin.html +++ b/InvenTree/templates/InvenTree/settings/plugin.html @@ -27,10 +27,10 @@ {{plugin_key}} - {{ plugin.plugin_name}} {% if plugin.has_urls %} - {% trans 'Has urls' %} + {% trans 'Has urls' %} {% endif %} {% if plugin.has_settings %} - {% trans 'Has settings' %} + {% trans 'Has settings' %} {% endif %} From ca1fce4cf390cf3bd4d661c8cadb14e71edecb0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 02:08:33 +0200 Subject: [PATCH 022/487] make plugin path a setting --- InvenTree/InvenTree/settings.py | 2 ++ InvenTree/InvenTree/urls.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index b1bf4ab05f..295bb442c9 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -650,6 +650,8 @@ MESSAGE_TAGS = { } # Plugins +PLUGIN_URL = 'plugin' + INTEGRATION_PLUGINS = inventree_plugins.load_integration_plugins() INTEGRATION_PLUGIN_SETTINGS = {} diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 11fa976869..1424a26503 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -179,7 +179,7 @@ urlpatterns = [ url(r'^api-doc/', include_docs_urls(title='InvenTree API')), # plugins - url(r'^plugin/', include(interation_urls)), + url(f'^{settings.PLUGIN_URL}/', include(interation_urls)), url(r'^markdownx/', include('markdownx.urls')), ] From dafed332bc52514f9402c1a4540123e4a65d3791 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 02:09:22 +0200 Subject: [PATCH 023/487] show base url in settings --- InvenTree/plugins/integration/integration.py | 5 +++++ InvenTree/templates/InvenTree/settings/plugin_settings.html | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index 81d64393b3..4a7b3e19d9 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -3,6 +3,7 @@ import logging from django.conf.urls import url, include +from django.conf import settings import plugins.plugin as plugin @@ -30,6 +31,10 @@ class IntegrationPlugin(plugin.InvenTreePlugin): return self.urlpatterns return None + @property + def base_url(self): + return f'{settings.PLUGIN_URL}/{self.plugin_name()}/' + @property def urlpatterns(self): """ diff --git a/InvenTree/templates/InvenTree/settings/plugin_settings.html b/InvenTree/templates/InvenTree/settings/plugin_settings.html index 7f4deeace8..2a84e976af 100644 --- a/InvenTree/templates/InvenTree/settings/plugin_settings.html +++ b/InvenTree/templates/InvenTree/settings/plugin_settings.html @@ -22,4 +22,10 @@ +{% if plugin.has_urls %} +

    {% trans "URLs" %}

    + +

    {% blocktrans with base=plugin.base_url %}The Base-URL for this plugin is {{ base }}.{% endblocktrans %}

    +{% endif %} + {% endblock %} From 1296e631d92e57b007dd8950a69bf534b7219d6b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 02:15:05 +0200 Subject: [PATCH 024/487] link to base in plugin site --- InvenTree/templates/InvenTree/settings/plugin_settings.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/InvenTree/settings/plugin_settings.html b/InvenTree/templates/InvenTree/settings/plugin_settings.html index 2a84e976af..5a6dae2cf2 100644 --- a/InvenTree/templates/InvenTree/settings/plugin_settings.html +++ b/InvenTree/templates/InvenTree/settings/plugin_settings.html @@ -24,8 +24,8 @@ {% if plugin.has_urls %}

    {% trans "URLs" %}

    - -

    {% blocktrans with base=plugin.base_url %}The Base-URL for this plugin is {{ base }}.{% endblocktrans %}

    + {% define plugin.base_url as base %} +

    {% blocktrans %}The Base-URL for this plugin is {{ base }}.{% endblocktrans %}

    {% endif %} {% endblock %} From 21dee0d459c2ce77cbd9a2dded57637f661f13b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 02:47:31 +0200 Subject: [PATCH 025/487] urls overview in plugin settings --- .../InvenTree/settings/plugin_settings.html | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/InvenTree/templates/InvenTree/settings/plugin_settings.html b/InvenTree/templates/InvenTree/settings/plugin_settings.html index 5a6dae2cf2..c7fd11d4ea 100644 --- a/InvenTree/templates/InvenTree/settings/plugin_settings.html +++ b/InvenTree/templates/InvenTree/settings/plugin_settings.html @@ -26,6 +26,25 @@

    {% trans "URLs" %}

    {% define plugin.base_url as base %}

    {% blocktrans %}The Base-URL for this plugin is {{ base }}.{% endblocktrans %}

    + + + + + + + + + + + {% for key, entry in plugin.urlpatterns.reverse_dict.items %}{% if key %} + + + + + + {% endif %}{% endfor %} + +
    {% trans "Name" %}{% trans "URL" %}
    {{key}}{{entry.1}}{% trans 'open' %}
    {% endif %} {% endblock %} From aa120a819782087245d87da6c41c1c630ed5300b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 02:48:25 +0200 Subject: [PATCH 026/487] open in new tab --- InvenTree/templates/InvenTree/settings/plugin_settings.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/InvenTree/settings/plugin_settings.html b/InvenTree/templates/InvenTree/settings/plugin_settings.html index c7fd11d4ea..f77093b91a 100644 --- a/InvenTree/templates/InvenTree/settings/plugin_settings.html +++ b/InvenTree/templates/InvenTree/settings/plugin_settings.html @@ -25,7 +25,7 @@ {% if plugin.has_urls %}

    {% trans "URLs" %}

    {% define plugin.base_url as base %} -

    {% blocktrans %}The Base-URL for this plugin is {{ base }}.{% endblocktrans %}

    +

    {% blocktrans %}The Base-URL for this plugin is {{ base }}.{% endblocktrans %}

    @@ -40,7 +40,7 @@ - + {% endif %}{% endfor %} From b9ba6b9225fe0f1e75550142db697e285ac2f5b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 02:49:07 +0200 Subject: [PATCH 027/487] make link a button --- InvenTree/templates/InvenTree/settings/plugin.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/templates/InvenTree/settings/plugin.html b/InvenTree/templates/InvenTree/settings/plugin.html index 515bd41132..688cb19c45 100644 --- a/InvenTree/templates/InvenTree/settings/plugin.html +++ b/InvenTree/templates/InvenTree/settings/plugin.html @@ -18,6 +18,7 @@ + @@ -33,6 +34,7 @@ {% trans 'Has settings' %} {% endif %} + {% endfor %} From ecc86e098989052a5351a760511f720706d29629 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 03:27:19 +0200 Subject: [PATCH 028/487] general linting fixes --- ci/check_js_templates.py | 4 +++- ci/check_locale_files.py | 6 +++--- ci/check_migration_files.py | 2 +- ci/check_version_number.py | 2 +- tasks.py | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ci/check_js_templates.py b/ci/check_js_templates.py index e3c1f0148f..b9db7fe612 100644 --- a/ci/check_js_templates.py +++ b/ci/check_js_templates.py @@ -28,6 +28,7 @@ print("=================================") print("Checking static javascript files:") print("=================================") + def check_invalid_tag(data): pattern = r"{%(\w+)" @@ -45,6 +46,7 @@ def check_invalid_tag(data): return err_count + def check_prohibited_tags(data): allowed_tags = [ @@ -78,7 +80,7 @@ def check_prohibited_tags(data): has_trans = True if not has_trans: - print(f" > file is missing 'trans' tags") + print(" > file is missing 'trans' tags") err_count += 1 return err_count diff --git a/ci/check_locale_files.py b/ci/check_locale_files.py index 9995ceaec5..06246cd923 100644 --- a/ci/check_locale_files.py +++ b/ci/check_locale_files.py @@ -24,7 +24,7 @@ for line in str(out.decode()).split('\n'): if len(locales) > 0: print("There are {n} unstaged locale files:".format(n=len(locales))) - for l in locales: - print(" - {l}".format(l=l)) + for lang in locales: + print(" - {l}".format(l=lang)) -sys.exit(len(locales)) \ No newline at end of file +sys.exit(len(locales)) diff --git a/ci/check_migration_files.py b/ci/check_migration_files.py index 88e5a90bee..8ef0ada13d 100644 --- a/ci/check_migration_files.py +++ b/ci/check_migration_files.py @@ -28,4 +28,4 @@ print("There are {n} unstaged migration files:".format(n=len(migrations))) for m in migrations: print(" - {m}".format(m=m)) -sys.exit(len(migrations)) \ No newline at end of file +sys.exit(len(migrations)) diff --git a/ci/check_version_number.py b/ci/check_version_number.py index ca2dbd71c7..6adc36b6d3 100644 --- a/ci/check_version_number.py +++ b/ci/check_version_number.py @@ -84,4 +84,4 @@ if __name__ == '__main__': print(f"Release tag '{args.tag}' does not match INVENTREE_SW_VERSION '{version}'") sys.exit(1) -sys.exit(0) \ No newline at end of file +sys.exit(0) diff --git a/tasks.py b/tasks.py index 1abbf23bc6..7f9d155301 100644 --- a/tasks.py +++ b/tasks.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from shutil import copyfile import os import json import sys @@ -143,6 +142,7 @@ def clean_settings(c): manage(c, "clean_settings") + @task(post=[rebuild]) def migrate(c): """ @@ -298,7 +298,7 @@ def export_records(c, filename='data.json'): # Get an absolute path to the file if not os.path.isabs(filename): filename = os.path.join(localDir(), filename) - filename = os.path.abspath(filename) + filename = os.path.abspath(filename) print(f"Exporting database records to file '{filename}'") From f74cd5901daf2ba8ee912573d3187e15b7bfe0c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 03:30:21 +0200 Subject: [PATCH 029/487] module registry mechanism --- InvenTree/plugins/integration/integration.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index 4a7b3e19d9..49f80d314a 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -16,13 +16,20 @@ class IntegrationPlugin(plugin.InvenTreePlugin): """ def __init__(self): - """ - """ - plugin.InvenTreePlugin.__init__(self) + self.add_mixin('base') + super().__init__() self.urls = self.setup_urls() self.settings = self.setup_settings() + def add_mixin(self, key: str): + if not hasattr(self, 'mixins'): + self.mixins = {} + self.mixins[key] = True + + def module(self, key): + return key in self.mixins + def setup_urls(self): """ setup url endpoints for this plugin From 8220ccb385eff0475d7b4d516d7061008021025b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 03:31:52 +0200 Subject: [PATCH 030/487] plugin settings as a module --- InvenTree/InvenTree/settings.py | 4 +-- InvenTree/plugins/integration/integration.py | 31 +++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 295bb442c9..0ca0c50735 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -660,7 +660,7 @@ INTEGRATION_PLUGIN_LIST = {} for plugin in INTEGRATION_PLUGINS: plugin = plugin() - if plugin.has_settings: - INTEGRATION_PLUGIN_LIST[plugin.plugin_name()] = plugin + INTEGRATION_PLUGIN_LIST[plugin.plugin_name()] = plugin + if plugin.module('settings') and plugin.has_settings: INTEGRATION_PLUGIN_SETTING[plugin.plugin_name()] = plugin.settingspatterns INTEGRATION_PLUGIN_SETTINGS.update(plugin.settingspatterns) diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index 49f80d314a..f101bcc1d0 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -10,6 +10,36 @@ import plugins.plugin as plugin logger = logging.getLogger("inventree") +class SettingsMixin: + """Mixin that enables settings for the plugin""" + def __init__(self): + super().__init__() + + self.add_mixin('settings') + self.settings = self.setup_settings() + + def setup_settings(self): + """ + setup settings for this plugin + """ + if self.SETTINGS: + return self.SETTINGS + return None + + @property + def has_settings(self): + """ + does this plugin use custom settings + """ + return bool(self.settings) + + @property + def settingspatterns(self): + if self.has_settings: + return {f'PLUGIN_{self.plugin_name().upper()}_{key}': value for key, value in self.settings.items()} + return None + + class IntegrationPlugin(plugin.InvenTreePlugin): """ The IntegrationPlugin class is used to integrate with 3rd party software @@ -20,7 +50,6 @@ class IntegrationPlugin(plugin.InvenTreePlugin): super().__init__() self.urls = self.setup_urls() - self.settings = self.setup_settings() def add_mixin(self, key: str): if not hasattr(self, 'mixins'): From dc70b5ef112764b30e27e8fa0f7e9c5c3a7896d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 03:40:21 +0200 Subject: [PATCH 031/487] urls as own mixin --- InvenTree/plugins/integration/integration.py | 39 +++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index f101bcc1d0..96cac27683 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -40,6 +40,43 @@ class SettingsMixin: return None +class UrlsMixin: + """Mixin that enables urls for the plugin""" + def __init__(self): + super().__init__() + + self.add_mixin('urls') + self.urls = self.setup_urls() + + def setup_urls(self): + """ + setup url endpoints for this plugin + """ + if self.urlpatterns: + return self.urlpatterns + return None + + @property + def base_url(self): + return f'{settings.PLUGIN_URL}/{self.plugin_name()}/' + + @property + def urlpatterns(self): + """ + retruns the urlpatterns for this plugin + """ + if self.has_urls: + return url(f'^{self.plugin_name()}/', include(self.urls), name=self.plugin_name()) + return None + + @property + def has_urls(self): + """ + does this plugin use custom urls + """ + return bool(self.urls) + + class IntegrationPlugin(plugin.InvenTreePlugin): """ The IntegrationPlugin class is used to integrate with 3rd party software @@ -49,8 +86,6 @@ class IntegrationPlugin(plugin.InvenTreePlugin): self.add_mixin('base') super().__init__() - self.urls = self.setup_urls() - def add_mixin(self, key: str): if not hasattr(self, 'mixins'): self.mixins = {} From d363063add8b268bade69d348fad680c8784cca6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 03:41:41 +0200 Subject: [PATCH 032/487] remove duplicate functions --- InvenTree/plugins/integration/integration.py | 49 -------------------- 1 file changed, 49 deletions(-) diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index 96cac27683..c556e34f61 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -93,52 +93,3 @@ class IntegrationPlugin(plugin.InvenTreePlugin): def module(self, key): return key in self.mixins - - def setup_urls(self): - """ - setup url endpoints for this plugin - """ - if self.urlpatterns: - return self.urlpatterns - return None - - @property - def base_url(self): - return f'{settings.PLUGIN_URL}/{self.plugin_name()}/' - - @property - def urlpatterns(self): - """ - retruns the urlpatterns for this plugin - """ - if self.has_urls: - return url(f'^{self.plugin_name()}/', include(self.urls), name=self.plugin_name()) - return None - - @property - def has_urls(self): - """ - does this plugin use custom urls - """ - return bool(self.urls) - - def setup_settings(self): - """ - setup settings for this plugin - """ - if self.SETTINGS: - return self.SETTINGS - return None - - @property - def has_settings(self): - """ - does this plugin use custom settings - """ - return bool(self.settings) - - @property - def settingspatterns(self): - if self.has_settings: - return {f'PLUGIN_{self.plugin_name().upper()}_{key}': value for key, value in self.settings.items()} - return None From 8fb6d7723bfce781db5848bd1143c812dcdbfe99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 03:42:27 +0200 Subject: [PATCH 033/487] ensure module --- InvenTree/InvenTree/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 1424a26503..b5cd6c691e 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -133,7 +133,7 @@ interation_urls = [] for plugin in integration_plugins: # initialize plugin = plugin() - if plugin.has_urls: + if plugin.module('urls') and plugin.has_urls: interation_urls.append(plugin.urlpatterns) urlpatterns = [ From 0cae3633d22644132852068c1a3f9de5c4429384 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 03:43:09 +0200 Subject: [PATCH 034/487] use button to open plugin urls --- InvenTree/templates/InvenTree/settings/plugin_settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/InvenTree/settings/plugin_settings.html b/InvenTree/templates/InvenTree/settings/plugin_settings.html index f77093b91a..7d9ff83de6 100644 --- a/InvenTree/templates/InvenTree/settings/plugin_settings.html +++ b/InvenTree/templates/InvenTree/settings/plugin_settings.html @@ -40,7 +40,7 @@ - + {% endif %}{% endfor %} From 21d187a3879e4146af5f7a246c6cda941c8a38cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 04:18:34 +0200 Subject: [PATCH 035/487] one module check fnc --- InvenTree/InvenTree/settings.py | 2 +- InvenTree/InvenTree/urls.py | 2 +- InvenTree/plugins/integration/integration.py | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 0ca0c50735..8a44468c15 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -661,6 +661,6 @@ INTEGRATION_PLUGIN_LIST = {} for plugin in INTEGRATION_PLUGINS: plugin = plugin() INTEGRATION_PLUGIN_LIST[plugin.plugin_name()] = plugin - if plugin.module('settings') and plugin.has_settings: + if plugin.module_enabled('settings'): INTEGRATION_PLUGIN_SETTING[plugin.plugin_name()] = plugin.settingspatterns INTEGRATION_PLUGIN_SETTINGS.update(plugin.settingspatterns) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index b5cd6c691e..ee26e518ef 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -133,7 +133,7 @@ interation_urls = [] for plugin in integration_plugins: # initialize plugin = plugin() - if plugin.module('urls') and plugin.has_urls: + if plugin.module_enabled('urls'): interation_urls.append(plugin.urlpatterns) urlpatterns = [ diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index c556e34f61..8977d7b826 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -15,7 +15,7 @@ class SettingsMixin: def __init__(self): super().__init__() - self.add_mixin('settings') + self.add_mixin('settings', 'has_settings') self.settings = self.setup_settings() def setup_settings(self): @@ -45,15 +45,15 @@ class UrlsMixin: def __init__(self): super().__init__() - self.add_mixin('urls') + self.add_mixin('urls', 'has_urls') self.urls = self.setup_urls() def setup_urls(self): """ setup url endpoints for this plugin """ - if self.urlpatterns: - return self.urlpatterns + if hasattr(self, 'URLS'): + return self.URLS return None @property @@ -86,10 +86,18 @@ class IntegrationPlugin(plugin.InvenTreePlugin): self.add_mixin('base') super().__init__() - def add_mixin(self, key: str): + def add_mixin(self, key: str, fnc_enabled=None): if not hasattr(self, 'mixins'): self.mixins = {} self.mixins[key] = True + if fnc_enabled: + self.mixins[key] = fnc_enabled def module(self, key): return key in self.mixins + + def module_enabled(self, key): + if self.module(key): + fnc_name = self.mixins.get(key) + return getattr(self, fnc_name, True) + return False From debad4ab9a86f2fe678089889fa78ca0a14fe960 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 04:23:31 +0200 Subject: [PATCH 036/487] refactor to make simpler --- InvenTree/plugins/integration/integration.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index 8977d7b826..0a315ee051 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -22,9 +22,7 @@ class SettingsMixin: """ setup settings for this plugin """ - if self.SETTINGS: - return self.SETTINGS - return None + return getattr(self, 'SETTINGS', None) @property def has_settings(self): @@ -52,9 +50,7 @@ class UrlsMixin: """ setup url endpoints for this plugin """ - if hasattr(self, 'URLS'): - return self.URLS - return None + return getattr(self, 'URLS', None) @property def base_url(self): @@ -86,12 +82,10 @@ class IntegrationPlugin(plugin.InvenTreePlugin): self.add_mixin('base') super().__init__() - def add_mixin(self, key: str, fnc_enabled=None): + def add_mixin(self, key: str, fnc_enabled=True): if not hasattr(self, 'mixins'): self.mixins = {} - self.mixins[key] = True - if fnc_enabled: - self.mixins[key] = fnc_enabled + self.mixins[key] = fnc_enabled def module(self, key): return key in self.mixins From 0b0c4cf337789060d5063497b89ba5da5de3a8fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 04:27:24 +0200 Subject: [PATCH 037/487] more code structure --- InvenTree/plugins/integration/integration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index 0a315ee051..c94042113b 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -4,12 +4,14 @@ import logging from django.conf.urls import url, include from django.conf import settings + import plugins.plugin as plugin logger = logging.getLogger("inventree") +# region mixins class SettingsMixin: """Mixin that enables settings for the plugin""" def __init__(self): @@ -71,6 +73,7 @@ class UrlsMixin: does this plugin use custom urls """ return bool(self.urls) +# endregion class IntegrationPlugin(plugin.InvenTreePlugin): From 1c781d9bf0e0fd7bcfb2a212579c3072cfdb944d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 04:32:31 +0200 Subject: [PATCH 038/487] rename --- InvenTree/InvenTree/settings.py | 2 +- InvenTree/InvenTree/urls.py | 2 +- InvenTree/plugins/integration/integration.py | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 8a44468c15..3388d7087e 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -661,6 +661,6 @@ INTEGRATION_PLUGIN_LIST = {} for plugin in INTEGRATION_PLUGINS: plugin = plugin() INTEGRATION_PLUGIN_LIST[plugin.plugin_name()] = plugin - if plugin.module_enabled('settings'): + if plugin.mixin_enabled('settings'): INTEGRATION_PLUGIN_SETTING[plugin.plugin_name()] = plugin.settingspatterns INTEGRATION_PLUGIN_SETTINGS.update(plugin.settingspatterns) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index ee26e518ef..3f018e15ad 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -133,7 +133,7 @@ interation_urls = [] for plugin in integration_plugins: # initialize plugin = plugin() - if plugin.module_enabled('urls'): + if plugin.mixin_enabled('urls'): interation_urls.append(plugin.urlpatterns) urlpatterns = [ diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index c94042113b..651abeb928 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -86,15 +86,15 @@ class IntegrationPlugin(plugin.InvenTreePlugin): super().__init__() def add_mixin(self, key: str, fnc_enabled=True): - if not hasattr(self, 'mixins'): - self.mixins = {} - self.mixins[key] = fnc_enabled + if not hasattr(self, '_mixins'): + self._mixins = {} + self._mixins[key] = fnc_enabled - def module(self, key): - return key in self.mixins + def mixin(self, key): + return key in self._mixins - def module_enabled(self, key): - if self.module(key): - fnc_name = self.mixins.get(key) + def mixin_enabled(self, key): + if self.mixin(key): + fnc_name = self._mixins.get(key) return getattr(self, fnc_name, True) return False From 3b2cb43ece01de30c4c1e070b46440946a3c1695 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 04:46:50 +0200 Subject: [PATCH 039/487] mixin_enabled check for templates --- InvenTree/part/templatetags/plugin_extras.py | 6 ++++++ InvenTree/templates/InvenTree/settings/plugin.html | 7 +++++-- .../templates/InvenTree/settings/plugin_settings.html | 10 ++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/InvenTree/part/templatetags/plugin_extras.py b/InvenTree/part/templatetags/plugin_extras.py index f400ab94ad..6f214340f7 100644 --- a/InvenTree/part/templatetags/plugin_extras.py +++ b/InvenTree/part/templatetags/plugin_extras.py @@ -19,3 +19,9 @@ def plugin_list(*args, **kwargs): def plugin_settings(plugin, *args, **kwargs): """ Return a list of all settings for a plugin """ return djangosettings.INTEGRATION_PLUGIN_SETTING.get(plugin) + + +@register.simple_tag() +def mixin_enabled(plugin, key, *args, **kwargs): + """ Return if the mixin is existant and configured in the plugin """ + return plugin.mixin_enabled(key) diff --git a/InvenTree/templates/InvenTree/settings/plugin.html b/InvenTree/templates/InvenTree/settings/plugin.html index 688cb19c45..44b486abae 100644 --- a/InvenTree/templates/InvenTree/settings/plugin.html +++ b/InvenTree/templates/InvenTree/settings/plugin.html @@ -25,12 +25,15 @@ {% plugin_list as pl_list %} {% for plugin_key, plugin in pl_list.items %} + {% mixin_enabled plugin 'urls' as urls %} + {% mixin_enabled plugin 'settings' as settings %} + diff --git a/InvenTree/templates/InvenTree/settings/plugin_settings.html b/InvenTree/templates/InvenTree/settings/plugin_settings.html index 7d9ff83de6..4f0d6d7f35 100644 --- a/InvenTree/templates/InvenTree/settings/plugin_settings.html +++ b/InvenTree/templates/InvenTree/settings/plugin_settings.html @@ -7,10 +7,14 @@ {% block heading %} -{% blocktrans with name=plugin.plugin_name %}Plugin Settings for {{name}}{% endblocktrans %} +{% blocktrans with name=plugin.plugin_name %}Plugin details for {{name}}{% endblocktrans %} {% endblock %} {% block content %} + +{% mixin_enabled plugin 'settings' as settings %} +{% if settings %} +

    {% trans "Settings" %}

    {% plugin_settings plugin_key as plugin_settings %}
    {{key}} {{entry.1}}{% trans 'open' %}{% trans 'open in new tab' %}
    {% trans "Name" %}{% trans "Author" %}
    # TODO
    {{key}} {{entry.1}}{% trans 'open in new tab' %}{% trans 'open in new tab' %}
    {{plugin_key}} - {{ plugin.plugin_name}} - {% if plugin.has_urls %} + {% if urls %} {% trans 'Has urls' %} {% endif %} - {% if plugin.has_settings %} + {% if settings %} {% trans 'Has settings' %} {% endif %}
    @@ -21,8 +25,10 @@ {% endfor %}
    +{% endif %} -{% if plugin.has_urls %} +{% mixin_enabled plugin 'urls' as urls %} +{% if urls %}

    {% trans "URLs" %}

    {% define plugin.base_url as base %}

    {% blocktrans %}The Base-URL for this plugin is {{ base }}.{% endblocktrans %}

    From c222b9c296f85ce5df3d96ad7c6d462a2e3531ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Sep 2021 14:46:32 +0200 Subject: [PATCH 040/487] Some sample code for internal testing --- InvenTree/plugins/integration/__init__.py | 0 .../plugins/integration/another_sample.py | 20 +++++++ InvenTree/plugins/integration/sample.py | 58 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 InvenTree/plugins/integration/__init__.py create mode 100644 InvenTree/plugins/integration/another_sample.py create mode 100644 InvenTree/plugins/integration/sample.py diff --git a/InvenTree/plugins/integration/__init__.py b/InvenTree/plugins/integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugins/integration/another_sample.py b/InvenTree/plugins/integration/another_sample.py new file mode 100644 index 0000000000..28d7cf5249 --- /dev/null +++ b/InvenTree/plugins/integration/another_sample.py @@ -0,0 +1,20 @@ +from plugins.integration.integration import * + +from django.http import HttpResponse +from django.utils.translation import ugettext_lazy as _ + + +class NoIntegrationPlugin(IntegrationPlugin): + """ + An basic integration plugin + """ + + PLUGIN_NAME = "NoIntegrationPlugin" + + +class WrongIntegrationPlugin(UrlsMixin, IntegrationPlugin): + """ + An basic integration plugin + """ + + PLUGIN_NAME = "WrongIntegrationPlugin" diff --git a/InvenTree/plugins/integration/sample.py b/InvenTree/plugins/integration/sample.py new file mode 100644 index 0000000000..8ade4373bb --- /dev/null +++ b/InvenTree/plugins/integration/sample.py @@ -0,0 +1,58 @@ +from plugins.integration.integration import * + +from django.http import HttpResponse +from django.utils.translation import ugettext_lazy as _ + + +class SimpleIntegrationPlugin(SettingsMixin, UrlsMixin, IntegrationPlugin): + """ + An basic integration plugin + """ + + PLUGIN_NAME = "SimpleIntegrationPlugin" + + def view_test(self, request): + return HttpResponse(f'Hi there {request.user.username} this works') + + def setup_urls(self): + he = [ + url(r'^he/', self.view_test, name='he'), + url(r'^ha/', self.view_test, name='ha'), + ] + + return [ + url(r'^hi/', self.view_test, name='hi'), + url(r'^ho/', include(he), name='ho'), + ] + + SETTINGS = { + 'PO_FUNCTION_ENABLE': { + 'name': _('Enable PO'), + 'description': _('Enable PO functionality in InvenTree interface'), + 'default': True, + 'validator': bool, + }, + } + + +class OtherIntegrationPlugin(UrlsMixin, IntegrationPlugin): + """ + An basic integration plugin + """ + + PLUGIN_NAME = "OtherIntegrationPlugin" + + # @cls_login_required() + def view_test(self, request): + return HttpResponse(f'Hi there {request.user.username} this works') + + def setup_urls(self): + he = [ + url(r'^he/', self.view_test, name='he'), + url(r'^ha/', self.view_test, name='ha'), + ] + + return [ + url(r'^hi/', self.view_test, name='hi'), + url(r'^ho/', include(he), name='ho'), + ] From 4c8318440c636cfdf09b6e08d02b53bbdc4608f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Sep 2021 14:54:55 +0200 Subject: [PATCH 041/487] renmae of plugin --- InvenTree/plugins/integration/sample.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/plugins/integration/sample.py b/InvenTree/plugins/integration/sample.py index 8ade4373bb..86e0cf0eea 100644 --- a/InvenTree/plugins/integration/sample.py +++ b/InvenTree/plugins/integration/sample.py @@ -4,12 +4,12 @@ from django.http import HttpResponse from django.utils.translation import ugettext_lazy as _ -class SimpleIntegrationPlugin(SettingsMixin, UrlsMixin, IntegrationPlugin): +class SampleIntegrationPlugin(SettingsMixin, UrlsMixin, IntegrationPlugin): """ - An basic integration plugin + An full integration plugin """ - PLUGIN_NAME = "SimpleIntegrationPlugin" + PLUGIN_NAME = "SampleIntegrationPlugin" def view_test(self, request): return HttpResponse(f'Hi there {request.user.username} this works') From 1c89e83d28749501f9cac1372e6ff1c6006ef2e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Sep 2021 16:27:43 +0200 Subject: [PATCH 042/487] navigation plugin --- InvenTree/plugins/integration/integration.py | 21 ++++++++++++++++++++ InvenTree/plugins/integration/sample.py | 6 +++++- InvenTree/templates/navbar.html | 18 +++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugins/integration/integration.py b/InvenTree/plugins/integration/integration.py index 651abeb928..5690dfe2be 100644 --- a/InvenTree/plugins/integration/integration.py +++ b/InvenTree/plugins/integration/integration.py @@ -73,6 +73,27 @@ class UrlsMixin: does this plugin use custom urls """ return bool(self.urls) + + +class NavigationMixin: + """Mixin that enables adding navigation links with the plugin""" + def __init__(self): + super().__init__() + self.add_mixin('navigation', 'has_naviation') + self.navigation = self.setup_navigation() + + def setup_navigation(self): + """ + setup navigation links for this plugin + """ + return getattr(self, 'NAVIGATION', None) + + @property + def has_naviation(self): + """ + does this plugin define navigation elements + """ + return bool(self.navigation) # endregion diff --git a/InvenTree/plugins/integration/sample.py b/InvenTree/plugins/integration/sample.py index 86e0cf0eea..9096482648 100644 --- a/InvenTree/plugins/integration/sample.py +++ b/InvenTree/plugins/integration/sample.py @@ -4,7 +4,7 @@ from django.http import HttpResponse from django.utils.translation import ugettext_lazy as _ -class SampleIntegrationPlugin(SettingsMixin, UrlsMixin, IntegrationPlugin): +class SampleIntegrationPlugin(SettingsMixin, UrlsMixin, NavigationMixin, IntegrationPlugin): """ An full integration plugin """ @@ -34,6 +34,10 @@ class SampleIntegrationPlugin(SettingsMixin, UrlsMixin, IntegrationPlugin): }, } + NAVIGATION= [ + {'name': 'SampleIntegration', 'link': 'SampleIntegrationPlugin:hi'}, + ] + class OtherIntegrationPlugin(UrlsMixin, IntegrationPlugin): """ diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index b109bd9daf..369218b524 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -1,5 +1,6 @@ {% load static %} {% load inventree_extras %} +{% load plugin_extras %} {% load i18n %} {% settings_value 'BARCODE_ENABLE' as barcodes %} @@ -57,6 +58,23 @@ {% endif %} + + {% plugin_list as pl_list %} + {% for plugin_key, plugin in pl_list.items %} + {% mixin_enabled plugin 'navigation' as navigation %} + {% if navigation %} + + + {% endif %} + {% endfor %} +