2
0
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:
Oliver Walters
2024-11-20 23:37:10 +00:00
472 changed files with 75725 additions and 69551 deletions
+11 -1
View File
@@ -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
+5 -1
View File
@@ -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).
+101 -15
View File
@@ -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
+4 -14
View File
@@ -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."""
+2 -4
View 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:
+1 -1
View File
@@ -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):
+3 -3
View File
@@ -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,
+3 -3
View File
@@ -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
+34 -21
View File
@@ -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'),
),
]
+7
View File
@@ -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}'
+1 -1
View File
@@ -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 %}
+2 -2
View File
@@ -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'))
-2
View File
@@ -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,
+17 -3
View File
@@ -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):
+81 -56
View File
@@ -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}
+1 -1
View File
@@ -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
+3 -2
View File
@@ -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.
+17 -18
View File
@@ -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):
+3 -2
View File
@@ -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):
+1 -1
View File
@@ -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()
+12
View File
@@ -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
+4 -73
View File
@@ -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',
)
]),
),
]
-63
View File
@@ -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'
)
+1 -31
View File
@@ -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."""
-35
View File
@@ -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':
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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
)
+238 -221
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"dependencies": {
"eslint": "^9.11.0",
"eslint": "^9.15.0",
"eslint-config-google": "^0.14.0"
},
"type": "module"
+1
View File
@@ -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
+18
View File
@@ -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
-9
View File
@@ -1,9 +0,0 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 80,
"importOrder": ["<THIRD_PARTY_MODULES>", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}
-7
View File
@@ -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,
};
+1
View File
@@ -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",
+1 -1
View File
@@ -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'];
}
+4 -4
View File
@@ -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