mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Merge branch 'inventree:master' into matmair/issue2279
This commit is contained in:
		| @@ -28,7 +28,7 @@ class InvenTreeConfig(AppConfig): | ||||
|  | ||||
|             self.start_background_tasks() | ||||
|  | ||||
|             if not isInTestMode(): | ||||
|             if not isInTestMode():  # pragma: no cover | ||||
|                 self.update_exchange_rates() | ||||
|  | ||||
|         if canAppAccessDatabase() or settings.TESTING_ENV: | ||||
| @@ -98,7 +98,7 @@ class InvenTreeConfig(AppConfig): | ||||
|             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*: | ||||
|  | ||||
|   | ||||
| @@ -3,14 +3,14 @@ Pull rendered copies of the templated | ||||
| only used for testing the js files! - This file is omited from coverage | ||||
| """ | ||||
|  | ||||
| from django.test import TestCase | ||||
| from django.contrib.auth import get_user_model | ||||
| from django.test import TestCase  # pragma: no cover | ||||
| from django.contrib.auth import get_user_model  # pragma: no cover | ||||
|  | ||||
| import os | ||||
| import pathlib | ||||
| import os  # pragma: no cover | ||||
| import pathlib  # pragma: no cover | ||||
|  | ||||
|  | ||||
| class RenderJavascriptFiles(TestCase): | ||||
| class RenderJavascriptFiles(TestCase):  # pragma: no cover | ||||
|     """ | ||||
|     A unit test to "render" javascript files. | ||||
|  | ||||
|   | ||||
| @@ -95,7 +95,7 @@ def user_roles(request): | ||||
|     } | ||||
|  | ||||
|     if user.is_superuser: | ||||
|         for ruleset in RuleSet.RULESET_MODELS.keys(): | ||||
|         for ruleset in RuleSet.RULESET_MODELS.keys():  # pragma: no cover | ||||
|             roles[ruleset] = { | ||||
|                 'view': True, | ||||
|                 'add': True, | ||||
|   | ||||
| @@ -68,7 +68,7 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): | ||||
|         import importlib | ||||
|         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 | ||||
|             try: | ||||
|                 task = AsyncTask(taskname, *args, **kwargs) | ||||
| @@ -94,13 +94,13 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): | ||||
|             # Retrieve function | ||||
|             try: | ||||
|                 _func = getattr(_mod, func) | ||||
|             except AttributeError: | ||||
|             except AttributeError:  # pragma: no cover | ||||
|                 # getattr does not work for local import | ||||
|                 _func = None | ||||
|  | ||||
|             try: | ||||
|                 if not _func: | ||||
|                     _func = eval(func) | ||||
|                     _func = eval(func)  # pragma: no cover | ||||
|             except NameError: | ||||
|                 logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'") | ||||
|                 return | ||||
| @@ -248,7 +248,7 @@ def update_exchange_rates(): | ||||
|         # Apps not yet loaded! | ||||
|         logger.info("Could not perform 'update_exchange_rates' - App registry not ready") | ||||
|         return | ||||
|     except: | ||||
|     except:  # pragma: no cover | ||||
|         # Other error? | ||||
|         return | ||||
|  | ||||
| @@ -257,7 +257,7 @@ def update_exchange_rates(): | ||||
|         backend = ExchangeBackend.objects.get(name='InvenTreeExchange') | ||||
|     except ExchangeBackend.DoesNotExist: | ||||
|         pass | ||||
|     except: | ||||
|     except:  # pragma: no cover | ||||
|         # Some other error | ||||
|         logger.warning("update_exchange_rates: Database not ready") | ||||
|         return | ||||
| @@ -274,7 +274,7 @@ def update_exchange_rates(): | ||||
|  | ||||
|         # Remove any exchange rates which are not in the provided currencies | ||||
|         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}") | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -17,6 +17,7 @@ from . import helpers | ||||
| from . import version | ||||
| from . import status | ||||
| from . import ready | ||||
| from . import config | ||||
|  | ||||
| from decimal import Decimal | ||||
|  | ||||
| @@ -24,6 +25,7 @@ import InvenTree.tasks | ||||
|  | ||||
| from stock.models import StockLocation | ||||
| from common.settings import currency_codes | ||||
| from common.models import InvenTreeSetting | ||||
|  | ||||
|  | ||||
| class ValidatorTest(TestCase): | ||||
| @@ -453,3 +455,55 @@ class TestSettings(TestCase): | ||||
|  | ||||
|         # make sure to clean up | ||||
|         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') | ||||
|   | ||||
| @@ -12,11 +12,14 @@ import common.models | ||||
| INVENTREE_SW_VERSION = "0.7.0 dev" | ||||
|  | ||||
| # 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 | ||||
|  | ||||
| v31 -> 2022-03-14 | ||||
|     - Adds "updated" field to SupplierPriceBreakList and SupplierPriceBreakDetail API endpoints | ||||
|  | ||||
| v30 -> 2022-03-09 | ||||
|     - Adds "exclude_location" field to BuildAutoAllocation API endpoint | ||||
|     - Allows BuildItem API endpoint to be filtered by BomItem relation | ||||
| @@ -171,7 +174,7 @@ def inventreeDocsVersion(): | ||||
|     if isInvenTreeDevelopmentVersion(): | ||||
|         return "latest" | ||||
|     else: | ||||
|         return INVENTREE_SW_VERSION | ||||
|         return INVENTREE_SW_VERSION  # pragma: no cover | ||||
|  | ||||
|  | ||||
| def isInvenTreeUpToDate(): | ||||
| @@ -189,10 +192,10 @@ def isInvenTreeUpToDate(): | ||||
|         return True | ||||
|  | ||||
|     # Extract "tuple" version (Python can directly compare version tuples) | ||||
|     latest_version = inventreeVersionTuple(latest) | ||||
|     inventree_version = inventreeVersionTuple() | ||||
|     latest_version = inventreeVersionTuple(latest)  # pragma: no cover | ||||
|     inventree_version = inventreeVersionTuple()  # pragma: no cover | ||||
|  | ||||
|     return inventree_version >= latest_version | ||||
|     return inventree_version >= latest_version  # pragma: no cover | ||||
|  | ||||
|  | ||||
| def inventreeApiVersion(): | ||||
| @@ -209,7 +212,7 @@ def inventreeCommitHash(): | ||||
|  | ||||
|     try: | ||||
|         return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip() | ||||
|     except: | ||||
|     except:  # pragma: no cover | ||||
|         return None | ||||
|  | ||||
|  | ||||
| @@ -219,5 +222,5 @@ def inventreeCommitDate(): | ||||
|     try: | ||||
|         d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip() | ||||
|         return d.split(' ')[0] | ||||
|     except: | ||||
|     except:  # pragma: no cover | ||||
|         return None | ||||
|   | ||||
| @@ -7,10 +7,10 @@ For more information on this file, see | ||||
| 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 | ||||
|   | ||||
| @@ -47,7 +47,7 @@ class InvenTreeBarcodePlugin(BarcodePlugin): | ||||
|             except json.JSONDecodeError: | ||||
|                 return False | ||||
|         else: | ||||
|             return False | ||||
|             return False  # pragma: no cover | ||||
|  | ||||
|         # 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... | ||||
| @@ -70,10 +70,10 @@ class InvenTreeBarcodePlugin(BarcodePlugin): | ||||
|                 # Initially try casting to an integer | ||||
|                 try: | ||||
|                     pk = int(data) | ||||
|                 except (TypeError, ValueError): | ||||
|                 except (TypeError, ValueError):  # pragma: no cover | ||||
|                     pk = None | ||||
|  | ||||
|                 if pk is None: | ||||
|                 if pk is None:  # pragma: no cover | ||||
|                     try: | ||||
|                         pk = self.data[k]['id'] | ||||
|                     except (AttributeError, KeyError): | ||||
| @@ -82,7 +82,7 @@ class InvenTreeBarcodePlugin(BarcodePlugin): | ||||
|                 try: | ||||
|                     item = StockItem.objects.get(pk=pk) | ||||
|                     return item | ||||
|                 except (ValueError, StockItem.DoesNotExist): | ||||
|                 except (ValueError, StockItem.DoesNotExist):  # pragma: no cover | ||||
|                     raise ValidationError({k, "Stock item does not exist"}) | ||||
|  | ||||
|         return None | ||||
| @@ -97,10 +97,10 @@ class InvenTreeBarcodePlugin(BarcodePlugin): | ||||
|                 # First try simple integer lookup | ||||
|                 try: | ||||
|                     pk = int(self.data[k]) | ||||
|                 except (TypeError, ValueError): | ||||
|                 except (TypeError, ValueError):  # pragma: no cover | ||||
|                     pk = None | ||||
|  | ||||
|                 if pk is None: | ||||
|                 if pk is None:  # pragma: no cover | ||||
|                     # Lookup by 'id' field | ||||
|                     try: | ||||
|                         pk = self.data[k]['id'] | ||||
| @@ -110,7 +110,7 @@ class InvenTreeBarcodePlugin(BarcodePlugin): | ||||
|                 try: | ||||
|                     loc = StockLocation.objects.get(pk=pk) | ||||
|                     return loc | ||||
|                 except (ValueError, StockLocation.DoesNotExist): | ||||
|                 except (ValueError, StockLocation.DoesNotExist):  # pragma: no cover | ||||
|                     raise ValidationError({k, "Stock location does not exist"}) | ||||
|  | ||||
|         return None | ||||
| @@ -125,10 +125,10 @@ class InvenTreeBarcodePlugin(BarcodePlugin): | ||||
|                 # Try integer lookup first | ||||
|                 try: | ||||
|                     pk = int(self.data[k]) | ||||
|                 except (TypeError, ValueError): | ||||
|                 except (TypeError, ValueError):  # pragma: no cover | ||||
|                     pk = None | ||||
|  | ||||
|                 if pk is None: | ||||
|                 if pk is None:  # pragma: no cover | ||||
|                     try: | ||||
|                         pk = self.data[k]['id'] | ||||
|                     except (AttributeError, KeyError): | ||||
| @@ -137,7 +137,7 @@ class InvenTreeBarcodePlugin(BarcodePlugin): | ||||
|                 try: | ||||
|                     part = Part.objects.get(pk=pk) | ||||
|                     return part | ||||
|                 except (ValueError, Part.DoesNotExist): | ||||
|                 except (ValueError, Part.DoesNotExist):  # pragma: no cover | ||||
|                     raise ValidationError({k, 'Part does not exist'}) | ||||
|  | ||||
|         return None | ||||
|   | ||||
| @@ -38,8 +38,18 @@ class BarcodeAPITest(APITestCase): | ||||
|  | ||||
|     def test_invalid(self): | ||||
|  | ||||
|         # test scan url | ||||
|         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) | ||||
|  | ||||
|     def test_empty(self): | ||||
| @@ -204,3 +214,57 @@ class BarcodeAPITest(APITestCase): | ||||
|  | ||||
|         self.assertIn('error', 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) | ||||
|   | ||||
| @@ -12,7 +12,7 @@ class SettingsAdmin(ImportExportModelAdmin): | ||||
|  | ||||
|     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 | ||||
|         """ | ||||
| @@ -27,7 +27,7 @@ class UserSettingsAdmin(ImportExportModelAdmin): | ||||
|  | ||||
|     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 | ||||
|         """ | ||||
|   | ||||
| @@ -575,7 +575,7 @@ class BaseInvenTreeSetting(models.Model): | ||||
|         try: | ||||
|             value = int(self.value) | ||||
|         except (ValueError, TypeError): | ||||
|             value = self.default_value() | ||||
|             value = self.default_value | ||||
|  | ||||
|         return value | ||||
|  | ||||
|   | ||||
| @@ -18,12 +18,12 @@ def currency_code_default(): | ||||
|  | ||||
|     try: | ||||
|         code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') | ||||
|     except ProgrammingError: | ||||
|     except ProgrammingError:  # pragma: no cover | ||||
|         # database is not initialized yet | ||||
|         code = '' | ||||
|  | ||||
|     if code not in CURRENCIES: | ||||
|         code = 'USD' | ||||
|         code = 'USD'  # pragma: no cover | ||||
|  | ||||
|     return code | ||||
|  | ||||
|   | ||||
							
								
								
									
										18
									
								
								InvenTree/common/test_tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								InvenTree/common/test_tasks.py
									
									
									
									
									
										Normal 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',) | ||||
