mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-09 08:31:24 +00:00
Merge branch 'master' into custom-states
This commit is contained in:
@@ -1,13 +1,23 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 280
|
||||
INVENTREE_API_VERSION = 283
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v283 - 2024-11-20 : https://github.com/inventree/InvenTree/pull/8524
|
||||
- Adds "note" field to the PartRelated API endpoint
|
||||
|
||||
v282 - 2024-11-19 : https://github.com/inventree/InvenTree/pull/8487
|
||||
- Remove the "test statistics" API endpoints
|
||||
- This is now provided via a custom plugin
|
||||
|
||||
v281 - 2024-11-15 : https://github.com/inventree/InvenTree/pull/8480
|
||||
- Fixes StockHistory API data serialization
|
||||
|
||||
v280 - 2024-11-10 : https://github.com/inventree/InvenTree/pull/8461
|
||||
- Makes schema for API information endpoint more informing
|
||||
- Removes general not found endpoint
|
||||
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -152,7 +153,10 @@ class DatePickerFormField(forms.DateField):
|
||||
def round_decimal(value, places, normalize=False):
|
||||
"""Round value to the specified number of places."""
|
||||
if type(value) in [Decimal, float]:
|
||||
value = round(value, places)
|
||||
try:
|
||||
value = round(value, places)
|
||||
except Exception:
|
||||
raise ValidationError(_('Invalid decimal value') + f' ({value})')
|
||||
|
||||
if normalize:
|
||||
# Remove any trailing zeroes
|
||||
|
||||
@@ -62,7 +62,7 @@ class ClassValidationMixin:
|
||||
|
||||
if len(missing_attributes) > 0:
|
||||
errors.append(
|
||||
f"did not provide the following attributes: {', '.join(missing_attributes)}"
|
||||
f'did not provide the following attributes: {", ".join(missing_attributes)}'
|
||||
)
|
||||
if len(missing_overrides) > 0:
|
||||
missing_overrides_list = []
|
||||
@@ -75,7 +75,7 @@ class ClassValidationMixin:
|
||||
else:
|
||||
missing_overrides_list.append(base_implementation.__name__)
|
||||
errors.append(
|
||||
f"did not override the required attributes: {', '.join(missing_overrides_list)}"
|
||||
f'did not override the required attributes: {", ".join(missing_overrides_list)}'
|
||||
)
|
||||
|
||||
if len(errors) > 0:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import io
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
@@ -179,12 +180,12 @@ def download_image_from_url(remote_url, timeout=2.5):
|
||||
|
||||
|
||||
def render_currency(
|
||||
money,
|
||||
decimal_places=None,
|
||||
currency=None,
|
||||
min_decimal_places=None,
|
||||
max_decimal_places=None,
|
||||
include_symbol=True,
|
||||
money: Money,
|
||||
decimal_places: Optional[int] = None,
|
||||
currency: Optional[str] = None,
|
||||
min_decimal_places: Optional[int] = None,
|
||||
max_decimal_places: Optional[int] = None,
|
||||
include_symbol: bool = True,
|
||||
):
|
||||
"""Render a currency / Money object to a formatted string (e.g. for reports).
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from django.core.validators import URLValidator
|
||||
from django.http import Http404
|
||||
|
||||
import pytz
|
||||
import structlog
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from InvenTree.cache import get_cache_config, is_global_cache_enabled
|
||||
@@ -85,36 +86,92 @@ ENABLE_CLASSIC_FRONTEND = get_boolean_setting(
|
||||
# Disable CUI parts if CUI tests are disabled
|
||||
if TESTING and '--exclude-tag=cui' in sys.argv:
|
||||
ENABLE_CLASSIC_FRONTEND = False
|
||||
|
||||
ENABLE_PLATFORM_FRONTEND = get_boolean_setting(
|
||||
'INVENTREE_PLATFORM_FRONTEND', 'platform_frontend', True
|
||||
)
|
||||
|
||||
# Configure logging settings
|
||||
log_level = get_setting('INVENTREE_LOG_LEVEL', 'log_level', 'WARNING')
|
||||
LOG_LEVEL = get_setting('INVENTREE_LOG_LEVEL', 'log_level', 'WARNING')
|
||||
JSON_LOG = get_boolean_setting('INVENTREE_JSON_LOG', 'json_log', False)
|
||||
WRITE_LOG = get_boolean_setting('INVENTREE_WRITE_LOG', 'write_log', False)
|
||||
|
||||
logging.basicConfig(level=log_level, format='%(asctime)s %(levelname)s %(message)s')
|
||||
|
||||
if log_level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
|
||||
log_level = 'WARNING' # pragma: no cover
|
||||
logging.basicConfig(level=LOG_LEVEL, format='%(asctime)s %(levelname)s %(message)s')
|
||||
|
||||
if LOG_LEVEL not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
|
||||
LOG_LEVEL = 'WARNING' # pragma: no cover
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {'console': {'class': 'logging.StreamHandler'}},
|
||||
'root': {'handlers': ['console'], 'level': log_level},
|
||||
'filters': {
|
||||
'require_not_maintenance_mode_503': {
|
||||
'()': 'maintenance_mode.logging.RequireNotMaintenanceMode503'
|
||||
}
|
||||
},
|
||||
'formatters': {
|
||||
'json_formatter': {
|
||||
'()': structlog.stdlib.ProcessorFormatter,
|
||||
'processor': structlog.processors.JSONRenderer(),
|
||||
},
|
||||
'plain_console': {
|
||||
'()': structlog.stdlib.ProcessorFormatter,
|
||||
'processor': structlog.dev.ConsoleRenderer(),
|
||||
},
|
||||
'key_value': {
|
||||
'()': structlog.stdlib.ProcessorFormatter,
|
||||
'processor': structlog.processors.KeyValueRenderer(
|
||||
key_order=['timestamp', 'level', 'event', 'logger']
|
||||
),
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {'class': 'logging.StreamHandler', 'formatter': 'plain_console'}
|
||||
},
|
||||
'loggers': {
|
||||
'django_structlog': {'handlers': ['console'], 'level': LOG_LEVEL},
|
||||
'inventree': {'handlers': ['console'], 'level': LOG_LEVEL},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Add handlers
|
||||
if WRITE_LOG and JSON_LOG: # pragma: no cover
|
||||
LOGGING['handlers']['log_file'] = {
|
||||
'class': 'logging.handlers.WatchedFileHandler',
|
||||
'filename': str(BASE_DIR.joinpath('logs.json')),
|
||||
'formatter': 'json_formatter',
|
||||
}
|
||||
LOGGING['loggers']['django_structlog']['handlers'] += ['log_file']
|
||||
elif WRITE_LOG: # pragma: no cover
|
||||
LOGGING['handlers']['log_file'] = {
|
||||
'class': 'logging.handlers.WatchedFileHandler',
|
||||
'filename': str(BASE_DIR.joinpath('logs.log')),
|
||||
'formatter': 'key_value',
|
||||
}
|
||||
LOGGING['loggers']['django_structlog']['handlers'] += ['log_file']
|
||||
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.stdlib.filter_by_level,
|
||||
structlog.processors.TimeStamper(fmt='iso'),
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.format_exc_info,
|
||||
structlog.processors.UnicodeDecoder(),
|
||||
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
||||
],
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
# Optionally add database-level logging
|
||||
if get_setting('INVENTREE_DB_LOGGING', 'db_logging', False):
|
||||
LOGGING['loggers'] = {'django.db.backends': {'level': log_level or 'DEBUG'}}
|
||||
LOGGING['loggers'] = {'django.db.backends': {'level': LOG_LEVEL or 'DEBUG'}}
|
||||
|
||||
# Get a logger instance for this setup file
|
||||
logger = logging.getLogger('inventree')
|
||||
logger = structlog.getLogger('inventree')
|
||||
|
||||
# Load SECRET_KEY
|
||||
SECRET_KEY = config.get_secret_key()
|
||||
@@ -160,7 +217,8 @@ PLUGIN_RETRY = get_setting(
|
||||
'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
|
||||
) # How often should plugin loading be tried?
|
||||
|
||||
PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
|
||||
# Hash of the plugin file (will be updated on each change)
|
||||
PLUGIN_FILE_HASH = ''
|
||||
|
||||
STATICFILES_DIRS = []
|
||||
|
||||
@@ -264,6 +322,7 @@ INSTALLED_APPS = [
|
||||
'dbbackup', # Backups - django-dbbackup
|
||||
'taggit', # Tagging
|
||||
'flags', # Flagging - django-flags
|
||||
'django_structlog', # Structured logging
|
||||
'allauth', # Base app for SSO
|
||||
'allauth.account', # Extend user with accounts
|
||||
'allauth.socialaccount', # Use 'social' providers
|
||||
@@ -299,6 +358,7 @@ MIDDLEWARE = CONFIG.get(
|
||||
'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA
|
||||
'maintenance_mode.middleware.MaintenanceModeMiddleware',
|
||||
'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting
|
||||
'django_structlog.middlewares.RequestMiddleware', # Structured logging
|
||||
],
|
||||
)
|
||||
|
||||
@@ -360,8 +420,8 @@ AUTHENTICATION_BACKENDS = CONFIG.get(
|
||||
# LDAP support
|
||||
LDAP_AUTH = get_boolean_setting('INVENTREE_LDAP_ENABLED', 'ldap.enabled', False)
|
||||
if LDAP_AUTH:
|
||||
import django_auth_ldap.config
|
||||
import ldap
|
||||
from django_auth_ldap.config import GroupOfUniqueNamesType, LDAPSearch
|
||||
|
||||
AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend')
|
||||
|
||||
@@ -412,7 +472,7 @@ if LDAP_AUTH:
|
||||
AUTH_LDAP_BIND_PASSWORD = get_setting(
|
||||
'INVENTREE_LDAP_BIND_PASSWORD', 'ldap.bind_password'
|
||||
)
|
||||
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
||||
AUTH_LDAP_USER_SEARCH = django_auth_ldap.config.LDAPSearch(
|
||||
get_setting('INVENTREE_LDAP_SEARCH_BASE_DN', 'ldap.search_base_dn'),
|
||||
ldap.SCOPE_SUBTREE,
|
||||
str(
|
||||
@@ -439,12 +499,38 @@ if LDAP_AUTH:
|
||||
'INVENTREE_LDAP_CACHE_TIMEOUT', 'ldap.cache_timeout', 3600, int
|
||||
)
|
||||
|
||||
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
|
||||
AUTH_LDAP_MIRROR_GROUPS = get_boolean_setting(
|
||||
'INVENTREE_LDAP_MIRROR_GROUPS', 'ldap.mirror_groups', False
|
||||
)
|
||||
AUTH_LDAP_GROUP_OBJECT_CLASS = get_setting(
|
||||
'INVENTREE_LDAP_GROUP_OBJECT_CLASS',
|
||||
'ldap.group_object_class',
|
||||
'groupOfUniqueNames',
|
||||
str,
|
||||
)
|
||||
AUTH_LDAP_GROUP_SEARCH = django_auth_ldap.config.LDAPSearch(
|
||||
get_setting('INVENTREE_LDAP_GROUP_SEARCH', 'ldap.group_search'),
|
||||
ldap.SCOPE_SUBTREE,
|
||||
'(objectClass=groupOfUniqueNames)',
|
||||
f'(objectClass={AUTH_LDAP_GROUP_OBJECT_CLASS})',
|
||||
)
|
||||
AUTH_LDAP_GROUP_TYPE_CLASS = get_setting(
|
||||
'INVENTREE_LDAP_GROUP_TYPE_CLASS',
|
||||
'ldap.group_type_class',
|
||||
'GroupOfUniqueNamesType',
|
||||
str,
|
||||
)
|
||||
AUTH_LDAP_GROUP_TYPE_CLASS_ARGS = get_setting(
|
||||
'INVENTREE_LDAP_GROUP_TYPE_CLASS_ARGS', 'ldap.group_type_class_args', [], list
|
||||
)
|
||||
AUTH_LDAP_GROUP_TYPE_CLASS_KWARGS = get_setting(
|
||||
'INVENTREE_LDAP_GROUP_TYPE_CLASS_KWARGS',
|
||||
'ldap.group_type_class_kwargs',
|
||||
{'name_attr': 'cn'},
|
||||
dict,
|
||||
)
|
||||
AUTH_LDAP_GROUP_TYPE = getattr(django_auth_ldap.config, AUTH_LDAP_GROUP_TYPE_CLASS)(
|
||||
*AUTH_LDAP_GROUP_TYPE_CLASS_ARGS, **AUTH_LDAP_GROUP_TYPE_CLASS_KWARGS
|
||||
)
|
||||
AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType(name_attr='cn')
|
||||
AUTH_LDAP_REQUIRE_GROUP = get_setting(
|
||||
'INVENTREE_LDAP_REQUIRE_GROUP', 'ldap.require_group'
|
||||
)
|
||||
|
||||
@@ -1112,8 +1112,3 @@ a {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.test-statistics-table-total-row {
|
||||
font-weight: bold;
|
||||
border-top-style: double;
|
||||
}
|
||||
|
||||
@@ -667,3 +667,15 @@ def admin_url(user, table, pk):
|
||||
pass
|
||||
|
||||
return url
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def cui_enabled():
|
||||
"""Return True if the CUI is enabled."""
|
||||
return settings.ENABLE_CLASSIC_FRONTEND
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def pui_enabled():
|
||||
"""Return True if the PUI is enabled."""
|
||||
return settings.ENABLE_PLATFORM_FRONTEND
|
||||
|
||||
@@ -1034,14 +1034,14 @@ class TestVersionNumber(TestCase):
|
||||
# Check that the current .git values work too
|
||||
|
||||
git_hash = str(
|
||||
subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8'
|
||||
subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']), 'utf-8'
|
||||
).strip()
|
||||
|
||||
# On some systems the hash is a different length, so just check the first 6 characters
|
||||
self.assertEqual(git_hash[:6], version.inventreeCommitHash()[:6])
|
||||
|
||||
d = (
|
||||
str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8')
|
||||
str(subprocess.check_output(['git', 'show', '-s', '--format=%ci']), 'utf-8')
|
||||
.strip()
|
||||
.split(' ')[0]
|
||||
)
|
||||
@@ -1184,18 +1184,8 @@ class TestSettings(InvenTreeTestCase):
|
||||
"""Test if install of plugins on startup works."""
|
||||
from plugin import registry
|
||||
|
||||
if not settings.DOCKER:
|
||||
# Check an install run
|
||||
response = registry.install_plugin_file()
|
||||
self.assertEqual(response, 'first_run')
|
||||
|
||||
# Set dynamic setting to True and rerun to launch install
|
||||
InvenTreeSetting.set_setting('PLUGIN_ON_STARTUP', True, self.user)
|
||||
registry.reload_plugins(full_reload=True)
|
||||
|
||||
# Check that there was another run
|
||||
response = registry.install_plugin_file()
|
||||
self.assertEqual(response, True)
|
||||
registry.reload_plugins(full_reload=True, collect=True)
|
||||
self.assertGreater(len(settings.PLUGIN_FILE_HASH), 0)
|
||||
|
||||
def test_helpers_cfg_file(self):
|
||||
"""Test get_config_file."""
|
||||
|
||||
@@ -35,7 +35,6 @@ from company.urls import company_urls, manufacturer_part_urls, supplier_part_url
|
||||
from order.urls import order_urls
|
||||
from part.urls import part_urls
|
||||
from plugin.urls import get_plugin_urls
|
||||
from stock.api import test_statistics_api_urls
|
||||
from stock.urls import stock_urls
|
||||
from web.urls import api_urls as web_api_urls
|
||||
from web.urls import urlpatterns as platform_urls
|
||||
@@ -110,7 +109,6 @@ apipatterns = [
|
||||
),
|
||||
]),
|
||||
),
|
||||
path('test-statistics/', include(test_statistics_api_urls)),
|
||||
path('user/', include(users.api.user_urls)),
|
||||
path('web/', include(web_api_urls)),
|
||||
# Plugin endpoints
|
||||
@@ -506,14 +504,14 @@ urlpatterns.append(
|
||||
)
|
||||
)
|
||||
|
||||
# Send any unknown URLs to the parts page
|
||||
# Send any unknown URLs to the index page
|
||||
urlpatterns += [
|
||||
re_path(
|
||||
r'^.*$',
|
||||
RedirectView.as_view(
|
||||
url='/index/'
|
||||
if settings.ENABLE_CLASSIC_FRONTEND
|
||||
else settings.FRONTEND_URL_BASE,
|
||||
else f'/{settings.FRONTEND_URL_BASE}/',
|
||||
permanent=False,
|
||||
),
|
||||
name='index',
|
||||
|
||||
@@ -333,11 +333,6 @@ src="{% static 'img/blank_image.png' %}"
|
||||
});
|
||||
});
|
||||
|
||||
{% if build.part.testable %}
|
||||
onPanelLoad("test-statistics", function() {
|
||||
prepareTestStatisticsTable('build', '{% url "api-test-statistics-by-build" build.pk %}')
|
||||
});
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -267,21 +267,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-hidden' id='panel-test-statistics'>
|
||||
<div class='panel-heading'>
|
||||
<h4>
|
||||
{% trans "Build test statistics" %}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
<div id='teststatistics-button-toolbar'>
|
||||
{% include "filter_list.html" with id="buildteststatistics" %}
|
||||
</div>
|
||||
{% include "test_statistics_table.html" with prefix="build-" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-hidden' id='panel-attachments'>
|
||||
<div class='panel-heading'>
|
||||
<div class='d-flex flex-wrap'>
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
{% trans "Child Build Orders" as text %}
|
||||
{% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %}
|
||||
{% if build.part.testable %}
|
||||
{% trans "Test Statistics" as text %}
|
||||
{% include "sidebar_item.html" with label='test-statistics' text=text icon="fa-chart-line" %}
|
||||
{% endif %}
|
||||
{% trans "Attachments" as text %}
|
||||
{% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %}
|
||||
|
||||
@@ -42,8 +42,13 @@ debug_shell: False
|
||||
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||
log_level: WARNING
|
||||
|
||||
# Configure if logs should be output in JSON format
|
||||
# Use environment variable INVENTREE_JSON_LOG
|
||||
json_log: False
|
||||
# Enable database-level logging, or use the environment variable INVENTREE_DB_LOGGING
|
||||
db_logging: False
|
||||
# Enable writing a log file, or use the environment variable INVENTREE_WRITE_LOG
|
||||
write_log: False
|
||||
|
||||
# Select default system language , or use the environment variable INVENTREE_LANGUAGE
|
||||
language: en-us
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -260,7 +260,7 @@ class BaseMachineType(
|
||||
error_parts.append(
|
||||
f'{config_type.name} settings: ' + ', '.join(missing)
|
||||
)
|
||||
self.handle_error(f"Missing {' and '.join(error_parts)}")
|
||||
self.handle_error(f'Missing {" and ".join(error_parts)}')
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
@@ -112,7 +112,7 @@ class MachineConfig(models.Model):
|
||||
"""Get machine errors for django admin interface."""
|
||||
return format_html_join(
|
||||
mark_safe('<br>'), '{}', ((str(error),) for error in self.errors)
|
||||
) or mark_safe(f"<i>{_('No errors')}</i>")
|
||||
) or mark_safe(f'<i>{_("No errors")}</i>')
|
||||
|
||||
@admin.display(description=_('Machine status'))
|
||||
def get_machine_status(self):
|
||||
|
||||
@@ -470,7 +470,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of this PurchaseOrder."""
|
||||
return f"{self.reference} - {self.supplier.name if self.supplier else _('deleted')}"
|
||||
return f'{self.reference} - {self.supplier.name if self.supplier else _("deleted")}'
|
||||
|
||||
reference = models.CharField(
|
||||
unique=True,
|
||||
@@ -996,7 +996,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of this SalesOrder."""
|
||||
return f"{self.reference} - {self.customer.name if self.customer else _('deleted')}"
|
||||
return f'{self.reference} - {self.customer.name if self.customer else _("deleted")}'
|
||||
|
||||
reference = models.CharField(
|
||||
unique=True,
|
||||
@@ -2194,7 +2194,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of this ReturnOrder."""
|
||||
return f"{self.reference} - {self.customer.name if self.customer else _('no customer')}"
|
||||
return f'{self.reference} - {self.customer.name if self.customer else _("no customer")}'
|
||||
|
||||
reference = models.CharField(
|
||||
unique=True,
|
||||
|
||||
@@ -1720,7 +1720,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
|
||||
|
||||
line_item = data['line_item']
|
||||
stock_items = data['stock_items']
|
||||
shipment = data['shipment']
|
||||
shipment = data.get('shipment', None)
|
||||
|
||||
allocations = []
|
||||
|
||||
@@ -1758,10 +1758,10 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
|
||||
"""Run validation against the provided shipment instance."""
|
||||
order = self.context['order']
|
||||
|
||||
if shipment.shipment_date is not None:
|
||||
if shipment and shipment.shipment_date is not None:
|
||||
raise ValidationError(_('Shipment has already been shipped'))
|
||||
|
||||
if shipment.order != order:
|
||||
if shipment and shipment.order != order:
|
||||
raise ValidationError(_('Shipment is not associated with this order'))
|
||||
|
||||
return shipment
|
||||
|
||||
@@ -1427,37 +1427,50 @@ class PartDetail(PartMixin, RetrieveUpdateDestroyAPI):
|
||||
return response
|
||||
|
||||
|
||||
class PartRelatedList(ListCreateAPI):
|
||||
"""API endpoint for accessing a list of PartRelated objects."""
|
||||
class PartRelatedFilter(rest_filters.FilterSet):
|
||||
"""FilterSet for PartRelated objects."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = PartRelated
|
||||
fields = ['part_1', 'part_2']
|
||||
|
||||
part = rest_filters.ModelChoiceFilter(
|
||||
queryset=Part.objects.all(), method='filter_part', label=_('Part')
|
||||
)
|
||||
|
||||
def filter_part(self, queryset, name, part):
|
||||
"""Filter queryset to include only PartRelated objects which reference the specified part."""
|
||||
return queryset.filter(Q(part_1=part) | Q(part_2=part)).distinct()
|
||||
|
||||
|
||||
class PartRelatedMixin:
|
||||
"""Mixin class for PartRelated API endpoints."""
|
||||
|
||||
queryset = PartRelated.objects.all()
|
||||
serializer_class = part_serializers.PartRelationSerializer
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Custom queryset filtering."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
"""Return an annotated queryset for the PartRelatedDetail endpoint."""
|
||||
queryset = super().get_queryset(*args, **kwargs)
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
# Add a filter for "part" - we can filter either part_1 or part_2
|
||||
part = params.get('part', None)
|
||||
|
||||
if part is not None:
|
||||
try:
|
||||
part = Part.objects.get(pk=part)
|
||||
queryset = queryset.filter(Q(part_1=part) | Q(part_2=part)).distinct()
|
||||
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
queryset = queryset.prefetch_related('part_1', 'part_2')
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class PartRelatedDetail(RetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for accessing detail view of a PartRelated object."""
|
||||
class PartRelatedList(PartRelatedMixin, ListCreateAPI):
|
||||
"""API endpoint for accessing a list of PartRelated objects."""
|
||||
|
||||
queryset = PartRelated.objects.all()
|
||||
serializer_class = part_serializers.PartRelationSerializer
|
||||
filterset_class = PartRelatedFilter
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
search_fields = ['part_1__name', 'part_2__name']
|
||||
|
||||
|
||||
class PartRelatedDetail(PartRelatedMixin, RetrieveUpdateDestroyAPI):
|
||||
"""API endpoint for accessing detail view of a PartRelated object."""
|
||||
|
||||
|
||||
class PartParameterTemplateFilter(rest_filters.FilterSet):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.16 on 2024-11-19 12:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0130_alter_parttesttemplate_part'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='partrelated',
|
||||
name='note',
|
||||
field=models.CharField(blank=True, help_text='Note for this relationship', max_length=500, verbose_name='Note'),
|
||||
),
|
||||
]
|
||||
@@ -4680,6 +4680,13 @@ class PartRelated(InvenTree.models.InvenTreeMetadataModel):
|
||||
help_text=_('Select Related Part'),
|
||||
)
|
||||
|
||||
note = models.CharField(
|
||||
max_length=500,
|
||||
blank=True,
|
||||
verbose_name=_('Note'),
|
||||
help_text=_('Note for this relationship'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of this Part-Part relationship."""
|
||||
return f'{self.part_1} <--> {self.part_2}'
|
||||
|
||||
@@ -1505,7 +1505,7 @@ class PartRelationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Metaclass defining serializer fields."""
|
||||
|
||||
model = PartRelated
|
||||
fields = ['pk', 'part_1', 'part_1_detail', 'part_2', 'part_2_detail']
|
||||
fields = ['pk', 'part_1', 'part_1_detail', 'part_2', 'part_2_detail', 'note']
|
||||
|
||||
part_1_detail = PartSerializer(source='part_1', read_only=True, many=False)
|
||||
part_2_detail = PartSerializer(source='part_2', read_only=True, many=False)
|
||||
|
||||
@@ -100,22 +100,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-hidden' id='panel-test-statistics'>
|
||||
<div class='panel-heading'>
|
||||
<div class='d-flex flex-wrap'>
|
||||
<h4>{% trans "Part Test Statistics" %}</h4>
|
||||
{% include "spacer.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<div id='teststatistics-button-toolbar'>
|
||||
{% include "filter_list.html" with id="partteststatistics" %}
|
||||
</div>
|
||||
|
||||
{% include "test_statistics_table.html" with prefix="part-" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-hidden' id='panel-purchase-orders'>
|
||||
<div class='panel-heading'>
|
||||
<div class='d-flex flex-wrap'>
|
||||
@@ -767,9 +751,6 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
onPanelLoad("test-statistics", function() {
|
||||
prepareTestStatisticsTable('part', '{% url "api-test-statistics-by-part" part.pk %}')
|
||||
});
|
||||
|
||||
onPanelLoad("part-stock", function() {
|
||||
$('#new-stock-item').click(function () {
|
||||
|
||||
@@ -53,8 +53,6 @@
|
||||
{% if part.testable %}
|
||||
{% trans "Test Templates" as text %}
|
||||
{% include "sidebar_item.html" with label="test-templates" text=text icon="fa-vial" %}
|
||||
{% trans "Test Statistics" as text %}
|
||||
{% include "sidebar_item.html" with label="test-statistics" text=text icon="fa-chart-line" %}
|
||||
{% endif %}
|
||||
{% if show_related %}
|
||||
{% trans "Related Parts" as text %}
|
||||
|
||||
@@ -295,7 +295,7 @@ class PartImport(FileManagementFormView):
|
||||
|
||||
# Set alerts
|
||||
if import_done:
|
||||
alert = f"<strong>{_('Part-Import')}</strong><br>{_(f'Imported {import_done} parts')}"
|
||||
alert = f'<strong>{_("Part-Import")}</strong><br>{_(f"Imported {import_done} parts")}'
|
||||
messages.success(self.request, alert)
|
||||
if import_error:
|
||||
error_text = '\n'.join([
|
||||
@@ -304,7 +304,7 @@ class PartImport(FileManagementFormView):
|
||||
])
|
||||
messages.error(
|
||||
self.request,
|
||||
f"<strong>{_('Some errors occurred:')}</strong><br><ul>{error_text}</ul>",
|
||||
f'<strong>{_("Some errors occurred:")}</strong><br><ul>{error_text}</ul>',
|
||||
)
|
||||
|
||||
return HttpResponseRedirect(reverse('part-index'))
|
||||
|
||||
@@ -23,8 +23,6 @@ class PluginAppConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
"""The ready method is extended to initialize plugins."""
|
||||
# skip loading if we run in a background thread
|
||||
|
||||
if not isInMainThread() and not isInWorkerThread():
|
||||
return
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from InvenTree.tasks import offload_task
|
||||
from plugin.registry import registry
|
||||
from plugin.registry import call_plugin_function, registry
|
||||
from stock.models import StockItem, StockLocation
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class LocatePluginView(GenericAPIView):
|
||||
StockItem.objects.get(pk=item_pk)
|
||||
|
||||
offload_task(
|
||||
registry.call_plugin_function,
|
||||
call_plugin_function,
|
||||
plugin,
|
||||
'locate_stock_item',
|
||||
item_pk,
|
||||
@@ -78,7 +78,7 @@ class LocatePluginView(GenericAPIView):
|
||||
StockLocation.objects.get(pk=location_pk)
|
||||
|
||||
offload_task(
|
||||
registry.call_plugin_function,
|
||||
call_plugin_function,
|
||||
plugin,
|
||||
'locate_stock_location',
|
||||
location_pk,
|
||||
|
||||
@@ -177,7 +177,17 @@ def get_modules(pkg, path=None):
|
||||
elif type(path) is not list:
|
||||
path = [path]
|
||||
|
||||
for finder, name, _ in pkgutil.walk_packages(path):
|
||||
packages = pkgutil.walk_packages(path)
|
||||
|
||||
while True:
|
||||
try:
|
||||
finder, name, _ = next(packages)
|
||||
except StopIteration:
|
||||
break
|
||||
except Exception as error:
|
||||
log_error({pkg.__name__: str(error)}, 'discovery')
|
||||
continue
|
||||
|
||||
try:
|
||||
if sys.version_info < (3, 12):
|
||||
module = finder.find_module(name).load_module(name)
|
||||
@@ -202,9 +212,13 @@ def get_modules(pkg, path=None):
|
||||
return [v for k, v in context.items()]
|
||||
|
||||
|
||||
def get_classes(module):
|
||||
def get_classes(module) -> list:
|
||||
"""Get all classes in a given module."""
|
||||
return inspect.getmembers(module, inspect.isclass)
|
||||
try:
|
||||
return inspect.getmembers(module, inspect.isclass)
|
||||
except Exception:
|
||||
log_error({module.__name__: 'Could not get classes'}, 'discovery')
|
||||
return []
|
||||
|
||||
|
||||
def get_plugins(pkg, baseclass, path=None):
|
||||
|
||||
@@ -19,12 +19,15 @@ logger = logging.getLogger('inventree')
|
||||
def pip_command(*args):
|
||||
"""Build and run a pip command using using the current python executable.
|
||||
|
||||
returns: subprocess.check_output
|
||||
throws: subprocess.CalledProcessError
|
||||
Returns: The output of the pip command
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: If the pip command fails
|
||||
"""
|
||||
python = sys.executable
|
||||
|
||||
command = [python, '-m', 'pip']
|
||||
|
||||
command.extend(args)
|
||||
|
||||
command = [str(x) for x in command]
|
||||
@@ -63,39 +66,55 @@ def handle_pip_error(error, path: str) -> list:
|
||||
raise ValidationError(errors[0])
|
||||
|
||||
|
||||
def check_package_path(packagename: str):
|
||||
"""Determine the install path of a particular package.
|
||||
def get_install_info(packagename: str) -> dict:
|
||||
"""Determine the install information for a particular package.
|
||||
|
||||
- If installed, return the installation path
|
||||
- If not installed, return False
|
||||
- Uses 'pip show' to determine the install location of a package.
|
||||
"""
|
||||
logger.debug('check_package_path: %s', packagename)
|
||||
logger.debug('get_install_info: %s', packagename)
|
||||
|
||||
# Remove version information
|
||||
for c in '<>=! ':
|
||||
for c in '<>=!@ ':
|
||||
packagename = packagename.split(c)[0]
|
||||
|
||||
info = {}
|
||||
|
||||
try:
|
||||
result = pip_command('show', packagename)
|
||||
|
||||
output = result.decode('utf-8').split('\n')
|
||||
|
||||
for line in output:
|
||||
# Check if line matches pattern "Location: ..."
|
||||
match = re.match(r'^Location:\s+(.+)$', line.strip())
|
||||
parts = line.split(':')
|
||||
|
||||
if match:
|
||||
return match.group(1)
|
||||
if len(parts) >= 2:
|
||||
key = str(parts[0].strip().lower().replace('-', '_'))
|
||||
value = str(parts[1].strip())
|
||||
|
||||
info[key] = value
|
||||
|
||||
except subprocess.CalledProcessError as error:
|
||||
log_error('check_package_path')
|
||||
log_error('get_install_info')
|
||||
|
||||
output = error.output.decode('utf-8')
|
||||
info['error'] = output
|
||||
logger.exception('Plugin lookup failed: %s', str(output))
|
||||
return False
|
||||
|
||||
# If we get here, the package is not installed
|
||||
return False
|
||||
return info
|
||||
|
||||
|
||||
def plugins_file_hash():
|
||||
"""Return the file hash for the plugins file."""
|
||||
import hashlib
|
||||
|
||||
pf = settings.PLUGIN_FILE
|
||||
|
||||
if not pf or not pf.exists():
|
||||
return None
|
||||
|
||||
with pf.open('rb') as f:
|
||||
# Note: Once we support 3.11 as a minimum, we can use hashlib.file_digest
|
||||
return hashlib.sha256(f.read()).hexdigest()
|
||||
|
||||
|
||||
def install_plugins_file():
|
||||
@@ -108,8 +127,10 @@ def install_plugins_file():
|
||||
logger.warning('Plugin file %s does not exist', str(pf))
|
||||
return
|
||||
|
||||
cmd = ['install', '--disable-pip-version-check', '-U', '-r', str(pf)]
|
||||
|
||||
try:
|
||||
pip_command('install', '-r', str(pf))
|
||||
pip_command(*cmd)
|
||||
except subprocess.CalledProcessError as error:
|
||||
output = error.output.decode('utf-8')
|
||||
logger.exception('Plugin file installation failed: %s', str(output))
|
||||
@@ -120,17 +141,25 @@ def install_plugins_file():
|
||||
log_error('pip')
|
||||
return False
|
||||
|
||||
# Update static files
|
||||
# Collect plugin static files
|
||||
plugin.staticfiles.collect_plugins_static_files()
|
||||
plugin.staticfiles.clear_plugins_static_files()
|
||||
|
||||
# At this point, the plugins file has been installed
|
||||
return True
|
||||
|
||||
|
||||
def update_plugins_file(install_name, remove=False):
|
||||
def update_plugins_file(install_name, full_package=None, version=None, remove=False):
|
||||
"""Add a plugin to the plugins file."""
|
||||
logger.info('Adding plugin to plugins file: %s', install_name)
|
||||
if remove:
|
||||
logger.info('Removing plugin from plugins file: %s', install_name)
|
||||
else:
|
||||
logger.info('Adding plugin to plugins file: %s', install_name)
|
||||
|
||||
# If a full package name is provided, use that instead
|
||||
if full_package and full_package != install_name:
|
||||
new_value = full_package
|
||||
else:
|
||||
new_value = f'{install_name}=={version}' if version else install_name
|
||||
|
||||
pf = settings.PLUGIN_FILE
|
||||
|
||||
@@ -140,7 +169,7 @@ def update_plugins_file(install_name, remove=False):
|
||||
|
||||
def compare_line(line: str):
|
||||
"""Check if a line in the file matches the installname."""
|
||||
return line.strip().split('==')[0] == install_name.split('==')[0]
|
||||
return re.match(rf'^{install_name}[\s=@]', line.strip())
|
||||
|
||||
# First, read in existing plugin file
|
||||
try:
|
||||
@@ -166,13 +195,13 @@ def update_plugins_file(install_name, remove=False):
|
||||
found = True
|
||||
if not remove:
|
||||
# Replace line with new install name
|
||||
output.append(install_name)
|
||||
output.append(new_value)
|
||||
else:
|
||||
output.append(line)
|
||||
|
||||
# Append plugin to file
|
||||
if not found and not remove:
|
||||
output.append(install_name)
|
||||
output.append(new_value)
|
||||
|
||||
# Write file back to disk
|
||||
try:
|
||||
@@ -203,15 +232,8 @@ def install_plugin(url=None, packagename=None, user=None, version=None):
|
||||
|
||||
logger.info('install_plugin: %s, %s', url, packagename)
|
||||
|
||||
# Check if we are running in a virtual environment
|
||||
# For now, just log a warning
|
||||
in_venv = sys.prefix != sys.base_prefix
|
||||
|
||||
if not in_venv:
|
||||
logger.warning('InvenTree is not running in a virtual environment')
|
||||
|
||||
# build up the command
|
||||
install_name = ['install', '-U']
|
||||
install_name = ['install', '-U', '--disable-pip-version-check']
|
||||
|
||||
full_pkg = ''
|
||||
|
||||
@@ -246,23 +268,25 @@ def install_plugin(url=None, packagename=None, user=None, version=None):
|
||||
ret['result'] = ret['success'] = _('Installed plugin successfully')
|
||||
ret['output'] = str(result, 'utf-8')
|
||||
|
||||
if packagename and (path := check_package_path(packagename)):
|
||||
# Override result information
|
||||
ret['result'] = _(f'Installed plugin into {path}')
|
||||
if packagename and (info := get_install_info(packagename)):
|
||||
if path := info.get('location'):
|
||||
ret['result'] = _(f'Installed plugin into {path}')
|
||||
ret['version'] = info.get('version')
|
||||
|
||||
except subprocess.CalledProcessError as error:
|
||||
handle_pip_error(error, 'plugin_install')
|
||||
|
||||
# Save plugin to plugins file
|
||||
update_plugins_file(full_pkg)
|
||||
if version := ret.get('version'):
|
||||
# Save plugin to plugins file
|
||||
update_plugins_file(packagename, full_package=full_pkg, version=version)
|
||||
|
||||
# Reload the plugin registry, to discover the new plugin
|
||||
from plugin.registry import registry
|
||||
# Reload the plugin registry, to discover the new plugin
|
||||
from plugin.registry import registry
|
||||
|
||||
registry.reload_plugins(full_reload=True, force_reload=True, collect=True)
|
||||
registry.reload_plugins(full_reload=True, force_reload=True, collect=True)
|
||||
|
||||
# Update static files
|
||||
plugin.staticfiles.collect_plugins_static_files()
|
||||
# Update static files
|
||||
plugin.staticfiles.collect_plugins_static_files()
|
||||
|
||||
return ret
|
||||
|
||||
@@ -303,23 +327,24 @@ def uninstall_plugin(cfg: plugin.models.PluginConfig, user=None, delete_config=T
|
||||
_('Plugin cannot be uninstalled as it is currently active')
|
||||
)
|
||||
|
||||
if not cfg.is_installed():
|
||||
raise ValidationError(_('Plugin is not installed'))
|
||||
|
||||
validate_package_plugin(cfg, user)
|
||||
package_name = cfg.package_name
|
||||
logger.info('Uninstalling plugin: %s', package_name)
|
||||
|
||||
cmd = ['uninstall', '-y', package_name]
|
||||
pkg_info = get_install_info(package_name)
|
||||
|
||||
try:
|
||||
result = pip_command(*cmd)
|
||||
|
||||
ret = {
|
||||
'result': _('Uninstalled plugin successfully'),
|
||||
'success': True,
|
||||
'output': str(result, 'utf-8'),
|
||||
}
|
||||
|
||||
except subprocess.CalledProcessError as error:
|
||||
handle_pip_error(error, 'plugin_uninstall')
|
||||
if path := pkg_info.get('location'):
|
||||
# Uninstall the plugin using pip
|
||||
logger.info('Uninstalling plugin: %s from %s', package_name, path)
|
||||
try:
|
||||
pip_command('uninstall', '-y', package_name)
|
||||
except subprocess.CalledProcessError as error:
|
||||
handle_pip_error(error, 'plugin_uninstall')
|
||||
else:
|
||||
# No matching install target found
|
||||
raise ValidationError(_('Plugin installation not found'))
|
||||
|
||||
# Update the plugins file
|
||||
update_plugins_file(package_name, remove=True)
|
||||
@@ -334,4 +359,4 @@ def uninstall_plugin(cfg: plugin.models.PluginConfig, user=None, delete_config=T
|
||||
# Reload the plugin registry
|
||||
registry.reload_plugins(full_reload=True, force_reload=True, collect=True)
|
||||
|
||||
return ret
|
||||
return {'result': _('Uninstalled plugin successfully'), 'success': True}
|
||||
|
||||
@@ -70,7 +70,7 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
|
||||
"""Nice name for printing."""
|
||||
name = f'{self.name} - {self.key}'
|
||||
if not self.active:
|
||||
name += '(not active)'
|
||||
name += ' (not active)'
|
||||
return name
|
||||
|
||||
# extra attributes from the registry
|
||||
|
||||
@@ -239,9 +239,10 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
|
||||
"""File that contains plugin definition."""
|
||||
return Path(inspect.getfile(cls))
|
||||
|
||||
def path(self) -> Path:
|
||||
@classmethod
|
||||
def path(cls) -> Path:
|
||||
"""Path to plugins base folder."""
|
||||
return self.file().parent
|
||||
return cls.file().parent
|
||||
|
||||
def _get_value(self, meta_name: str, package_name: str) -> str:
|
||||
"""Extract values from class meta or package info.
|
||||
|
||||
@@ -159,7 +159,7 @@ class PluginsRegistry:
|
||||
# Update the registry hash value
|
||||
self.update_plugin_hash()
|
||||
|
||||
def call_plugin_function(self, slug, func, *args, **kwargs):
|
||||
def call_plugin_function(self, slug: str, func: str, *args, **kwargs):
|
||||
"""Call a member function (named by 'func') of the plugin named by 'slug'.
|
||||
|
||||
As this is intended to be run by the background worker,
|
||||
@@ -287,6 +287,7 @@ class PluginsRegistry:
|
||||
|
||||
if collect:
|
||||
logger.info('Collecting plugins')
|
||||
self.install_plugin_file()
|
||||
self.plugin_modules = self.collect_plugins()
|
||||
|
||||
self.plugins_loaded = False
|
||||
@@ -365,31 +366,32 @@ class PluginsRegistry:
|
||||
collected_plugins = []
|
||||
|
||||
# Collect plugins from paths
|
||||
for plugin in self.plugin_dirs():
|
||||
logger.debug("Loading plugins from directory '%s'", plugin)
|
||||
for plugin_dir in self.plugin_dirs():
|
||||
logger.debug("Loading plugins from directory '%s'", plugin_dir)
|
||||
|
||||
parent_path = None
|
||||
parent_obj = Path(plugin)
|
||||
parent_obj = Path(plugin_dir)
|
||||
|
||||
# If a "path" is provided, some special handling is required
|
||||
if parent_obj.name is not plugin and len(parent_obj.parts) > 1:
|
||||
if parent_obj.name is not plugin_dir and len(parent_obj.parts) > 1:
|
||||
# Ensure PosixPath object is converted to a string, before passing to get_plugins
|
||||
parent_path = str(parent_obj.parent)
|
||||
plugin = parent_obj.name
|
||||
plugin_dir = parent_obj.name
|
||||
|
||||
# Gather Modules
|
||||
if parent_path:
|
||||
# On python 3.12 use new loader method
|
||||
if sys.version_info < (3, 12):
|
||||
raw_module = _load_source(
|
||||
plugin, str(parent_obj.joinpath('__init__.py'))
|
||||
plugin_dir, str(parent_obj.joinpath('__init__.py'))
|
||||
)
|
||||
else:
|
||||
raw_module = SourceFileLoader(
|
||||
plugin, str(parent_obj.joinpath('__init__.py'))
|
||||
plugin_dir, str(parent_obj.joinpath('__init__.py'))
|
||||
).load_module()
|
||||
else:
|
||||
raw_module = importlib.import_module(plugin)
|
||||
raw_module = importlib.import_module(plugin_dir)
|
||||
|
||||
modules = get_plugins(raw_module, InvenTreePlugin, path=parent_path)
|
||||
|
||||
for item in modules or []:
|
||||
@@ -429,16 +431,13 @@ class PluginsRegistry:
|
||||
|
||||
def install_plugin_file(self):
|
||||
"""Make sure all plugins are installed in the current environment."""
|
||||
if settings.PLUGIN_FILE_CHECKED:
|
||||
logger.info('Plugin file was already checked')
|
||||
return True
|
||||
from plugin.installer import install_plugins_file, plugins_file_hash
|
||||
|
||||
from plugin.installer import install_plugins_file
|
||||
file_hash = plugins_file_hash()
|
||||
|
||||
if install_plugins_file():
|
||||
settings.PLUGIN_FILE_CHECKED = True
|
||||
return 'first_run'
|
||||
return False
|
||||
if file_hash != settings.PLUGIN_FILE_HASH:
|
||||
install_plugins_file()
|
||||
settings.PLUGIN_FILE_HASH = file_hash
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -807,7 +806,7 @@ class PluginsRegistry:
|
||||
registry: PluginsRegistry = PluginsRegistry()
|
||||
|
||||
|
||||
def call_function(plugin_name, function_name, *args, **kwargs):
|
||||
def call_plugin_function(plugin_name: str, function_name: str, *args, **kwargs):
|
||||
"""Global helper function to call a specific member function of a plugin."""
|
||||
return registry.call_plugin_function(plugin_name, function_name, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
|
||||
Normally here the connection to the printer and transfer of the label would take place.
|
||||
"""
|
||||
# Test that the expected kwargs are present
|
||||
print(f"Printing Label: {kwargs['filename']} (User: {kwargs['user']})")
|
||||
print(f'Printing Label: {kwargs["filename"]} (User: {kwargs["user"]})')
|
||||
|
||||
pdf_data = kwargs['pdf_data']
|
||||
png_file = self.render_to_png(
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.test import TestCase
|
||||
from plugin import InvenTreePlugin, registry
|
||||
from plugin.helpers import MixinImplementationError
|
||||
from plugin.mixins import ScheduleMixin
|
||||
from plugin.registry import call_function
|
||||
from plugin.registry import call_plugin_function
|
||||
|
||||
|
||||
class ExampleScheduledTaskPluginTests(TestCase):
|
||||
@@ -67,10 +67,10 @@ class ExampleScheduledTaskPluginTests(TestCase):
|
||||
def test_calling(self):
|
||||
"""Check if a function can be called without errors."""
|
||||
# Check with right parameters
|
||||
self.assertEqual(call_function('schedule', 'member_func'), False)
|
||||
self.assertEqual(call_plugin_function('schedule', 'member_func'), False)
|
||||
|
||||
# Check with wrong key
|
||||
self.assertEqual(call_function('does_not_exist', 'member_func'), None)
|
||||
self.assertEqual(call_plugin_function('does_not_exist', 'member_func'), None)
|
||||
|
||||
|
||||
class ScheduledTaskPluginTests(TestCase):
|
||||
|
||||
@@ -75,7 +75,7 @@ def copy_plugin_static_files(slug, check_reload=True):
|
||||
if not plugin:
|
||||
return
|
||||
|
||||
logger.info("Copying static files for plugin '%s'", slug)
|
||||
logger.info("Collecting static files for plugin '%s'", slug)
|
||||
|
||||
# Get the source path for the plugin
|
||||
source_path = plugin.path().joinpath('static')
|
||||
@@ -114,7 +114,8 @@ def copy_plugin_static_files(slug, check_reload=True):
|
||||
logger.debug('- copied %s to %s', str(item), str(destination_path))
|
||||
copied += 1
|
||||
|
||||
logger.info("Copied %s static files for plugin '%s'.", copied, slug)
|
||||
if copied > 0:
|
||||
logger.info("Copied %s static files for plugin '%s'.", copied, slug)
|
||||
|
||||
|
||||
def clear_plugin_static_files(slug: str, recursive: bool = True):
|
||||
|
||||
@@ -252,7 +252,7 @@ class RegistryTests(TestCase):
|
||||
def test_package_loading(self):
|
||||
"""Test that package distributed plugins work."""
|
||||
# Install sample package
|
||||
subprocess.check_output('pip install inventree-zapier'.split())
|
||||
subprocess.check_output(['pip', 'install', 'inventree-zapier'])
|
||||
|
||||
# Reload to discover plugin
|
||||
registry.reload_plugins(full_reload=True, collect=True)
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional
|
||||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
@@ -28,7 +30,7 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def getindex(container: list, index: int):
|
||||
def getindex(container: list, index: int) -> Any:
|
||||
"""Return the value contained at the specified index of the list.
|
||||
|
||||
This function is provideed to get around template rendering limitations.
|
||||
@@ -55,7 +57,7 @@ def getindex(container: list, index: int):
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def getkey(container: dict, key):
|
||||
def getkey(container: dict, key: str) -> Any:
|
||||
"""Perform key lookup in the provided dict object.
|
||||
|
||||
This function is provided to get around template rendering limitations.
|
||||
@@ -82,7 +84,7 @@ def asset(filename):
|
||||
filename: Asset filename (relative to the 'assets' media directory)
|
||||
|
||||
Raises:
|
||||
FileNotFoundError if file does not exist
|
||||
FileNotFoundError: If file does not exist
|
||||
"""
|
||||
if type(filename) is SafeString:
|
||||
# Prepend an empty string to enforce 'stringiness'
|
||||
@@ -104,30 +106,31 @@ def asset(filename):
|
||||
|
||||
@register.simple_tag()
|
||||
def uploaded_image(
|
||||
filename,
|
||||
replace_missing=True,
|
||||
replacement_file='blank_image.png',
|
||||
validate=True,
|
||||
filename: str,
|
||||
replace_missing: bool = True,
|
||||
replacement_file: str = 'blank_image.png',
|
||||
validate: bool = True,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
rotate: Optional[float] = None,
|
||||
**kwargs,
|
||||
):
|
||||
) -> str:
|
||||
"""Return raw image data from an 'uploaded' image.
|
||||
|
||||
Arguments:
|
||||
filename: The filename of the image relative to the MEDIA_ROOT directory
|
||||
replace_missing: Optionally return a placeholder image if the provided filename does not exist (default = True)
|
||||
replacement_file: The filename of the placeholder image (default = 'blank_image.png')
|
||||
validate: Optionally validate that the file is a valid image file (default = True)
|
||||
|
||||
kwargs:
|
||||
width: Optional width of the image (default = None)
|
||||
height: Optional height of the image (default = None)
|
||||
validate: Optionally validate that the file is a valid image file
|
||||
width: Optional width of the image
|
||||
height: Optional height of the image
|
||||
rotate: Optional rotation to apply to the image
|
||||
|
||||
Returns:
|
||||
Binary image data to be rendered directly in a <img> tag
|
||||
|
||||
Raises:
|
||||
FileNotFoundError if the file does not exist
|
||||
FileNotFoundError: If the file does not exist
|
||||
"""
|
||||
if type(filename) is SafeString:
|
||||
# Prepend an empty string to enforce 'stringiness'
|
||||
@@ -169,9 +172,6 @@ def uploaded_image(
|
||||
# A placeholder image showing that the image is missing
|
||||
img = Image.new('RGB', (64, 64), color='red')
|
||||
|
||||
width = kwargs.get('width')
|
||||
height = kwargs.get('height')
|
||||
|
||||
if width is not None:
|
||||
try:
|
||||
width = int(width)
|
||||
@@ -199,7 +199,7 @@ def uploaded_image(
|
||||
img = img.resize((wsize, height))
|
||||
|
||||
# Optionally rotate the image
|
||||
if rotate := kwargs.get('rotate'):
|
||||
if rotate is not None:
|
||||
try:
|
||||
rotate = int(rotate)
|
||||
img = img.rotate(rotate)
|
||||
@@ -213,7 +213,7 @@ def uploaded_image(
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def encode_svg_image(filename):
|
||||
def encode_svg_image(filename: str) -> str:
|
||||
"""Return a base64-encoded svg image data string."""
|
||||
if type(filename) is SafeString:
|
||||
# Prepend an empty string to enforce 'stringiness'
|
||||
@@ -243,7 +243,7 @@ def encode_svg_image(filename):
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def part_image(part: Part, preview=False, thumbnail=False, **kwargs):
|
||||
def part_image(part: Part, preview: bool = False, thumbnail: bool = False, **kwargs):
|
||||
"""Return a fully-qualified path for a part image.
|
||||
|
||||
Arguments:
|
||||
@@ -252,7 +252,7 @@ def part_image(part: Part, preview=False, thumbnail=False, **kwargs):
|
||||
thumbnail: Return the thumbnail image (default = False)
|
||||
|
||||
Raises:
|
||||
TypeError if provided part is not a Part instance
|
||||
TypeError: If provided part is not a Part instance
|
||||
"""
|
||||
if type(part) is not Part:
|
||||
raise TypeError(_('part_image tag requires a Part instance'))
|
||||
@@ -268,7 +268,7 @@ def part_image(part: Part, preview=False, thumbnail=False, **kwargs):
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def part_parameter(part: Part, parameter_name: str):
|
||||
def part_parameter(part: Part, parameter_name: str) -> str:
|
||||
"""Return a PartParameter object for the given part and parameter name.
|
||||
|
||||
Arguments:
|
||||
@@ -284,7 +284,9 @@ def part_parameter(part: Part, parameter_name: str):
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def company_image(company, preview=False, thumbnail=False, **kwargs):
|
||||
def company_image(
|
||||
company: Company, preview: bool = False, thumbnail: bool = False, **kwargs
|
||||
) -> str:
|
||||
"""Return a fully-qualified path for a company image.
|
||||
|
||||
Arguments:
|
||||
@@ -293,7 +295,7 @@ def company_image(company, preview=False, thumbnail=False, **kwargs):
|
||||
thumbnail: Return the thumbnail image (default = False)
|
||||
|
||||
Raises:
|
||||
TypeError if provided company is not a Company instance
|
||||
TypeError: If provided company is not a Company instance
|
||||
"""
|
||||
if type(company) is not Company:
|
||||
raise TypeError(_('company_image tag requires a Company instance'))
|
||||
@@ -309,7 +311,7 @@ def company_image(company, preview=False, thumbnail=False, **kwargs):
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def logo_image(**kwargs):
|
||||
def logo_image(**kwargs) -> str:
|
||||
"""Return a fully-qualified path for the logo image.
|
||||
|
||||
- If a custom logo has been provided, return a path to that logo
|
||||
@@ -322,7 +324,7 @@ def logo_image(**kwargs):
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def internal_link(link, text):
|
||||
def internal_link(link, text) -> str:
|
||||
"""Make a <a></a> href which points to an InvenTree URL.
|
||||
|
||||
Uses the InvenTree.helpers_model.construct_absolute_url function to build the URL.
|
||||
@@ -396,13 +398,20 @@ def render_html_text(text: str, **kwargs):
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def format_number(number, **kwargs):
|
||||
def format_number(
|
||||
number,
|
||||
decimal_places: Optional[int] = None,
|
||||
integer: bool = False,
|
||||
leading: int = 0,
|
||||
separator: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Render a number with optional formatting options.
|
||||
|
||||
kwargs:
|
||||
Arguments:
|
||||
decimal_places: Number of decimal places to render
|
||||
integer: Boolean, whether to render the number as an integer
|
||||
leading: Number of leading zeros
|
||||
leading: Number of leading zeros (default = 0)
|
||||
separator: Character to use as a thousands separator (default = None)
|
||||
"""
|
||||
try:
|
||||
number = Decimal(str(number))
|
||||
@@ -410,28 +419,28 @@ def format_number(number, **kwargs):
|
||||
# If the number cannot be converted to a Decimal, just return the original value
|
||||
return str(number)
|
||||
|
||||
if kwargs.get('integer', False):
|
||||
if integer:
|
||||
# Convert to integer
|
||||
number = Decimal(int(number))
|
||||
|
||||
# Normalize the number (remove trailing zeroes)
|
||||
number = number.normalize()
|
||||
|
||||
decimals = kwargs.get('decimal_places')
|
||||
|
||||
if decimals is not None:
|
||||
if decimal_places is not None:
|
||||
try:
|
||||
decimals = int(decimals)
|
||||
number = round(number, decimals)
|
||||
decimal_places = int(decimal_places)
|
||||
number = round(number, decimal_places)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Re-encode, and normalize again
|
||||
value = Decimal(number).normalize()
|
||||
value = format(value, 'f')
|
||||
value = str(value)
|
||||
|
||||
leading = kwargs.get('leading')
|
||||
if separator:
|
||||
value = f'{value:,}'
|
||||
value = value.replace(',', separator)
|
||||
else:
|
||||
value = f'{value}'
|
||||
|
||||
if leading is not None:
|
||||
try:
|
||||
@@ -444,37 +453,39 @@ def format_number(number, **kwargs):
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def format_datetime(datetime, timezone=None, fmt=None):
|
||||
def format_datetime(
|
||||
dt: datetime, timezone: Optional[str] = None, fmt: Optional[str] = None
|
||||
):
|
||||
"""Format a datetime object for display.
|
||||
|
||||
Arguments:
|
||||
datetime: The datetime object to format
|
||||
dt: The datetime object to format
|
||||
timezone: The timezone to use for the date (defaults to the server timezone)
|
||||
fmt: The format string to use (defaults to ISO formatting)
|
||||
"""
|
||||
datetime = InvenTree.helpers.to_local_time(datetime, timezone)
|
||||
dt = InvenTree.helpers.to_local_time(dt, timezone)
|
||||
|
||||
if fmt:
|
||||
return datetime.strftime(fmt)
|
||||
return dt.strftime(fmt)
|
||||
else:
|
||||
return datetime.isoformat()
|
||||
return dt.isoformat()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def format_date(date, timezone=None, fmt=None):
|
||||
def format_date(dt: date, timezone: Optional[str] = None, fmt: Optional[str] = None):
|
||||
"""Format a date object for display.
|
||||
|
||||
Arguments:
|
||||
date: The date to format
|
||||
dt: The date to format
|
||||
timezone: The timezone to use for the date (defaults to the server timezone)
|
||||
fmt: The format string to use (defaults to ISO formatting)
|
||||
"""
|
||||
date = InvenTree.helpers.to_local_time(date, timezone).date()
|
||||
dt = InvenTree.helpers.to_local_time(dt, timezone).date()
|
||||
|
||||
if fmt:
|
||||
return date.strftime(fmt)
|
||||
return dt.strftime(fmt)
|
||||
else:
|
||||
return date.isoformat()
|
||||
return dt.isoformat()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
|
||||
@@ -156,6 +156,18 @@ class ReportTagTest(TestCase):
|
||||
self.assertEqual(report_tags.multiply(2.3, 4), 9.2)
|
||||
self.assertEqual(report_tags.divide(100, 5), 20)
|
||||
|
||||
def test_number_tags(self):
|
||||
"""Simple tests for number formatting tags."""
|
||||
fn = report_tags.format_number
|
||||
|
||||
self.assertEqual(fn(1234), '1234')
|
||||
self.assertEqual(fn(1234.5678, decimal_places=2), '1234.57')
|
||||
self.assertEqual(fn(1234.5678, decimal_places=3), '1234.568')
|
||||
self.assertEqual(fn(-9999.5678, decimal_places=2, separator=','), '-9,999.57')
|
||||
self.assertEqual(
|
||||
fn(9988776655.4321, integer=True, separator=' '), '9 988 776 655'
|
||||
)
|
||||
|
||||
@override_settings(TIME_ZONE='America/New_York')
|
||||
def test_date_tags(self):
|
||||
"""Test for date formatting tags.
|
||||
|
||||
@@ -62,7 +62,7 @@ if __name__ == '__main__':
|
||||
percentage = int(covered / total * 100) if total > 0 else 0
|
||||
|
||||
if verbose:
|
||||
print(f"| {locale.ljust(4, ' ')} : {str(percentage).rjust(4, ' ')}% |")
|
||||
print(f'| {locale.ljust(4, " ")} : {str(percentage).rjust(4, " ")}% |')
|
||||
|
||||
locales_perc[locale] = percentage
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_filters import rest_framework as rest_filters
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
@@ -880,6 +880,7 @@ class StockApiMixin:
|
||||
|
||||
for key in [
|
||||
'part_detail',
|
||||
'path_detail',
|
||||
'location_detail',
|
||||
'supplier_part_detail',
|
||||
'tests',
|
||||
@@ -1240,54 +1241,6 @@ class StockItemTestResultFilter(rest_filters.FilterSet):
|
||||
return queryset.filter(template__key=key)
|
||||
|
||||
|
||||
class TestStatisticsFilter(rest_filters.FilterSet):
|
||||
"""API filter for the filtering the test results belonging to a specific build."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = StockItemTestResult
|
||||
fields = []
|
||||
|
||||
# Created date filters
|
||||
finished_before = InvenTreeDateFilter(
|
||||
label='Finished before', field_name='finished_datetime', lookup_expr='lte'
|
||||
)
|
||||
finished_after = InvenTreeDateFilter(
|
||||
label='Finished after', field_name='finished_datetime', lookup_expr='gte'
|
||||
)
|
||||
|
||||
|
||||
class TestStatistics(GenericAPIView):
|
||||
"""API endpoint for accessing a test statistics broken down by test templates."""
|
||||
|
||||
queryset = StockItemTestResult.objects.all()
|
||||
serializer_class = StockSerializers.TestStatisticsSerializer
|
||||
pagination_class = None
|
||||
filterset_class = TestStatisticsFilter
|
||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||
|
||||
@extend_schema(
|
||||
responses={200: StockSerializers.TestStatisticsSerializer(many=False)}
|
||||
)
|
||||
def get(self, request, pk, *args, **kwargs):
|
||||
"""Return test execution count matrix broken down by test result."""
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
if request.resolver_match.url_name == 'api-test-statistics-by-part':
|
||||
serializer.context['type'] = 'by-part'
|
||||
elif request.resolver_match.url_name == 'api-test-statistics-by-build':
|
||||
serializer.context['type'] = 'by-build'
|
||||
serializer.context['finished_datetime_after'] = self.request.query_params.get(
|
||||
'finished_datetime_after'
|
||||
)
|
||||
serializer.context['finished_datetime_before'] = self.request.query_params.get(
|
||||
'finished_datetime_before'
|
||||
)
|
||||
serializer.context['pk'] = pk
|
||||
return Response([serializer.data])
|
||||
|
||||
|
||||
class StockItemTestResultList(StockItemTestResultMixin, ListCreateDestroyAPIView):
|
||||
"""API endpoint for listing (and creating) a StockItemTestResult object."""
|
||||
|
||||
@@ -1396,6 +1349,8 @@ class StockTrackingList(DataExportViewMixin, ListAPI):
|
||||
'salesorder': (SalesOrder, SalesOrderSerializer),
|
||||
'returnorder': (ReturnOrder, ReturnOrderSerializer),
|
||||
'buildorder': (Build, BuildSerializer),
|
||||
'item': (StockItem, StockSerializers.StockItemSerializer),
|
||||
'stockitem': (StockItem, StockSerializers.StockItemSerializer),
|
||||
}
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
@@ -1663,27 +1618,3 @@ stock_api_urls = [
|
||||
# Anything else
|
||||
path('', StockList.as_view(), name='api-stock-list'),
|
||||
]
|
||||
|
||||
test_statistics_api_urls = [
|
||||
# Test statistics endpoints
|
||||
path(
|
||||
'by-part/',
|
||||
include([
|
||||
path(
|
||||
'<int:pk>/',
|
||||
TestStatistics.as_view(),
|
||||
name='api-test-statistics-by-part',
|
||||
)
|
||||
]),
|
||||
),
|
||||
path(
|
||||
'by-build/',
|
||||
include([
|
||||
path(
|
||||
'<int:pk>/',
|
||||
TestStatistics.as_view(),
|
||||
name='api-test-statistics-by-build',
|
||||
)
|
||||
]),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -34,7 +34,6 @@ import InvenTree.tasks
|
||||
import report.mixins
|
||||
import report.models
|
||||
import stock.tasks
|
||||
from build import models as BuildModels
|
||||
from common.icons import validate_icon
|
||||
from common.settings import get_global_setting
|
||||
from company import models as CompanyModels
|
||||
@@ -48,7 +47,6 @@ from InvenTree.status_codes import (
|
||||
)
|
||||
from part import models as PartModels
|
||||
from plugin.events import trigger_event
|
||||
from stock import models as StockModels # noqa: PLW0406
|
||||
from stock.generators import generate_batch_code
|
||||
from users.models import Owner
|
||||
|
||||
@@ -2579,67 +2577,6 @@ class StockItemTestResult(InvenTree.models.InvenTreeMetadataModel):
|
||||
"""Return key for test."""
|
||||
return InvenTree.helpers.generateTestKey(self.test_name)
|
||||
|
||||
def calculate_test_statistics_for_test_template(
|
||||
self, query_base, test_template, ret, start, end
|
||||
):
|
||||
"""Helper function to calculate the passed/failed/total tests count per test template type."""
|
||||
query = query_base & Q(template=test_template.pk)
|
||||
if start is not None and end is not None:
|
||||
query = query & Q(started_datetime__range=(start, end))
|
||||
elif start is not None and end is None:
|
||||
query = query & Q(started_datetime__gt=start)
|
||||
elif start is None and end is not None:
|
||||
query = query & Q(started_datetime__lt=end)
|
||||
|
||||
passed = StockModels.StockItemTestResult.objects.filter(
|
||||
query & Q(result=True)
|
||||
).count()
|
||||
failed = StockModels.StockItemTestResult.objects.filter(
|
||||
query & ~Q(result=True)
|
||||
).count()
|
||||
if test_template.test_name not in ret:
|
||||
ret[test_template.test_name] = {'passed': 0, 'failed': 0, 'total': 0}
|
||||
ret[test_template.test_name]['passed'] += passed
|
||||
ret[test_template.test_name]['failed'] += failed
|
||||
ret[test_template.test_name]['total'] += passed + failed
|
||||
ret['total']['passed'] += passed
|
||||
ret['total']['failed'] += failed
|
||||
ret['total']['total'] += passed + failed
|
||||
return ret
|
||||
|
||||
def build_test_statistics(self, build_order_pk, start, end):
|
||||
"""Generate a statistics matrix for each test template based on the test executions result counts."""
|
||||
build = BuildModels.Build.objects.get(pk=build_order_pk)
|
||||
if not build or not build.part.trackable:
|
||||
return {}
|
||||
|
||||
test_templates = build.part.getTestTemplates()
|
||||
ret = {'total': {'passed': 0, 'failed': 0, 'total': 0}}
|
||||
for build_item in build.get_build_outputs():
|
||||
for test_template in test_templates:
|
||||
query_base = Q(stock_item=build_item)
|
||||
ret = self.calculate_test_statistics_for_test_template(
|
||||
query_base, test_template, ret, start, end
|
||||
)
|
||||
return ret
|
||||
|
||||
def part_test_statistics(self, part_pk, start, end):
|
||||
"""Generate a statistics matrix for each test template based on the test executions result counts."""
|
||||
part = PartModels.Part.objects.get(pk=part_pk)
|
||||
|
||||
if not part or not part.trackable:
|
||||
return {}
|
||||
|
||||
test_templates = part.getTestTemplates()
|
||||
ret = {'total': {'passed': 0, 'failed': 0, 'total': 0}}
|
||||
for bo in part.stock_entries():
|
||||
for test_template in test_templates:
|
||||
query_base = Q(stock_item=bo)
|
||||
ret = self.calculate_test_statistics_for_test_template(
|
||||
query_base, test_template, ret, start, end
|
||||
)
|
||||
return ret
|
||||
|
||||
stock_item = models.ForeignKey(
|
||||
StockItem, on_delete=models.CASCADE, related_name='test_results'
|
||||
)
|
||||
|
||||
@@ -425,7 +425,7 @@ class StockItemSerializer(
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Add detail fields."""
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
location_detail = kwargs.pop('location_detail', False)
|
||||
supplier_part_detail = kwargs.pop('supplier_part_detail', False)
|
||||
tests = kwargs.pop('tests', False)
|
||||
@@ -904,36 +904,6 @@ class UninstallStockItemSerializer(serializers.Serializer):
|
||||
item.uninstall_into_location(location, request.user, note)
|
||||
|
||||
|
||||
class TestStatisticsLineField(serializers.DictField):
|
||||
"""DRF field definition for one column of the test statistics."""
|
||||
|
||||
test_name = serializers.CharField()
|
||||
results = serializers.DictField(child=serializers.IntegerField(min_value=0))
|
||||
|
||||
|
||||
class TestStatisticsSerializer(serializers.Serializer):
|
||||
"""DRF serializer class for the test statistics."""
|
||||
|
||||
results = serializers.ListField(child=TestStatisticsLineField(), read_only=True)
|
||||
|
||||
def to_representation(self, obj):
|
||||
"""Just pass through the test statistics from the model."""
|
||||
if self.context['type'] == 'by-part':
|
||||
return obj.part_test_statistics(
|
||||
self.context['pk'],
|
||||
self.context['finished_datetime_after'],
|
||||
self.context['finished_datetime_before'],
|
||||
)
|
||||
elif self.context['type'] == 'by-build':
|
||||
return obj.build_test_statistics(
|
||||
self.context['pk'],
|
||||
self.context['finished_datetime_after'],
|
||||
self.context['finished_datetime_before'],
|
||||
)
|
||||
|
||||
raise ValidationError(_('Unsupported statistic type: ' + self.context['type']))
|
||||
|
||||
|
||||
class ConvertStockItemSerializer(serializers.Serializer):
|
||||
"""DRF serializer class for converting a StockItem to a valid variant part."""
|
||||
|
||||
|
||||
@@ -2399,38 +2399,3 @@ class StockMetadataAPITest(InvenTreeAPITestCase):
|
||||
'api-stock-item-metadata': StockItem,
|
||||
}.items():
|
||||
self.metatester(apikey, model)
|
||||
|
||||
|
||||
class StockStatisticsTest(StockAPITestCase):
|
||||
"""Tests for the StockStatistics API endpoints."""
|
||||
|
||||
fixtures = [*StockAPITestCase.fixtures, 'build']
|
||||
|
||||
def test_test_statistics(self):
|
||||
"""Test the test statistics API endpoints."""
|
||||
part = Part.objects.first()
|
||||
response = self.get(
|
||||
reverse('api-test-statistics-by-part', kwargs={'pk': part.pk}),
|
||||
{},
|
||||
expected_code=200,
|
||||
)
|
||||
self.assertEqual(response.data, [{}])
|
||||
|
||||
# Now trackable part
|
||||
part1 = Part.objects.filter(trackable=True).first()
|
||||
response = self.get(
|
||||
reverse(
|
||||
'api-test-statistics-by-part',
|
||||
kwargs={'pk': part1.stock_items.first().pk},
|
||||
),
|
||||
{},
|
||||
expected_code=404,
|
||||
)
|
||||
self.assertIn('detail', response.data)
|
||||
|
||||
# 105
|
||||
|
||||
bld = build.models.Build.objects.first()
|
||||
url = reverse('api-test-statistics-by-build', kwargs={'pk': bld.pk})
|
||||
response = self.get(url, {}, expected_code=200)
|
||||
self.assertEqual(response.data, [{}])
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
duplicateStockItem,
|
||||
editStockItem,
|
||||
editStockLocation,
|
||||
filterTestStatisticsTableDateRange,
|
||||
findStockItemBySerialNumber,
|
||||
installStockItem,
|
||||
loadInstalledInTable,
|
||||
@@ -83,7 +82,6 @@
|
||||
loadStockTestResultsTable,
|
||||
loadStockTrackingTable,
|
||||
loadTableFilters,
|
||||
prepareTestStatisticsTable,
|
||||
mergeStockItems,
|
||||
removeStockRow,
|
||||
serializeStockItem,
|
||||
@@ -3418,105 +3416,3 @@ function setStockStatus(items, options={}) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Load TestStatistics table.
|
||||
*/
|
||||
function loadTestStatisticsTable(table, prefix, url, options, filters = {}) {
|
||||
inventreeGet(url, filters, {
|
||||
async: true,
|
||||
success: function(data) {
|
||||
const keys = ['passed', 'failed', 'total']
|
||||
let header = '';
|
||||
let rows = []
|
||||
let passed= '';
|
||||
let failed = '';
|
||||
let total = '';
|
||||
$('.test-stat-result-cell').remove();
|
||||
$.each(data[0], function(key, value){
|
||||
if (key != "total") {
|
||||
header += '<th class="test-stat-result-cell">' + key + '</th>';
|
||||
keys.forEach(function(keyName) {
|
||||
var tdText = '-';
|
||||
if (value['total'] != '0' && value[keyName] != '0') {
|
||||
let percentage = ''
|
||||
if (keyName != 'total' && value[total] != 0) {
|
||||
percentage = ' (' + (100.0 * (parseFloat(value[keyName]) / parseFloat(value['total']))).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 2}) + '%)';
|
||||
}
|
||||
tdText = value[keyName] + percentage;
|
||||
}
|
||||
rows[keyName] += '<td class="test-stat-result-cell">' + tdText + '</td>';
|
||||
})
|
||||
}
|
||||
});
|
||||
$('#' + prefix + '-test-statistics-table-header-id').after(header);
|
||||
|
||||
keys.forEach(function(keyName) {
|
||||
let valueStr = data[0]['total'][keyName];
|
||||
if (keyName != 'total' && data[0]['total']['total'] != '0') {
|
||||
valueStr += ' (' + (100.0 * (parseFloat(valueStr) / parseFloat(data[0]['total']['total']))).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 2}) + '%)';
|
||||
}
|
||||
rows[keyName] += '<td class="test-stat-result-cell">' + valueStr + '</td>';
|
||||
$('#' + prefix + '-test-statistics-table-body-' + keyName).after(rows[keyName]);
|
||||
});
|
||||
$('#' + prefix + '-test-statistics-table').show();
|
||||
setupFilterList(prefix + "teststatistics", table, "#filter-list-" + prefix + "teststatistics", options);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function prepareTestStatisticsTable(keyName, apiUrl)
|
||||
{
|
||||
let options = {
|
||||
custom_actions: [
|
||||
{
|
||||
icon: 'fa-calendar-week',
|
||||
actions: [
|
||||
{
|
||||
icon: 'fa-calendar-week',
|
||||
title: '{% trans "This week" %}',
|
||||
label: 'this-week',
|
||||
callback: function(data) {
|
||||
filterTestStatisticsTableDateRange(data, 'this-week', $("#test-statistics-table"), keyName + 'teststatistics', options);
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'fa-calendar-week',
|
||||
title: '{% trans "This month" %}',
|
||||
label: 'this-month',
|
||||
callback: function(data) {
|
||||
filterTestStatisticsTableDateRange(data, 'this-month', $("#test-statistics-table"), keyName + 'teststatistics', options);
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
callback: function(table, filters, options) {
|
||||
loadTestStatisticsTable($("#test-statistics-table"), keyName, apiUrl, options, filters);
|
||||
}
|
||||
}
|
||||
setupFilterList(keyName + 'teststatistics', $("#test-statistics-table"), '#filter-list-' + keyName + 'teststatistics', options);
|
||||
|
||||
// Load test statistics table
|
||||
loadTestStatisticsTable($("#test-statistics-table"), keyName, apiUrl, options);
|
||||
}
|
||||
|
||||
function filterTestStatisticsTableDateRange(data, range, table, tableKey, options)
|
||||
{
|
||||
var startDateString = '';
|
||||
var d = new Date();
|
||||
if (range == "this-week") {
|
||||
startDateString = moment(new Date(d.getFullYear(), d.getMonth(), d.getDate() - (d.getDay() == 0 ? 6 : d.getDay() - 1))).format('YYYY-MM-DD');
|
||||
} else if (range == "this-month") {
|
||||
startDateString = moment(new Date(d.getFullYear(), d.getMonth(), 1)).format('YYYY-MM-DD');
|
||||
} else {
|
||||
console.warn(`Invalid range specified for filterTestStatisticsTableDateRange`);
|
||||
return;
|
||||
}
|
||||
var filters = addTableFilter(tableKey, 'finished_datetime_after', startDateString);
|
||||
removeTableFilter(tableKey, 'finished_datetime_before')
|
||||
|
||||
reloadTableFilters(table, filters, options);
|
||||
setupFilterList(tableKey, table, "#filter-list-" + tableKey, options);
|
||||
}
|
||||
|
||||
@@ -469,20 +469,6 @@ function getStockTestTableFilters() {
|
||||
};
|
||||
}
|
||||
|
||||
// Return a dictionary of filters for the "test statistics" table
|
||||
function getTestStatisticsTableFilters() {
|
||||
|
||||
return {
|
||||
finished_datetime_after: {
|
||||
type: 'date',
|
||||
title: '{% trans "Interval start" %}',
|
||||
},
|
||||
finished_datetime_before: {
|
||||
type: 'date',
|
||||
title: '{% trans "Interval end" %}',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Return a dictionary of filters for the "stocktracking" table
|
||||
function getStockTrackingTableFilters() {
|
||||
@@ -870,8 +856,6 @@ function getAvailableTableFilters(tableKey) {
|
||||
return getBuildItemTableFilters();
|
||||
case 'buildlines':
|
||||
return getBuildLineTableFilters();
|
||||
case 'buildteststatistics':
|
||||
return getTestStatisticsTableFilters();
|
||||
case 'bom':
|
||||
return getBOMTableFilters();
|
||||
case 'category':
|
||||
@@ -894,8 +878,6 @@ function getAvailableTableFilters(tableKey) {
|
||||
return getPartTableFilters();
|
||||
case 'parttests':
|
||||
return getPartTestTemplateFilters();
|
||||
case 'partteststatistics':
|
||||
return getTestStatisticsTableFilters();
|
||||
case 'plugins':
|
||||
return getPluginTableFilters();
|
||||
case 'purchaseorder':
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<nav class="navbar {% if sticky %}fixed-top{% endif %} navbar-expand-lg navbar-light">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header clearfix content-heading">
|
||||
<a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% inventree_logo %}" alt="{% trans 'InvenTree logo' %}" width="32" height="32" style="display:block; margin: auto;"/></a>
|
||||
<a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% inventree_logo %}" alt="{% trans 'InvenTree logo' %}" max-width="64" height="32" style="display:block; margin: auto;"/></a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse" id="navbar-objects">
|
||||
<ul class="navbar-nav">
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% inventree_customize 'hide_pui_banner' as hidden %}
|
||||
{% pui_enabled as pui %}
|
||||
|
||||
{% if not hidden %}
|
||||
{% if pui and not hidden %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% if mode == 'admin' %}
|
||||
{% trans "Platform UI - the new UI for InvenTree - provides more modern administration options." %}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
<table class='table table-striped table-condensed' id='{{ prefix }}test-statistics-table' style="display: none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="{{ prefix }}test-statistics-table-header-id"></th>
|
||||
<th id="{{ prefix }}test-statistics-table-header-total">{% trans "Total" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr id="{{ prefix }}test-statistics-table-passed-row">
|
||||
<td id="{{ prefix }}test-statistics-table-body-passed">{% trans "Passed" %}</td>
|
||||
</tr>
|
||||
<tr id="{{ prefix }}test-statistics-table-failed-row">
|
||||
<td id="{{ prefix }}test-statistics-table-body-failed">{% trans "Failed" %}</td>
|
||||
</tr>
|
||||
<tr id="{{ prefix }}test-statistics-table-total-row" class="test-statistics-table-total-row">
|
||||
<td id="{{ prefix }}test-statistics-table-body-total">{% trans "Total" %}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -18,7 +18,7 @@ class RedirectAssetView(TemplateView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Redirect to static asset."""
|
||||
return redirect(
|
||||
f"{settings.STATIC_URL}web/assets/{kwargs['path']}", permanent=True
|
||||
f'{settings.STATIC_URL}web/assets/{kwargs["path"]}', permanent=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
Generated
+238
-221
@@ -5,28 +5,24 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"eslint": "^9.11.0",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-google": "^0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
|
||||
"integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
|
||||
"integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
|
||||
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
|
||||
}
|
||||
@@ -35,6 +31,7 @@
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
@@ -43,17 +40,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/regexpp": {
|
||||
"version": "4.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
|
||||
"integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
|
||||
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
|
||||
"integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz",
|
||||
"integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.4",
|
||||
"debug": "^4.3.1",
|
||||
@@ -63,10 +62,20 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz",
|
||||
"integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
|
||||
"integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
|
||||
"integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
@@ -86,9 +95,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.0.tgz",
|
||||
"integrity": "sha512-LPkkenkDqyzTFauZLLAPhIb48fj6drrfMvRGSL9tS3AcZBSVTllemLSNyCvHNNL2t797S/6DJNSIwRwXgMO/eQ==",
|
||||
"version": "9.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz",
|
||||
"integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
@@ -97,14 +107,16 @@
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
|
||||
"integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
|
||||
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
|
||||
"integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
@@ -112,10 +124,46 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/node": {
|
||||
"version": "0.16.6",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
|
||||
"integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/core": "^0.19.1",
|
||||
"@humanwhocodes/retry": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
|
||||
"integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/module-importer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.22"
|
||||
},
|
||||
@@ -125,9 +173,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/retry": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
|
||||
"integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
|
||||
"integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
@@ -136,42 +185,23 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@nodelib/fs.stat": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.walk": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -183,6 +213,7 @@
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
@@ -191,6 +222,7 @@
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -202,18 +234,11 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
@@ -227,17 +252,20 @@
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -247,6 +275,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -255,6 +284,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
@@ -270,6 +300,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
@@ -280,17 +311,20 @@
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -301,11 +335,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
@@ -319,12 +354,14 @@
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -333,27 +370,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.11.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.0.tgz",
|
||||
"integrity": "sha512-yVS6XODx+tMFMDFcG4+Hlh+qG7RM6cCJXtQhCKLSsr3XkLvWggHjCqjfh0XsPPnt1c56oaT6PMgW9XWQQjdHXA==",
|
||||
"version": "9.15.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz",
|
||||
"integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.11.0",
|
||||
"@eslint/config-array": "^0.18.0",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "9.11.0",
|
||||
"@eslint/plugin-kit": "^0.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.19.0",
|
||||
"@eslint/core": "^0.9.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "9.15.0",
|
||||
"@eslint/plugin-kit": "^0.2.3",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.3.0",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"@humanwhocodes/retry": "^0.4.1",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
"cross-spawn": "^7.0.5",
|
||||
"debug": "^4.3.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"eslint-scope": "^8.0.2",
|
||||
"eslint-visitor-keys": "^4.0.0",
|
||||
"espree": "^10.1.0",
|
||||
"eslint-scope": "^8.2.0",
|
||||
"eslint-visitor-keys": "^4.2.0",
|
||||
"espree": "^10.3.0",
|
||||
"esquery": "^1.5.0",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -363,14 +404,11 @@
|
||||
"ignore": "^5.2.0",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"is-path-inside": "^3.0.3",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"minimatch": "^3.1.2",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"text-table": "^0.2.0"
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
"bin": {
|
||||
"eslint": "bin/eslint.js"
|
||||
@@ -394,6 +432,7 @@
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz",
|
||||
"integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
@@ -402,9 +441,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
|
||||
"integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
|
||||
"integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^5.2.0"
|
||||
@@ -417,9 +457,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-visitor-keys": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
|
||||
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
|
||||
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -428,13 +469,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
|
||||
"integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
|
||||
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.12.0",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^4.0.0"
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -444,9 +486,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
|
||||
"integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"estraverse": "^5.1.0"
|
||||
},
|
||||
@@ -458,6 +501,7 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
@@ -469,6 +513,7 @@
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
@@ -477,6 +522,7 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -484,30 +530,26 @@
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flat-cache": "^4.0.0"
|
||||
},
|
||||
@@ -519,6 +561,7 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^6.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
@@ -534,6 +577,7 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flatted": "^3.2.9",
|
||||
"keyv": "^4.5.4"
|
||||
@@ -543,14 +587,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
|
||||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
|
||||
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
},
|
||||
@@ -562,6 +608,7 @@
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -573,14 +620,16 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
||||
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
@@ -589,6 +638,7 @@
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parent-module": "^1.0.0",
|
||||
"resolve-from": "^4.0.0"
|
||||
@@ -604,6 +654,7 @@
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
@@ -612,6 +663,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -620,6 +672,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
@@ -627,23 +680,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-path-inside": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
@@ -654,22 +701,26 @@
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
@@ -678,6 +729,7 @@
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prelude-ls": "^1.2.1",
|
||||
"type-check": "~0.4.0"
|
||||
@@ -690,6 +742,7 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^5.0.0"
|
||||
},
|
||||
@@ -703,12 +756,14 @@
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -717,26 +772,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
||||
"integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aashutoshrathi/word-wrap": "^1.2.3",
|
||||
"deep-is": "^0.1.3",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
"levn": "^0.4.1",
|
||||
"prelude-ls": "^1.2.1",
|
||||
"type-check": "^0.4.0"
|
||||
"type-check": "^0.4.0",
|
||||
"word-wrap": "^1.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@@ -746,6 +804,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"yocto-queue": "^0.1.0"
|
||||
},
|
||||
@@ -760,6 +819,7 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^3.0.2"
|
||||
},
|
||||
@@ -774,6 +834,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"callsites": "^3.0.0"
|
||||
},
|
||||
@@ -785,6 +846,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -793,6 +855,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -801,6 +864,7 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
@@ -809,72 +873,25 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
@@ -886,17 +903,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -905,6 +912,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
@@ -916,6 +924,7 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
@@ -923,15 +932,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prelude-ls": "^1.2.1"
|
||||
},
|
||||
@@ -943,6 +948,7 @@
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
@@ -951,6 +957,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
@@ -961,10 +968,20 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"eslint": "^9.11.0",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-google": "^0.14.0"
|
||||
},
|
||||
"type": "module"
|
||||
|
||||
@@ -26,6 +26,7 @@ django-q-sentry # sentry.io integration for django-q
|
||||
django-sesame # Magic link authentication
|
||||
django-sql-utils # Advanced query annotation / aggregation
|
||||
django-sslserver # Secure HTTP development server
|
||||
django-structlog # Structured logging
|
||||
django-stdimage # Advanced ImageField management
|
||||
django-taggit # Tagging support
|
||||
django-user-sessions # user sessions in DB
|
||||
|
||||
@@ -10,6 +10,7 @@ asgiref==3.8.1 \
|
||||
# via
|
||||
# django
|
||||
# django-cors-headers
|
||||
# django-structlog
|
||||
async-timeout==4.0.3 \
|
||||
--hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f \
|
||||
--hash=sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028
|
||||
@@ -389,6 +390,7 @@ django==4.2.16 \
|
||||
# django-sql-utils
|
||||
# django-sslserver
|
||||
# django-stdimage
|
||||
# django-structlog
|
||||
# django-taggit
|
||||
# django-user-sessions
|
||||
# django-weasyprint
|
||||
@@ -445,6 +447,10 @@ django-import-export==3.3.9 \
|
||||
--hash=sha256:16797965e93a8001fe812c61e3b71fb858c57c1bd16da195fe276d6de685348e \
|
||||
--hash=sha256:dd6cabc08ed6d1bd37a392e7fb542bd7d196b615c800168f5c69f0f55f49b103
|
||||
# via -r src/backend/requirements.in
|
||||
django-ipware==7.0.1 \
|
||||
--hash=sha256:d9ec43d2bf7cdf216fed8d494a084deb5761a54860a53b2e74346a4f384cff47 \
|
||||
--hash=sha256:db16bbee920f661ae7f678e4270460c85850f03c6761a4eaeb489bdc91f64709
|
||||
# via django-structlog
|
||||
django-js-asset==2.2.0 \
|
||||
--hash=sha256:0c57a82cae2317e83951d956110ce847f58ff0cdc24e314dbc18b35033917e94 \
|
||||
--hash=sha256:7ef3e858e13d06f10799b56eea62b1e76706f42cf4e709be4e13356bc0ae30d8
|
||||
@@ -503,6 +509,10 @@ django-stdimage==6.0.2 \
|
||||
--hash=sha256:880ab14828be56b53f711c3afae83c219ddd5d9af00850626736feb48382bf7f \
|
||||
--hash=sha256:9a73f7da48c48074580e2b032d5bdb7164935dbe4b9dc4fb88a7e112f3d521c8
|
||||
# via -r src/backend/requirements.in
|
||||
django-structlog==8.1.0 \
|
||||
--hash=sha256:0229b9a2efbd24a4e3500169788e53915c2429521e34e41dd58ccc56039bef3f \
|
||||
--hash=sha256:1072564bd6f36e8d3ba9893e7b31c1c46e94301189fedaecc0fb8a46525a3214
|
||||
# via -r src/backend/requirements.in
|
||||
django-taggit==6.1.0 \
|
||||
--hash=sha256:ab776264bbc76cb3d7e49e1bf9054962457831bd21c3a42db9138b41956e4cf0 \
|
||||
--hash=sha256:c4d1199e6df34125dd36db5eb0efe545b254dec3980ce5dd80e6bab3e78757c3
|
||||
@@ -1229,6 +1239,10 @@ python-fsutil==0.14.1 \
|
||||
--hash=sha256:0d45e623f0f4403f674bdd8ae7aa7d24a4b3132ea45c65416bd2865e6b20b035 \
|
||||
--hash=sha256:8fb204fa8059f37bdeee8a1dc0fff010170202ea47c4225ee71bb3c26f3997be
|
||||
# via django-maintenance-mode
|
||||
python-ipware==3.0.0 \
|
||||
--hash=sha256:9117b1c4dddcb5d5ca49e6a9617de2fc66aec2ef35394563ac4eecabdf58c062 \
|
||||
--hash=sha256:fc936e6e7ec9fcc107f9315df40658f468ac72f739482a707181742882e36b60
|
||||
# via django-ipware
|
||||
python3-openid==3.2.0 \
|
||||
--hash=sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf \
|
||||
--hash=sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b
|
||||
@@ -1646,6 +1660,10 @@ sqlparse==0.5.1 \
|
||||
# via
|
||||
# django
|
||||
# django-sql-utils
|
||||
structlog==24.4.0 \
|
||||
--hash=sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610 \
|
||||
--hash=sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4
|
||||
# via django-structlog
|
||||
tablib[html, ods, xls, xlsx, yaml]==3.5.0 \
|
||||
--hash=sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9 \
|
||||
--hash=sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"importOrder": ["<THIRD_PARTY_MODULES>", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
root: true,
|
||||
};
|
||||
@@ -54,6 +54,7 @@
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"mantine-contextmenu": "^7.11.3",
|
||||
"mantine-datatable": "^7.12.4",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -23,7 +23,7 @@ export function setApiDefaults() {
|
||||
api.defaults.xsrfHeaderName = 'X-CSRFToken';
|
||||
|
||||
if (token) {
|
||||
api.defaults.headers['Authorization'] = `Token ${token}`;
|
||||
api.defaults.headers.Authorization = `Token ${token}`;
|
||||
} else {
|
||||
delete api.defaults.headers['Authorization'];
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Alert } from '@mantine/core';
|
||||
import { ErrorBoundary, FallbackRender } from '@sentry/react';
|
||||
import { ErrorBoundary, type FallbackRender } from '@sentry/react';
|
||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import { type ReactNode, useCallback } from 'react';
|
||||
|
||||
function DefaultFallback({ title }: Readonly<{ title: string }>): ReactNode {
|
||||
return (
|
||||
<Alert
|
||||
color="red"
|
||||
color='red'
|
||||
icon={<IconExclamationCircle />}
|
||||
title={t`Error rendering component` + `: ${title}`}
|
||||
title={`${t`Error rendering component`}: ${title}`}
|
||||
>
|
||||
{t`An error occurred while rendering this component. Refer to the console for more information.`}
|
||||
</Alert>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { ActionIcon, FloatingPosition, Group, Tooltip } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
import {
|
||||
ActionIcon,
|
||||
type FloatingPosition,
|
||||
Group,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { identifierString } from '../../functions/conversion';
|
||||
|
||||
@@ -46,7 +51,7 @@ export function ActionButton(props: ActionButtonProps) {
|
||||
}}
|
||||
variant={props.variant ?? 'transparent'}
|
||||
>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Group gap='xs' wrap='nowrap'>
|
||||
{props.icon}
|
||||
</Group>
|
||||
</ActionIcon>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { IconPlus } from '@tabler/icons-react';
|
||||
|
||||
import { ActionButton, ActionButtonProps } from './ActionButton';
|
||||
import { ActionButton, type ActionButtonProps } from './ActionButton';
|
||||
|
||||
/**
|
||||
* A generic icon button which is used to add or create a new item
|
||||
*/
|
||||
export function AddItemButton(props: Readonly<ActionButtonProps>) {
|
||||
return <ActionButton {...props} color="green" icon={<IconPlus />} />;
|
||||
return <ActionButton {...props} color='green' icon={<IconPlus />} />;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { t } from '@lingui/macro';
|
||||
import { IconUserStar } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import type { ModelType } from '../../enums/ModelType';
|
||||
import { useServerApiState } from '../../states/ApiState';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
@@ -11,7 +11,7 @@ import { ActionButton } from './ActionButton';
|
||||
|
||||
export type AdminButtonProps = {
|
||||
model: ModelType;
|
||||
pk: number | undefined;
|
||||
id: number | undefined;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -46,12 +46,12 @@ export default function AdminButton(props: Readonly<AdminButtonProps>) {
|
||||
}
|
||||
|
||||
// No primary key provided
|
||||
if (!props.pk) {
|
||||
if (!props.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [user, props.model, props.pk]);
|
||||
}, [user, props.model, props.id]);
|
||||
|
||||
const openAdmin = useCallback(
|
||||
(event: any) => {
|
||||
@@ -63,7 +63,7 @@ export default function AdminButton(props: Readonly<AdminButtonProps>) {
|
||||
}
|
||||
|
||||
// Generate the URL for the admin interface
|
||||
const url = `${host}/${server.server.django_admin}${modelDef.admin_url}${props.pk}/`;
|
||||
const url = `${host}/${server.server.django_admin}${modelDef.admin_url}${props.id}/`;
|
||||
|
||||
if (event?.ctrlKey || event?.shiftKey) {
|
||||
// Open the link in a new tab
|
||||
@@ -72,20 +72,19 @@ export default function AdminButton(props: Readonly<AdminButtonProps>) {
|
||||
window.open(url, '_self');
|
||||
}
|
||||
},
|
||||
[props.model, props.pk]
|
||||
[props.model, props.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
icon={<IconUserStar />}
|
||||
color="blue"
|
||||
size="lg"
|
||||
radius="sm"
|
||||
variant="filled"
|
||||
color='blue'
|
||||
size='lg'
|
||||
variant='filled'
|
||||
tooltip={t`Open in admin interface`}
|
||||
hidden={!enabled}
|
||||
onClick={openAdmin}
|
||||
tooltipAlignment="bottom"
|
||||
tooltipAlignment='bottom'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user