Merge branch 'inventree:master' into matmair/issue2694

This commit is contained in:
Matthias Mair 2022-03-16 15:16:07 +01:00 committed by GitHub
commit 966eb60dd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 336 additions and 76 deletions

View File

@ -28,7 +28,7 @@ class InvenTreeConfig(AppConfig):
self.start_background_tasks() self.start_background_tasks()
if not isInTestMode(): if not isInTestMode(): # pragma: no cover
self.update_exchange_rates() self.update_exchange_rates()
if canAppAccessDatabase() or settings.TESTING_ENV: if canAppAccessDatabase() or settings.TESTING_ENV:
@ -98,7 +98,7 @@ class InvenTreeConfig(AppConfig):
schedule_type=Schedule.DAILY, schedule_type=Schedule.DAILY,
) )
def update_exchange_rates(self): def update_exchange_rates(self): # pragma: no cover
""" """
Update exchange rates each time the server is started, *if*: Update exchange rates each time the server is started, *if*:

View File

@ -3,14 +3,14 @@ Pull rendered copies of the templated
only used for testing the js files! - This file is omited from coverage only used for testing the js files! - This file is omited from coverage
""" """
from django.test import TestCase from django.test import TestCase # pragma: no cover
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model # pragma: no cover
import os import os # pragma: no cover
import pathlib import pathlib # pragma: no cover
class RenderJavascriptFiles(TestCase): class RenderJavascriptFiles(TestCase): # pragma: no cover
""" """
A unit test to "render" javascript files. A unit test to "render" javascript files.

View File

@ -95,7 +95,7 @@ def user_roles(request):
} }
if user.is_superuser: if user.is_superuser:
for ruleset in RuleSet.RULESET_MODELS.keys(): for ruleset in RuleSet.RULESET_MODELS.keys(): # pragma: no cover
roles[ruleset] = { roles[ruleset] = {
'view': True, 'view': True,
'add': True, 'add': True,

View File

@ -68,7 +68,7 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
import importlib import importlib
from InvenTree.status import is_worker_running from InvenTree.status import is_worker_running
if is_worker_running() and not force_sync: if is_worker_running() and not force_sync: # pragma: no cover
# Running as asynchronous task # Running as asynchronous task
try: try:
task = AsyncTask(taskname, *args, **kwargs) task = AsyncTask(taskname, *args, **kwargs)
@ -94,13 +94,13 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
# Retrieve function # Retrieve function
try: try:
_func = getattr(_mod, func) _func = getattr(_mod, func)
except AttributeError: except AttributeError: # pragma: no cover
# getattr does not work for local import # getattr does not work for local import
_func = None _func = None
try: try:
if not _func: if not _func:
_func = eval(func) _func = eval(func) # pragma: no cover
except NameError: except NameError:
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'") logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
return return
@ -248,7 +248,7 @@ def update_exchange_rates():
# Apps not yet loaded! # Apps not yet loaded!
logger.info("Could not perform 'update_exchange_rates' - App registry not ready") logger.info("Could not perform 'update_exchange_rates' - App registry not ready")
return return
except: except: # pragma: no cover
# Other error? # Other error?
return return
@ -257,7 +257,7 @@ def update_exchange_rates():
backend = ExchangeBackend.objects.get(name='InvenTreeExchange') backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
except ExchangeBackend.DoesNotExist: except ExchangeBackend.DoesNotExist:
pass pass
except: except: # pragma: no cover
# Some other error # Some other error
logger.warning("update_exchange_rates: Database not ready") logger.warning("update_exchange_rates: Database not ready")
return return
@ -274,7 +274,7 @@ def update_exchange_rates():
# Remove any exchange rates which are not in the provided currencies # Remove any exchange rates which are not in the provided currencies
Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=currency_codes()).delete() Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=currency_codes()).delete()
except Exception as e: except Exception as e: # pragma: no cover
logger.error(f"Error updating exchange rates: {e}") logger.error(f"Error updating exchange rates: {e}")

View File

