diff --git a/src/backend/InvenTree/.gitignore b/src/backend/InvenTree/.gitignore
new file mode 100644
index 0000000000..4af4473ebe
--- /dev/null
+++ b/src/backend/InvenTree/.gitignore
@@ -0,0 +1,2 @@
+# Files generated during unit testing
+_testfolder/
diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index 7d1eccf75b..ebfdca7691 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -11,29 +11,32 @@ v304 - 2025-01-25 : https://github.com/inventree/InvenTree/pull/6293
- Removes a considerable amount of old auth endpoints
- Introduces allauth based REST API
-v303 - 2025-01-20 - https://github.com/inventree/InvenTree/pull/8915
+v304 - 2025-01-22 : https://github.com/inventree/InvenTree/pull/8940
+ - Adds "category" filter to build list API
+
+v303 - 2025-01-20 : https://github.com/inventree/InvenTree/pull/8915
- Adds "start_date" field to Build model and API endpoints
- Adds additional API filtering and sorting options for Build list
-v302 - 2025-01-18 - https://github.com/inventree/InvenTree/pull/8905
+v302 - 2025-01-18 : https://github.com/inventree/InvenTree/pull/8905
- Fix schema definition on the /label/print endpoint
-v301 - 2025-01-14 - https://github.com/inventree/InvenTree/pull/8894
+v301 - 2025-01-14 : https://github.com/inventree/InvenTree/pull/8894
- Remove ui preferences from the API
-v300 - 2025-01-13 - https://github.com/inventree/InvenTree/pull/8886
+v300 - 2025-01-13 : https://github.com/inventree/InvenTree/pull/8886
- Allow null value for 'expiry_date' field introduced in #8867
-v299 - 2025-01-10 - https://github.com/inventree/InvenTree/pull/8867
+v299 - 2025-01-10 : https://github.com/inventree/InvenTree/pull/8867
- Adds 'expiry_date' field to the PurchaseOrderReceive API endpoint
- Adds 'default_expiry` field to the PartBriefSerializer, affecting API endpoints which use it
-v298 - 2025-01-07 - https://github.com/inventree/InvenTree/pull/8848
+v298 - 2025-01-07 : https://github.com/inventree/InvenTree/pull/8848
- Adds 'created_by' field to PurchaseOrder API endpoints
- Adds 'created_by' field to SalesOrder API endpoints
- Adds 'created_by' field to ReturnOrder API endpoints
-v297 - 2024-12-29 - https://github.com/inventree/InvenTree/pull/8438
+v297 - 2024-12-29 : https://github.com/inventree/InvenTree/pull/8438
- Adjustments to the CustomUserState API endpoints and serializers
v296 - 2024-12-25 : https://github.com/inventree/InvenTree/pull/8732
diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py
index 455548effb..4f8274b5ea 100644
--- a/src/backend/InvenTree/build/api.py
+++ b/src/backend/InvenTree/build/api.py
@@ -13,7 +13,7 @@ from rest_framework.exceptions import ValidationError
import build.admin
import build.serializers
import common.models
-import part.models
+import part.models as part_models
from build.models import Build, BuildItem, BuildLine
from build.status_codes import BuildStatus, BuildStatusGroups
from generic.states.api import StatusView
@@ -77,7 +77,10 @@ class BuildFilter(rest_filters.FilterSet):
return queryset
part = rest_filters.ModelChoiceFilter(
- queryset=part.models.Part.objects.all(), field_name='part', method='filter_part'
+ queryset=part_models.Part.objects.all(),
+ field_name='part',
+ method='filter_part',
+ label=_('Part'),
)
def filter_part(self, queryset, name, part):
@@ -94,6 +97,17 @@ class BuildFilter(rest_filters.FilterSet):
else:
return queryset.filter(part=part)
+ category = rest_filters.ModelChoiceFilter(
+ queryset=part_models.PartCategory.objects.all(),
+ method='filter_category',
+ label=_('Category'),
+ )
+
+ def filter_category(self, queryset, name, category):
+ """Filter by part category (including sub-categories)."""
+ categories = category.get_descendants(include_self=True)
+ return queryset.filter(part__category__in=categories)
+
ancestor = rest_filters.ModelChoiceFilter(
queryset=Build.objects.all(),
label=_('Ancestor Build'),
@@ -417,7 +431,7 @@ class BuildLineFilter(rest_filters.FilterSet):
)
part = rest_filters.ModelChoiceFilter(
- queryset=part.models.Part.objects.all(),
+ queryset=part_models.Part.objects.all(),
label=_('Part'),
field_name='bom_item__sub_part',
)
@@ -729,7 +743,7 @@ class BuildItemFilter(rest_filters.FilterSet):
return queryset
part = rest_filters.ModelChoiceFilter(
- queryset=part.models.Part.objects.all(),
+ queryset=part_models.Part.objects.all(),
label=_('Part'),
method='filter_part',
field_name='stock_item__part',
diff --git a/src/backend/InvenTree/plugin/helpers.py b/src/backend/InvenTree/plugin/helpers.py
index 4c78ff14e7..d3f45c4a19 100644
--- a/src/backend/InvenTree/plugin/helpers.py
+++ b/src/backend/InvenTree/plugin/helpers.py
@@ -10,7 +10,6 @@ import traceback
from importlib.metadata import entry_points
from importlib.util import module_from_spec
-from django import template
from django.conf import settings
from django.core.exceptions import AppRegistryNotReady
from django.db.utils import IntegrityError
@@ -244,35 +243,3 @@ def get_plugins(pkg, baseclass, path=None):
# endregion
-
-
-# region templates
-def render_template(plugin, template_file, context=None):
- """Locate and render a template file, available in the global template context."""
- try:
- tmp = template.loader.get_template(template_file)
- except template.TemplateDoesNotExist:
- logger.exception(
- "Plugin %s could not locate template '%s'", plugin.slug, template_file
- )
-
- return f"""
-
- Template file {template_file} does not exist.
-
- """
-
- # Render with the provided context
- html = tmp.render(context)
-
- return html
-
-
-def render_text(text, context=None):
- """Locate a raw string with provided context."""
- ctx = template.Context(context)
-
- return template.Template(text).render(ctx)
-
-
-# endregion
diff --git a/src/backend/InvenTree/plugin/models.py b/src/backend/InvenTree/plugin/models.py
index 18d4c6c51f..3068dbcfad 100644
--- a/src/backend/InvenTree/plugin/models.py
+++ b/src/backend/InvenTree/plugin/models.py
@@ -142,7 +142,7 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
def save(self, force_insert=False, force_update=False, *args, **kwargs):
"""Extend save method to reload plugins if the 'active' status changes."""
- reload = kwargs.pop('no_reload', False) # check if no_reload flag is set
+ no_reload = kwargs.pop('no_reload', False) # check if no_reload flag is set
super().save(force_insert, force_update, *args, **kwargs)
@@ -150,10 +150,10 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
# Force active if builtin
self.active = True
- if not reload and self.active != self.__org_active:
+ if not no_reload and self.active != self.__org_active:
if settings.PLUGIN_TESTING:
- warnings.warn('A reload was triggered', stacklevel=2)
- registry.reload_plugins()
+ warnings.warn('A plugin registry reload was triggered', stacklevel=2)
+ registry.reload_plugins(full_reload=True, force_reload=True, collect=True)
@admin.display(boolean=True, description=_('Installed'))
def is_installed(self) -> bool:
diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py
index 014f41d72d..f6d0b17b23 100644
--- a/src/backend/InvenTree/plugin/registry.py
+++ b/src/backend/InvenTree/plugin/registry.py
@@ -453,6 +453,8 @@ class PluginsRegistry:
Args:
plugin: Plugin module
+ configs: Plugin configuration dictionary
+ force_reload (bool, optional): Force reload of plugin. Defaults to False.
"""
from InvenTree import version
@@ -485,6 +487,7 @@ class PluginsRegistry:
# Check if this is a 'builtin' plugin
builtin = plugin.check_is_builtin()
+ sample = plugin.check_is_sample()
package_name = None
@@ -510,11 +513,37 @@ class PluginsRegistry:
# Initialize package - we can be sure that an admin has activated the plugin
logger.debug('Loading plugin `%s`', plg_name)
+ # If this is a third-party plugin, reload the source module
+ # This is required to ensure that separate processes are using the same code
+ if not builtin and not sample:
+ plugin_name = plugin.__name__
+ module_name = plugin.__module__
+
+ if plugin_module := sys.modules.get(module_name):
+ logger.debug('Reloading plugin `%s`', plg_name)
+ # Reload the module
+ try:
+ importlib.reload(plugin_module)
+ plugin = getattr(plugin_module, plugin_name)
+ except ModuleNotFoundError:
+ # No module found - try to import it directly
+ try:
+ raw_module = _load_source(
+ module_name, plugin_module.__file__
+ )
+ plugin = getattr(raw_module, plugin_name)
+ except Exception:
+ pass
+ except Exception:
+ logger.exception('Failed to reload plugin `%s`', plg_name)
+
try:
t_start = time.time()
plg_i: InvenTreePlugin = plugin()
dt = time.time() - t_start
logger.debug('Loaded plugin `%s` in %.3fs', plg_name, dt)
+ except ModuleNotFoundError as e:
+ raise e
except Exception as error:
handle_error(
error, log_name='init'
@@ -745,7 +774,7 @@ class PluginsRegistry:
if old_hash != self.registry_hash:
try:
- logger.debug(
+ logger.info(
'Updating plugin registry hash: %s', str(self.registry_hash)
)
set_global_setting(
@@ -839,11 +868,16 @@ def _load_source(modname, filename):
See https://docs.python.org/3/whatsnew/3.12.html#imp
"""
- loader = importlib.machinery.SourceFileLoader(modname, filename)
- spec = importlib.util.spec_from_file_location(modname, filename, loader=loader)
+ if modname in sys.modules:
+ del sys.modules[modname]
+
+ # loader = importlib.machinery.SourceFileLoader(modname, filename)
+ spec = importlib.util.spec_from_file_location(modname, filename) # , loader=loader)
module = importlib.util.module_from_spec(spec)
- # The module is always executed and not cached in sys.modules.
- # Uncomment the following line to cache the module.
- # sys.modules[module.__name__] = module
- loader.exec_module(module)
+
+ sys.modules[module.__name__] = module
+
+ if spec.loader:
+ spec.loader.exec_module(module)
+
return module
diff --git a/src/backend/InvenTree/plugin/test_api.py b/src/backend/InvenTree/plugin/test_api.py
index 2b73bea416..5f3a481ce8 100644
--- a/src/backend/InvenTree/plugin/test_api.py
+++ b/src/backend/InvenTree/plugin/test_api.py
@@ -205,7 +205,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
plg_inactive.active = True
plg_inactive.save()
- self.assertEqual(cm.warning.args[0], 'A reload was triggered')
+ self.assertEqual(cm.warning.args[0], 'A plugin registry reload was triggered')
def test_check_plugin(self):
"""Test check_plugin function."""
diff --git a/src/backend/InvenTree/plugin/test_helpers.py b/src/backend/InvenTree/plugin/test_helpers.py
deleted file mode 100644
index 95cff3ea95..0000000000
--- a/src/backend/InvenTree/plugin/test_helpers.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""Unit tests for helpers.py."""
-
-from django.test import TestCase
-
-from .helpers import render_template
-
-
-class HelperTests(TestCase):
- """Tests for helpers."""
-
- def test_render_template(self):
- """Check if render_template helper works."""
-
- class ErrorSource:
- slug = 'sampleplg'
-
- # working sample
- response = render_template(ErrorSource(), 'sample/sample.html', {'abc': 123})
- self.assertEqual(response, '123
\n')
-
- # Wrong sample
- response = render_template(
- ErrorSource(), 'sample/wrongsample.html', {'abc': 123}
- )
- self.assertIn('lert alert-block alert-danger', response)
- self.assertIn('Template file sample/wrongsample.html', response)
diff --git a/src/backend/InvenTree/plugin/test_plugin.py b/src/backend/InvenTree/plugin/test_plugin.py
index cc034acdee..5f0810073b 100644
--- a/src/backend/InvenTree/plugin/test_plugin.py
+++ b/src/backend/InvenTree/plugin/test_plugin.py
@@ -4,9 +4,11 @@ import os
import shutil
import subprocess
import tempfile
+import textwrap
from datetime import datetime
from pathlib import Path
from unittest import mock
+from unittest.mock import patch
from django.test import TestCase, override_settings
@@ -18,6 +20,9 @@ from plugin.samples.integration.another_sample import (
)
from plugin.samples.integration.sample import SampleIntegrationPlugin
+# Directory for testing plugins during CI
+PLUGIN_TEST_DIR = '_testfolder/test_plugins'
+
class PluginTagTests(TestCase):
"""Tests for the plugin extras."""
@@ -287,3 +292,111 @@ class RegistryTests(TestCase):
self.assertEqual(
registry.errors.get('init')[0]['broken_sample'], "'This is a dummy error'"
)
+
+ @override_settings(PLUGIN_TESTING=True, PLUGIN_TESTING_SETUP=True)
+ @patch.dict(os.environ, {'INVENTREE_PLUGIN_TEST_DIR': PLUGIN_TEST_DIR})
+ def test_registry_reload(self):
+ """Test that the registry correctly reloads plugin modules.
+
+ - Create a simple plugin which we can change the version
+ - Ensure that the "hash" of the plugin registry changes
+ """
+ dummy_file = os.path.join(PLUGIN_TEST_DIR, 'dummy_ci_plugin.py')
+
+ # Ensure the plugin dir exists
+ os.makedirs(PLUGIN_TEST_DIR, exist_ok=True)
+
+ # Create an __init__.py file
+ init_file = os.path.join(PLUGIN_TEST_DIR, '__init__.py')
+ if not os.path.exists(init_file):
+ with open(os.path.join(init_file), 'w', encoding='utf-8') as f:
+ f.write('')
+
+ def plugin_content(version):
+ """Return the content of the plugin file."""
+ content = f"""
+ from plugin import InvenTreePlugin
+
+ PLG_VERSION = "{version}"
+
+ print(">>> LOADING DUMMY PLUGIN v" + PLG_VERSION + " <<<")
+
+ class DummyCIPlugin(InvenTreePlugin):
+
+ NAME = "DummyCIPlugin"
+ SLUG = "dummyci"
+ TITLE = "Dummy plugin for CI testing"
+
+ VERSION = PLG_VERSION
+
+ """
+
+ return textwrap.dedent(content)
+
+ def create_plugin_file(
+ version: str, enabled: bool = True, reload: bool = True
+ ) -> str:
+ """Create a plugin file with the given version.
+
+ Arguments:
+ version: The version string to use for the plugin file
+ enabled: Whether the plugin should be enabled or not
+
+ Returns:
+ str: The plugin registry hash
+ """
+ import time
+
+ content = plugin_content(version)
+
+ with open(dummy_file, 'w', encoding='utf-8') as f:
+ f.write(content)
+
+ # Wait for the file to be written
+ time.sleep(2)
+
+ if reload:
+ # Ensure the plugin is activated
+ registry.set_plugin_state('dummyci', enabled)
+ registry.reload_plugins(
+ full_reload=True, collect=True, force_reload=True
+ )
+
+ registry.update_plugin_hash()
+
+ return registry.registry_hash
+
+ # Initial hash, with plugin disabled
+ hash_disabled = create_plugin_file('0.0.1', enabled=False, reload=False)
+
+ # Perform initial registry reload
+ registry.reload_plugins(full_reload=True, collect=True, force_reload=True)
+
+ # Start plugin in known state
+ registry.set_plugin_state('dummyci', False)
+
+ hash_disabled = create_plugin_file('0.0.1', enabled=False)
+
+ # Enable the plugin
+ hash_enabled = create_plugin_file('0.1.0', enabled=True)
+
+ # Hash must be different!
+ self.assertNotEqual(hash_disabled, hash_enabled)
+
+ plugin_hash = hash_enabled
+
+ for v in ['0.1.1', '7.1.2', '1.2.1', '4.0.1']:
+ h = create_plugin_file(v, enabled=True)
+ self.assertNotEqual(plugin_hash, h)
+ plugin_hash = h
+
+ # Revert back to original 'version'
+ h = create_plugin_file('0.1.0', enabled=True)
+ self.assertEqual(hash_enabled, h)
+
+ # Disable the plugin
+ h = create_plugin_file('0.0.1', enabled=False)
+ self.assertEqual(hash_disabled, h)
+
+ # Finally, ensure that the plugin file is removed after testing
+ os.remove(dummy_file)
diff --git a/src/frontend/package.json b/src/frontend/package.json
index bdc84ebdb0..eb3d4a295e 100644
--- a/src/frontend/package.json
+++ b/src/frontend/package.json
@@ -91,7 +91,7 @@
"nyc": "^17.1.0",
"rollup-plugin-license": "^3.5.3",
"typescript": "^5.7.3",
- "vite": "^6.0.7",
+ "vite": "^6.0.9",
"vite-plugin-babel-macros": "^1.0.6",
"vite-plugin-istanbul": "^6.0.2"
}
diff --git a/src/frontend/src/tables/FilterSelectDrawer.tsx b/src/frontend/src/tables/FilterSelectDrawer.tsx
index cdb82344cc..c209d30d04 100644
--- a/src/frontend/src/tables/FilterSelectDrawer.tsx
+++ b/src/frontend/src/tables/FilterSelectDrawer.tsx
@@ -179,10 +179,17 @@ function FilterAddGroup({
// Determine the "type" of filter (default = boolean)
const filterType: TableFilterType = useMemo(() => {
- return (
- availableFilters?.find((flt) => flt.name === selectedFilter)?.type ??
- 'boolean'
- );
+ const filter = availableFilters?.find((flt) => flt.name === selectedFilter);
+
+ if (filter?.type) {
+ return filter.type;
+ } else if (filter?.choices) {
+ // If choices are provided, it is a choice filter
+ return 'choice';
+ } else {
+ // Default fallback
+ return 'boolean';
+ }
}, [selectedFilter]);
const setSelectedValue = useCallback(
diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx
index faa96952ff..9aae96df0a 100644
--- a/src/frontend/src/tables/build/BuildOrderTable.tsx
+++ b/src/frontend/src/tables/build/BuildOrderTable.tsx
@@ -8,7 +8,9 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useBuildOrderFields } from '../../forms/BuildForms';
+import { shortenString } from '../../functions/tables';
import {
+ useFilters,
useOwnerFilters,
useProjectCodeFilters,
useUserFilters
@@ -131,6 +133,17 @@ export function BuildOrderTable({
const ownerFilters = useOwnerFilters();
const userFilters = useUserFilters();
+ const categoryFilters = useFilters({
+ url: apiUrl(ApiEndpoints.category_list),
+ transform: (item) => ({
+ value: item.pk,
+ label: shortenString({
+ str: item.pathstring,
+ len: 50
+ })
+ })
+ });
+
const tableFilters: TableFilter[] = useMemo(() => {
const filters: TableFilter[] = [
OutstandingFilter(),
@@ -177,7 +190,13 @@ export function BuildOrderTable({
description: t`Filter by user who issued this order`,
choices: userFilters.choices
},
- ResponsibleFilter({ choices: ownerFilters.choices })
+ ResponsibleFilter({ choices: ownerFilters.choices }),
+ {
+ name: 'category',
+ label: t`Category`,
+ description: t`Filter by part category`,
+ choices: categoryFilters.choices
+ }
];
// If we are filtering on a specific part, we can include the "include variants" filter
@@ -193,6 +212,7 @@ export function BuildOrderTable({
return filters;
}, [
partId,
+ categoryFilters.choices,
projectCodeFilters.choices,
ownerFilters.choices,
userFilters.choices
diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock
index bb478953fa..e7ff844e01 100644
--- a/src/frontend/yarn.lock
+++ b/src/frontend/yarn.lock
@@ -1724,41 +1724,21 @@
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.19.2.tgz#0c896535473291cb41f152c180bedd5680a3b273"
integrity sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==
-"@rollup/rollup-android-arm-eabi@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz#e0f5350845090ca09690fe4a472717f3b8aae225"
- integrity sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==
-
"@rollup/rollup-android-arm-eabi@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz#14c737dc19603a096568044eadaa60395eefb809"
integrity sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==
-"@rollup/rollup-android-arm64@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz#08270faef6747e2716d3e978a8bbf479f75fb19a"
- integrity sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==
-
"@rollup/rollup-android-arm64@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz#9d81ea54fc5650eb4ebbc0a7d84cee331bfa30ad"
integrity sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==
-"@rollup/rollup-darwin-arm64@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz#691671133b350661328d42c8dbdedd56dfb97dfd"
- integrity sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==
-
"@rollup/rollup-darwin-arm64@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz#29448cb1370cf678b50743d2e392be18470abc23"
integrity sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==
-"@rollup/rollup-darwin-x64@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz#b2ec52a1615f24b1cd40bc8906ae31af81e8a342"
- integrity sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==
-
"@rollup/rollup-darwin-x64@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz#0ca99741c3ed096700557a43bb03359450c7857d"
@@ -1774,41 +1754,21 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz#dfba762a023063dc901610722995286df4a48360"
integrity sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==
-"@rollup/rollup-linux-arm-gnueabihf@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz#217f01f304808920680bd269002df38e25d9205f"
- integrity sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==
-
"@rollup/rollup-linux-arm-gnueabihf@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz#b9da54171726266c5ef4237f462a85b3c3cf6ac9"
integrity sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==
-"@rollup/rollup-linux-arm-musleabihf@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz#93ac1c5a1e389f4482a2edaeec41fcffee54a930"
- integrity sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==
-
"@rollup/rollup-linux-arm-musleabihf@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz#b9db69b3f85f5529eb992936d8f411ee6d04297b"
integrity sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==
-"@rollup/rollup-linux-arm64-gnu@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz#a7f146787d6041fecc4ecdf1aa72234661ca94a4"
- integrity sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==
-
"@rollup/rollup-linux-arm64-gnu@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz#2550cf9bb4d47d917fd1ab4af756d7bbc3ee1528"
integrity sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==
-"@rollup/rollup-linux-arm64-musl@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz#6a37236189648e678bd564d6e8ca798f42cf42c5"
- integrity sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==
-
"@rollup/rollup-linux-arm64-musl@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz#9d06b26d286c7dded6336961a2f83e48330e0c80"
@@ -1819,81 +1779,41 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz#e957bb8fee0c8021329a34ca8dfa825826ee0e2e"
integrity sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==
-"@rollup/rollup-linux-powerpc64le-gnu@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz#5661420dc463bec31ecb2d17d113de858cfcfe2d"
- integrity sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==
-
"@rollup/rollup-linux-powerpc64le-gnu@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz#e8585075ddfb389222c5aada39ea62d6d2511ccc"
integrity sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==
-"@rollup/rollup-linux-riscv64-gnu@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz#cb00342b7432bdef723aa606281de2f522d6dcf7"
- integrity sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==
-
"@rollup/rollup-linux-riscv64-gnu@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz#7d0d40cee7946ccaa5a4e19a35c6925444696a9e"
integrity sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==
-"@rollup/rollup-linux-s390x-gnu@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz#0708889674dccecccd28e2befccf791e0767fcb7"
- integrity sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==
-
"@rollup/rollup-linux-s390x-gnu@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz#c2dcd8a4b08b2f2778eceb7a5a5dfde6240ebdea"
integrity sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==
-"@rollup/rollup-linux-x64-gnu@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz#a135b040b21582e91cfed2267ccfc7d589e1dbc6"
- integrity sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==
-
"@rollup/rollup-linux-x64-gnu@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz#183637d91456877cb83d0a0315eb4788573aa588"
integrity sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==
-"@rollup/rollup-linux-x64-musl@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz#88395a81a3ab7ee3dc8dc31a73ff62ed3185f34d"
- integrity sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==
-
"@rollup/rollup-linux-x64-musl@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz#036a4c860662519f1f9453807547fd2a11d5bb01"
integrity sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==
-"@rollup/rollup-win32-arm64-msvc@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz#12ee49233b1125f2c1da38392f63b1dbb0c31bba"
- integrity sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==
-
"@rollup/rollup-win32-arm64-msvc@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz#51cad812456e616bfe4db5238fb9c7497e042a52"
integrity sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==
-"@rollup/rollup-win32-ia32-msvc@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz#0f987b134c6b3123c22842b33ba0c2b6fb78cc3b"
- integrity sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==
-
"@rollup/rollup-win32-ia32-msvc@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz#661c8b3e4cd60f51deaa39d153aac4566e748e5e"
integrity sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==
-"@rollup/rollup-win32-x64-msvc@4.22.5":
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz#f2feb149235a5dc1deb5439758f8871255e5a161"
- integrity sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==
-
"@rollup/rollup-win32-x64-msvc@4.30.1":
version "4.30.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz#73bf1885ff052b82fbb0f82f8671f73c36e9137c"
@@ -4204,16 +4124,7 @@ postcss@8.4.38:
picocolors "^1.0.0"
source-map-js "^1.2.0"
-postcss@^8.4.43:
- version "8.4.45"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.45.tgz#538d13d89a16ef71edbf75d895284ae06b79e603"
- integrity sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==
- dependencies:
- nanoid "^3.3.7"
- picocolors "^1.0.1"
- source-map-js "^1.2.0"
-
-postcss@^8.4.49:
+postcss@^8.4.43, postcss@^8.4.49:
version "8.5.1"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.1.tgz#e2272a1f8a807fafa413218245630b5db10a3214"
integrity sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==
@@ -4579,32 +4490,7 @@ rollup-plugin-license@^3.5.3:
spdx-expression-validate "~2.0.0"
spdx-satisfies "~5.0.1"
-rollup@^4.20.0:
- version "4.22.5"
- resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.22.5.tgz#d5108cc470249417e50492456253884d19f5d40f"
- integrity sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==
- dependencies:
- "@types/estree" "1.0.6"
- optionalDependencies:
- "@rollup/rollup-android-arm-eabi" "4.22.5"
- "@rollup/rollup-android-arm64" "4.22.5"
- "@rollup/rollup-darwin-arm64" "4.22.5"
- "@rollup/rollup-darwin-x64" "4.22.5"
- "@rollup/rollup-linux-arm-gnueabihf" "4.22.5"
- "@rollup/rollup-linux-arm-musleabihf" "4.22.5"
- "@rollup/rollup-linux-arm64-gnu" "4.22.5"
- "@rollup/rollup-linux-arm64-musl" "4.22.5"
- "@rollup/rollup-linux-powerpc64le-gnu" "4.22.5"
- "@rollup/rollup-linux-riscv64-gnu" "4.22.5"
- "@rollup/rollup-linux-s390x-gnu" "4.22.5"
- "@rollup/rollup-linux-x64-gnu" "4.22.5"
- "@rollup/rollup-linux-x64-musl" "4.22.5"
- "@rollup/rollup-win32-arm64-msvc" "4.22.5"
- "@rollup/rollup-win32-ia32-msvc" "4.22.5"
- "@rollup/rollup-win32-x64-msvc" "4.22.5"
- fsevents "~2.3.2"
-
-rollup@^4.23.0:
+rollup@^4.20.0, rollup@^4.23.0:
version "4.30.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.30.1.tgz#d5c3d066055259366cdc3eb6f1d051c5d6afaf74"
integrity sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==
@@ -5147,10 +5033,10 @@ vite@^5.0.0, vite@^5.0.11:
optionalDependencies:
fsevents "~2.3.3"
-vite@^6.0.7:
- version "6.0.7"
- resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.7.tgz#f0f8c120733b04af52b4a1e3e7cb54eb851a799b"
- integrity sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==
+vite@^6.0.9:
+ version "6.0.9"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.9.tgz#0a830b767ef7aa762360b56bdef955c1395dc1ee"
+ integrity sha512-MSgUxHcaXLtnBPktkbUSoQUANApKYuxZ6DrbVENlIorbhL2dZydTLaZ01tjUoE3szeFzlFk9ANOKk0xurh4MKA==
dependencies:
esbuild "^0.24.2"
postcss "^8.4.49"
diff --git a/tasks.py b/tasks.py
index 14187cea0e..709bc4ee71 100644
--- a/tasks.py
+++ b/tasks.py
@@ -910,13 +910,28 @@ def gunicorn(c, address='0.0.0.0:8000', workers=None):
run(c, cmd, pty=True)
-@task(pre=[wait], help={'address': 'Server address:port (default=127.0.0.1:8000)'})
-def server(c, address='127.0.0.1:8000'):
+@task(
+ pre=[wait],
+ help={
+ 'address': 'Server address:port (default=127.0.0.1:8000)',
+ 'no_reload': 'Do not automatically reload the server in response to code changes',
+ 'no_threading': 'Disable multi-threading for the development server',
+ },
+)
+def server(c, address='127.0.0.1:8000', no_reload=False, no_threading=False):
"""Launch a (development) server using Django's in-built webserver.
Note: This is *not* sufficient for a production installation.
"""
- manage(c, f'runserver {address}', pty=True)
+ cmd = f'runserver {address}'
+
+ if no_reload:
+ cmd += ' --noreload'
+
+ if no_threading:
+ cmd += ' --nothreading'
+
+ manage(c, cmd, pty=True)
@task(pre=[wait])