mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
Refactor: Move settings definitions (and most validators) out of models.py (#8646)
* move out settings definitions * fix import paths * move validators into appropiate settings * fix refactor error * fix import paths * move types out * add validator tests * add option to reload internal issues * fix tests
This commit is contained in:
parent
dd83735710
commit
7bfd36f7cb
@ -1,15 +1,13 @@
|
|||||||
"""Custom field validators for InvenTree."""
|
"""Custom field validators for InvenTree."""
|
||||||
|
|
||||||
import re
|
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import pint
|
import pint
|
||||||
from jinja2 import Template
|
|
||||||
from moneyed import CURRENCIES
|
from moneyed import CURRENCIES
|
||||||
|
|
||||||
import InvenTree.conversion
|
import InvenTree.conversion
|
||||||
@ -137,41 +135,3 @@ def validate_overage(value):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
raise ValidationError(_('Invalid value for overage'))
|
raise ValidationError(_('Invalid value for overage'))
|
||||||
|
|
||||||
|
|
||||||
def validate_part_name_format(value):
|
|
||||||
"""Validate part name format.
|
|
||||||
|
|
||||||
Make sure that each template container has a field of Part Model
|
|
||||||
"""
|
|
||||||
# Make sure that the field_name exists in Part model
|
|
||||||
from part.models import Part
|
|
||||||
|
|
||||||
jinja_template_regex = re.compile('{{.*?}}')
|
|
||||||
field_name_regex = re.compile('(?<=part\\.)[A-z]+')
|
|
||||||
|
|
||||||
for jinja_template in jinja_template_regex.findall(str(value)):
|
|
||||||
# make sure at least one and only one field is present inside the parser
|
|
||||||
field_names = field_name_regex.findall(jinja_template)
|
|
||||||
if len(field_names) < 1:
|
|
||||||
raise ValidationError({
|
|
||||||
'value': 'At least one field must be present inside a jinja template container i.e {{}}'
|
|
||||||
})
|
|
||||||
|
|
||||||
for field_name in field_names:
|
|
||||||
try:
|
|
||||||
Part._meta.get_field(field_name)
|
|
||||||
except FieldDoesNotExist:
|
|
||||||
raise ValidationError({
|
|
||||||
'value': f'{field_name} does not exist in Part Model'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Attempt to render the template with a dummy Part instance
|
|
||||||
p = Part(name='test part', description='some test part')
|
|
||||||
|
|
||||||
try:
|
|
||||||
Template(value).render({'part': p})
|
|
||||||
except Exception as exc:
|
|
||||||
raise ValidationError({'value': str(exc)})
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
0
src/backend/InvenTree/common/setting/__init__.py
Normal file
0
src/backend/InvenTree/common/setting/__init__.py
Normal file
1079
src/backend/InvenTree/common/setting/system.py
Normal file
1079
src/backend/InvenTree/common/setting/system.py
Normal file
File diff suppressed because it is too large
Load Diff
50
src/backend/InvenTree/common/setting/tests.py
Normal file
50
src/backend/InvenTree/common/setting/tests.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"""Tests for the various validators in the settings."""
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
import common.setting.system
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsValidatorTests(TestCase):
|
||||||
|
"""Tests settings validators."""
|
||||||
|
|
||||||
|
def test_validate_part_name_format(self):
|
||||||
|
"""Test error cases for validate_part_name_format."""
|
||||||
|
# No field
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
common.setting.system.validate_part_name_format('abc{{}}')
|
||||||
|
self.assertEqual(
|
||||||
|
err.exception.messages[0],
|
||||||
|
'At least one field must be present inside a jinja template container i.e {{}}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wrong field name
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
common.setting.system.validate_part_name_format('{{part.wrong}}')
|
||||||
|
self.assertEqual(
|
||||||
|
err.exception.messages[0], 'wrong does not exist in Part Model'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Broken templates
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
common.setting.system.validate_part_name_format('{{')
|
||||||
|
self.assertEqual(err.exception.messages[0], "unexpected 'end of template'")
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError) as err:
|
||||||
|
common.setting.system.validate_part_name_format(None)
|
||||||
|
self.assertEqual(err.exception.messages[0], "Can't compile non template nodes")
|
||||||
|
|
||||||
|
# Correct template
|
||||||
|
self.assertTrue(
|
||||||
|
common.setting.system.validate_part_name_format('{{part.name}}'),
|
||||||
|
'test part',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_instance_name_no_multi(self):
|
||||||
|
"""Test valid cases for update_instance_name."""
|
||||||
|
self.assertIsNone(common.setting.system.update_instance_name('abc'))
|
||||||
|
|
||||||
|
def test_update_instance_url_no_multi(self):
|
||||||
|
"""Test update_instance_url."""
|
||||||
|
self.assertIsNone(common.setting.system.update_instance_url('abc.com'))
|
57
src/backend/InvenTree/common/setting/type.py
Normal file
57
src/backend/InvenTree/common/setting/type.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""Types for settings."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import Any, Callable, TypedDict, Union
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
from typing import NotRequired # pragma: no cover
|
||||||
|
else:
|
||||||
|
|
||||||
|
class NotRequired: # pragma: no cover
|
||||||
|
"""NotRequired type helper is only supported with Python 3.11+."""
|
||||||
|
|
||||||
|
def __class_getitem__(cls, item):
|
||||||
|
"""Return the item."""
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsKeyType(TypedDict, total=False):
|
||||||
|
"""Type definitions for a SettingsKeyType.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: Translatable string name of the setting (required)
|
||||||
|
description: Translatable string description of the setting (required)
|
||||||
|
units: Units of the particular setting (optional)
|
||||||
|
validator: Validation function/list of functions for the setting (optional, default: None, e.g: bool, int, str, MinValueValidator, ...)
|
||||||
|
default: Default value or function that returns default value (optional)
|
||||||
|
choices: Function that returns or value of list[tuple[str: key, str: display value]] (optional)
|
||||||
|
hidden: Hide this setting from settings page (optional)
|
||||||
|
before_save: Function that gets called after save with *args, **kwargs (optional)
|
||||||
|
after_save: Function that gets called after save with *args, **kwargs (optional)
|
||||||
|
protected: Protected values are not returned to the client, instead "***" is returned (optional, default: False)
|
||||||
|
required: Is this setting required to work, can be used in combination with .check_all_settings(...) (optional, default: False)
|
||||||
|
model: Auto create a dropdown menu to select an associated model instance (e.g. 'company.company', 'auth.user' and 'auth.group' are possible too, optional)
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
units: str
|
||||||
|
validator: Union[Callable, list[Callable], tuple[Callable]]
|
||||||
|
default: Union[Callable, Any]
|
||||||
|
choices: Union[list[tuple[str, str]], Callable[[], list[tuple[str, str]]]]
|
||||||
|
hidden: bool
|
||||||
|
before_save: Callable[..., None]
|
||||||
|
after_save: Callable[..., None]
|
||||||
|
protected: bool
|
||||||
|
required: bool
|
||||||
|
model: str
|
||||||
|
|
||||||
|
|
||||||
|
class InvenTreeSettingsKeyType(SettingsKeyType):
|
||||||
|
"""InvenTreeSettingsKeyType has additional properties only global settings support.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
requires_restart: If True, a server restart is required after changing the setting
|
||||||
|
"""
|
||||||
|
|
||||||
|
requires_restart: NotRequired[bool]
|
339
src/backend/InvenTree/common/setting/user.py
Normal file
339
src/backend/InvenTree/common/setting/user.py
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
"""User settings definition."""
|
||||||
|
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from common.setting.type import InvenTreeSettingsKeyType
|
||||||
|
from plugin import registry
|
||||||
|
|
||||||
|
|
||||||
|
def label_printer_options():
|
||||||
|
"""Build a list of available label printer options."""
|
||||||
|
printers = []
|
||||||
|
label_printer_plugins = registry.with_mixin('labels')
|
||||||
|
if label_printer_plugins:
|
||||||
|
printers.extend([
|
||||||
|
(p.slug, p.name + ' - ' + p.human_name) for p in label_printer_plugins
|
||||||
|
])
|
||||||
|
return printers
|
||||||
|
|
||||||
|
|
||||||
|
USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
||||||
|
'HOMEPAGE_HIDE_INACTIVE': {
|
||||||
|
'name': _('Hide inactive parts'),
|
||||||
|
'description': _('Hide inactive parts in results displayed on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_PART_STARRED': {
|
||||||
|
'name': _('Show subscribed parts'),
|
||||||
|
'description': _('Show subscribed parts on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_CATEGORY_STARRED': {
|
||||||
|
'name': _('Show subscribed categories'),
|
||||||
|
'description': _('Show subscribed part categories on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_PART_LATEST': {
|
||||||
|
'name': _('Show latest parts'),
|
||||||
|
'description': _('Show latest parts on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_BOM_REQUIRES_VALIDATION': {
|
||||||
|
'name': _('Show invalid BOMs'),
|
||||||
|
'description': _('Show BOMs that await validation on the homepage'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_STOCK_RECENT': {
|
||||||
|
'name': _('Show recent stock changes'),
|
||||||
|
'description': _('Show recently changed stock items on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_STOCK_LOW': {
|
||||||
|
'name': _('Show low stock'),
|
||||||
|
'description': _('Show low stock items on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_SHOW_STOCK_DEPLETED': {
|
||||||
|
'name': _('Show depleted stock'),
|
||||||
|
'description': _('Show depleted stock items on the homepage'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_BUILD_STOCK_NEEDED': {
|
||||||
|
'name': _('Show needed stock'),
|
||||||
|
'description': _('Show stock items needed for builds on the homepage'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_STOCK_EXPIRED': {
|
||||||
|
'name': _('Show expired stock'),
|
||||||
|
'description': _('Show expired stock items on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_STOCK_STALE': {
|
||||||
|
'name': _('Show stale stock'),
|
||||||
|
'description': _('Show stale stock items on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_BUILD_PENDING': {
|
||||||
|
'name': _('Show pending builds'),
|
||||||
|
'description': _('Show pending builds on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_BUILD_OVERDUE': {
|
||||||
|
'name': _('Show overdue builds'),
|
||||||
|
'description': _('Show overdue builds on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_PO_OUTSTANDING': {
|
||||||
|
'name': _('Show outstanding POs'),
|
||||||
|
'description': _('Show outstanding POs on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_PO_OVERDUE': {
|
||||||
|
'name': _('Show overdue POs'),
|
||||||
|
'description': _('Show overdue POs on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_SO_OUTSTANDING': {
|
||||||
|
'name': _('Show outstanding SOs'),
|
||||||
|
'description': _('Show outstanding SOs on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_SO_OVERDUE': {
|
||||||
|
'name': _('Show overdue SOs'),
|
||||||
|
'description': _('Show overdue SOs on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_SO_SHIPMENTS_PENDING': {
|
||||||
|
'name': _('Show pending SO shipments'),
|
||||||
|
'description': _('Show pending SO shipments on the homepage'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'HOMEPAGE_NEWS': {
|
||||||
|
'name': _('Show News'),
|
||||||
|
'description': _('Show news on the homepage'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'LABEL_INLINE': {
|
||||||
|
'name': _('Inline label display'),
|
||||||
|
'description': _(
|
||||||
|
'Display PDF labels in the browser, instead of downloading as a file'
|
||||||
|
),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'LABEL_DEFAULT_PRINTER': {
|
||||||
|
'name': _('Default label printer'),
|
||||||
|
'description': _('Configure which label printer should be selected by default'),
|
||||||
|
'default': '',
|
||||||
|
'choices': label_printer_options,
|
||||||
|
},
|
||||||
|
'REPORT_INLINE': {
|
||||||
|
'name': _('Inline report display'),
|
||||||
|
'description': _(
|
||||||
|
'Display PDF reports in the browser, instead of downloading as a file'
|
||||||
|
),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_SHOW_PARTS': {
|
||||||
|
'name': _('Search Parts'),
|
||||||
|
'description': _('Display parts in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_SHOW_SUPPLIER_PARTS': {
|
||||||
|
'name': _('Search Supplier Parts'),
|
||||||
|
'description': _('Display supplier parts in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_SHOW_MANUFACTURER_PARTS': {
|
||||||
|
'name': _('Search Manufacturer Parts'),
|
||||||
|
'description': _('Display manufacturer parts in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_HIDE_INACTIVE_PARTS': {
|
||||||
|
'name': _('Hide Inactive Parts'),
|
||||||
|
'description': _('Excluded inactive parts from search preview window'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_SHOW_CATEGORIES': {
|
||||||
|
'name': _('Search Categories'),
|
||||||
|
'description': _('Display part categories in search preview window'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_SHOW_STOCK': {
|
||||||
|
'name': _('Search Stock'),
|
||||||
|
'description': _('Display stock items in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_HIDE_UNAVAILABLE_STOCK': {
|
||||||
|
'name': _('Hide Unavailable Stock Items'),
|
||||||
|
'description': _(
|
||||||
|
'Exclude stock items which are not available from the search preview window'
|
||||||
|
),
|
||||||
|
'validator': bool,
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_SHOW_LOCATIONS': {
|
||||||
|
'name': _('Search Locations'),
|
||||||
|
'description': _('Display stock locations in search preview window'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_SHOW_COMPANIES': {
|
||||||
|
'name': _('Search Companies'),
|
||||||
|
'description': _('Display companies in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_SHOW_BUILD_ORDERS': {
|
||||||
|
'name': _('Search Build Orders'),
|
||||||
|
'description': _('Display build orders in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS': {
|
||||||
|
'name': _('Search Purchase Orders'),
|
||||||
|
'description': _('Display purchase orders in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_PURCHASE_ORDERS': {
|
||||||
|
'name': _('Exclude Inactive Purchase Orders'),
|
||||||
|
'description': _('Exclude inactive purchase orders from search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_SHOW_SALES_ORDERS': {
|
||||||
|
'name': _('Search Sales Orders'),
|
||||||
|
'description': _('Display sales orders in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_SALES_ORDERS': {
|
||||||
|
'name': _('Exclude Inactive Sales Orders'),
|
||||||
|
'description': _('Exclude inactive sales orders from search preview window'),
|
||||||
|
'validator': bool,
|
||||||
|
'default': True,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_SHOW_RETURN_ORDERS': {
|
||||||
|
'name': _('Search Return Orders'),
|
||||||
|
'description': _('Display return orders in search preview window'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_RETURN_ORDERS': {
|
||||||
|
'name': _('Exclude Inactive Return Orders'),
|
||||||
|
'description': _('Exclude inactive return orders from search preview window'),
|
||||||
|
'validator': bool,
|
||||||
|
'default': True,
|
||||||
|
},
|
||||||
|
'SEARCH_PREVIEW_RESULTS': {
|
||||||
|
'name': _('Search Preview Results'),
|
||||||
|
'description': _(
|
||||||
|
'Number of results to show in each section of the search preview window'
|
||||||
|
),
|
||||||
|
'default': 10,
|
||||||
|
'validator': [int, MinValueValidator(1)],
|
||||||
|
},
|
||||||
|
'SEARCH_REGEX': {
|
||||||
|
'name': _('Regex Search'),
|
||||||
|
'description': _('Enable regular expressions in search queries'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'SEARCH_WHOLE': {
|
||||||
|
'name': _('Whole Word Search'),
|
||||||
|
'description': _('Search queries return results for whole word matches'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'PART_SHOW_QUANTITY_IN_FORMS': {
|
||||||
|
'name': _('Show Quantity in Forms'),
|
||||||
|
'description': _('Display available part quantity in some forms'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'FORMS_CLOSE_USING_ESCAPE': {
|
||||||
|
'name': _('Escape Key Closes Forms'),
|
||||||
|
'description': _('Use the escape key to close modal forms'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'STICKY_HEADER': {
|
||||||
|
'name': _('Fixed Navbar'),
|
||||||
|
'description': _('The navbar position is fixed to the top of the screen'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'DATE_DISPLAY_FORMAT': {
|
||||||
|
'name': _('Date Format'),
|
||||||
|
'description': _('Preferred format for displaying dates'),
|
||||||
|
'default': 'YYYY-MM-DD',
|
||||||
|
'choices': [
|
||||||
|
('YYYY-MM-DD', '2022-02-22'),
|
||||||
|
('YYYY/MM/DD', '2022/22/22'),
|
||||||
|
('DD-MM-YYYY', '22-02-2022'),
|
||||||
|
('DD/MM/YYYY', '22/02/2022'),
|
||||||
|
('MM-DD-YYYY', '02-22-2022'),
|
||||||
|
('MM/DD/YYYY', '02/22/2022'),
|
||||||
|
('MMM DD YYYY', 'Feb 22 2022'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'DISPLAY_SCHEDULE_TAB': {
|
||||||
|
'name': _('Part Scheduling'),
|
||||||
|
'description': _('Display part scheduling information'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'DISPLAY_STOCKTAKE_TAB': {
|
||||||
|
'name': _('Part Stocktake'),
|
||||||
|
'description': _(
|
||||||
|
'Display part stocktake information (if stocktake functionality is enabled)'
|
||||||
|
),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'TABLE_STRING_MAX_LENGTH': {
|
||||||
|
'name': _('Table String Length'),
|
||||||
|
'description': _('Maximum length limit for strings displayed in table views'),
|
||||||
|
'validator': [int, MinValueValidator(0)],
|
||||||
|
'default': 100,
|
||||||
|
},
|
||||||
|
'NOTIFICATION_ERROR_REPORT': {
|
||||||
|
'name': _('Receive error reports'),
|
||||||
|
'description': _('Receive notifications for system errors'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
'LAST_USED_PRINTING_MACHINES': {
|
||||||
|
'name': _('Last used printing machines'),
|
||||||
|
'description': _('Save the last used printing machines for a user'),
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
}
|
@ -28,20 +28,6 @@ def cache(func):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def barcode_plugins() -> list:
|
|
||||||
"""Return a list of plugin choices which can be used for barcode generation."""
|
|
||||||
try:
|
|
||||||
from plugin import registry
|
|
||||||
|
|
||||||
plugins = registry.with_mixin('barcode', active=True)
|
|
||||||
except Exception:
|
|
||||||
plugins = []
|
|
||||||
|
|
||||||
return [
|
|
||||||
(plug.slug, plug.human_name) for plug in plugins if plug.has_barcode_generation
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def generate_barcode(model_instance: InvenTreeBarcodeMixin):
|
def generate_barcode(model_instance: InvenTreeBarcodeMixin):
|
||||||
"""Generate a barcode for a given model instance."""
|
"""Generate a barcode for a given model instance."""
|
||||||
from common.settings import get_global_setting
|
from common.settings import get_global_setting
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from importlib import reload
|
from importlib import reload
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -28,7 +29,14 @@ class AppMixin:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _activate_mixin(
|
def _activate_mixin(
|
||||||
cls, registry, plugins, force_reload=False, full_reload: bool = False
|
cls,
|
||||||
|
registry,
|
||||||
|
plugins,
|
||||||
|
force_reload=False,
|
||||||
|
full_reload: bool = False,
|
||||||
|
_internal: Optional[list] = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Activate AppMixin plugins - add custom apps and reload.
|
"""Activate AppMixin plugins - add custom apps and reload.
|
||||||
|
|
||||||
@ -37,6 +45,7 @@ class AppMixin:
|
|||||||
plugins (dict): List of IntegrationPlugins that should be installed
|
plugins (dict): List of IntegrationPlugins that should be installed
|
||||||
force_reload (bool, optional): Only reload base apps. Defaults to False.
|
force_reload (bool, optional): Only reload base apps. Defaults to False.
|
||||||
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
|
_internal (dict, optional): Internal use only (for testing). Defaults to None.
|
||||||
"""
|
"""
|
||||||
from common.settings import get_global_setting
|
from common.settings import get_global_setting
|
||||||
|
|
||||||
@ -61,9 +70,11 @@ class AppMixin:
|
|||||||
# first startup or force loading of base apps -> registry is prob false
|
# first startup or force loading of base apps -> registry is prob false
|
||||||
if registry.apps_loading or force_reload:
|
if registry.apps_loading or force_reload:
|
||||||
registry.apps_loading = False
|
registry.apps_loading = False
|
||||||
registry._reload_apps(force_reload=True, full_reload=full_reload)
|
registry._reload_apps(
|
||||||
|
force_reload=True, full_reload=full_reload, _internal=_internal
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
registry._reload_apps(full_reload=full_reload)
|
registry._reload_apps(full_reload=full_reload, _internal=_internal)
|
||||||
|
|
||||||
# rediscover models/ admin sites
|
# rediscover models/ admin sites
|
||||||
cls._reregister_contrib_apps(cls, registry)
|
cls._reregister_contrib_apps(cls, registry)
|
||||||
|
@ -27,7 +27,13 @@ class UrlsMixin:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _activate_mixin(
|
def _activate_mixin(
|
||||||
cls, registry, plugins, force_reload=False, full_reload: bool = False
|
cls,
|
||||||
|
registry,
|
||||||
|
plugins,
|
||||||
|
force_reload=False,
|
||||||
|
full_reload: bool = False,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Activate UrlsMixin plugins - add custom urls .
|
"""Activate UrlsMixin plugins - add custom urls .
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from collections import OrderedDict
|
|||||||
from importlib.machinery import SourceFileLoader
|
from importlib.machinery import SourceFileLoader
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -212,17 +212,20 @@ class PluginsRegistry:
|
|||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region loading / unloading
|
# region loading / unloading
|
||||||
def _load_plugins(self, full_reload: bool = False):
|
def _load_plugins(
|
||||||
|
self, full_reload: bool = False, _internal: Optional[list] = None
|
||||||
|
):
|
||||||
"""Load and activate all IntegrationPlugins.
|
"""Load and activate all IntegrationPlugins.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
|
_internal (list, optional): Internal apps to reload (used for testing). Defaults to None
|
||||||
"""
|
"""
|
||||||
logger.info('Loading plugins')
|
logger.info('Loading plugins')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._init_plugins()
|
self._init_plugins()
|
||||||
self._activate_plugins(full_reload=full_reload)
|
self._activate_plugins(full_reload=full_reload, _internal=_internal)
|
||||||
except (OperationalError, ProgrammingError, IntegrityError):
|
except (OperationalError, ProgrammingError, IntegrityError):
|
||||||
# Exception if the database has not been migrated yet, or is not ready
|
# Exception if the database has not been migrated yet, or is not ready
|
||||||
pass
|
pass
|
||||||
@ -262,6 +265,7 @@ class PluginsRegistry:
|
|||||||
full_reload: bool = False,
|
full_reload: bool = False,
|
||||||
force_reload: bool = False,
|
force_reload: bool = False,
|
||||||
collect: bool = False,
|
collect: bool = False,
|
||||||
|
_internal: Optional[list] = None,
|
||||||
):
|
):
|
||||||
"""Reload the plugin registry.
|
"""Reload the plugin registry.
|
||||||
|
|
||||||
@ -271,6 +275,7 @@ class PluginsRegistry:
|
|||||||
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
||||||
collect (bool, optional): Collect plugins before reloading. Defaults to False.
|
collect (bool, optional): Collect plugins before reloading. Defaults to False.
|
||||||
|
_internal (list, optional): Internal apps to reload (used for testing). Defaults to None
|
||||||
"""
|
"""
|
||||||
# Do not reload when currently loading
|
# Do not reload when currently loading
|
||||||
if self.is_loading:
|
if self.is_loading:
|
||||||
@ -293,7 +298,7 @@ class PluginsRegistry:
|
|||||||
self.plugins_loaded = False
|
self.plugins_loaded = False
|
||||||
self._unload_plugins(force_reload=force_reload)
|
self._unload_plugins(force_reload=force_reload)
|
||||||
self.plugins_loaded = True
|
self.plugins_loaded = True
|
||||||
self._load_plugins(full_reload=full_reload)
|
self._load_plugins(full_reload=full_reload, _internal=_internal)
|
||||||
|
|
||||||
self.update_plugin_hash()
|
self.update_plugin_hash()
|
||||||
|
|
||||||
@ -601,7 +606,12 @@ class PluginsRegistry:
|
|||||||
# Final list of mixins
|
# Final list of mixins
|
||||||
return order
|
return order
|
||||||
|
|
||||||
def _activate_plugins(self, force_reload=False, full_reload: bool = False):
|
def _activate_plugins(
|
||||||
|
self,
|
||||||
|
force_reload=False,
|
||||||
|
full_reload: bool = False,
|
||||||
|
_internal: Optional[list] = None,
|
||||||
|
):
|
||||||
"""Run activation functions for all plugins.
|
"""Run activation functions for all plugins.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -618,7 +628,11 @@ class PluginsRegistry:
|
|||||||
for mixin in self.__get_mixin_order():
|
for mixin in self.__get_mixin_order():
|
||||||
if hasattr(mixin, '_activate_mixin'):
|
if hasattr(mixin, '_activate_mixin'):
|
||||||
mixin._activate_mixin(
|
mixin._activate_mixin(
|
||||||
self, plugins, force_reload=force_reload, full_reload=full_reload
|
self,
|
||||||
|
plugins,
|
||||||
|
force_reload=force_reload,
|
||||||
|
full_reload=full_reload,
|
||||||
|
_internal=_internal,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug('Done activating')
|
logger.debug('Done activating')
|
||||||
@ -649,21 +663,31 @@ class PluginsRegistry:
|
|||||||
except Exception as error: # pragma: no cover
|
except Exception as error: # pragma: no cover
|
||||||
handle_error(error, do_raise=False)
|
handle_error(error, do_raise=False)
|
||||||
|
|
||||||
def _reload_apps(self, force_reload: bool = False, full_reload: bool = False):
|
def _reload_apps(
|
||||||
|
self,
|
||||||
|
force_reload: bool = False,
|
||||||
|
full_reload: bool = False,
|
||||||
|
_internal: Optional[list] = None,
|
||||||
|
):
|
||||||
"""Internal: reload apps using django internal functions.
|
"""Internal: reload apps using django internal functions.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
||||||
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
|
_internal (list, optional): Internal use only (for testing). Defaults to None.
|
||||||
"""
|
"""
|
||||||
|
loadable_apps = settings.INSTALLED_APPS
|
||||||
|
if _internal:
|
||||||
|
loadable_apps += _internal
|
||||||
|
|
||||||
if force_reload:
|
if force_reload:
|
||||||
# we can not use the built in functions as we need to brute force the registry
|
# we can not use the built in functions as we need to brute force the registry
|
||||||
apps.app_configs = OrderedDict()
|
apps.app_configs = OrderedDict()
|
||||||
apps.apps_ready = apps.models_ready = apps.loading = apps.ready = False
|
apps.apps_ready = apps.models_ready = apps.loading = apps.ready = False
|
||||||
apps.clear_cache()
|
apps.clear_cache()
|
||||||
self._try_reload(apps.populate, settings.INSTALLED_APPS)
|
self._try_reload(apps.populate, loadable_apps)
|
||||||
else:
|
else:
|
||||||
self._try_reload(apps.set_installed_apps, settings.INSTALLED_APPS)
|
self._try_reload(apps.set_installed_apps, loadable_apps)
|
||||||
|
|
||||||
def _clean_installed_apps(self):
|
def _clean_installed_apps(self):
|
||||||
for plugin in self.installed_apps:
|
for plugin in self.installed_apps:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user