@ -17,6 +17,7 @@ from . import helpers
from . import version from . import version
from . import status from . import status
from . import ready from . import ready
from . import config
from decimal import Decimal from decimal import Decimal
@ -24,6 +25,7 @@ import InvenTree.tasks
from stock.models import StockLocation from stock.models import StockLocation
from common.settings import currency_codes from common.settings import currency_codes
from common.models import InvenTreeSetting
class ValidatorTest(TestCase): class ValidatorTest(TestCase):
@ -453,3 +455,55 @@ class TestSettings(TestCase):
# make sure to clean up # make sure to clean up
settings.TESTING_ENV = False settings.TESTING_ENV = False
def test_helpers_cfg_file(self):
# normal run - not configured
self.assertIn('InvenTree/InvenTree/config.yaml', config.get_config_file())
# with env set
with self.env:
self.env.set('INVENTREE_CONFIG_FILE', 'my_special_conf.yaml')
self.assertIn('InvenTree/InvenTree/my_special_conf.yaml', config.get_config_file())
def test_helpers_plugin_file(self):
# normal run - not configured
self.assertIn('InvenTree/InvenTree/plugins.txt', config.get_plugin_file())
# with env set
with self.env:
self.env.set('INVENTREE_PLUGIN_FILE', 'my_special_plugins.txt')
self.assertIn('my_special_plugins.txt', config.get_plugin_file())
def test_helpers_setting(self):
TEST_ENV_NAME = '123TEST'
# check that default gets returned if not present
self.assertEqual(config.get_setting(TEST_ENV_NAME, None, '123!'), '123!')
# with env set
with self.env:
self.env.set(TEST_ENV_NAME, '321')
self.assertEqual(config.get_setting(TEST_ENV_NAME, None), '321')
class TestInstanceName(TestCase):
"""
Unit tests for instance name
"""
def setUp(self):
# Create a user for auth
user = get_user_model()
self.user = user.objects.create_superuser('testuser', 'test@testing.com', 'password')
self.client.login(username='testuser', password='password')
def test_instance_name(self):
# default setting
self.assertEqual(version.inventreeInstanceTitle(), 'InvenTree')
# set up required setting
InvenTreeSetting.set_setting("INVENTREE_INSTANCE_TITLE", True, self.user)
InvenTreeSetting.set_setting("INVENTREE_INSTANCE", "Testing title", self.user)
self.assertEqual(version.inventreeInstanceTitle(), 'Testing title')

View File

@ -12,11 +12,14 @@ import common.models
INVENTREE_SW_VERSION = "0.7.0 dev" INVENTREE_SW_VERSION = "0.7.0 dev"
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 30 INVENTREE_API_VERSION = 31
""" """
Increment this API version number whenever there is a significant change to the API that any clients need to know about Increment this API version number whenever there is a significant change to the API that any clients need to know about
v31 -> 2022-03-14
- Adds "updated" field to SupplierPriceBreakList and SupplierPriceBreakDetail API endpoints
v30 -> 2022-03-09 v30 -> 2022-03-09
- Adds "exclude_location" field to BuildAutoAllocation API endpoint - Adds "exclude_location" field to BuildAutoAllocation API endpoint
- Allows BuildItem API endpoint to be filtered by BomItem relation - Allows BuildItem API endpoint to be filtered by BomItem relation
@ -171,7 +174,7 @@ def inventreeDocsVersion():
if isInvenTreeDevelopmentVersion(): if isInvenTreeDevelopmentVersion():
return "latest" return "latest"
else: else:
return INVENTREE_SW_VERSION return INVENTREE_SW_VERSION # pragma: no cover
def isInvenTreeUpToDate(): def isInvenTreeUpToDate():
@ -189,10 +192,10 @@ def isInvenTreeUpToDate():
return True return True
# Extract "tuple" version (Python can directly compare version tuples) # Extract "tuple" version (Python can directly compare version tuples)
latest_version = inventreeVersionTuple(latest) latest_version = inventreeVersionTuple(latest) # pragma: no cover
inventree_version = inventreeVersionTuple() inventree_version = inventreeVersionTuple() # pragma: no cover
return inventree_version >= latest_version return inventree_version >= latest_version # pragma: no cover
def inventreeApiVersion(): def inventreeApiVersion():
@ -209,7 +212,7 @@ def inventreeCommitHash():
try: try:
return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip() return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
except: except: # pragma: no cover
return None return None
@ -219,5 +222,5 @@ def inventreeCommitDate():
try: try:
d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip() d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip()
return d.split(' ')[0] return d.split(' ')[0]
except: except: # pragma: no cover
return None return None

