mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +00:00 
			
		
		
		
	Merge branch 'inventree:master' into fix-set_user
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}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ from . import helpers
 | 
			
		||||
from . import version
 | 
			
		||||
from . import status
 | 
			
		||||
from . import ready
 | 
			
		||||
from . import config
 | 
			
		||||
 | 
			
		||||
from decimal import Decimal
 | 
			
		||||
 | 
			
		||||
@@ -25,6 +26,7 @@ import InvenTree.tasks
 | 
			
		||||
 | 
			
		||||
from stock.models import StockLocation
 | 
			
		||||
from common.settings import currency_codes
 | 
			
		||||
from common.models import InvenTreeSetting
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ValidatorTest(TestCase):
 | 
			
		||||
@@ -462,3 +464,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
 | 
			
		||||
        """
 | 
			
		||||
 
 | 
			
		||||
@@ -570,7 +570,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);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    ]
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -265,9 +265,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)} | ' \
 | 
			
		||||
@@ -317,7 +317,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]
 | 
			
		||||
@@ -373,7 +373,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)
 | 
			
		||||
@@ -574,7 +574,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