Compare commits

..

15 Commits
0.7.2 ... 0.7.7

Author SHA1 Message Date
Oliver
216e09664b Increment version number to 0.7.7 (#3403) 2022-07-26 12:01:52 +10:00
Oliver
a0813dd3c1 Make sure PIP is upgraded to latest version (#3402)
Co-authored-by: Matthias Mair <code@mjmair.com>
2022-07-26 12:01:43 +10:00
Oliver
cb540ebe90 [FR] Automated releases (#3316) (#3380)
* [FR] Automated releases
automated messages on the socials
Fixes #3078

* Add more details to Reddit

* Fix twitter text

* fix syntax

* Update release.yml

Add hashtags to twitter post

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
(cherry picked from commit 6133c745d7)

Co-authored-by: Matthias Mair <code@mjmair.com>
2022-07-21 16:10:53 +10:00
Oliver
2ab1d989ae Bump version number for stable branch (#3379) 2022-07-21 16:10:23 +10:00
Oliver
57cb769317 Handle exception when path is not relative to base path (#3378)
(cherry picked from commit 2bdba081b5)
2022-07-21 15:22:54 +10:00
Oliver
efc360f22f Fix API filtering for PurchaseOrderLineItem (#3356) (#3376)
(cherry picked from commit f0d69ec458)
2022-07-21 13:57:56 +10:00
nwns
ffe66472fe Allow supplier parts to be search by part.keywords field (#3278) (#3375)
Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2022-07-21 11:20:29 +10:00
Oliver
c17b34a864 Specify upper bound for mariadb python version (#3333)
- 1.1.0 and above causes build process to break
2022-07-15 14:30:34 +10:00
Oliver
5d44811f98 Bump version number to 0.7.5 (#3324)
* Bump version number to 0.7.5

* Add fix for stock migration

- Ensure the serial number is not too large when performing migration
- Add unit test for data migration

(cherry picked from commit 661fbf0e3d)
(cherry picked from commit 233446c2bb)

* Add similar fixes for PO and SO migrations

(cherry picked from commit bde23c130c)
(cherry picked from commit 4261090e6d)

* And similar fix for BuildOrder reference field

(cherry picked from commit ca0f4e0031)
(cherry picked from commit 9fa4ee48d6)

* Fix for plugin unit testing

* Revert test database name

(cherry picked from commit 53333c29c3)

* Override default URL behaviour for unit test

(cherry picked from commit 2c12a69529)
2022-07-15 11:58:06 +10:00
Oliver
49c61f74b1 Migration bug fix (#3325)
* Add fix for stock migration

- Ensure the serial number is not too large when performing migration
- Add unit test for data migration

(cherry picked from commit 661fbf0e3d)
(cherry picked from commit 233446c2bb)

* Add similar fixes for PO and SO migrations

(cherry picked from commit bde23c130c)
(cherry picked from commit 4261090e6d)

* And similar fix for BuildOrder reference field

(cherry picked from commit ca0f4e0031)
(cherry picked from commit 9fa4ee48d6)

* Fix for plugin unit testing

* Revert test database name

(cherry picked from commit 53333c29c3)

* Override default URL behaviour for unit test

(cherry picked from commit 2c12a69529)
2022-07-15 11:56:02 +10:00
Oliver
a19d342800 Fix translation issue with javascript (#3246) (#3252)
* Adds a custom translation node class to strip dirty characters from translated strings

* Update javascript files to use new template tag

* Override behaviour of {% load i18n %}

- No longer requires custom tag loading
- All templates now use escaped translation values
- Requires re-ordering of app loading
- Revert js_i18n to simply i18n

* CI step now lints JS files compiled in each locale

* Checking that the CI step fails

* Revert "Checking that the CI step fails"

This reverts commit ba2be0470d.

(cherry picked from commit 44b42050aa)
2022-06-25 11:28:17 +10:00
Oliver
32b81aa598 Bump version number to 0.7.4 (#3241) 2022-06-23 14:23:17 +10:00
Oliver
5196fd5546 Prevent newline chars from breaking part detail page (#3245) 2022-06-23 14:23:07 +10:00
Oliver
f9aa5a60fd Override 2FA token removal form (#3240)
- Requires user to input valid token to remove 2FA for their account

Co-authored-by: Matthias Mair <code@mjmair.com>
2022-06-23 13:04:53 +10:00
Oliver
b9c6cd70d4 Enable "sanitize" option for EasyMDE editor (#3206)
* Enable "sanitize" option for EasyMDE editor

* Bump version number
2022-06-16 13:31:20 +10:00
23 changed files with 411 additions and 62 deletions

View File

@@ -77,8 +77,8 @@ jobs:
python check_js_templates.py
- name: Lint Javascript Files
run: |
invoke render-js-files
npx eslint js_tmp/*.js
python InvenTree/manage.py prerender
npx eslint InvenTree/InvenTree/static_i18n/i18n/*.js
html:
name: html template files

31
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
# Runs on releases
name: Publish release notes
on:
release:
types: [published]
jobs:
tweet:
runs-on: ubuntu-latest
steps:
- uses: ethomson/send-tweet-action@v1
with:
status: "InvenTree release ${{ github.event.release.tag_name }} is out now! Release notes: ${{ github.event.release.html_url }} #opensource #inventree"
consumer-key: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
consumer-secret: ${{ secrets.TWITTER_CONSUMER_API_SECRET }}
access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }}
access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
reddit:
runs-on: ubuntu-latest
steps:
- uses: bluwy/release-for-reddit-action@v1
with:
username: ${{ secrets.REDDIT_USERNAME }}
password: ${{ secrets.REDDIT_PASSWORD }}
app-id: ${{ secrets.REDDIT_APP_ID }}
app-secret: ${{ secrets.REDDIT_APP_SECRET }}
subreddit: InvenTree
title: "InvenTree version ${{ github.event.release.tag_name }} released"
comment: "${{ github.event.release.body }}"

View File

@@ -17,6 +17,7 @@ from allauth.account.forms import SignupForm, set_form_field_order
from allauth.exceptions import ImmediateHttpResponse
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth_2fa.adapter import OTPAdapter
from allauth_2fa.forms import TOTPDeviceRemoveForm
from allauth_2fa.utils import user_has_valid_totp_device
from crispy_forms.bootstrap import (AppendedText, Div, PrependedAppendedText,
PrependedText, StrictButton)
@@ -325,3 +326,36 @@ class CustomSocialAccountAdapter(RegistratonMixin, DefaultSocialAccountAdapter):
# Otherwise defer to the original allauth adapter.
return super().login(request, user)
# Temporary fix for django-allauth-2fa # TODO remove
# See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq
class CustomTOTPDeviceRemoveForm(TOTPDeviceRemoveForm):
"""Custom Form to ensure a token is provided before removing MFA"""
# User must input a valid token so 2FA can be removed
token = forms.CharField(
label=_('Token'),
)
def __init__(self, user, **kwargs):
"""Add token field."""
super().__init__(user, **kwargs)
self.fields['token'].widget.attrs.update(
{
'autofocus': 'autofocus',
'autocomplete': 'off',
}
)
def clean_token(self):
"""Ensure at least one valid token is provided."""
# Ensure that the user has provided a valid token
token = self.cleaned_data.get('token')
# Verify that the user has provided a valid token
for device in self.user.totpdevice_set.filter(confirmed=True):
if device.verify_token(token):
return token
raise forms.ValidationError(_("The entered token is not valid"))

View File

@@ -217,18 +217,6 @@ logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")
INSTALLED_APPS = [
# Core django modules
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'user_sessions', # db user sessions
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
# Maintenance
'maintenance_mode',
# InvenTree apps
'build.apps.BuildConfig',
'common.apps.CommonConfig',
@@ -242,6 +230,18 @@ INSTALLED_APPS = [
'plugin.apps.PluginAppConfig',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
# Core django modules
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'user_sessions', # db user sessions
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
# Maintenance
'maintenance_mode',
# Third part add-ons
'django_filters', # Extended filter functionality
'rest_framework', # DRF (Django Rest Framework)
@@ -522,7 +522,7 @@ if "mysql" in db_engine: # pragma: no cover
# https://docs.djangoproject.com/en/3.2/ref/databases/#mysql-isolation-level
if "isolation_level" not in db_options:
serializable = _is_true(
os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "true")
os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "false")
)
db_options["isolation_level"] = (
"serializable" if serializable else "read committed"

View File

@@ -36,9 +36,10 @@ from .views import (AppearanceSelectView, CurrencyRefreshView,
CustomConnectionsView, CustomEmailView,
CustomPasswordResetFromKeyView,
CustomSessionDeleteOtherView, CustomSessionDeleteView,
DatabaseStatsView, DynamicJsView, EditUserView, IndexView,
NotificationsView, SearchView, SetPasswordView,
SettingCategorySelectView, SettingsView, auth_request)
CustomTwoFactorRemove, DatabaseStatsView, DynamicJsView,
EditUserView, IndexView, NotificationsView, SearchView,
SetPasswordView, SettingCategorySelectView, SettingsView,
auth_request)
admin.site.site_header = "InvenTree Admin"
@@ -169,6 +170,11 @@ frontendpatterns = [
re_path(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
re_path(r'^accounts/social/connections/', CustomConnectionsView.as_view(), name='socialaccount_connections'),
re_path(r"^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", CustomPasswordResetFromKeyView.as_view(), name="account_reset_password_from_key"),
# Temporary fix for django-allauth-2fa # TODO remove
# See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq
re_path(r'^accounts/two_factor/remove/?$', CustomTwoFactorRemove.as_view(), name='two-factor-remove'),
re_path(r'^accounts/', include('allauth_2fa.urls')), # MFA support
re_path(r'^accounts/', include('allauth.urls')), # included urlpatterns
]

View File

@@ -12,7 +12,7 @@ import common.models
from InvenTree.api_version import INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = "0.7.2"
INVENTREE_SW_VERSION = "0.7.7"
def inventreeInstanceName():

View File

@@ -27,6 +27,7 @@ from allauth.account.models import EmailAddress
from allauth.account.views import EmailView, PasswordResetFromKeyView
from allauth.socialaccount.forms import DisconnectForm
from allauth.socialaccount.views import ConnectionsView
from allauth_2fa.views import TwoFactorRemove
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from user_sessions.views import SessionDeleteOtherView, SessionDeleteView
@@ -35,8 +36,8 @@ from common.settings import currency_code_default, currency_codes
from part.models import PartCategory
from users.models import RuleSet, check_user_role
from .forms import (DeleteForm, EditUserForm, SetPasswordForm,
SettingCategorySelectForm)
from .forms import (CustomTOTPDeviceRemoveForm, DeleteForm, EditUserForm,
SetPasswordForm, SettingCategorySelectForm)
from .helpers import str2bool
@@ -880,3 +881,12 @@ class NotificationsView(TemplateView):
"""
template_name = "InvenTree/notifications/notifications.html"
# Temporary fix for django-allauth-2fa # TODO remove
# See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq
class CustomTwoFactorRemove(TwoFactorRemove):
"""Use custom form."""
form_class = CustomTOTPDeviceRemoveForm
success_url = reverse_lazy("settings")

View File

@@ -24,6 +24,10 @@ def build_refs(apps, schema_editor):
except: # pragma: no cover
ref = 0
# Clip integer value to ensure it does not overflow database field
if ref > 0x7fffffff:
ref = 0x7fffffff
build.reference_int = ref
build.save()

View File

@@ -365,6 +365,7 @@ class SupplierPartList(generics.ListCreateAPIView):
'part__IPN',
'part__name',
'part__description',
'part__keywords',
]

View File

@@ -523,7 +523,7 @@ class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
search_fields = [
'part__part__name',
'part__part__description',
'part__MPN',
'part__manufacturer_part__MPN',
'part__SKU',
'reference',
]

View File

@@ -23,6 +23,10 @@ def build_refs(apps, schema_editor):
except: # pragma: no cover
ref = 0
# Clip integer value to ensure it does not overflow database field
if ref > 0x7fffffff:
ref = 0x7fffffff
order.reference_int = ref
order.save()
@@ -40,6 +44,10 @@ def build_refs(apps, schema_editor):
except: # pragma: no cover
ref = 0
# Clip integer value to ensure it does not overflow database field
if ref > 0x7fffffff:
ref = 0x7fffffff
order.reference_int = ref
order.save()

View File

@@ -56,6 +56,19 @@ class TestRefIntMigrations(MigratorTestCase):
with self.assertRaises(AttributeError):
print(sales_order.reference_int)
# Create orders with very large reference values
self.po_pk = PurchaseOrder.objects.create(
supplier=supplier,
reference='999999999999999999999999999999999',
description='Big reference field',
).pk
self.so_pk = SalesOrder.objects.create(
customer=supplier,
reference='999999999999999999999999999999999',
description='Big reference field',
).pk
def test_ref_field(self):
"""
Test that the 'reference_int' field has been created and is filled out correctly
@@ -73,6 +86,15 @@ class TestRefIntMigrations(MigratorTestCase):
self.assertEqual(po.reference_int, ii)
self.assertEqual(so.reference_int, ii)
# Tests for orders with overly large reference values
po = PurchaseOrder.objects.get(pk=self.po_pk)
self.assertEqual(po.reference, '999999999999999999999999999999999')
self.assertEqual(po.reference_int, 0x7fffffff)
so = SalesOrder.objects.get(pk=self.so_pk)
self.assertEqual(so.reference, '999999999999999999999999999999999')
self.assertEqual(so.reference_int, 0x7fffffff)
class TestShipmentMigration(MigratorTestCase):
"""

View File

@@ -966,7 +966,7 @@
{% if bom_parts %}
var bom_colors = randomColor({hue: 'green', count: {{ bom_parts|length }} })
var bomdata = {
labels: [{% for line in bom_parts %}'{{ line.name }}',{% endfor %}],
labels: [{% for line in bom_parts %}'{{ line.name|escapejs }}',{% endfor %}],
datasets: [
{
label: 'Price',

View File

@@ -0,0 +1,121 @@
"""This module provides custom translation tags specifically for use with javascript code.
Translated strings are escaped, such that they can be used as string literals in a javascript file.
"""
import django.templatetags.i18n
from django import template
from django.template import TemplateSyntaxError
from django.templatetags.i18n import TranslateNode
import bleach
register = template.Library()
class CustomTranslateNode(TranslateNode):
"""Custom translation node class, which sanitizes the translated strings for javascript use"""
def render(self, context):
"""Custom render function overrides / extends default behaviour"""
result = super().render(context)
result = bleach.clean(result)
# Remove any escape sequences
for seq in ['\a', '\b', '\f', '\n', '\r', '\t', '\v']:
result = result.replace(seq, '')
# Remove other disallowed characters
for c in ['\\', '`', ';', '|', '&']:
result = result.replace(c, '')
# Escape any quotes contained in the string
result = result.replace("'", r"\'")
result = result.replace('"', r'\"')
# Return the 'clean' resulting string
return result
@register.tag("translate")
@register.tag("trans")
def do_translate(parser, token):
"""Custom translation function, lifted from https://github.com/django/django/blob/main/django/templatetags/i18n.py
The only difference is that we pass this to our custom rendering node class
"""
bits = token.split_contents()
if len(bits) < 2:
raise TemplateSyntaxError("'%s' takes at least one argument" % bits[0])
message_string = parser.compile_filter(bits[1])
remaining = bits[2:]
noop = False
asvar = None
message_context = None
seen = set()
invalid_context = {"as", "noop"}
while remaining:
option = remaining.pop(0)
if option in seen:
raise TemplateSyntaxError(
"The '%s' option was specified more than once." % option,
)
elif option == "noop":
noop = True
elif option == "context":
try:
value = remaining.pop(0)
except IndexError:
raise TemplateSyntaxError(
"No argument provided to the '%s' tag for the context option."
% bits[0]
)
if value in invalid_context:
raise TemplateSyntaxError(
"Invalid argument '%s' provided to the '%s' tag for the context "
"option" % (value, bits[0]),
)
message_context = parser.compile_filter(value)
elif option == "as":
try:
value = remaining.pop(0)
except IndexError:
raise TemplateSyntaxError(
"No argument provided to the '%s' tag for the as option." % bits[0]
)
asvar = value
else:
raise TemplateSyntaxError(
"Unknown argument for '%s' tag: '%s'. The only options "
"available are 'noop', 'context' \"xxx\", and 'as VAR'."
% (
bits[0],
option,
)
)
seen.add(option)
return CustomTranslateNode(message_string, noop, asvar, message_context)
# Re-register tags which we have not explicitly overridden
register.tag("blocktrans", django.templatetags.i18n.do_block_translate)
register.tag("blocktranslate", django.templatetags.i18n.do_block_translate)
register.tag("language", django.templatetags.i18n.language)
register.tag("get_available_languages", django.templatetags.i18n.do_get_available_languages)
register.tag("get_language_info", django.templatetags.i18n.do_get_language_info)
register.tag("get_language_info_list", django.templatetags.i18n.do_get_language_info_list)
register.tag("get_current_language", django.templatetags.i18n.do_get_current_language)
register.tag("get_current_language_bidi", django.templatetags.i18n.do_get_current_language_bidi)
register.filter("language_name", django.templatetags.i18n.language_name)
register.filter("language_name_translated", django.templatetags.i18n.language_name_translated)
register.filter("language_name_local", django.templatetags.i18n.language_name_local)
register.filter("language_bidi", django.templatetags.i18n.language_bidi)

View File

@@ -467,6 +467,10 @@ class APICallMixin:
if endpoint_is_url:
url = endpoint
else:
if endpoint.startswith('/'):
endpoint = endpoint[1:]
url = f'{self.api_url}/{endpoint}'
# build kwargs for call
@@ -474,6 +478,7 @@ class APICallMixin:
'url': url,
'headers': headers,
}
if data:
kwargs['data'] = json.dumps(data)

View File

@@ -1,4 +1,4 @@
""" Unit tests for base mixins for plugins """
"""Unit tests for base mixins for plugins."""
from django.conf import settings
from django.test import TestCase
@@ -17,7 +17,10 @@ from plugin.urls import PLUGIN_BASE
class BaseMixinDefinition:
"""Mixin to test the meta functions of all mixins."""
def test_mixin_name(self):
"""Test that the mixin registers itseld correctly."""
# mixin name
self.assertIn(self.MIXIN_NAME, [item['key'] for item in self.mixin.registered_mixins])
# human name
@@ -25,6 +28,8 @@ class BaseMixinDefinition:
class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase):
"""Tests for SettingsMixin."""
MIXIN_HUMAN_NAME = 'Settings'
MIXIN_NAME = 'settings'
MIXIN_ENABLE_CHECK = 'has_settings'
@@ -32,6 +37,7 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase):
TEST_SETTINGS = {'SETTING1': {'default': '123', }}
def setUp(self):
"""Setup for all tests."""
class SettingsCls(SettingsMixin, InvenTreePlugin):
SETTINGS = self.TEST_SETTINGS
self.mixin = SettingsCls()
@@ -43,6 +49,7 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase):
super().setUp()
def test_function(self):
"""Test that the mixin functions."""
# settings variable
self.assertEqual(self.mixin.settings, self.TEST_SETTINGS)
@@ -60,11 +67,14 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase):
class UrlsMixinTest(BaseMixinDefinition, TestCase):
"""Tests for UrlsMixin."""
MIXIN_HUMAN_NAME = 'URLs'
MIXIN_NAME = 'urls'
MIXIN_ENABLE_CHECK = 'has_urls'
def setUp(self):
"""Setup for all tests."""
class UrlsCls(UrlsMixin, InvenTreePlugin):
def test():
return 'ccc'
@@ -76,6 +86,7 @@ class UrlsMixinTest(BaseMixinDefinition, TestCase):
self.mixin_nothing = NoUrlsCls()
def test_function(self):
"""Test that the mixin functions."""
plg_name = self.mixin.plugin_name()
# base_url
@@ -99,26 +110,32 @@ class UrlsMixinTest(BaseMixinDefinition, TestCase):
class AppMixinTest(BaseMixinDefinition, TestCase):
"""Tests for AppMixin."""
MIXIN_HUMAN_NAME = 'App registration'
MIXIN_NAME = 'app'
MIXIN_ENABLE_CHECK = 'has_app'
def setUp(self):
"""Setup for all tests."""
class TestCls(AppMixin, InvenTreePlugin):
pass
self.mixin = TestCls()
def test_function(self):
# test that this plugin is in settings
"""Test that the sample plugin registers in settings."""
self.assertIn('plugin.samples.integration', settings.INSTALLED_APPS)
class NavigationMixinTest(BaseMixinDefinition, TestCase):
"""Tests for NavigationMixin."""
MIXIN_HUMAN_NAME = 'Navigation Links'
MIXIN_NAME = 'navigation'
MIXIN_ENABLE_CHECK = 'has_naviation'
def setUp(self):
"""Setup for all tests."""
class NavigationCls(NavigationMixin, InvenTreePlugin):
NAVIGATION = [
{'name': 'aa', 'link': 'plugin:test:test_view'},
@@ -131,6 +148,7 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase):
self.nothing_mixin = NothingNavigationCls()
def test_function(self):
"""Test that a correct configuration functions."""
# check right configuration
self.assertEqual(self.mixin.navigation, [{'name': 'aa', 'link': 'plugin:test:test_view'}, ])
@@ -139,7 +157,7 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase):
self.assertEqual(self.nothing_mixin.navigation_name, '')
def test_fail(self):
# check wrong links fails
"""Test that wrong links fail."""
with self.assertRaises(NotImplementedError):
class NavigationCls(NavigationMixin, InvenTreePlugin):
NAVIGATION = ['aa', 'aa']
@@ -147,11 +165,15 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase):
class APICallMixinTest(BaseMixinDefinition, TestCase):
"""Tests for APICallMixin."""
MIXIN_HUMAN_NAME = 'API calls'
MIXIN_NAME = 'api_call'
MIXIN_ENABLE_CHECK = 'has_api_call'
def setUp(self):
"""Setup for all tests."""
class MixinCls(APICallMixin, SettingsMixin, InvenTreePlugin):
NAME = "Sample API Caller"
@@ -163,40 +185,47 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
'API_URL': {
'name': 'External URL',
'description': 'Where is your API located?',
'default': 'reqres.in',
'default': 'https://api.github.com',
},
}
API_URL_SETTING = 'API_URL'
API_TOKEN_SETTING = 'API_TOKEN'
@property
def api_url(self):
"""Override API URL for this test"""
return "https://api.github.com"
def get_external_url(self, simple: bool = True):
'''
returns data from the sample endpoint
'''
return self.api_call('api/users/2', simple_response=simple)
"""Returns data from the sample endpoint."""
return self.api_call('orgs/inventree', simple_response=simple)
self.mixin = MixinCls()
class WrongCLS(APICallMixin, InvenTreePlugin):
pass
self.mixin_wrong = WrongCLS()
class WrongCLS2(APICallMixin, InvenTreePlugin):
API_URL_SETTING = 'test'
self.mixin_wrong2 = WrongCLS2()
def test_base_setup(self):
"""Test that the base settings work"""
"""Test that the base settings work."""
# check init
self.assertTrue(self.mixin.has_api_call)
# api_url
self.assertEqual('https://reqres.in', self.mixin.api_url)
self.assertEqual('https://api.github.com', self.mixin.api_url)
# api_headers
headers = self.mixin.api_headers
self.assertEqual(headers, {'Bearer': '', 'Content-Type': 'application/json'})
def test_args(self):
"""Test that building up args work"""
"""Test that building up args work."""
# api_build_url_args
# 1 arg
result = self.mixin.api_build_url_args({'a': 'b'})
@@ -209,11 +238,13 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
self.assertEqual(result, '?a=b&c=d,e,f')
def test_api_call(self):
"""Test that api calls work"""
"""Test that api calls work."""
# api_call
result = self.mixin.get_external_url()
self.assertTrue(result)
self.assertIn('data', result,)
for key in ['login', 'email', 'name', 'twitter_username']:
self.assertIn(key, result)
# api_call without json conversion
result = self.mixin.get_external_url(False)
@@ -221,25 +252,25 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
self.assertEqual(result.reason, 'OK')
# api_call with full url
result = self.mixin.api_call('https://reqres.in/api/users/2', endpoint_is_url=True)
result = self.mixin.api_call('https://api.github.com/orgs/inventree', endpoint_is_url=True)
self.assertTrue(result)
# api_call with post and data
result = self.mixin.api_call(
'api/users/',
data={"name": "morpheus", "job": "leader"},
method='POST'
'repos/inventree/InvenTree',
method='GET'
)
self.assertTrue(result)
self.assertEqual(result['name'], 'morpheus')
self.assertEqual(result['name'], 'InvenTree')
self.assertEqual(result['html_url'], 'https://github.com/inventree/InvenTree')
# api_call with filter
result = self.mixin.api_call('api/users', url_args={'page': '2'})
result = self.mixin.api_call('repos/inventree/InvenTree/stargazers', url_args={'page': '2'})
self.assertTrue(result)
self.assertEqual(result['page'], 2)
def test_function_errors(self):
"""Test function errors"""
"""Test function errors."""
# wrongly defined plugins should not load
with self.assertRaises(MixinNotImplementedError):
self.mixin_wrong.has_api_call()
@@ -250,7 +281,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
class PanelMixinTests(InvenTreeTestCase):
"""Test that the PanelMixin plugin operates correctly"""
"""Test that the PanelMixin plugin operates correctly."""
fixtures = [
'category',
@@ -262,8 +293,7 @@ class PanelMixinTests(InvenTreeTestCase):
roles = 'all'
def test_installed(self):
"""Test that the sample panel plugin is installed"""
"""Test that the sample panel plugin is installed."""
plugins = registry.with_mixin('panel')
self.assertTrue(len(plugins) > 0)
@@ -275,8 +305,7 @@ class PanelMixinTests(InvenTreeTestCase):
self.assertEqual(len(plugins), 0)
def test_disabled(self):
"""Test that the panels *do not load* if the plugin is not enabled"""
"""Test that the panels *do not load* if the plugin is not enabled."""
plugin = registry.get_plugin('samplepanel')
plugin.set_setting('ENABLE_HELLO_WORLD', True)
@@ -305,10 +334,7 @@ class PanelMixinTests(InvenTreeTestCase):
self.assertNotIn('Custom Part Panel', str(response.content))
def test_enabled(self):
"""
Test that the panels *do* load if the plugin is enabled
"""
"""Test that the panels *do* load if the plugin is enabled."""
plugin = registry.get_plugin('samplepanel')
self.assertEqual(len(registry.with_mixin('panel', active=True)), 0)
@@ -382,8 +408,7 @@ class PanelMixinTests(InvenTreeTestCase):
self.assertEqual(Error.objects.count(), n_errors + len(urls))
def test_mixin(self):
"""Test that ImplementationError is raised"""
"""Test that ImplementationError is raised."""
with self.assertRaises(MixinNotImplementedError):
class Wrong(PanelMixin, InvenTreePlugin):
pass

View File

@@ -307,7 +307,11 @@ class InvenTreePlugin(MixinBase, MetaBase):
"""
if self._is_package:
return self.__module__ # pragma: no cover
return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR)
try:
return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR)
except ValueError:
return pathlib.Path(self.def_path)
@property
def settings_url(self):

View File

@@ -28,6 +28,9 @@ def update_serials(apps, schema_editor):
except:
serial = 0
# Ensure the integer value is not too large for the database field
if serial > 0x7fffffff:
serial = 0x7fffffff
item.serial_int = serial
item.save()

View File

@@ -0,0 +1,69 @@
"""Unit tests for data migrations in the 'stock' app"""
from django_test_migrations.contrib.unittest_case import MigratorTestCase
from InvenTree import helpers
class TestSerialNumberMigration(MigratorTestCase):
"""Test data migration which updates serial numbers"""
migrate_from = ('stock', '0067_alter_stockitem_part')
migrate_to = ('stock', helpers.getNewestMigrationFile('stock'))
def prepare(self):
"""Create initial data for this migration"""
Part = self.old_state.apps.get_model('part', 'part')
StockItem = self.old_state.apps.get_model('stock', 'stockitem')
# Create a base part
my_part = Part.objects.create(
name='PART-123',
description='Some part',
active=True,
trackable=True,
level=0,
tree_id=0,
lft=0, rght=0
)
# Create some serialized stock items
for sn in range(10, 20):
StockItem.objects.create(
part=my_part,
quantity=1,
serial=sn,
level=0,
tree_id=0,
lft=0, rght=0
)
# Create a stock item with a very large serial number
item = StockItem.objects.create(
part=my_part,
quantity=1,
serial='9999999999999999999999999999999999999999999999999999999999999',
level=0,
tree_id=0,
lft=0, rght=0
)
self.big_ref_pk = item.pk
def test_migrations(self):
"""Test that the migrations have been applied correctly"""
StockItem = self.new_state.apps.get_model('stock', 'stockitem')
# Check that the serial number integer conversion has been applied correctly
for sn in range(10, 20):
item = StockItem.objects.get(serial_int=sn)
self.assertEqual(item.serial, str(sn))
big_ref_item = StockItem.objects.get(pk=self.big_ref_pk)
# Check that the StockItem maximum serial number
self.assertEqual(big_ref_item.serial, '9999999999999999999999999999999999999999999999999999999999999')
self.assertEqual(big_ref_item.serial_int, 0x7fffffff)

View File

@@ -266,6 +266,11 @@ function setupNotesField(element, url, options={}) {
initialValue: initial,
toolbar: toolbar_icons,
shortcuts: [],
renderingConfig: {
markedOptions: {
sanitize: true,
}
}
});

View File

@@ -9,7 +9,7 @@ invoke>=1.4.0 # Invoke build tool
psycopg2>=2.9.1
mysqlclient>=2.0.3
pgcli>=3.1.0
mariadb>=1.0.7
mariadb>=1.0.7,<1.1.0
# gunicorn web server
gunicorn>=20.1.0

View File

@@ -7,8 +7,8 @@ coverage==5.3 # Unit test coverage
coveralls==2.1.2 # Coveralls linking (for Travis)
cryptography==3.4.8 # Cryptography support
django-admin-shell==0.1.2 # Python shell for the admin interface
django-allauth==0.45.0 # SSO for external providers via OpenID
django-allauth-2fa==0.8 # MFA / 2FA
django-allauth==0.48.0 # SSO for external providers via OpenID
django-allauth-2fa==0.9 # MFA / 2FA # IMPORTANT: Do only change after reviewing GHSA-8j76-mm54-52xq
django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files
django-cors-headers==3.2.0 # CORS headers extension for DRF
django-crispy-forms==1.11.2 # Form helpers

View File

@@ -94,6 +94,7 @@ def install(c):
print("Installing required python packages from 'requirements.txt'")
# Install required Python packages with PIP
c.run('pip3 install --upgrade pip')
c.run('pip3 install -U -r requirements.txt')
@@ -554,9 +555,9 @@ def test_translations(c):
# complie regex
reg = re.compile(
r"[a-zA-Z0-9]{1}"+ # match any single letter and number
r"(?![^{\(\<]*[}\)\>])"+ # that is not inside curly brackets, brackets or a tag
r"(?<![^\%][^\(][)][a-z])"+ # that is not a specially formatted variable with singles
r"[a-zA-Z0-9]{1}" + # match any single letter and number
r"(?![^{\(\<]*[}\)\>])" + # that is not inside curly brackets, brackets or a tag
r"(?<![^\%][^\(][)][a-z])" + # that is not a specially formatted variable with singles
r"(?![^\\][\n])" # that is not a newline
)
last_string = ''