View File

@ -7,10 +7,10 @@ For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
""" """
import os import os # pragma: no cover
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application # pragma: no cover
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "InvenTree.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "InvenTree.settings") # pragma: no cover
application = get_wsgi_application() application = get_wsgi_application() # pragma: no cover

View File

@ -47,7 +47,7 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
except json.JSONDecodeError: except json.JSONDecodeError:
return False return False
else: else:
return False return False # pragma: no cover
# If any of the following keys are in the JSON data, # If any of the following keys are in the JSON data,
# let's go ahead and assume that the code is a valid InvenTree one... # let's go ahead and assume that the code is a valid InvenTree one...
@ -70,10 +70,10 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
# Initially try casting to an integer # Initially try casting to an integer
try: try:
pk = int(data) pk = int(data)
except (TypeError, ValueError): except (TypeError, ValueError): # pragma: no cover
pk = None pk = None
if pk is None: if pk is None: # pragma: no cover
try: try:
pk = self.data[k]['id'] pk = self.data[k]['id']
except (AttributeError, KeyError): except (AttributeError, KeyError):
@ -82,7 +82,7 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
try: try:
item = StockItem.objects.get(pk=pk) item = StockItem.objects.get(pk=pk)
return item return item
except (ValueError, StockItem.DoesNotExist): except (ValueError, StockItem.DoesNotExist): # pragma: no cover
raise ValidationError({k, "Stock item does not exist"}) raise ValidationError({k, "Stock item does not exist"})
return None return None
@ -97,10 +97,10 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
# First try simple integer lookup # First try simple integer lookup
try: try:
pk = int(self.data[k]) pk = int(self.data[k])
except (TypeError, ValueError): except (TypeError, ValueError): # pragma: no cover
pk = None pk = None
if pk is None: if pk is None: # pragma: no cover
# Lookup by 'id' field # Lookup by 'id' field
try: try:
pk = self.data[k]['id'] pk = self.data[k]['id']
@ -110,7 +110,7 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
try: try:
loc = StockLocation.objects.get(pk=pk) loc = StockLocation.objects.get(pk=pk)
return loc return loc
except (ValueError, StockLocation.DoesNotExist): except (ValueError, StockLocation.DoesNotExist): # pragma: no cover
raise ValidationError({k, "Stock location does not exist"}) raise ValidationError({k, "Stock location does not exist"})
return None return None
@ -125,10 +125,10 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
# Try integer lookup first # Try integer lookup first
try: try:
pk = int(self.data[k]) pk = int(self.data[k])
except (TypeError, ValueError): except (TypeError, ValueError): # pragma: no cover
pk = None pk = None
if pk is None: if pk is None: # pragma: no cover
try: try:
pk = self.data[k]['id'] pk = self.data[k]['id']
except (AttributeError, KeyError): except (AttributeError, KeyError):
@ -137,7 +137,7 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
try: try:
part = Part.objects.get(pk=pk) part = Part.objects.get(pk=pk)
return part return part
except (ValueError, Part.DoesNotExist): except (ValueError, Part.DoesNotExist): # pragma: no cover
raise ValidationError({k, 'Part does not exist'}) raise ValidationError({k, 'Part does not exist'})
return None return None

View File

