mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	Setting caching (#3178)
* Revert "Remove stat context variables" This reverts commit0989c308d0. * Add a caching framework for inventree settings - Actions that use "settings" require a DB hit every time - For example the part.full_name() method looks at the PART_NAME_FORMAT setting - This means 1 DB hit for every part which is serialized!! * Fixes for DebugToolbar integration - Requires different INTERNAL_IPS when running behind docker - Some issues with TEMPLATES framework * Revert "Revert "Remove stat context variables"" This reverts commit52e6359265. * Add unit tests for settings caching * Update existing unit tests to handle cache framework * Fix for unit test * Re-enable cache for default part values * Clear cache for further unit tests
This commit is contained in:
		| @@ -18,7 +18,6 @@ import sys | |||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  |  | ||||||
| import django.conf.locale | import django.conf.locale | ||||||
| from django.contrib.messages import constants as messages |  | ||||||
| from django.core.files.storage import default_storage | from django.core.files.storage import default_storage | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
| @@ -302,12 +301,24 @@ AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [ | |||||||
|     'allauth.account.auth_backends.AuthenticationBackend',      # SSO login via external providers |     'allauth.account.auth_backends.AuthenticationBackend',      # SSO login via external providers | ||||||
| ]) | ]) | ||||||
|  |  | ||||||
|  | DEBUG_TOOLBAR_ENABLED = DEBUG and CONFIG.get('debug_toolbar', False) | ||||||
|  |  | ||||||
| # If the debug toolbar is enabled, add the modules | # If the debug toolbar is enabled, add the modules | ||||||
| if DEBUG and CONFIG.get('debug_toolbar', False):  # pragma: no cover | if DEBUG_TOOLBAR_ENABLED:  # pragma: no cover | ||||||
|     logger.info("Running with DEBUG_TOOLBAR enabled") |     logger.info("Running with DEBUG_TOOLBAR enabled") | ||||||
|     INSTALLED_APPS.append('debug_toolbar') |     INSTALLED_APPS.append('debug_toolbar') | ||||||
|     MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') |     MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') | ||||||
|  |  | ||||||
|  | # Internal IP addresses allowed to see the debug toolbar | ||||||
|  | INTERNAL_IPS = [ | ||||||
|  |     '127.0.0.1', | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | if DOCKER: | ||||||
|  |     # Internal IP addresses are different when running under docker | ||||||
|  |     hostname, ___, ips = socket.gethostbyname_ex(socket.gethostname()) | ||||||
|  |     INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + ["127.0.0.1", "10.0.2.2"] | ||||||
|  |  | ||||||
| # Allow secure http developer server in debug mode | # Allow secure http developer server in debug mode | ||||||
| if DEBUG: | if DEBUG: | ||||||
|     INSTALLED_APPS.append('sslserver') |     INSTALLED_APPS.append('sslserver') | ||||||
| @@ -354,6 +365,12 @@ TEMPLATES = [ | |||||||
|     }, |     }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | if DEBUG_TOOLBAR_ENABLED: | ||||||
|  |     # Note that the APP_DIRS value must be set when using debug_toolbar | ||||||
|  |     # But this will kill template loading for plugins | ||||||
|  |     TEMPLATES[0]['APP_DIRS'] = True | ||||||
|  |     del TEMPLATES[0]['OPTIONS']['loaders'] | ||||||
|  |  | ||||||
| REST_FRAMEWORK = { | REST_FRAMEWORK = { | ||||||
|     'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler', |     'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler', | ||||||
|     'DATETIME_FORMAT': '%Y-%m-%d %H:%M', |     'DATETIME_FORMAT': '%Y-%m-%d %H:%M', | ||||||
| @@ -810,17 +827,6 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4' | |||||||
| # Use database transactions when importing / exporting data | # Use database transactions when importing / exporting data | ||||||
| IMPORT_EXPORT_USE_TRANSACTIONS = True | IMPORT_EXPORT_USE_TRANSACTIONS = True | ||||||
|  |  | ||||||
| # Internal IP addresses allowed to see the debug toolbar |  | ||||||
| INTERNAL_IPS = [ |  | ||||||
|     '127.0.0.1', |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| MESSAGE_TAGS = { |  | ||||||
|     messages.SUCCESS: 'alert alert-block alert-success', |  | ||||||
|     messages.ERROR: 'alert alert-block alert-danger', |  | ||||||
|     messages.INFO: 'alert alert-block alert-info', |  | ||||||
| } |  | ||||||
|  |  | ||||||
| SITE_ID = 1 | SITE_ID = 1 | ||||||
|  |  | ||||||
| # Load the allauth social backends | # Load the allauth social backends | ||||||
|   | |||||||
| @@ -188,10 +188,10 @@ if settings.DEBUG: | |||||||
|     urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) |     urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) | ||||||
|  |  | ||||||
|     # Debug toolbar access (only allowed in DEBUG mode) |     # Debug toolbar access (only allowed in DEBUG mode) | ||||||
|     if 'debug_toolbar' in settings.INSTALLED_APPS:  # pragma: no cover |     if settings.DEBUG_TOOLBAR_ENABLED: | ||||||
|         import debug_toolbar |         import debug_toolbar | ||||||
|         urlpatterns = [ |         urlpatterns = [ | ||||||
|             path('__debug/', include(debug_toolbar.urls)), |             path('__debug__/', include(debug_toolbar.urls)), | ||||||
|         ] + urlpatterns |         ] + urlpatterns | ||||||
|  |  | ||||||
| # Send any unknown URLs to the parts page | # Send any unknown URLs to the parts page | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ class CommonConfig(AppConfig): | |||||||
|         try: |         try: | ||||||
|             import common.models |             import common.models | ||||||
|  |  | ||||||
|             if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED', backup_value=False, create=False): |             if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED', backup_value=False, create=False, cache=False): | ||||||
|                 logger.info("Clearing SERVER_RESTART_REQUIRED flag") |                 logger.info("Clearing SERVER_RESTART_REQUIRED flag") | ||||||
|                 common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None) |                 common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None) | ||||||
|         except Exception: |         except Exception: | ||||||
|   | |||||||
| @@ -21,7 +21,8 @@ from django.contrib.auth.models import Group, User | |||||||
| from django.contrib.contenttypes.fields import GenericForeignKey | from django.contrib.contenttypes.fields import GenericForeignKey | ||||||
| from django.contrib.contenttypes.models import ContentType | from django.contrib.contenttypes.models import ContentType | ||||||
| from django.contrib.humanize.templatetags.humanize import naturaltime | from django.contrib.humanize.templatetags.humanize import naturaltime | ||||||
| from django.core.exceptions import ValidationError | from django.core.cache import cache | ||||||
|  | from django.core.exceptions import AppRegistryNotReady, ValidationError | ||||||
| from django.core.validators import MinValueValidator, URLValidator | from django.core.validators import MinValueValidator, URLValidator | ||||||
| from django.db import models, transaction | from django.db import models, transaction | ||||||
| from django.db.utils import IntegrityError, OperationalError | from django.db.utils import IntegrityError, OperationalError | ||||||
| @@ -69,11 +70,56 @@ class BaseInvenTreeSetting(models.Model): | |||||||
|         """Enforce validation and clean before saving.""" |         """Enforce validation and clean before saving.""" | ||||||
|         self.key = str(self.key).upper() |         self.key = str(self.key).upper() | ||||||
|  |  | ||||||
|  |         do_cache = kwargs.pop('cache', True) | ||||||
|  |  | ||||||
|         self.clean(**kwargs) |         self.clean(**kwargs) | ||||||
|         self.validate_unique(**kwargs) |         self.validate_unique(**kwargs) | ||||||
|  |  | ||||||
|  |         # Update this setting in the cache | ||||||
|  |         if do_cache: | ||||||
|  |             self.save_to_cache() | ||||||
|  |  | ||||||
|         super().save() |         super().save() | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def cache_key(self): | ||||||
|  |         """Generate a unique cache key for this settings object""" | ||||||
|  |         return self.__class__.create_cache_key(self.key, **self.get_kwargs()) | ||||||
|  |  | ||||||
|  |     def save_to_cache(self): | ||||||
|  |         """Save this setting object to cache""" | ||||||
|  |  | ||||||
|  |         ckey = self.cache_key | ||||||
|  |  | ||||||
|  |         logger.debug(f"Saving setting '{ckey}' to cache") | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             cache.set( | ||||||
|  |                 ckey, | ||||||
|  |                 self, | ||||||
|  |                 timeout=3600 | ||||||
|  |             ) | ||||||
|  |         except TypeError: | ||||||
|  |             # Some characters cause issues with caching; ignore and move on | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def create_cache_key(cls, setting_key, **kwargs): | ||||||
|  |         """Create a unique cache key for a particular setting object. | ||||||
|  |  | ||||||
|  |         The cache key uses the following elements to ensure the key is 'unique': | ||||||
|  |         - The name of the class | ||||||
|  |         - The unique KEY string | ||||||
|  |         - Any key:value kwargs associated with the particular setting type (e.g. user-id) | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         key = f"{str(cls.__name__)}:{setting_key}" | ||||||
|  |  | ||||||
|  |         for k, v in kwargs.items(): | ||||||
|  |             key += f"_{k}:{v}" | ||||||
|  |  | ||||||
|  |         return key | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def allValues(cls, user=None, exclude_hidden=False): |     def allValues(cls, user=None, exclude_hidden=False): | ||||||
|         """Return a dict of "all" defined global settings. |         """Return a dict of "all" defined global settings. | ||||||
| @@ -220,11 +266,12 @@ class BaseInvenTreeSetting(models.Model): | |||||||
|  |  | ||||||
|         - Key is case-insensitive |         - Key is case-insensitive | ||||||
|         - Returns None if no match is made |         - Returns None if no match is made | ||||||
|  |  | ||||||
|  |         First checks the cache to see if this object has recently been accessed, | ||||||
|  |         and returns the cached version if so. | ||||||
|         """ |         """ | ||||||
|         key = str(key).strip().upper() |         key = str(key).strip().upper() | ||||||
|  |  | ||||||
|         settings = cls.objects.all() |  | ||||||
|  |  | ||||||
|         filters = { |         filters = { | ||||||
|             'key__iexact': key, |             'key__iexact': key, | ||||||
|         } |         } | ||||||
| @@ -253,7 +300,25 @@ class BaseInvenTreeSetting(models.Model): | |||||||
|         if method is not None: |         if method is not None: | ||||||
|             filters['method'] = method |             filters['method'] = method | ||||||
|  |  | ||||||
|  |         # Perform cache lookup by default | ||||||
|  |         do_cache = kwargs.pop('cache', True) | ||||||
|  |  | ||||||
|  |         ckey = cls.create_cache_key(key, **kwargs) | ||||||
|  |  | ||||||
|  |         if do_cache: | ||||||
|             try: |             try: | ||||||
|  |                 # First attempt to find the setting object in the cache | ||||||
|  |                 cached_setting = cache.get(ckey) | ||||||
|  |  | ||||||
|  |                 if cached_setting is not None: | ||||||
|  |                     return cached_setting | ||||||
|  |  | ||||||
|  |             except AppRegistryNotReady: | ||||||
|  |                 # Cache is not ready yet | ||||||
|  |                 do_cache = False | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             settings = cls.objects.all() | ||||||
|             setting = settings.filter(**filters).first() |             setting = settings.filter(**filters).first() | ||||||
|         except (ValueError, cls.DoesNotExist): |         except (ValueError, cls.DoesNotExist): | ||||||
|             setting = None |             setting = None | ||||||
| @@ -282,6 +347,10 @@ class BaseInvenTreeSetting(models.Model): | |||||||
|                     # It might be the case that the database isn't created yet |                     # It might be the case that the database isn't created yet | ||||||
|                     pass |                     pass | ||||||
|  |  | ||||||
|  |         if setting and do_cache: | ||||||
|  |             # Cache this setting object | ||||||
|  |             setting.save_to_cache() | ||||||
|  |  | ||||||
|         return setting |         return setting | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
| @@ -1507,11 +1576,6 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): | |||||||
|         help_text=_('User'), |         help_text=_('User'), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def get_setting_object(cls, key, user=None): |  | ||||||
|         """Return setting object for provided user.""" |  | ||||||
|         return super().get_setting_object(key, user=user) |  | ||||||
|  |  | ||||||
|     def validate_unique(self, exclude=None, **kwargs): |     def validate_unique(self, exclude=None, **kwargs): | ||||||
|         """Return if the setting (including key) is unique.""" |         """Return if the setting (including key) is unique.""" | ||||||
|         return super().validate_unique(exclude=exclude, user=self.user) |         return super().validate_unique(exclude=exclude, user=self.user) | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ def currency_code_default(): | |||||||
|     from common.models import InvenTreeSetting |     from common.models import InvenTreeSetting | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', create=False) |         code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', create=False, cache=False) | ||||||
|     except ProgrammingError:  # pragma: no cover |     except ProgrammingError:  # pragma: no cover | ||||||
|         # database is not initialized yet |         # database is not initialized yet | ||||||
|         code = '' |         code = '' | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ import json | |||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
| from http import HTTPStatus | from http import HTTPStatus | ||||||
|  |  | ||||||
|  | from django.contrib.auth import get_user_model | ||||||
|  | from django.core.cache import cache | ||||||
| from django.test import Client, TestCase | from django.test import Client, TestCase | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| @@ -45,10 +47,10 @@ class SettingsTest(InvenTreeTestCase): | |||||||
|         """Test settings functions and properties.""" |         """Test settings functions and properties.""" | ||||||
|         # define settings to check |         # define settings to check | ||||||
|         instance_ref = 'INVENTREE_INSTANCE' |         instance_ref = 'INVENTREE_INSTANCE' | ||||||
|         instance_obj = InvenTreeSetting.get_setting_object(instance_ref) |         instance_obj = InvenTreeSetting.get_setting_object(instance_ref, cache=False) | ||||||
|  |  | ||||||
|         stale_ref = 'STOCK_STALE_DAYS' |         stale_ref = 'STOCK_STALE_DAYS' | ||||||
|         stale_days = InvenTreeSetting.get_setting_object(stale_ref) |         stale_days = InvenTreeSetting.get_setting_object(stale_ref, cache=False) | ||||||
|  |  | ||||||
|         report_size_obj = InvenTreeSetting.get_setting_object('REPORT_DEFAULT_PAGE_SIZE') |         report_size_obj = InvenTreeSetting.get_setting_object('REPORT_DEFAULT_PAGE_SIZE') | ||||||
|         report_test_obj = InvenTreeSetting.get_setting_object('REPORT_ENABLE_TEST_REPORT') |         report_test_obj = InvenTreeSetting.get_setting_object('REPORT_ENABLE_TEST_REPORT') | ||||||
| @@ -189,6 +191,56 @@ class SettingsTest(InvenTreeTestCase): | |||||||
|                 if setting.default_value not in [True, False]: |                 if setting.default_value not in [True, False]: | ||||||
|                     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 | ||||||
|  |  | ||||||
|  |     def test_global_setting_caching(self): | ||||||
|  |         """Test caching operations for the global settings class""" | ||||||
|  |  | ||||||
|  |         key = 'PART_NAME_FORMAT' | ||||||
|  |  | ||||||
|  |         cache_key = InvenTreeSetting.create_cache_key(key) | ||||||
|  |         self.assertEqual(cache_key, 'InvenTreeSetting:PART_NAME_FORMAT') | ||||||
|  |  | ||||||
|  |         cache.clear() | ||||||
|  |  | ||||||
|  |         self.assertIsNone(cache.get(cache_key)) | ||||||
|  |  | ||||||
|  |         # First request should set cache | ||||||
|  |         val = InvenTreeSetting.get_setting(key) | ||||||
|  |         self.assertEqual(cache.get(cache_key).value, val) | ||||||
|  |  | ||||||
|  |         for val in ['A', '{{ part.IPN }}', 'C']: | ||||||
|  |             # Check that the cached value is updated whenever the setting is saved | ||||||
|  |             InvenTreeSetting.set_setting(key, val, None) | ||||||
|  |             self.assertEqual(cache.get(cache_key).value, val) | ||||||
|  |             self.assertEqual(InvenTreeSetting.get_setting(key), val) | ||||||
|  |  | ||||||
|  |     def test_user_setting_caching(self): | ||||||
|  |         """Test caching operation for the user settings class""" | ||||||
|  |  | ||||||
|  |         cache.clear() | ||||||
|  |  | ||||||
|  |         # Generate a number of new usesr | ||||||
|  |         for idx in range(5): | ||||||
|  |             get_user_model().objects.create( | ||||||
|  |                 username=f"User_{idx}", | ||||||
|  |                 password="hunter42", | ||||||
|  |                 email="email@dot.com", | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         key = 'SEARCH_PREVIEW_RESULTS' | ||||||
|  |  | ||||||
|  |         # Check that the settings are correctly cached for each separate user | ||||||
|  |         for user in get_user_model().objects.all(): | ||||||
|  |             setting = InvenTreeUserSetting.get_setting_object(key, user=user) | ||||||
|  |             cache_key = setting.cache_key | ||||||
|  |             self.assertEqual(cache_key, f"InvenTreeUserSetting:SEARCH_PREVIEW_RESULTS_user:{user.username}") | ||||||
|  |             InvenTreeUserSetting.set_setting(key, user.pk, None, user=user) | ||||||
|  |             self.assertIsNotNone(cache.get(cache_key)) | ||||||
|  |  | ||||||
|  |         # Iterate through a second time, ensure the values have been cached correctly | ||||||
|  |         for user in get_user_model().objects.all(): | ||||||
|  |             value = InvenTreeUserSetting.get_setting(key, user=user) | ||||||
|  |             self.assertEqual(value, user.pk) | ||||||
|  |  | ||||||
|  |  | ||||||
| class GlobalSettingsApiTest(InvenTreeAPITestCase): | class GlobalSettingsApiTest(InvenTreeAPITestCase): | ||||||
|     """Tests for the global settings API.""" |     """Tests for the global settings API.""" | ||||||
| @@ -199,7 +251,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase): | |||||||
|  |  | ||||||
|         # Read out each of the global settings value, to ensure they are instantiated in the database |         # Read out each of the global settings value, to ensure they are instantiated in the database | ||||||
|         for key in InvenTreeSetting.SETTINGS: |         for key in InvenTreeSetting.SETTINGS: | ||||||
|             InvenTreeSetting.get_setting_object(key) |             InvenTreeSetting.get_setting_object(key, cache=False) | ||||||
|  |  | ||||||
|         response = self.get(url, expected_code=200) |         response = self.get(url, expected_code=200) | ||||||
|  |  | ||||||
| @@ -422,7 +474,8 @@ class UserSettingsApiTest(InvenTreeAPITestCase): | |||||||
|         """Test a integer user setting value.""" |         """Test a integer user setting value.""" | ||||||
|         setting = InvenTreeUserSetting.get_setting_object( |         setting = InvenTreeUserSetting.get_setting_object( | ||||||
|             'SEARCH_PREVIEW_RESULTS', |             'SEARCH_PREVIEW_RESULTS', | ||||||
|             user=self.user |             user=self.user, | ||||||
|  |             cache=False, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         url = reverse('api-user-setting-detail', kwargs={'key': setting.key}) |         url = reverse('api-user-setting-detail', kwargs={'key': setting.key}) | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| import os | import os | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from django.core.cache import cache | ||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| @@ -394,6 +395,9 @@ class PartSettingsTest(InvenTreeTestCase): | |||||||
|  |  | ||||||
|     def make_part(self): |     def make_part(self): | ||||||
|         """Helper function to create a simple part.""" |         """Helper function to create a simple part.""" | ||||||
|  |  | ||||||
|  |         cache.clear() | ||||||
|  |  | ||||||
|         part = Part.objects.create( |         part = Part.objects.create( | ||||||
|             name='Test Part', |             name='Test Part', | ||||||
|             description='I am but a humble test part', |             description='I am but a humble test part', | ||||||
| @@ -404,6 +408,9 @@ class PartSettingsTest(InvenTreeTestCase): | |||||||
|  |  | ||||||
|     def test_defaults(self): |     def test_defaults(self): | ||||||
|         """Test that the default values for the part settings are correct.""" |         """Test that the default values for the part settings are correct.""" | ||||||
|  |  | ||||||
|  |         cache.clear() | ||||||
|  |  | ||||||
|         self.assertTrue(part.settings.part_component_default()) |         self.assertTrue(part.settings.part_component_default()) | ||||||
|         self.assertTrue(part.settings.part_purchaseable_default()) |         self.assertTrue(part.settings.part_purchaseable_default()) | ||||||
|         self.assertFalse(part.settings.part_salable_default()) |         self.assertFalse(part.settings.part_salable_default()) | ||||||
| @@ -411,6 +418,9 @@ class PartSettingsTest(InvenTreeTestCase): | |||||||
|  |  | ||||||
|     def test_initial(self): |     def test_initial(self): | ||||||
|         """Test the 'initial' default values (no default values have been set)""" |         """Test the 'initial' default values (no default values have been set)""" | ||||||
|  |  | ||||||
|  |         cache.clear() | ||||||
|  |  | ||||||
|         part = self.make_part() |         part = self.make_part() | ||||||
|  |  | ||||||
|         self.assertTrue(part.component) |         self.assertTrue(part.component) | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ class PluginAppConfig(AppConfig): | |||||||
|                     # this is the first startup |                     # this is the first startup | ||||||
|                     try: |                     try: | ||||||
|                         from common.models import InvenTreeSetting |                         from common.models import InvenTreeSetting | ||||||
|                         if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False): |                         if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False, cache=False): | ||||||
|                             # make sure all plugins are installed |                             # make sure all plugins are installed | ||||||
|                             registry.install_plugin_file() |                             registry.install_plugin_file() | ||||||
|                     except Exception:  # pragma: no cover |                     except Exception:  # pragma: no cover | ||||||
|   | |||||||
| @@ -204,7 +204,7 @@ class BuildReportTest(ReportTest): | |||||||
|         self.assertEqual(headers['Content-Disposition'], 'attachment; filename="report.pdf"') |         self.assertEqual(headers['Content-Disposition'], 'attachment; filename="report.pdf"') | ||||||
|  |  | ||||||
|         # Now, set the download type to be "inline" |         # Now, set the download type to be "inline" | ||||||
|         inline = InvenTreeUserSetting.get_setting_object('REPORT_INLINE', self.user) |         inline = InvenTreeUserSetting.get_setting_object('REPORT_INLINE', user=self.user) | ||||||
|         inline.value = True |         inline.value = True | ||||||
|         inline.save() |         inline.save() | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user