mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-25 10:27:39 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into make-fields-filterable
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 407
|
||||
INVENTREE_API_VERSION = 408
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v408 -> 2025-10-13: https://github.com/inventree/InvenTree/pull/10561
|
||||
- Allow search of assembly fields in BOM API endpoint
|
||||
|
||||
v407 -> 2025-10-09: https://github.com/inventree/InvenTree/pull/10538
|
||||
- Breaking: Set error status code for plugin action call instead of just returning error data
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@ def cache_password():
|
||||
return cache_setting('password', None)
|
||||
|
||||
|
||||
def cache_user():
|
||||
"""Return the cash username."""
|
||||
return cache_setting('user', None)
|
||||
|
||||
|
||||
def is_global_cache_enabled() -> bool:
|
||||
"""Check if the global cache is enabled.
|
||||
|
||||
@@ -85,9 +90,10 @@ def get_cache_config(global_cache: bool) -> dict:
|
||||
if global_cache:
|
||||
# Build Redis URL with optional password
|
||||
password = cache_password()
|
||||
user = cache_user() or ''
|
||||
|
||||
if password:
|
||||
redis_url = f'redis://:{password}@{cache_host()}:{cache_port()}/0'
|
||||
redis_url = f'redis://{user}:{password}@{cache_host()}:{cache_port()}/0'
|
||||
else:
|
||||
redis_url = f'redis://{cache_host()}:{cache_port()}/0'
|
||||
|
||||
|
||||
@@ -76,8 +76,9 @@ def get_root_dir() -> Path:
|
||||
|
||||
def inventreeInstaller() -> Optional[str]:
|
||||
"""Returns the installer for the running codebase - if set or detectable."""
|
||||
# First look in the environment variables, e.g. if running in docker
|
||||
load_version_file()
|
||||
|
||||
# First look in the environment variables, e.g. if running in docker
|
||||
installer = os.environ.get('INVENTREE_PKG_INSTALLER', '')
|
||||
|
||||
if installer:
|
||||
@@ -121,6 +122,11 @@ def get_testfolder_dir() -> Path:
|
||||
return get_base_dir().joinpath('_testfolder').resolve()
|
||||
|
||||
|
||||
def get_version_file() -> Path:
|
||||
"""Returns the path of the InvenTree VERSION file. This does not ensure that the file exists."""
|
||||
return get_root_dir().joinpath('VERSION').resolve()
|
||||
|
||||
|
||||
def ensure_dir(path: Path, storage=None) -> None:
|
||||
"""Ensure that a directory exists.
|
||||
|
||||
@@ -592,3 +598,28 @@ def check_config_dir(
|
||||
pass
|
||||
|
||||
return
|
||||
|
||||
|
||||
VERSION_LOADED = False
|
||||
"""Flag to indicate if the VERSION file has been loaded in this process."""
|
||||
|
||||
|
||||
def load_version_file():
|
||||
"""Load the VERSION file if it exists and place the contents into the general execution environment.
|
||||
|
||||
Returns:
|
||||
True if the VERSION file was loaded (now or previously), False otherwise.
|
||||
"""
|
||||
global VERSION_LOADED
|
||||
if VERSION_LOADED:
|
||||
return True
|
||||
|
||||
# Load the VERSION file if it exists
|
||||
from dotenv import load_dotenv
|
||||
|
||||
version_file = get_version_file()
|
||||
if version_file.exists():
|
||||
load_dotenv(version_file)
|
||||
VERSION_LOADED = True
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -23,7 +23,6 @@ from django.http import Http404, HttpResponseGone
|
||||
|
||||
import structlog
|
||||
from corsheaders.defaults import default_headers as default_cors_headers
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from InvenTree.cache import get_cache_config, is_global_cache_enabled
|
||||
from InvenTree.config import (
|
||||
@@ -80,11 +79,7 @@ BASE_DIR = config.get_base_dir()
|
||||
|
||||
# Load configuration data
|
||||
CONFIG = config.load_config_data(set_cache=True)
|
||||
|
||||
# Load VERSION data if it exists
|
||||
version_file = config.get_root_dir().joinpath('VERSION')
|
||||
if version_file.exists():
|
||||
load_dotenv(version_file)
|
||||
config.load_version_file()
|
||||
|
||||
# Default action is to run the system in Debug mode
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
|
||||
@@ -670,6 +670,11 @@ def check_for_migrations(force: bool = False, reload_registry: bool = True) -> b
|
||||
|
||||
Returns bool indicating if migrations are up to date
|
||||
"""
|
||||
from . import ready
|
||||
|
||||
if ready.isRunningMigrations() or ready.isRunningBackup():
|
||||
# Migrations are already running!
|
||||
return False
|
||||
|
||||
def set_pending_migrations(n: int):
|
||||
"""Helper function to inform the user about pending migrations."""
|
||||
@@ -720,6 +725,8 @@ def check_for_migrations(force: bool = False, reload_registry: bool = True) -> b
|
||||
if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.sqlite3':
|
||||
raise e
|
||||
logger.exception('Error during migrations: %s', e)
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.exception('Error during migrations: %s', e)
|
||||
else:
|
||||
set_pending_migrations(0)
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ def inventreeBranch():
|
||||
branch = os.environ.get('INVENTREE_PKG_BRANCH', '')
|
||||
|
||||
if branch:
|
||||
return branch
|
||||
return ' '.join(branch.splitlines())
|
||||
|
||||
if main_branch is None:
|
||||
return None
|
||||
@@ -298,7 +298,7 @@ def inventree_identifier(override_announce: bool = False):
|
||||
from common.settings import get_global_setting
|
||||
|
||||
if override_announce or get_global_setting(
|
||||
'INVENTREE_ANNOUNCE_ID', enviroment_key='INVENTREE_ANNOUNCE_ID'
|
||||
'INVENTREE_ANNOUNCE_ID', environment_key='INVENTREE_ANNOUNCE_ID'
|
||||
):
|
||||
return get_global_setting('INVENTREE_INSTANCE_ID', default='')
|
||||
return None
|
||||
|
||||
@@ -48,7 +48,7 @@ def currency_codes() -> list:
|
||||
from common.settings import get_global_setting
|
||||
|
||||
codes = get_global_setting(
|
||||
'CURRENCY_CODES', create=False, enviroment_key='INVENTREE_CURRENCY_CODES'
|
||||
'CURRENCY_CODES', create=False, environment_key='INVENTREE_CURRENCY_CODES'
|
||||
).strip()
|
||||
|
||||
if not codes:
|
||||
|
||||
@@ -26,12 +26,12 @@ def global_setting_overrides() -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def get_global_setting(key, backup_value=None, enviroment_key=None, **kwargs):
|
||||
def get_global_setting(key, backup_value=None, environment_key=None, **kwargs):
|
||||
"""Return the value of a global setting using the provided key."""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
if enviroment_key:
|
||||
value = environ.get(enviroment_key)
|
||||
if environment_key:
|
||||
value = environ.get(environment_key)
|
||||
if value:
|
||||
return value
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ sentry_enabled: False
|
||||
#sentry_dsn: https://custom@custom.ingest.sentry.io/custom
|
||||
|
||||
# OpenTelemetry tracing/metrics - disabled by default - refer to the documentation for full list of options
|
||||
# This can be used to send tracing data, logs and metrics to OpenTelemtry compatible backends
|
||||
# This can be used to send tracing data, logs and metrics to OpenTelemetry compatible backends
|
||||
tracing:
|
||||
enabled: false
|
||||
|
||||
@@ -142,9 +142,9 @@ allowed_hosts:
|
||||
# use_x_forwarded_proto: true
|
||||
|
||||
# Cookie settings (nominally the default settings should be fine)
|
||||
cookie:
|
||||
secure: false
|
||||
samesite: false
|
||||
# cookie:
|
||||
# secure: false
|
||||
# samesite: false
|
||||
|
||||
# Cross Origin Resource Sharing (CORS) settings (see https://github.com/adamchainz/django-cors-headers)
|
||||
cors:
|
||||
@@ -203,7 +203,7 @@ remote_login_header: HTTP_REMOTE_USER
|
||||
# - 'allauth.socialaccount.providers.github'
|
||||
|
||||
# Add specific settings for social account providers (if required)
|
||||
# Refer to the djngo-allauth documentation for more details:
|
||||
# Refer to the django-allauth documentation for more details:
|
||||
# https://docs.allauth.org/en/latest/socialaccount/provider_configuration.html
|
||||
# social_providers:
|
||||
# github:
|
||||
|
||||
@@ -1639,6 +1639,11 @@ class BomList(
|
||||
|
||||
search_fields = [
|
||||
'reference',
|
||||
'part__name',
|
||||
'part__description',
|
||||
'part__IPN',
|
||||
'part__revision',
|
||||
'part__keywords',
|
||||
'sub_part__name',
|
||||
'sub_part__description',
|
||||
'sub_part__IPN',
|
||||
|
||||
@@ -68,27 +68,29 @@ const AboutContent = ({
|
||||
});
|
||||
|
||||
function fillTable(lookup: AboutLookupRef[], data: any, alwaysLink = false) {
|
||||
return lookup.map((map: AboutLookupRef, idx) => (
|
||||
<Table.Tr key={idx}>
|
||||
<Table.Td>{map.title}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group justify='space-between' gap='xs'>
|
||||
{alwaysLink ? (
|
||||
<Anchor href={data[map.ref]} target='_blank'>
|
||||
{data[map.ref]}
|
||||
</Anchor>
|
||||
) : map.link ? (
|
||||
<Anchor href={map.link} target='_blank'>
|
||||
{data[map.ref]}
|
||||
</Anchor>
|
||||
) : (
|
||||
data[map.ref]
|
||||
)}
|
||||
{map.copy && <CopyButton value={data[map.ref]} />}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
return lookup
|
||||
.filter((entry: AboutLookupRef) => !!data[entry.ref])
|
||||
.map((entry: AboutLookupRef, idx) => (
|
||||
<Table.Tr key={idx}>
|
||||
<Table.Td>{entry.title}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group justify='space-between' gap='xs'>
|
||||
{alwaysLink ? (
|
||||
<Anchor href={data[entry.ref]} target='_blank'>
|
||||
{data[entry.ref]}
|
||||
</Anchor>
|
||||
) : entry.link ? (
|
||||
<Anchor href={entry.link} target='_blank'>
|
||||
{data[entry.ref]}
|
||||
</Anchor>
|
||||
) : (
|
||||
data[entry.ref]
|
||||
)}
|
||||
{entry.copy && <CopyButton value={data[entry.ref]} />}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
}
|
||||
/* renderer */
|
||||
if (isLoading) return <Trans>Loading</Trans>;
|
||||
|
||||
@@ -106,14 +106,37 @@ export async function doBasicLogin(
|
||||
}
|
||||
})
|
||||
.catch(async (err) => {
|
||||
if (err?.response?.status == 401) {
|
||||
await handlePossibleMFAError(err);
|
||||
} else if (err?.response?.status == 409) {
|
||||
notifications.hide('auth-login-error');
|
||||
|
||||
if (err?.response?.status) {
|
||||
switch (err.response.status) {
|
||||
case 401:
|
||||
await handlePossibleMFAError(err);
|
||||
break;
|
||||
case 409:
|
||||
notifications.show({
|
||||
title: t`Already logged in`,
|
||||
message: t`There is a conflicting session on the server for this browser. Please logout of that first.`,
|
||||
color: 'red',
|
||||
id: 'auth-login-error',
|
||||
autoClose: false
|
||||
});
|
||||
break;
|
||||
default:
|
||||
notifications.show({
|
||||
title: `${t`Login failed`} (${err.response.status})`,
|
||||
message: t`Check your input and try again.`,
|
||||
id: 'auth-login-error',
|
||||
color: 'red'
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
notifications.show({
|
||||
title: t`Already logged in`,
|
||||
message: t`There is a conflicting session on the server for this browser. Please logout of that first.`,
|
||||
title: t`Login failed`,
|
||||
message: t`No response from server.`,
|
||||
color: 'red',
|
||||
autoClose: false
|
||||
id: 'login-error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -238,17 +238,6 @@ export default function BuildDetail() {
|
||||
icon: 'manufacturers',
|
||||
hidden: !build.external
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'purchase_order',
|
||||
label: t`Purchase Order`,
|
||||
icon: 'purchase_orders',
|
||||
copy: true,
|
||||
hidden: !build.external,
|
||||
value_formatter: () => {
|
||||
return 'TODO: external PO';
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'reference',
|
||||
|
||||
@@ -352,6 +352,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
||||
// Reset the pagination state when the search term changes
|
||||
useEffect(() => {
|
||||
tableState.setPage(1);
|
||||
tableState.clearSelectedRecords();
|
||||
}, [
|
||||
tableState.searchTerm,
|
||||
tableState.filterSet.activeFilters,
|
||||
|
||||
@@ -504,7 +504,10 @@ export function StockItemTable({
|
||||
return {
|
||||
items: table.selectedRecords,
|
||||
model: ModelType.stockitem,
|
||||
refresh: table.refreshTable,
|
||||
refresh: () => {
|
||||
table.clearSelectedRecords();
|
||||
table.refreshTable();
|
||||
},
|
||||
filters: {
|
||||
in_stock: true
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import { doLogin } from './login.js';
|
||||
test('Login - Failures', async ({ page }) => {
|
||||
const loginWithError = async () => {
|
||||
await page.getByRole('button', { name: 'Log In' }).click();
|
||||
await page.getByText('Login failed').waitFor();
|
||||
await page.getByText('Check your input and try again').waitFor();
|
||||
await page.getByText('Login failed', { exact: true }).waitFor();
|
||||
await page.getByText('Check your input and try again').first().waitFor();
|
||||
await page.locator('#login').getByRole('button').click();
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user