@ -38,8 +38,18 @@ class BarcodeAPITest(APITestCase):
def test_invalid(self): def test_invalid(self):
# test scan url
response = self.client.post(self.scan_url, format='json', data={}) response = self.client.post(self.scan_url, format='json', data={})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# test wrong assign urls
response = self.client.post(self.assign_url, format='json', data={})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response = self.client.post(self.assign_url, format='json', data={'barcode': '123'})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response = self.client.post(self.assign_url, format='json', data={'barcode': '123', 'stockitem': '123'})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_empty(self): def test_empty(self):
@ -204,3 +214,57 @@ class BarcodeAPITest(APITestCase):
self.assertIn('error', data) self.assertIn('error', data)
self.assertNotIn('success', data) self.assertNotIn('success', data)
class TestInvenTreeBarcode(APITestCase):
fixtures = [
'category',
'part',
'location',
'stock'
]
def setUp(self):
# Create a user for auth
user = get_user_model()
user.objects.create_user('testuser', 'test@testing.com', 'password')
self.client.login(username='testuser', password='password')
def test_errors(self):
"""
Test all possible error cases for assigment action
"""
def test_assert_error(barcode_data):
response = self.client.post(
reverse('api-barcode-link'), format='json',
data={
'barcode': barcode_data,
'stockitem': 521
}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('error', response.data)
# test with already existing stock
test_assert_error('{"stockitem": 521}')
# test with already existing stock location
test_assert_error('{"stocklocation": 7}')
# test with already existing part location
test_assert_error('{"part": 10004}')
# test with hash
test_assert_error('{"blbla": 10004}')
def test_scan(self):
"""
Test that a barcode can be scanned
"""
response = self.client.post(reverse('api-barcode-scan'), format='json', data={'barcode': 'blbla=10004'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('success', response.data)

View File

@ -12,7 +12,7 @@ class SettingsAdmin(ImportExportModelAdmin):
list_display = ('key', 'value') list_display = ('key', 'value')
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None): # pragma: no cover
""" """
Prevent the 'key' field being edited once the setting is created Prevent the 'key' field being edited once the setting is created
""" """
@ -27,7 +27,7 @@ class UserSettingsAdmin(ImportExportModelAdmin):
list_display = ('key', 'value', 'user', ) list_display = ('key', 'value', 'user', )
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None): # pragma: no cover
""" """
Prevent the 'key' field being edited once the setting is created Prevent the 'key' field being edited once the setting is created
""" """

View File

@ -570,7 +570,7 @@ class BaseInvenTreeSetting(models.Model):
try: try:
value = int(self.value) value = int(self.value)
except (ValueError, TypeError): except (ValueError, TypeError):
value = self.default_value() value = self.default_value
return value return value

View File

@ -18,12 +18,12 @@ def currency_code_default():
try: try:
code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
except ProgrammingError: except ProgrammingError: # pragma: no cover
# database is not initialized yet # database is not initialized yet
code = '' code = ''
if code not in CURRENCIES: if code not in CURRENCIES:
code = 'USD' code = 'USD' # pragma: no cover
return code return code

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from django.test import TestCase
from common.models import NotificationEntry
from InvenTree.tasks import offload_task
class TaskTest(TestCase):
"""
Tests for common tasks
"""
def test_delete(self):
# check empty run
self.assertEqual(NotificationEntry.objects.all().count(), 0)
offload_task('common.tasks.delete_old_notifications',)

View File

@ -6,7 +6,9 @@ from datetime import timedelta
from django.test import TestCase, Client from django.test import TestCase, Client
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.urls import reverse
from InvenTree.api_tester import InvenTreeAPITestCase
from .models import InvenTreeSetting, WebhookEndpoint, WebhookMessage, NotificationEntry from .models import InvenTreeSetting, WebhookEndpoint, WebhookMessage, NotificationEntry
from .api import WebhookView from .api import WebhookView
@ -46,6 +48,67 @@ class SettingsTest(TestCase):
# Check object lookup (case insensitive) # Check object lookup (case insensitive)
self.assertEqual(InvenTreeSetting.get_setting_object('iNvEnTrEE_inSTanCE').pk, 1) self.assertEqual(InvenTreeSetting.get_setting_object('iNvEnTrEE_inSTanCE').pk, 1)
def test_settings_functions(self):
"""
Test settings functions and properties
"""
# define settings to check
instance_ref = 'INVENTREE_INSTANCE'
instance_obj = InvenTreeSetting.get_setting_object(instance_ref)
stale_ref = 'STOCK_STALE_DAYS'
stale_days = InvenTreeSetting.get_setting_object(stale_ref)
report_size_obj = InvenTreeSetting.get_setting_object('REPORT_DEFAULT_PAGE_SIZE')
report_test_obj = InvenTreeSetting.get_setting_object('REPORT_ENABLE_TEST_REPORT')
# check settings base fields
self.assertEqual(instance_obj.name, 'InvenTree Instance Name')
self.assertEqual(instance_obj.get_setting_name(instance_ref), 'InvenTree Instance Name')
self.assertEqual(instance_obj.description, 'String descriptor for the server instance')
self.assertEqual(instance_obj.get_setting_description(instance_ref), 'String descriptor for the server instance')
# check units
self.assertEqual(instance_obj.units, '')
self.assertEqual(instance_obj.get_setting_units(instance_ref), '')
self.assertEqual(instance_obj.get_setting_units(stale_ref), 'days')
# check as_choice
self.assertEqual(instance_obj.as_choice(), 'My very first InvenTree Instance')
self.assertEqual(report_size_obj.as_choice(), 'A4')
# check is_choice
self.assertEqual(instance_obj.is_choice(), False)
self.assertEqual(report_size_obj.is_choice(), True)
# check setting_type
self.assertEqual(instance_obj.setting_type(), 'string')
self.assertEqual(report_test_obj.setting_type(), 'boolean')
self.assertEqual(stale_days.setting_type(), 'integer')
# check as_int
self.assertEqual(stale_days.as_int(), 0)
self.assertEqual(instance_obj.as_int(), 'InvenTree server') # not an int -> return default
# check as_bool
self.assertEqual(report_test_obj.as_bool(), True)
# check to_native_value
self.assertEqual(stale_days.to_native_value(), 0)
def test_allValues(self):
"""
Make sure that the allValues functions returns correctly
"""
# define testing settings
# check a few keys
result = InvenTreeSetting.allValues()
self.assertIn('INVENTREE_INSTANCE', result)
self.assertIn('PART_COPY_TESTS', result)
self.assertIn('STOCK_OWNERSHIP_CONTROL', result)
self.assertIn('SIGNUP_GROUP', result)
def test_required_values(self): def test_required_values(self):
""" """
- Ensure that every global setting has a name. - Ensure that every global setting has a name.
@ -93,6 +156,14 @@ class SettingsTest(TestCase):
raise ValueError(f'Non-boolean default value specified for {key}') # pragma: no cover raise ValueError(f'Non-boolean default value specified for {key}') # pragma: no cover
class SettingsApiTest(InvenTreeAPITestCase):
def test_settings_api(self):
# test setting with choice
url = reverse('api-user-setting-list')
self.get(url, expected_code=200)
class WebhookMessageTests(TestCase): class WebhookMessageTests(TestCase):
def setUp(self): def setUp(self):
self.endpoint_def = WebhookEndpoint.objects.create() self.endpoint_def = WebhookEndpoint.objects.create()
@ -223,3 +294,26 @@ class NotificationTest(TestCase):
self.assertFalse(NotificationEntry.check_recent('test.notification2', 1, delta)) self.assertFalse(NotificationEntry.check_recent('test.notification2', 1, delta))
self.assertTrue(NotificationEntry.check_recent('test.notification', 1, delta)) self.assertTrue(NotificationEntry.check_recent('test.notification', 1, delta))
class LoadingTest(TestCase):
"""
Tests for the common config
"""
def test_restart_flag(self):
"""
Test that the restart flag is reset on start
"""
import common.models
from plugin import registry
# set flag true
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)
# reload the app
registry.reload_plugins()
# now it should be false again
self.assertFalse(common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED'))

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-03-14 22:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('company', '0041_alter_company_options'),
]
operations = [
migrations.AddField(
model_name='supplierpricebreak',
name='updated',
field=models.DateTimeField(auto_now=True, null=True, verbose_name='last updated'),
),
]