| @@ -6,7 +6,9 @@ from datetime import timedelta | ||||
|  | ||||
| from django.test import TestCase, Client | ||||
| 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 .api import WebhookView | ||||
|  | ||||
| @@ -46,6 +48,67 @@ class SettingsTest(TestCase): | ||||
|         # Check object lookup (case insensitive) | ||||
|         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): | ||||
|         """ | ||||
|         - 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 | ||||
|  | ||||
|  | ||||
| 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): | ||||
|     def setUp(self): | ||||
|         self.endpoint_def = WebhookEndpoint.objects.create() | ||||
| @@ -223,3 +294,26 @@ class NotificationTest(TestCase): | ||||
|         self.assertFalse(NotificationEntry.check_recent('test.notification2', 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')) | ||||
|   | ||||
| @@ -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'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -693,6 +693,7 @@ class SupplierPriceBreak(common.models.PriceBreak): | ||||
|  | ||||
|     Attributes: | ||||
|         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 | ||||
|         cost: Cost at specified quantity | ||||
|         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'),) | ||||
|  | ||||
|     updated = models.DateTimeField(auto_now=True, null=True, verbose_name=_('last updated')) | ||||
|  | ||||
|     class Meta: | ||||
|         unique_together = ("part", "quantity") | ||||
|  | ||||
|   | ||||
| @@ -278,4 +278,5 @@ class SupplierPriceBreakSerializer(InvenTreeModelSerializer): | ||||
|             'quantity', | ||||
|             'price', | ||||
|             'price_currency', | ||||
|             'updated', | ||||
|         ] | ||||
|   | ||||
| @@ -268,6 +268,14 @@ $('#price-break-table').inventreeTable({ | ||||
|                 return html; | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             field: 'updated', | ||||
|             title: '{% trans "Last updated" %}', | ||||
|             sortable: true, | ||||
|             formatter: function(value) { | ||||
|                 return renderDate(value); | ||||
|             } | ||||
|         }, | ||||
|     ] | ||||
| }); | ||||
|  | ||||
| @@ -349,4 +357,4 @@ $('#delete-part').click(function() { | ||||
|  | ||||
| enableSidebar('supplierpart'); | ||||
|  | ||||
| {% endblock %} | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -53,7 +53,7 @@ class InvenTreePluginBase(): | ||||
|         else: | ||||
|             return self.plugin_name() | ||||
|  | ||||
|     def plugin_config(self, raise_error=False): | ||||
|     def plugin_config(self): | ||||
|         """ | ||||
|         Return the PluginConfig object associated with this plugin | ||||
|         """ | ||||
| @@ -65,12 +65,9 @@ class InvenTreePluginBase(): | ||||
|                 key=self.plugin_slug(), | ||||
|                 name=self.plugin_name(), | ||||
|             ) | ||||
|         except (OperationalError, ProgrammingError) as error: | ||||
|         except (OperationalError, ProgrammingError): | ||||
|             cfg = None | ||||
|  | ||||
|             if raise_error: | ||||
|                 raise error | ||||
|  | ||||
|         return cfg | ||||
|  | ||||
|     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 | ||||
|     """ | ||||
|     def __init__(self): | ||||
|     def __init__(self):  # pragma: no cover | ||||
|         warnings.warn("Using the InvenTreePlugin is depreceated", DeprecationWarning) | ||||
|         super().__init__() | ||||
|   | ||||
| @@ -21,7 +21,7 @@ from django.utils.text import slugify | ||||
|  | ||||
| try: | ||||
|     from importlib import metadata | ||||
| except: | ||||
| except:  # pragma: no cover | ||||
|     import importlib_metadata as metadata | ||||
|     # TODO remove when python minimum is 3.8 | ||||
|  | ||||
| @@ -84,7 +84,7 @@ class PluginsRegistry: | ||||
|         """ | ||||
|         if not settings.PLUGINS_ENABLED: | ||||
|             # Plugins not enabled, do nothing | ||||
|             return | ||||
|             return  # pragma: no cover | ||||
|  | ||||
|         logger.info('Start loading plugins') | ||||
|  | ||||
| @@ -120,7 +120,7 @@ class PluginsRegistry: | ||||
|                 # We do not want to end in an endless loop | ||||
|                 retry_counter -= 1 | ||||
|  | ||||
|                 if retry_counter <= 0: | ||||
|                 if retry_counter <= 0:  # pragma: no cover | ||||
|                     if settings.PLUGIN_TESTING: | ||||
|                         print('[PLUGIN] Max retries, breaking loading') | ||||
|                     # TODO error for server status | ||||
| @@ -143,14 +143,14 @@ class PluginsRegistry: | ||||
|  | ||||
|         if not settings.PLUGINS_ENABLED: | ||||
|             # Plugins not enabled, do nothing | ||||
|             return | ||||
|             return  # pragma: no cover | ||||
|  | ||||
|         logger.info('Start unloading plugins') | ||||
|  | ||||
|         # Set maintanace mode | ||||
|         _maintenance = bool(get_maintenance_mode()) | ||||
|         if not _maintenance: | ||||
|             set_maintenance_mode(True) | ||||
|             set_maintenance_mode(True)  # pragma: no cover | ||||
|  | ||||
|         # remove all plugins from registry | ||||
|         self._clean_registry() | ||||
| @@ -160,7 +160,7 @@ class PluginsRegistry: | ||||
|  | ||||
|         # remove maintenance | ||||
|         if not _maintenance: | ||||
|             set_maintenance_mode(False) | ||||
|             set_maintenance_mode(False)  # pragma: no cover | ||||
|         logger.info('Finished unloading plugins') | ||||
|  | ||||
|     def reload_plugins(self): | ||||
| @@ -170,7 +170,7 @@ class PluginsRegistry: | ||||
|  | ||||
|         # Do not reload whe currently loading | ||||
|         if self.is_loading: | ||||
|             return | ||||
|             return  # pragma: no cover | ||||
|  | ||||
|         logger.info('Start reloading plugins') | ||||
|  | ||||
| @@ -187,7 +187,7 @@ class PluginsRegistry: | ||||
|  | ||||
|         if not settings.PLUGINS_ENABLED: | ||||
|             # Plugins not enabled, do nothing | ||||
|             return | ||||
|             return  # pragma: no cover | ||||
|  | ||||
|         self.plugin_modules = []  # clear | ||||
|  | ||||
| @@ -200,7 +200,7 @@ class PluginsRegistry: | ||||
|         # 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): | ||||
|             # 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: | ||||
|                     plugin = entry.load() | ||||
|                     plugin.is_package = True | ||||
| @@ -257,7 +257,7 @@ class PluginsRegistry: | ||||
|             except (OperationalError, ProgrammingError) as error: | ||||
|                 # Exception if the database has not been migrated yet - check if test are running - raise if not | ||||
|                 if not settings.PLUGIN_TESTING: | ||||
|                     raise error | ||||
|                     raise error  # pragma: no cover | ||||
|                 plugin_db_setting = None | ||||
|  | ||||
|             # Always activate if testing | ||||
| @@ -267,7 +267,7 @@ class PluginsRegistry: | ||||
|                     # option1: package, option2: file-based | ||||
|                     if (plugin.__name__ == disabled) or (plugin.__module__ == disabled): | ||||
|                         # 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 | ||||
|                             # TODO save the error to the plugin | ||||
|                             plugin_db_setting.save(no_reload=True) | ||||
| @@ -445,7 +445,7 @@ class PluginsRegistry: | ||||
|             try: | ||||
|                 app_name = plugin_path.split('.')[-1] | ||||
|                 app_config = apps.get_app_config(app_name) | ||||
|             except LookupError: | ||||
|             except LookupError:  # pragma: no cover | ||||
|                 # the plugin was never loaded correctly | ||||
|                 logger.debug(f'{app_name} App was not found during deregistering') | ||||
|                 break | ||||
| @@ -499,7 +499,7 @@ class PluginsRegistry: | ||||
|                     # remove model from admin site | ||||
|                     admin.site.unregister(model) | ||||
|                     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 | ||||
|                 logger.debug(f'{app_name} App was not found during deregistering') | ||||
|                 break | ||||
| @@ -572,7 +572,7 @@ class PluginsRegistry: | ||||
|         try: | ||||
|             cmd(*args, **kwargs) | ||||
|             return True, [] | ||||
|         except Exception as error: | ||||
|         except Exception as error:  # pragma: no cover | ||||
|             handle_error(error) | ||||
|     # endregion | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,2 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| from __future__ import unicode_literals  # pragma: no cover | ||||
|   | ||||
| @@ -222,6 +222,7 @@ | ||||
|     lft: 0 | ||||
|     rght: 0 | ||||
|     expiry_date: "1990-10-10" | ||||
|     uid: 9e5ae7fc20568ed4814c10967bba8b65 | ||||
|  | ||||
| - model: stock.stockitem | ||||
|   pk: 521 | ||||
| @@ -235,6 +236,7 @@ | ||||
|     lft: 0 | ||||
|     rght: 0 | ||||
|     status: 60 | ||||
|     uid: 1be0dfa925825c5c6c79301449e50c2d | ||||
|  | ||||
| - model: stock.stockitem | ||||
|   pk: 522 | ||||
| @@ -248,4 +250,4 @@ | ||||
|     lft: 0 | ||||
|     rght: 0 | ||||
|     expiry_date: "1990-10-10" | ||||
|     status: 70 | ||||
|     status: 70 | ||||
|   | ||||
| @@ -49,7 +49,7 @@ class InvenTreeGroupAdminForm(forms.ModelForm): | ||||
|             'users', | ||||
|         ] | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|     def __init__(self, *args, **kwargs):  # pragma: no cover | ||||
|         super().__init__(*args, **kwargs) | ||||
|  | ||||
|         if self.instance.pk: | ||||
| @@ -65,12 +65,12 @@ class InvenTreeGroupAdminForm(forms.ModelForm): | ||||
|         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. | ||||
|  | ||||
|         self.instance.user_set.set(self.cleaned_data['users']) | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|     def save(self, *args, **kwargs):  # pragma: no cover | ||||
|         # Default save | ||||
|         instance = super().save() | ||||
|         # Save many-to-many data | ||||
| @@ -78,7 +78,7 @@ class InvenTreeGroupAdminForm(forms.ModelForm): | ||||
|         return instance | ||||
|  | ||||
|  | ||||
| class RoleGroupAdmin(admin.ModelAdmin): | ||||
| class RoleGroupAdmin(admin.ModelAdmin):  # pragma: no cover | ||||
|     """ | ||||
|     Custom admin interface for the Group model | ||||
|     """ | ||||
|   | ||||
| @@ -100,7 +100,7 @@ class RoleDetails(APIView): | ||||
|             if len(permissions) > 0: | ||||
|                 roles[role] = permissions | ||||
|             else: | ||||
|                 roles[role] = None | ||||
|                 roles[role] = None  # pragma: no cover | ||||
|  | ||||
|         data = { | ||||
|             'user': user.pk, | ||||
|   | ||||
| @@ -266,9 +266,9 @@ class RuleSet(models.Model): | ||||
|             model=model | ||||
|         ) | ||||
|  | ||||
|     def __str__(self, debug=False): | ||||
|     def __str__(self, debug=False):  # pragma: no cover | ||||
|         """ Ruleset string representation """ | ||||
|         if debug:  # pragma: no cover | ||||
|         if debug: | ||||
|             # Makes debugging easier | ||||
|             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)} | ' \ | ||||
| @@ -318,7 +318,7 @@ def split_permission(app, perm): | ||||
|     """split permission string into permission and model""" | ||||
|     permission_name, *model = perm.split('_') | ||||
|     # handle models that have underscores | ||||
|     if len(model) > 1: | ||||
|     if len(model) > 1:  # pragma: no cover | ||||
|         app += '_' + '_'.join(model[:-1]) | ||||
|         perm = permission_name + '_' + model[-1:][0] | ||||
|     model = model[-1:][0] | ||||
| @@ -374,7 +374,7 @@ def update_group_roles(group, debug=False): | ||||
|             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)) | ||||
|  | ||||
|         permission_string = RuleSet.get_model_permission_string(model, action) | ||||
| @@ -575,7 +575,7 @@ class Owner(models.Model): | ||||
|         return owners | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_api_url(): | ||||
|     def get_api_url():  # pragma: no cover | ||||
|         return reverse('api-owner-list') | ||||
|  | ||||
|     class Meta: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user