mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-13 10:33:07 +00:00
Setting caching (#3178)
* Revert "Remove stat context variables" This reverts commit 0989c308d0cea9b9405a1338d257b542c6d33d73. * 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 commit 52e6359265226126da7ed6ed2aed2b83aa33de17. * 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:
parent
90aa7b8444
commit
6eddcd3c23
@ -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:
|
||||||
|
# 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:
|
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()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user