View File

@ -693,6 +693,7 @@ class SupplierPriceBreak(common.models.PriceBreak):
Attributes: Attributes:
part: Link to a SupplierPart object that this price break applies to part: Link to a SupplierPart object that this price break applies to
updated: Automatic DateTime field that shows last time the price break was updated
quantity: Quantity required for price break quantity: Quantity required for price break
cost: Cost at specified quantity cost: Cost at specified quantity
currency: Reference to the currency of this pricebreak (leave empty for base currency) currency: Reference to the currency of this pricebreak (leave empty for base currency)
@ -704,6 +705,8 @@ class SupplierPriceBreak(common.models.PriceBreak):
part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks', verbose_name=_('Part'),) part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks', verbose_name=_('Part'),)
updated = models.DateTimeField(auto_now=True, null=True, verbose_name=_('last updated'))
class Meta: class Meta:
unique_together = ("part", "quantity") unique_together = ("part", "quantity")

View File

@ -278,4 +278,5 @@ class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
'quantity', 'quantity',
'price', 'price',
'price_currency', 'price_currency',
'updated',
] ]

View File

@ -268,6 +268,14 @@ $('#price-break-table').inventreeTable({
return html; return html;
} }
}, },
{
field: 'updated',
title: '{% trans "Last updated" %}',
sortable: true,
formatter: function(value) {
return renderDate(value);
}
},
] ]
}); });

View File

@ -53,7 +53,7 @@ class InvenTreePluginBase():
else: else:
return self.plugin_name() return self.plugin_name()
def plugin_config(self, raise_error=False): def plugin_config(self):
""" """
Return the PluginConfig object associated with this plugin Return the PluginConfig object associated with this plugin
""" """
@ -65,12 +65,9 @@ class InvenTreePluginBase():
key=self.plugin_slug(), key=self.plugin_slug(),
name=self.plugin_name(), name=self.plugin_name(),
) )
except (OperationalError, ProgrammingError) as error: except (OperationalError, ProgrammingError):
cfg = None cfg = None
if raise_error:
raise error
return cfg return cfg
def is_active(self): def is_active(self):
@ -91,6 +88,6 @@ class InvenTreePlugin(InvenTreePluginBase):
""" """
This is here for leagcy reasons and will be removed in the next major release This is here for leagcy reasons and will be removed in the next major release
""" """
def __init__(self): def __init__(self): # pragma: no cover
warnings.warn("Using the InvenTreePlugin is depreceated", DeprecationWarning) warnings.warn("Using the InvenTreePlugin is depreceated", DeprecationWarning)
super().__init__() super().__init__()

View File

@ -21,7 +21,7 @@ from django.utils.text import slugify
try: try:
from importlib import metadata from importlib import metadata
except: except: # pragma: no cover
import importlib_metadata as metadata import importlib_metadata as metadata
# TODO remove when python minimum is 3.8 # TODO remove when python minimum is 3.8
@ -84,7 +84,7 @@ class PluginsRegistry:
""" """
if not settings.PLUGINS_ENABLED: if not settings.PLUGINS_ENABLED:
# Plugins not enabled, do nothing # Plugins not enabled, do nothing
return return # pragma: no cover
logger.info('Start loading plugins') logger.info('Start loading plugins')
@ -120,7 +120,7 @@ class PluginsRegistry:
# We do not want to end in an endless loop # We do not want to end in an endless loop
retry_counter -= 1 retry_counter -= 1
if retry_counter <= 0: if retry_counter <= 0: # pragma: no cover
if settings.PLUGIN_TESTING: if settings.PLUGIN_TESTING:
print('[PLUGIN] Max retries, breaking loading') print('[PLUGIN] Max retries, breaking loading')
# TODO error for server status # TODO error for server status
@ -143,14 +143,14 @@ class PluginsRegistry:
if not settings.PLUGINS_ENABLED: if not settings.PLUGINS_ENABLED:
# Plugins not enabled, do nothing # Plugins not enabled, do nothing
return return # pragma: no cover
logger.info('Start unloading plugins') logger.info('Start unloading plugins')
# Set maintanace mode # Set maintanace mode
_maintenance = bool(get_maintenance_mode()) _maintenance = bool(get_maintenance_mode())
if not _maintenance: if not _maintenance:
set_maintenance_mode(True) set_maintenance_mode(True) # pragma: no cover
# remove all plugins from registry # remove all plugins from registry
self._clean_registry() self._clean_registry()
@ -160,7 +160,7 @@ class PluginsRegistry:
# remove maintenance # remove maintenance
if not _maintenance: if not _maintenance:
set_maintenance_mode(False) set_maintenance_mode(False) # pragma: no cover
logger.info('Finished unloading plugins') logger.info('Finished unloading plugins')
def reload_plugins(self): def reload_plugins(self):
@ -170,7 +170,7 @@ class PluginsRegistry:
# Do not reload whe currently loading # Do not reload whe currently loading
if self.is_loading: if self.is_loading:
return return # pragma: no cover
logger.info('Start reloading plugins') logger.info('Start reloading plugins')
@ -187,7 +187,7 @@ class PluginsRegistry:
if not settings.PLUGINS_ENABLED: if not settings.PLUGINS_ENABLED:
# Plugins not enabled, do nothing # Plugins not enabled, do nothing
return return # pragma: no cover
self.plugin_modules = [] # clear self.plugin_modules = [] # clear
@ -200,7 +200,7 @@ class PluginsRegistry:
# Check if not running in testing mode and apps should be loaded from hooks # Check if not running in testing mode and apps should be loaded from hooks
if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP): if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP):
# Collect plugins from setup entry points # Collect plugins from setup entry points
for entry in metadata.entry_points().get('inventree_plugins', []): for entry in metadata.entry_points().get('inventree_plugins', []): # pragma: no cover
try: try:
plugin = entry.load() plugin = entry.load()
plugin.is_package = True plugin.is_package = True
@ -257,7 +257,7 @@ class PluginsRegistry:
except (OperationalError, ProgrammingError) as error: except (OperationalError, ProgrammingError) as error:
# Exception if the database has not been migrated yet - check if test are running - raise if not # Exception if the database has not been migrated yet - check if test are running - raise if not
if not settings.PLUGIN_TESTING: if not settings.PLUGIN_TESTING:
raise error raise error # pragma: no cover
plugin_db_setting = None plugin_db_setting = None
# Always activate if testing # Always activate if testing
@ -267,7 +267,7 @@ class PluginsRegistry:
# option1: package, option2: file-based # option1: package, option2: file-based
if (plugin.__name__ == disabled) or (plugin.__module__ == disabled): if (plugin.__name__ == disabled) or (plugin.__module__ == disabled):
# Errors are bad so disable the plugin in the database # Errors are bad so disable the plugin in the database
if not settings.PLUGIN_TESTING: if not settings.PLUGIN_TESTING: # pragma: no cover
plugin_db_setting.active = False plugin_db_setting.active = False
# TODO save the error to the plugin # TODO save the error to the plugin
plugin_db_setting.save(no_reload=True) plugin_db_setting.save(no_reload=True)
@ -445,7 +445,7 @@ class PluginsRegistry:
try: try:
app_name = plugin_path.split('.')[-1] app_name = plugin_path.split('.')[-1]
app_config = apps.get_app_config(app_name) app_config = apps.get_app_config(app_name)
except LookupError: except LookupError: # pragma: no cover
# the plugin was never loaded correctly # the plugin was never loaded correctly
logger.debug(f'{app_name} App was not found during deregistering') logger.debug(f'{app_name} App was not found during deregistering')
break break
@ -499,7 +499,7 @@ class PluginsRegistry:
# remove model from admin site # remove model from admin site
admin.site.unregister(model) admin.site.unregister(model)
models += [model._meta.model_name] models += [model._meta.model_name]
except LookupError: except LookupError: # pragma: no cover
# if an error occurs the app was never loaded right -> so nothing to do anymore # if an error occurs the app was never loaded right -> so nothing to do anymore
logger.debug(f'{app_name} App was not found during deregistering') logger.debug(f'{app_name} App was not found during deregistering')
break break
@ -572,7 +572,7 @@ class PluginsRegistry:
try: try:
cmd(*args, **kwargs) cmd(*args, **kwargs)
return True, [] return True, []
except Exception as error: except Exception as error: # pragma: no cover
handle_error(error) handle_error(error)
# endregion # endregion

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals # pragma: no cover

View File

@ -222,6 +222,7 @@
lft: 0 lft: 0
rght: 0 rght: 0
expiry_date: "1990-10-10" expiry_date: "1990-10-10"
uid: 9e5ae7fc20568ed4814c10967bba8b65
- model: stock.stockitem - model: stock.stockitem
pk: 521 pk: 521
@ -235,6 +236,7 @@
lft: 0 lft: 0
rght: 0 rght: 0
status: 60 status: 60
uid: 1be0dfa925825c5c6c79301449e50c2d
- model: stock.stockitem - model: stock.stockitem
pk: 522 pk: 522

View File

@ -49,7 +49,7 @@ class InvenTreeGroupAdminForm(forms.ModelForm):
'users', 'users',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs): # pragma: no cover
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.instance.pk: if self.instance.pk:
@ -65,12 +65,12 @@ class InvenTreeGroupAdminForm(forms.ModelForm):
help_text=_('Select which users are assigned to this group') help_text=_('Select which users are assigned to this group')
) )
def save_m2m(self): def save_m2m(self): # pragma: no cover
# Add the users to the Group. # Add the users to the Group.
self.instance.user_set.set(self.cleaned_data['users']) self.instance.user_set.set(self.cleaned_data['users'])
def save(self, *args, **kwargs): def save(self, *args, **kwargs): # pragma: no cover
# Default save # Default save
instance = super().save() instance = super().save()
# Save many-to-many data # Save many-to-many data
@ -78,7 +78,7 @@ class InvenTreeGroupAdminForm(forms.ModelForm):
return instance return instance
class RoleGroupAdmin(admin.ModelAdmin): class RoleGroupAdmin(admin.ModelAdmin): # pragma: no cover
""" """
Custom admin interface for the Group model Custom admin interface for the Group model
""" """

View File

@ -100,7 +100,7 @@ class RoleDetails(APIView):
if len(permissions) > 0: if len(permissions) > 0:
roles[role] = permissions roles[role] = permissions
else: else:
roles[role] = None roles[role] = None # pragma: no cover
data = { data = {
'user': user.pk, 'user': user.pk,

View File

@ -267,9 +267,9 @@ class RuleSet(models.Model):
model=model model=model
) )
def __str__(self, debug=False): def __str__(self, debug=False): # pragma: no cover
""" Ruleset string representation """ """ Ruleset string representation """
if debug: # pragma: no cover if debug:
# Makes debugging easier # Makes debugging easier
return f'{str(self.group).ljust(15)}: {self.name.title().ljust(15)} | ' \ return f'{str(self.group).ljust(15)}: {self.name.title().ljust(15)} | ' \
f'v: {str(self.can_view).ljust(5)} | a: {str(self.can_add).ljust(5)} | ' \ f'v: {str(self.can_view).ljust(5)} | a: {str(self.can_add).ljust(5)} | ' \
@ -319,7 +319,7 @@ def split_permission(app, perm):
"""split permission string into permission and model""" """split permission string into permission and model"""
permission_name, *model = perm.split('_') permission_name, *model = perm.split('_')
# handle models that have underscores # handle models that have underscores
if len(model) > 1: if len(model) > 1: # pragma: no cover
app += '_' + '_'.join(model[:-1]) app += '_' + '_'.join(model[:-1])
perm = permission_name + '_' + model[-1:][0] perm = permission_name + '_' + model[-1:][0]
model = model[-1:][0] model = model[-1:][0]
@ -375,7 +375,7 @@ def update_group_roles(group, debug=False):
allowed - Whether or not the action is allowed allowed - Whether or not the action is allowed
""" """
if action not in ['view', 'add', 'change', 'delete']: if action not in ['view', 'add', 'change', 'delete']: # pragma: no cover
raise ValueError("Action {a} is invalid".format(a=action)) raise ValueError("Action {a} is invalid".format(a=action))
permission_string = RuleSet.get_model_permission_string(model, action) permission_string = RuleSet.get_model_permission_string(model, action)
@ -576,7 +576,7 @@ class Owner(models.Model):
return owners return owners
@staticmethod @staticmethod
def get_api_url(): def get_api_url(): # pragma: no cover
return reverse('api-owner-list') return reverse('api-owner-list')
class Meta: class Meta:

View File

@ -20,7 +20,3 @@ max-complexity = 20
[coverage:run] [coverage:run]
source = ./InvenTree source = ./InvenTree
[coverage:report]
omit=
InvenTree/wsgi.py
InvenTree/ci_render_js.py