mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-07 07:31:24 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue6281
This commit is contained in:
@@ -104,14 +104,16 @@ class InvenTreeResource(ModelResource):
|
||||
attribute = getattr(field, 'attribute', field_name)
|
||||
|
||||
# Check if the associated database field is a non-nullable string
|
||||
if db_field := db_fields.get(attribute):
|
||||
if (
|
||||
if (
|
||||
(db_field := db_fields.get(attribute))
|
||||
and (
|
||||
isinstance(db_field, CharField)
|
||||
and db_field.blank
|
||||
and not db_field.null
|
||||
):
|
||||
if column not in self.CONVERT_NULL_FIELDS:
|
||||
self.CONVERT_NULL_FIELDS.append(column)
|
||||
)
|
||||
and column not in self.CONVERT_NULL_FIELDS
|
||||
):
|
||||
self.CONVERT_NULL_FIELDS.append(column)
|
||||
|
||||
return super().before_import(dataset, using_transactions, dry_run, **kwargs)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from pathlib import Path
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import JsonResponse
|
||||
from django.urls import include, path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_q.models import OrmQ
|
||||
@@ -21,19 +20,16 @@ from rest_framework.views import APIView
|
||||
|
||||
import InvenTree.version
|
||||
import users.models
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||
from InvenTree.mixins import ListCreateAPI
|
||||
from InvenTree.permissions import RolePermission
|
||||
from InvenTree.templatetags.inventree_extras import plugins_info
|
||||
from part.models import Part
|
||||
from plugin.serializers import MetadataSerializer
|
||||
from users.models import ApiToken
|
||||
|
||||
from .email import is_email_configured
|
||||
from .helpers_email import is_email_configured
|
||||
from .mixins import ListAPI, RetrieveUpdateAPI
|
||||
from .status import check_system_health, is_worker_running
|
||||
from .version import inventreeApiText
|
||||
from .views import AjaxView
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
@@ -80,7 +76,7 @@ class LicenseView(APIView):
|
||||
# Ensure we do not have any duplicate 'name' values in the list
|
||||
for entry in data:
|
||||
name = None
|
||||
for key in entry.keys():
|
||||
for key in entry:
|
||||
if key.lower() == 'name':
|
||||
name = entry[key]
|
||||
break
|
||||
@@ -199,8 +195,35 @@ class VersionTextView(ListAPI):
|
||||
return JsonResponse(inventreeApiText())
|
||||
|
||||
|
||||
class InfoView(AjaxView):
|
||||
"""Simple JSON endpoint for InvenTree information.
|
||||
class InfoApiSerializer(serializers.Serializer):
|
||||
"""InvenTree server information - some information might be blanked if called without elevated credentials."""
|
||||
|
||||
server = serializers.CharField(read_only=True)
|
||||
version = serializers.CharField(read_only=True)
|
||||
instance = serializers.CharField(read_only=True)
|
||||
apiVersion = serializers.IntegerField(read_only=True) # noqa: N815
|
||||
worker_running = serializers.BooleanField(read_only=True)
|
||||
worker_count = serializers.IntegerField(read_only=True)
|
||||
worker_pending_tasks = serializers.IntegerField(read_only=True)
|
||||
plugins_enabled = serializers.BooleanField(read_only=True)
|
||||
plugins_install_disabled = serializers.BooleanField(read_only=True)
|
||||
active_plugins = serializers.JSONField(read_only=True)
|
||||
email_configured = serializers.BooleanField(read_only=True)
|
||||
debug_mode = serializers.BooleanField(read_only=True)
|
||||
docker_mode = serializers.BooleanField(read_only=True)
|
||||
default_locale = serializers.ChoiceField(
|
||||
choices=settings.LOCALE_CODES, read_only=True
|
||||
)
|
||||
system_health = serializers.BooleanField(read_only=True)
|
||||
database = serializers.CharField(read_only=True)
|
||||
platform = serializers.CharField(read_only=True)
|
||||
installer = serializers.CharField(read_only=True)
|
||||
target = serializers.CharField(read_only=True)
|
||||
django_admin = serializers.CharField(read_only=True)
|
||||
|
||||
|
||||
class InfoView(APIView):
|
||||
"""JSON endpoint for InvenTree server information.
|
||||
|
||||
Use to confirm that the server is running, etc.
|
||||
"""
|
||||
@@ -211,6 +234,13 @@ class InfoView(AjaxView):
|
||||
"""Return the current number of outstanding background tasks."""
|
||||
return OrmQ.objects.count()
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
response=InfoApiSerializer, description='InvenTree server information'
|
||||
)
|
||||
}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Serve current server information."""
|
||||
is_staff = request.user.is_staff
|
||||
@@ -224,6 +254,7 @@ class InfoView(AjaxView):
|
||||
'instance': InvenTree.version.inventreeInstanceName(),
|
||||
'apiVersion': InvenTree.version.inventreeApiVersion(),
|
||||
'worker_running': is_worker_running(),
|
||||
'worker_count': settings.BACKGROUND_WORKER_COUNT,
|
||||
'worker_pending_tasks': self.worker_pending_tasks(),
|
||||
'plugins_enabled': settings.PLUGINS_ENABLED,
|
||||
'plugins_install_disabled': settings.PLUGINS_INSTALL_DISABLED,
|
||||
@@ -238,6 +269,9 @@ class InfoView(AjaxView):
|
||||
'platform': InvenTree.version.inventreePlatform() if is_staff else None,
|
||||
'installer': InvenTree.version.inventreeInstaller() if is_staff else None,
|
||||
'target': InvenTree.version.inventreeTarget() if is_staff else None,
|
||||
'django_admin': settings.INVENTREE_ADMIN_URL
|
||||
if (is_staff and settings.INVENTREE_ADMIN_ENABLED)
|
||||
else None,
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
@@ -260,7 +294,7 @@ class InfoView(AjaxView):
|
||||
return False
|
||||
|
||||
|
||||
class NotFoundView(AjaxView):
|
||||
class NotFoundView(APIView):
|
||||
"""Simple JSON view when accessing an invalid API view."""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
@@ -279,22 +313,27 @@ class NotFoundView(AjaxView):
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def patch(self, request, *args, **kwargs):
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def put(self, request, *args, **kwargs):
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""Return 404."""
|
||||
return self.not_found(request)
|
||||
@@ -311,8 +350,25 @@ class BulkDeleteMixin:
|
||||
- Speed (single API call and DB query)
|
||||
"""
|
||||
|
||||
def validate_delete(self, queryset, request) -> None:
|
||||
"""Perform validation right before deletion.
|
||||
|
||||
Arguments:
|
||||
queryset: The queryset to be deleted
|
||||
request: The request object
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
ValidationError: If the deletion should not proceed
|
||||
"""
|
||||
|
||||
def filter_delete_queryset(self, queryset, request):
|
||||
"""Provide custom filtering for the queryset *before* it is deleted."""
|
||||
"""Provide custom filtering for the queryset *before* it is deleted.
|
||||
|
||||
The default implementation does nothing, just returns the queryset.
|
||||
"""
|
||||
return queryset
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
@@ -365,11 +421,29 @@ class BulkDeleteMixin:
|
||||
|
||||
# Filter by provided item ID values
|
||||
if items:
|
||||
queryset = queryset.filter(id__in=items)
|
||||
try:
|
||||
queryset = queryset.filter(id__in=items)
|
||||
except Exception:
|
||||
raise ValidationError({
|
||||
'non_field_errors': _('Invalid items list provided')
|
||||
})
|
||||
|
||||
# Filter by provided filters
|
||||
if filters:
|
||||
queryset = queryset.filter(**filters)
|
||||
try:
|
||||
queryset = queryset.filter(**filters)
|
||||
except Exception:
|
||||
raise ValidationError({
|
||||
'non_field_errors': _('Invalid filters provided')
|
||||
})
|
||||
|
||||
if queryset.count() == 0:
|
||||
raise ValidationError({
|
||||
'non_field_errors': _('No items found to delete')
|
||||
})
|
||||
|
||||
# Run a final validation step (should raise an error if the deletion should not proceed)
|
||||
self.validate_delete(queryset, request)
|
||||
|
||||
n_deleted = queryset.count()
|
||||
queryset.delete()
|
||||
@@ -380,44 +454,6 @@ class BulkDeleteMixin:
|
||||
class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI):
|
||||
"""Custom API endpoint which provides BulkDelete functionality in addition to List and Create."""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class APIDownloadMixin:
|
||||
"""Mixin for enabling a LIST endpoint to be downloaded a file.
|
||||
|
||||
To download the data, add the ?export=<fmt> to the query string.
|
||||
|
||||
The implementing class must provided a download_queryset method,
|
||||
e.g.
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
dataset = StockItemResource().export(queryset=queryset)
|
||||
|
||||
filedata = dataset.export(export_format)
|
||||
|
||||
filename = 'InvenTree_Stocktake_{date}.{fmt}'.format(
|
||||
date=datetime.now().strftime("%d-%b-%Y"),
|
||||
fmt=export_format
|
||||
)
|
||||
|
||||
return DownloadFile(filedata, filename)
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Generic handler for a download request."""
|
||||
export_format = request.query_params.get('export', None)
|
||||
|
||||
if export_format and export_format in ['csv', 'tsv', 'xls', 'xlsx']:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
return self.download_queryset(queryset, export_format)
|
||||
# Default to the parent class implementation
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""This function must be implemented to provide a downloadFile request."""
|
||||
raise NotImplementedError('download_queryset method not implemented!')
|
||||
|
||||
|
||||
class APISearchViewSerializer(serializers.Serializer):
|
||||
"""Serializer for the APISearchView."""
|
||||
|
||||
@@ -1,15 +1,307 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 209
|
||||
INVENTREE_API_VERSION = 294
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
v209 - 2024-06-25 : https://github.com/inventree/InvenTree/pull/6293
|
||||
v294 - 2024-12-22 : https://github.com/inventree/InvenTree/pull/6293
|
||||
- Removes a considerable amount of old auth endpoints
|
||||
- Introduces allauth based REST API
|
||||
|
||||
v293 - 2024-12-14 : https://github.com/inventree/InvenTree/pull/8658
|
||||
- Adds new fields to the supplier barcode API endpoints
|
||||
|
||||
v292 - 2024-12-03 : https://github.com/inventree/InvenTree/pull/8625
|
||||
- Add "on_order" and "in_stock" annotations to SupplierPart API
|
||||
- Enhanced filtering for the SupplierPart API
|
||||
|
||||
v291 - 2024-11-30 : https://github.com/inventree/InvenTree/pull/8596
|
||||
- Allow null / empty values for plugin settings
|
||||
|
||||
v290 - 2024-11-29 : https://github.com/inventree/InvenTree/pull/8590
|
||||
- Adds "quantity" field to ReturnOrderLineItem model and API
|
||||
|
||||
v289 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8570
|
||||
- Enable status change when transferring stock items
|
||||
|
||||
v288 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8574
|
||||
- Adds "consumed" filter to StockItem API
|
||||
|
||||
v287 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8571
|
||||
- Adds ability to set stock status when returning items from a customer
|
||||
|
||||
v286 - 2024-11-26 : https://github.com/inventree/InvenTree/pull/8054
|
||||
- Adds "SelectionList" and "SelectionListEntry" API endpoints
|
||||
|
||||
v285 - 2024-11-25 : https://github.com/inventree/InvenTree/pull/8559
|
||||
- Adds better description for registration endpoints
|
||||
|
||||
v284 - 2024-11-25 : https://github.com/inventree/InvenTree/pull/8544
|
||||
- Adds new date filters to the StockItem API
|
||||
- Adds new date filters to the BuildOrder API
|
||||
- Adds new date filters to the SalesOrder API
|
||||
- Adds new date filters to the PurchaseOrder API
|
||||
- Adds new date filters to the ReturnOrder API
|
||||
|
||||
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
|
||||
|
||||
v279 - 2024-11-09 : https://github.com/inventree/InvenTree/pull/8458
|
||||
- Adds "order_outstanding" and "part" filters to the BuildLine API endpoint
|
||||
- Adds "order_outstanding" filter to the SalesOrderLineItem API endpoint
|
||||
|
||||
v278 - 2024-11-07 : https://github.com/inventree/InvenTree/pull/8445
|
||||
- Updates to the SalesOrder API endpoints
|
||||
- Add "shipment count" information to the SalesOrder API endpoints
|
||||
- Allow null value for SalesOrderAllocation.shipment field
|
||||
- Additional filtering options for allocation endpoints
|
||||
|
||||
v277 - 2024-11-01 : https://github.com/inventree/InvenTree/pull/8278
|
||||
- Allow build order list to be filtered by "outstanding" (alias for "active")
|
||||
|
||||
v276 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8403
|
||||
- Adds 'destination' field to the PurchaseOrder model and API endpoints
|
||||
|
||||
v275 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8396
|
||||
- Adds SKU and MPN fields to the StockItem serializer
|
||||
- Additional export options for the StockItem serializer
|
||||
|
||||
v274 - 2024-10-29 : https://github.com/inventree/InvenTree/pull/8392
|
||||
- Add more detailed information to NotificationEntry API serializer
|
||||
|
||||
v273 - 2024-10-28 : https://github.com/inventree/InvenTree/pull/8376
|
||||
- Fixes for the BuildLine API endpoint
|
||||
|
||||
v272 - 2024-10-25 : https://github.com/inventree/InvenTree/pull/8343
|
||||
- Adjustments to BuildLine API serializers
|
||||
|
||||
v271 - 2024-10-22 : https://github.com/inventree/InvenTree/pull/8331
|
||||
- Fixes for SalesOrderLineItem endpoints
|
||||
|
||||
v270 - 2024-10-19 : https://github.com/inventree/InvenTree/pull/8307
|
||||
- Adds missing date fields from order API endpoint(s)
|
||||
|
||||
v269 - 2024-10-16 : https://github.com/inventree/InvenTree/pull/8295
|
||||
- Adds "include_variants" filter to the BuildOrder API endpoint
|
||||
- Adds "include_variants" filter to the SalesOrder API endpoint
|
||||
- Adds "include_variants" filter to the PurchaseOrderLineItem API endpoint
|
||||
- Adds "include_variants" filter to the ReturnOrder API endpoint
|
||||
|
||||
268 - 2024-10-11 : https://github.com/inventree/InvenTree/pull/8274
|
||||
- Adds "in_stock" attribute to the StockItem serializer
|
||||
|
||||
267 - 2024-10-8 : https://github.com/inventree/InvenTree/pull/8250
|
||||
- Remove "allocations" field from the SalesOrderShipment API endpoint(s)
|
||||
- Add "allocated_items" field to the SalesOrderShipment API endpoint(s)
|
||||
|
||||
266 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8249
|
||||
- Tweak SalesOrderShipment API for more efficient data retrieval
|
||||
|
||||
265 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8228
|
||||
- Adds API endpoint for providing custom admin integration details for plugins
|
||||
|
||||
264 - 2024-10-03 : https://github.com/inventree/InvenTree/pull/8231
|
||||
- Adds Sales Order Shipment attachment model type
|
||||
|
||||
263 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8194
|
||||
- Adds Sales Order Shipment report
|
||||
|
||||
262 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8220
|
||||
- Tweak permission requirements for uninstalling plugins via API
|
||||
|
||||
261 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8184
|
||||
- Fixes for BuildOrder API serializers
|
||||
|
||||
v260 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190
|
||||
- Adds facility for server-side context data to be passed to client-side plugins
|
||||
|
||||
v259 - 2024-09-20 : https://github.com/inventree/InvenTree/pull/8137
|
||||
- Implements new API endpoint for enabling custom UI features via plugins
|
||||
|
||||
v258 - 2024-09-24 : https://github.com/inventree/InvenTree/pull/8163
|
||||
- Enhances the existing PartScheduling API endpoint
|
||||
- Adds a formal DRF serializer to the endpoint
|
||||
|
||||
v257 - 2024-09-22 : https://github.com/inventree/InvenTree/pull/8150
|
||||
- Adds API endpoint for reporting barcode scan history
|
||||
|
||||
v256 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/7704
|
||||
- Adjustments for "stocktake" (stock history) API endpoints
|
||||
|
||||
v255 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/8145
|
||||
- Enables copying line items when duplicating an order
|
||||
|
||||
v254 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7470
|
||||
- Implements new API endpoints for enabling custom UI functionality via plugins
|
||||
|
||||
v253 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7944
|
||||
- Adjustments for user API endpoints
|
||||
|
||||
v252 - 2024-09-13 : https://github.com/inventree/InvenTree/pull/8040
|
||||
- Add endpoint for listing all known units
|
||||
|
||||
v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
|
||||
- Adds "attach_to_model" field to the ReporTemplate model
|
||||
|
||||
v250 - 2024-09-04 : https://github.com/inventree/InvenTree/pull/8069
|
||||
- Fixes 'revision' field definition in Part serializer
|
||||
|
||||
v249 - 2024-08-23 : https://github.com/inventree/InvenTree/pull/7978
|
||||
- Sort status enums
|
||||
|
||||
v248 - 2024-08-23 : https://github.com/inventree/InvenTree/pull/7965
|
||||
- Small adjustments to labels for new custom status fields
|
||||
|
||||
v247 - 2024-08-22 : https://github.com/inventree/InvenTree/pull/7956
|
||||
- Adjust "attachment" field on StockItemTestResult serializer
|
||||
- Allow null values for attachment
|
||||
|
||||
v246 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7862
|
||||
- Adds custom status fields to various serializers
|
||||
- Adds endpoints to admin custom status fields
|
||||
|
||||
v245 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7520
|
||||
- Documented pagination fields (no functional changes)
|
||||
|
||||
v244 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7941
|
||||
- Adds "create_child_builds" field to the Build API
|
||||
- Write-only field to create child builds from the API
|
||||
- Only available when creating a new build order
|
||||
|
||||
v243 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7940
|
||||
- Expose "ancestor" filter to the BuildOrder API
|
||||
|
||||
v242 - 2024-08-20 : https://github.com/inventree/InvenTree/pull/7932
|
||||
- Adds "level" attribute to BuildOrder serializer
|
||||
- Allow ordering of BuildOrder API by "level" attribute
|
||||
- Allow "parent" filter for BuildOrder API to have "cascade=True" option
|
||||
|
||||
v241 - 2024-08-18 : https://github.com/inventree/InvenTree/pull/7906
|
||||
- Adjusts required fields for the MeUserDetail endpoint
|
||||
|
||||
v240 - 2024-08-16 : https://github.com/inventree/InvenTree/pull/7900
|
||||
- Adjust "issued_by" filter for the BuildOrder list endpoint
|
||||
- Adjust "assigned_to" filter for the BuildOrder list endpoint
|
||||
|
||||
v239 - 2024-08-15 : https://github.com/inventree/InvenTree/pull/7888
|
||||
- Adds "testable" field to the Part model
|
||||
- Adds associated filters to various API endpoints
|
||||
|
||||
v238 - 2024-08-14 : https://github.com/inventree/InvenTree/pull/7874
|
||||
- Add "assembly" filter to BuildLine API endpoint
|
||||
|
||||
v237 - 2024-08-13 : https://github.com/inventree/InvenTree/pull/7863
|
||||
- Reimplement "bulk delete" operation for Attachment model
|
||||
- Fix permission checks for Attachment API endpoints
|
||||
|
||||
v236 - 2024-08-10 : https://github.com/inventree/InvenTree/pull/7844
|
||||
- Adds "supplier_name" to the PurchaseOrder API serializer
|
||||
|
||||
v235 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7837
|
||||
- Adds "on_order" quantity to SalesOrderLineItem serializer
|
||||
- Adds "building" quantity to SalesOrderLineItem serializer
|
||||
|
||||
v234 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7829
|
||||
- Fixes bug in the plugin metadata endpoint
|
||||
|
||||
v233 - 2024-08-04 : https://github.com/inventree/InvenTree/pull/7807
|
||||
- Adds new endpoints for managing state of build orders
|
||||
- Adds new endpoints for managing state of purchase orders
|
||||
- Adds new endpoints for managing state of sales orders
|
||||
- Adds new endpoints for managing state of return orders
|
||||
|
||||
v232 - 2024-08-03 : https://github.com/inventree/InvenTree/pull/7793
|
||||
- Allow ordering of SalesOrderShipment API by 'shipment_date' and 'delivery_date'
|
||||
|
||||
v231 - 2024-08-03 : https://github.com/inventree/InvenTree/pull/7794
|
||||
- Optimize BuildItem and BuildLine serializers to improve API efficiency
|
||||
|
||||
v230 - 2024-05-05 : https://github.com/inventree/InvenTree/pull/7164
|
||||
- Adds test statistics endpoint
|
||||
|
||||
v229 - 2024-07-31 : https://github.com/inventree/InvenTree/pull/7775
|
||||
- Add extra exportable fields to the BomItem serializer
|
||||
|
||||
v228 - 2024-07-18 : https://github.com/inventree/InvenTree/pull/7684
|
||||
- Adds "icon" field to the PartCategory.path and StockLocation.path API
|
||||
- Adds icon packages API endpoint
|
||||
|
||||
v227 - 2024-07-19 : https://github.com/inventree/InvenTree/pull/7693/
|
||||
- Adds endpoints to list and revoke the tokens issued to the current user
|
||||
|
||||
v226 - 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648
|
||||
- Adds barcode generation API endpoint
|
||||
|
||||
v225 - 2024-07-17 : https://github.com/inventree/InvenTree/pull/7671
|
||||
- Adds "filters" field to DataImportSession API
|
||||
|
||||
v224 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7667
|
||||
- Add notes field to ManufacturerPart and SupplierPart API endpoints
|
||||
|
||||
v223 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7649
|
||||
- Allow adjustment of "packaging" field when receiving items against a purchase order
|
||||
|
||||
v222 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7635
|
||||
- Adjust the BomItem API endpoint to improve data import process
|
||||
|
||||
v221 - 2024-07-13 : https://github.com/inventree/InvenTree/pull/7636
|
||||
- Adds missing fields from StockItemBriefSerializer
|
||||
- Adds missing fields from PartBriefSerializer
|
||||
- Adds extra exportable fields to BuildItemSerializer
|
||||
|
||||
v220 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7585
|
||||
- Adds "revision_of" field to Part serializer
|
||||
- Adds new API filters for "revision" status
|
||||
|
||||
v219 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7611
|
||||
- Adds new fields to the BuildItem API endpoints
|
||||
- Adds new ordering / filtering options to the BuildItem API endpoints
|
||||
|
||||
v218 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7619
|
||||
- Adds "can_build" field to the BomItem API
|
||||
|
||||
v217 - 2024-07-09 : https://github.com/inventree/InvenTree/pull/7599
|
||||
- Fixes bug in "project_code" field for order API endpoints
|
||||
|
||||
v216 - 2024-07-08 : https://github.com/inventree/InvenTree/pull/7595
|
||||
- Moves API endpoint for contenttype lookup by model name
|
||||
|
||||
v215 - 2024-07-09 : https://github.com/inventree/InvenTree/pull/7591
|
||||
- Adds additional fields to the BuildLine serializer
|
||||
|
||||
v214 - 2024-07-08 : https://github.com/inventree/InvenTree/pull/7587
|
||||
- Adds "default_location_detail" field to the Part API
|
||||
|
||||
v213 - 2024-07-06 : https://github.com/inventree/InvenTree/pull/7527
|
||||
- Adds 'locked' field to Part API
|
||||
|
||||
v212 - 2024-07-06 : https://github.com/inventree/InvenTree/pull/7562
|
||||
- Makes API generation more robust (no functional changes)
|
||||
|
||||
v211 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/6911
|
||||
- Adds API endpoints for managing data import and export
|
||||
|
||||
v210 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/7518
|
||||
- Adds translateable text to User API fields
|
||||
|
||||
v209 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/7514
|
||||
- Add "top_level" filter to PartCategory API endpoint
|
||||
- Add "top_level" filter to StockLocation API endpoint
|
||||
|
||||
v208 - 2024-06-19 : https://github.com/inventree/InvenTree/pull/7479
|
||||
- Adds documentation for the user roles API endpoint (no functional changes)
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ from django.core.exceptions import AppRegistryNotReady
|
||||
from django.db import transaction
|
||||
from django.db.utils import IntegrityError, OperationalError
|
||||
|
||||
from allauth.socialaccount.signals import social_account_updated
|
||||
|
||||
import InvenTree.conversion
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
@@ -38,9 +40,14 @@ class InvenTreeConfig(AppConfig):
|
||||
- Adding users set in the current environment
|
||||
"""
|
||||
# skip loading if plugin registry is not loaded or we run in a background thread
|
||||
|
||||
if not InvenTree.ready.isPluginRegistryLoaded():
|
||||
return
|
||||
|
||||
# Skip if not in worker or main thread
|
||||
if (
|
||||
not InvenTree.ready.isPluginRegistryLoaded()
|
||||
or not InvenTree.ready.isInMainThread()
|
||||
not InvenTree.ready.isInMainThread()
|
||||
and not InvenTree.ready.isInWorkerThread()
|
||||
):
|
||||
return
|
||||
|
||||
@@ -50,7 +57,6 @@ class InvenTreeConfig(AppConfig):
|
||||
|
||||
if InvenTree.ready.canAppAccessDatabase() or settings.TESTING_ENV:
|
||||
self.remove_obsolete_tasks()
|
||||
|
||||
self.collect_tasks()
|
||||
self.start_background_tasks()
|
||||
|
||||
@@ -70,6 +76,12 @@ class InvenTreeConfig(AppConfig):
|
||||
self.add_user_on_startup()
|
||||
self.add_user_from_file()
|
||||
|
||||
# register event receiver and connect signal for SSO group sync. The connected signal is
|
||||
# used for account updates whereas the receiver is used for the initial account creation.
|
||||
from InvenTree import sso
|
||||
|
||||
social_account_updated.connect(sso.ensure_sso_groups)
|
||||
|
||||
def remove_obsolete_tasks(self):
|
||||
"""Delete any obsolete scheduled tasks in the database."""
|
||||
obsolete = [
|
||||
@@ -117,7 +129,7 @@ class InvenTreeConfig(AppConfig):
|
||||
for task in tasks:
|
||||
ref_name = f'{task.func.__module__}.{task.func.__name__}'
|
||||
|
||||
if ref_name in existing_tasks.keys():
|
||||
if ref_name in existing_tasks:
|
||||
# This task already exists - update the details if required
|
||||
existing_task = existing_tasks[ref_name]
|
||||
|
||||
|
||||
+24
-133
@@ -1,10 +1,10 @@
|
||||
"""Helper forms which subclass Django forms to provide additional functionality."""
|
||||
"""Overrides for allauth and adjacent packages to enforce InvenTree specific auth settings and restirctions."""
|
||||
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.auth.models import Group
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -12,9 +12,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from allauth.account.adapter import DefaultAccountAdapter
|
||||
from allauth.account.forms import LoginForm, SignupForm, set_form_field_order
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from crispy_forms.bootstrap import AppendedText, PrependedAppendedText, PrependedText
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Field, Layout
|
||||
from allauth_2fa.forms import TOTPDeviceForm
|
||||
|
||||
import InvenTree.helpers_model
|
||||
import InvenTree.sso
|
||||
@@ -24,125 +22,6 @@ from InvenTree.exceptions import log_error
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class HelperForm(forms.ModelForm):
|
||||
"""Provides simple integration of crispy_forms extension."""
|
||||
|
||||
# Custom field decorations can be specified here, per form class
|
||||
field_prefix = {}
|
||||
field_suffix = {}
|
||||
field_placeholder = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Setup layout."""
|
||||
super(forms.ModelForm, self).__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
|
||||
self.helper.form_tag = False
|
||||
self.helper.form_show_errors = True
|
||||
|
||||
"""
|
||||
Create a default 'layout' for this form.
|
||||
Ref: https://django-crispy-forms.readthedocs.io/en/latest/layouts.html
|
||||
This is required to do fancy things later (like adding PrependedText, etc).
|
||||
|
||||
Simply create a 'blank' layout for each available field.
|
||||
"""
|
||||
|
||||
self.rebuild_layout()
|
||||
|
||||
def rebuild_layout(self):
|
||||
"""Build crispy layout out of current fields."""
|
||||
layouts = []
|
||||
|
||||
for field in self.fields:
|
||||
prefix = self.field_prefix.get(field, None)
|
||||
suffix = self.field_suffix.get(field, None)
|
||||
placeholder = self.field_placeholder.get(field, '')
|
||||
|
||||
# Look for font-awesome icons
|
||||
if prefix and prefix.startswith('fa-'):
|
||||
prefix = f"<i class='fas {prefix}'/>"
|
||||
|
||||
if suffix and suffix.startswith('fa-'):
|
||||
suffix = f"<i class='fas {suffix}'/>"
|
||||
|
||||
if prefix and suffix:
|
||||
layouts.append(
|
||||
Field(
|
||||
PrependedAppendedText(
|
||||
field,
|
||||
prepended_text=prefix,
|
||||
appended_text=suffix,
|
||||
placeholder=placeholder,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
elif prefix:
|
||||
layouts.append(
|
||||
Field(PrependedText(field, prefix, placeholder=placeholder))
|
||||
)
|
||||
|
||||
elif suffix:
|
||||
layouts.append(
|
||||
Field(AppendedText(field, suffix, placeholder=placeholder))
|
||||
)
|
||||
|
||||
else:
|
||||
layouts.append(Field(field, placeholder=placeholder))
|
||||
|
||||
self.helper.layout = Layout(*layouts)
|
||||
|
||||
|
||||
class EditUserForm(HelperForm):
|
||||
"""Form for editing user information."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = User
|
||||
fields = ['first_name', 'last_name']
|
||||
|
||||
|
||||
class SetPasswordForm(HelperForm):
|
||||
"""Form for setting user password."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
model = User
|
||||
fields = ['enter_password', 'confirm_password', 'old_password']
|
||||
|
||||
enter_password = forms.CharField(
|
||||
max_length=100,
|
||||
min_length=8,
|
||||
required=True,
|
||||
initial='',
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
|
||||
label=_('Enter password'),
|
||||
help_text=_('Enter new password'),
|
||||
)
|
||||
|
||||
confirm_password = forms.CharField(
|
||||
max_length=100,
|
||||
min_length=8,
|
||||
required=True,
|
||||
initial='',
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
|
||||
label=_('Confirm password'),
|
||||
help_text=_('Confirm new password'),
|
||||
)
|
||||
|
||||
old_password = forms.CharField(
|
||||
label=_('Old password'),
|
||||
strip=False,
|
||||
required=False,
|
||||
widget=forms.PasswordInput(
|
||||
attrs={'autocomplete': 'current-password', 'autofocus': True}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# override allauth
|
||||
class CustomLoginForm(LoginForm):
|
||||
"""Custom login form to override default allauth behaviour."""
|
||||
@@ -184,7 +63,7 @@ class CustomSignupForm(SignupForm):
|
||||
|
||||
# check for two password fields
|
||||
if not get_global_setting('LOGIN_SIGNUP_PWD_TWICE'):
|
||||
self.fields.pop('password2')
|
||||
self.fields.pop('password2', None)
|
||||
|
||||
# reorder fields
|
||||
set_form_field_order(
|
||||
@@ -205,9 +84,22 @@ class CustomSignupForm(SignupForm):
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class CustomTOTPDeviceForm(TOTPDeviceForm):
|
||||
"""Ensure that db registration is enabled."""
|
||||
|
||||
def __init__(self, user, metadata=None, **kwargs):
|
||||
"""Override to check if registration is open."""
|
||||
if not settings.MFA_ENABLED:
|
||||
raise forms.ValidationError(_('MFA Registration is disabled.'))
|
||||
super().__init__(user, metadata, **kwargs)
|
||||
|
||||
|
||||
def registration_enabled():
|
||||
"""Determine whether user registration is enabled."""
|
||||
if get_global_setting('LOGIN_ENABLE_REG') or InvenTree.sso.registration_enabled():
|
||||
if (
|
||||
get_global_setting('LOGIN_ENABLE_REG')
|
||||
or InvenTree.sso.sso_registration_enabled()
|
||||
):
|
||||
if settings.EMAIL_HOST:
|
||||
return True
|
||||
else:
|
||||
@@ -249,9 +141,8 @@ class RegistratonMixin:
|
||||
raise forms.ValidationError(
|
||||
_('The provided primary email address is not valid.')
|
||||
)
|
||||
else:
|
||||
if split_email[1] == option[1:]:
|
||||
return super().clean_email(email)
|
||||
elif split_email[1] == option[1:]:
|
||||
return super().clean_email(email)
|
||||
|
||||
logger.info('The provided email domain for %s is not approved', email)
|
||||
raise forms.ValidationError(_('The provided email domain is not approved.'))
|
||||
@@ -263,7 +154,9 @@ class RegistratonMixin:
|
||||
|
||||
# Check if a default group is set in settings
|
||||
start_group = get_global_setting('SIGNUP_GROUP')
|
||||
if start_group:
|
||||
if (
|
||||
start_group and user.groups.count() == 0
|
||||
): # check that no group has been added through SSO group sync
|
||||
try:
|
||||
group = Group.objects.get(id=start_group)
|
||||
user.groups.add(group)
|
||||
@@ -306,10 +199,8 @@ class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultAccountAdapt
|
||||
|
||||
def get_email_confirmation_url(self, request, emailconfirmation):
|
||||
"""Construct the email confirmation url."""
|
||||
from InvenTree.helpers_model import construct_absolute_url
|
||||
|
||||
url = super().get_email_confirmation_url(request, emailconfirmation)
|
||||
url = construct_absolute_url(url)
|
||||
url = InvenTree.helpers_model.construct_absolute_url(url)
|
||||
return url
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
"""Pull rendered copies of the templated.
|
||||
|
||||
Only used for testing the js files! - This file is omitted from coverage.
|
||||
"""
|
||||
|
||||
import os # pragma: no cover
|
||||
import pathlib # pragma: no cover
|
||||
|
||||
from InvenTree.unit_test import InvenTreeTestCase # pragma: no cover
|
||||
|
||||
|
||||
class RenderJavascriptFiles(InvenTreeTestCase): # pragma: no cover
|
||||
"""A unit test to "render" javascript files.
|
||||
|
||||
The server renders templated javascript files,
|
||||
we need the fully-rendered files for linting and static tests.
|
||||
"""
|
||||
|
||||
def download_file(self, filename, prefix):
|
||||
"""Function to `download`(copy) a file to a temporary firectory."""
|
||||
url = os.path.join(prefix, filename)
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
output_dir = os.path.join(here, '..', '..', 'js_tmp')
|
||||
|
||||
output_dir = os.path.abspath(output_dir)
|
||||
|
||||
if not os.path.exists(output_dir):
|
||||
os.mkdir(output_dir)
|
||||
|
||||
output_file = os.path.join(output_dir, filename)
|
||||
|
||||
with open(output_file, 'wb') as output:
|
||||
output.write(response.content)
|
||||
|
||||
def download_files(self, subdir, prefix):
|
||||
"""Download files in directory."""
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
js_template_dir = os.path.join(here, '..', 'templates', 'js')
|
||||
|
||||
directory = os.path.join(js_template_dir, subdir)
|
||||
|
||||
directory = os.path.abspath(directory)
|
||||
|
||||
js_files = pathlib.Path(directory).rglob('*.js')
|
||||
|
||||
n = 0
|
||||
|
||||
for f in js_files:
|
||||
js = os.path.basename(f)
|
||||
|
||||
self.download_file(js, prefix)
|
||||
|
||||
n += 1
|
||||
|
||||
return n
|
||||
|
||||
def test_render_files(self):
|
||||
"""Look for all javascript files."""
|
||||
n = 0
|
||||
|
||||
print('Rendering javascript files...')
|
||||
|
||||
n += self.download_files('translated', '/js/i18n')
|
||||
n += self.download_files('dynamic', '/js/dynamic')
|
||||
|
||||
print(f'Rendered {n} javascript files.')
|
||||
@@ -131,7 +131,7 @@ def load_config_data(set_cache: bool = False) -> map:
|
||||
|
||||
cfg_file = get_config_file()
|
||||
|
||||
with open(cfg_file, 'r') as cfg:
|
||||
with open(cfg_file, encoding='utf-8') as cfg:
|
||||
data = yaml.safe_load(cfg)
|
||||
|
||||
# Set the cache if requested
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Provides extra global data to all templates."""
|
||||
|
||||
import InvenTree.email
|
||||
import InvenTree.status
|
||||
from generic.states import StatusCode
|
||||
from InvenTree.helpers import inheritors
|
||||
from users.models import RuleSet, check_user_role
|
||||
|
||||
|
||||
def health_status(request):
|
||||
"""Provide system health status information to the global context.
|
||||
|
||||
- Not required for AJAX requests
|
||||
- Do not provide if it is already provided to the context
|
||||
"""
|
||||
if request.path.endswith('.js'):
|
||||
# Do not provide to script requests
|
||||
return {} # pragma: no cover
|
||||
|
||||
if hasattr(request, '_inventree_health_status'):
|
||||
# Do not duplicate efforts
|
||||
return {}
|
||||
|
||||
request._inventree_health_status = True
|
||||
|
||||
status = {
|
||||
'django_q_running': InvenTree.status.is_worker_running(),
|
||||
'email_configured': InvenTree.email.is_email_configured(),
|
||||
}
|
||||
|
||||
# The following keys are required to denote system health
|
||||
health_keys = ['django_q_running']
|
||||
|
||||
all_healthy = True
|
||||
|
||||
for k in health_keys:
|
||||
if status[k] is not True:
|
||||
all_healthy = False
|
||||
|
||||
status['system_healthy'] = all_healthy
|
||||
|
||||
status['up_to_date'] = InvenTree.version.isInvenTreeUpToDate()
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def status_codes(request):
|
||||
"""Provide status code enumerations."""
|
||||
if hasattr(request, '_inventree_status_codes'):
|
||||
# Do not duplicate efforts
|
||||
return {}
|
||||
|
||||
request._inventree_status_codes = True
|
||||
return {cls.__name__: cls.template_context() for cls in inheritors(StatusCode)}
|
||||
|
||||
|
||||
def user_roles(request):
|
||||
"""Return a map of the current roles assigned to the user.
|
||||
|
||||
Roles are denoted by their simple names, and then the permission type.
|
||||
|
||||
Permissions can be access as follows:
|
||||
|
||||
- roles.part.view
|
||||
- roles.build.delete
|
||||
|
||||
Each value will return a boolean True / False
|
||||
"""
|
||||
user = request.user
|
||||
|
||||
roles = {}
|
||||
|
||||
for role in RuleSet.get_ruleset_models().keys():
|
||||
permissions = {}
|
||||
|
||||
for perm in ['view', 'add', 'change', 'delete']:
|
||||
permissions[perm] = user.is_superuser or check_user_role(user, role, perm)
|
||||
|
||||
roles[role] = permissions
|
||||
|
||||
return {'roles': roles}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -95,7 +96,7 @@ def from_engineering_notation(value):
|
||||
"""
|
||||
value = str(value).strip()
|
||||
|
||||
pattern = '(\d+)([a-zA-Z]+)(\d+)(.*)'
|
||||
pattern = r'(\d+)([a-zA-Z]+)(\d+)(.*)'
|
||||
|
||||
if match := re.match(pattern, value):
|
||||
left, prefix, right, suffix = match.groups()
|
||||
@@ -133,7 +134,7 @@ def convert_value(value, unit):
|
||||
return value
|
||||
|
||||
|
||||
def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
def convert_physical_value(value: str, unit: Optional[str] = None, strip_units=True):
|
||||
"""Validate that the provided value is a valid physical quantity.
|
||||
|
||||
Arguments:
|
||||
@@ -153,7 +154,7 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
if unit:
|
||||
try:
|
||||
valid = unit in ureg
|
||||
except Exception as exc:
|
||||
except Exception:
|
||||
valid = False
|
||||
|
||||
if not valid:
|
||||
@@ -196,14 +197,14 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
try:
|
||||
value = convert_value(attempt, unit)
|
||||
break
|
||||
except Exception as exc:
|
||||
except Exception:
|
||||
value = None
|
||||
|
||||
if value is None:
|
||||
if unit:
|
||||
raise ValidationError(_(f'Could not convert {original} to {unit}'))
|
||||
else:
|
||||
raise ValidationError(_('Invalid quantity supplied'))
|
||||
raise ValidationError(_('Invalid quantity provided'))
|
||||
|
||||
# Calculate the "magnitude" of the value, as a float
|
||||
# If the value is specified strangely (e.g. as a fraction or a dozen), this can cause issues
|
||||
@@ -217,7 +218,7 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
|
||||
magnitude = float(ureg.Quantity(magnitude).to_base_units().magnitude)
|
||||
except Exception as exc:
|
||||
raise ValidationError(_(f'Invalid quantity supplied ({exc})'))
|
||||
raise ValidationError(_('Invalid quantity provided') + f': ({exc})')
|
||||
|
||||
if strip_units:
|
||||
return magnitude
|
||||
@@ -245,8 +246,4 @@ def is_dimensionless(value):
|
||||
if value.units == ureg.dimensionless:
|
||||
return True
|
||||
|
||||
if value.to_base_units().units == ureg.dimensionless:
|
||||
return True
|
||||
|
||||
# At this point, the value is not dimensionless
|
||||
return False
|
||||
return value.to_base_units().units == ureg.dimensionless
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Custom exception handling for the DRF API."""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import sys
|
||||
@@ -9,17 +8,13 @@ import traceback
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.db.utils import IntegrityError, OperationalError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import rest_framework.views as drfviews
|
||||
from error_report.models import Error
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
import InvenTree.sentry
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
@@ -36,16 +31,15 @@ def log_error(path, error_name=None, error_info=None, error_data=None):
|
||||
error_info: The error information (optional, overrides 'info')
|
||||
error_data: The error data (optional, overrides 'data')
|
||||
"""
|
||||
from error_report.models import Error
|
||||
|
||||
kind, info, data = sys.exc_info()
|
||||
|
||||
# Check if the error is on the ignore list
|
||||
if kind in settings.IGNORED_ERRORS:
|
||||
return
|
||||
|
||||
if error_name:
|
||||
kind = error_name
|
||||
else:
|
||||
kind = getattr(kind, '__name__', 'Unknown Error')
|
||||
kind = error_name or getattr(kind, '__name__', 'Unknown Error')
|
||||
|
||||
if error_info:
|
||||
info = error_info
|
||||
@@ -80,6 +74,8 @@ def exception_handler(exc, context):
|
||||
|
||||
If sentry error reporting is enabled, we will also provide the original exception to sentry.io
|
||||
"""
|
||||
import InvenTree.sentry
|
||||
|
||||
response = None
|
||||
|
||||
# Pass exception to sentry.io handler
|
||||
|
||||
@@ -31,10 +31,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
|
||||
# Find the selected exchange rate plugin
|
||||
slug = get_global_setting('CURRENCY_UPDATE_PLUGIN', create=False)
|
||||
|
||||
if slug:
|
||||
plugin = registry.get_plugin(slug)
|
||||
else:
|
||||
plugin = None
|
||||
plugin = registry.get_plugin(slug) if slug else None
|
||||
|
||||
if not plugin:
|
||||
# Find the first active currency exchange plugin
|
||||
|
||||
@@ -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 _
|
||||
|
||||
@@ -93,9 +94,8 @@ class InvenTreeModelMoneyField(ModelMoneyField):
|
||||
allow_negative = kwargs.pop('allow_negative', False)
|
||||
|
||||
# If no validators are provided, add some "standard" ones
|
||||
if len(validators) == 0:
|
||||
if not allow_negative:
|
||||
validators.append(MinMoneyValidator(0))
|
||||
if len(validators) == 0 and not allow_negative:
|
||||
validators.append(MinMoneyValidator(0))
|
||||
|
||||
kwargs['validators'] = validators
|
||||
|
||||
@@ -134,9 +134,9 @@ class DatePickerFormField(forms.DateField):
|
||||
def __init__(self, **kwargs):
|
||||
"""Set up custom values."""
|
||||
help_text = kwargs.get('help_text', _('Enter date'))
|
||||
label = kwargs.get('label', None)
|
||||
label = kwargs.get('label')
|
||||
required = kwargs.get('required', False)
|
||||
initial = kwargs.get('initial', None)
|
||||
initial = kwargs.get('initial')
|
||||
|
||||
widget = forms.DateInput(attrs={'type': 'date'})
|
||||
|
||||
@@ -153,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
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
"""Helpers for file handling in InvenTree."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
TEMPLATES_DIR = Path(__file__).parent.parent
|
||||
MEDIA_STORAGE_DIR = settings.MEDIA_ROOT
|
||||
@@ -118,10 +118,7 @@ class InvenTreeOrderingFilter(filters.OrderingFilter):
|
||||
field = field[1:]
|
||||
|
||||
# Are aliases defined for this field?
|
||||
if field in aliases:
|
||||
alias = aliases[field]
|
||||
else:
|
||||
alias = field
|
||||
alias = aliases.get(field, field)
|
||||
|
||||
"""
|
||||
Potentially, a single field could be "aliased" to multiple field,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import re
|
||||
import string
|
||||
from typing import Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import translation
|
||||
@@ -106,15 +107,16 @@ def construct_format_regex(fmt_string: str) -> str:
|
||||
# Add a named capture group for the format entry
|
||||
if name:
|
||||
# Check if integer values are required
|
||||
if _fmt.endswith('d'):
|
||||
c = '\d'
|
||||
else:
|
||||
c = '.'
|
||||
c = '\\d' if _fmt.endswith('d') else '.'
|
||||
|
||||
# Specify width
|
||||
# TODO: Introspect required width
|
||||
w = '+'
|
||||
|
||||
# replace invalid regex group name '?' with a valid name
|
||||
if name == '?':
|
||||
name = 'wild'
|
||||
|
||||
pattern += f'(?P<{name}>{c}{w})'
|
||||
|
||||
pattern += '$'
|
||||
@@ -160,7 +162,7 @@ def extract_named_group(name: str, value: str, fmt_string: str) -> str:
|
||||
"""
|
||||
info = parse_format_string(fmt_string)
|
||||
|
||||
if name not in info.keys():
|
||||
if name not in info:
|
||||
raise NameError(_(f"Value '{name}' does not appear in pattern format"))
|
||||
|
||||
# Construct a regular expression for matching against the provided format string
|
||||
@@ -182,8 +184,8 @@ def extract_named_group(name: str, value: str, fmt_string: str) -> str:
|
||||
|
||||
def format_money(
|
||||
money: Money,
|
||||
decimal_places: int = None,
|
||||
format: str = None,
|
||||
decimal_places: Optional[int] = None,
|
||||
fmt: Optional[str] = None,
|
||||
include_symbol: bool = True,
|
||||
) -> str:
|
||||
"""Format money object according to the currently set local.
|
||||
@@ -191,7 +193,7 @@ def format_money(
|
||||
Args:
|
||||
money (Money): The money object to format
|
||||
decimal_places (int): Number of decimal places to use
|
||||
format (str): Format pattern according LDML / the babel format pattern syntax (https://babel.pocoo.org/en/latest/numbers.html)
|
||||
fmt (str): Format pattern according LDML / the babel format pattern syntax (https://babel.pocoo.org/en/latest/numbers.html)
|
||||
|
||||
Returns:
|
||||
str: The formatted string
|
||||
@@ -199,10 +201,10 @@ def format_money(
|
||||
Raises:
|
||||
ValueError: format string is incorrectly specified
|
||||
"""
|
||||
language = None and translation.get_language() or settings.LANGUAGE_CODE
|
||||
language = (None) or settings.LANGUAGE_CODE
|
||||
locale = Locale.parse(translation.to_locale(language))
|
||||
if format:
|
||||
pattern = parse_pattern(format)
|
||||
if fmt:
|
||||
pattern = parse_pattern(fmt)
|
||||
else:
|
||||
pattern = locale.currency_formats['standard']
|
||||
if decimal_places is not None:
|
||||
|
||||
@@ -2,32 +2,31 @@
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import inspect
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from pathlib import Path
|
||||
from typing import TypeVar, Union
|
||||
from typing import Optional, TypeVar, Union
|
||||
from wsgiref.util import FileWrapper
|
||||
|
||||
import django.utils.timezone as timezone
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.storage import StaticFilesStorage
|
||||
from django.core.exceptions import FieldError, ValidationError
|
||||
from django.core.files.storage import Storage, default_storage
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import pytz
|
||||
import regex
|
||||
import bleach
|
||||
from bleach import clean
|
||||
from djmoney.money import Money
|
||||
from PIL import Image
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
import InvenTree.version
|
||||
from common.currency import currency_code_default
|
||||
|
||||
from .settings import MEDIA_URL, STATIC_URL
|
||||
@@ -99,10 +98,7 @@ def generateTestKey(test_name: str) -> str:
|
||||
if char.isidentifier():
|
||||
return True
|
||||
|
||||
if char.isalnum():
|
||||
return True
|
||||
|
||||
return False
|
||||
return bool(char.isalnum())
|
||||
|
||||
# Remove any characters that cannot be used to represent a variable
|
||||
key = ''.join([c for c in key if valid_char(c)])
|
||||
@@ -396,41 +392,9 @@ def WrapWithQuotes(text, quote='"'):
|
||||
return text
|
||||
|
||||
|
||||
def MakeBarcode(cls_name, object_pk: int, object_data=None, **kwargs):
|
||||
"""Generate a string for a barcode. Adds some global InvenTree parameters.
|
||||
|
||||
Args:
|
||||
cls_name: string describing the object type e.g. 'StockItem'
|
||||
object_pk (int): ID (Primary Key) of the object in the database
|
||||
object_data: Python dict object containing extra data which will be rendered to string (must only contain stringable values)
|
||||
|
||||
Returns:
|
||||
json string of the supplied data plus some other data
|
||||
"""
|
||||
if object_data is None:
|
||||
object_data = {}
|
||||
|
||||
brief = kwargs.get('brief', True)
|
||||
|
||||
data = {}
|
||||
|
||||
if brief:
|
||||
data[cls_name] = object_pk
|
||||
else:
|
||||
data['tool'] = 'InvenTree'
|
||||
data['version'] = InvenTree.version.inventreeVersion()
|
||||
data['instance'] = InvenTree.version.inventreeInstanceName()
|
||||
|
||||
# Ensure PK is included
|
||||
object_data['id'] = object_pk
|
||||
data[cls_name] = object_data
|
||||
|
||||
return str(json.dumps(data, sort_keys=True))
|
||||
|
||||
|
||||
def GetExportFormats():
|
||||
"""Return a list of allowable file formats for exporting data."""
|
||||
return ['csv', 'tsv', 'xls', 'xlsx', 'json', 'yaml']
|
||||
"""Return a list of allowable file formats for importing or exporting tabular data."""
|
||||
return ['csv', 'xlsx', 'tsv', 'json']
|
||||
|
||||
|
||||
def DownloadFile(
|
||||
@@ -469,7 +433,7 @@ def DownloadFile(
|
||||
return response
|
||||
|
||||
|
||||
def increment_serial_number(serial):
|
||||
def increment_serial_number(serial, part=None):
|
||||
"""Given a serial number, (attempt to) generate the *next* serial number.
|
||||
|
||||
Note: This method is exposed to custom plugins.
|
||||
@@ -480,6 +444,7 @@ def increment_serial_number(serial):
|
||||
Returns:
|
||||
incremented value, or None if incrementing could not be performed.
|
||||
"""
|
||||
from InvenTree.exceptions import log_error
|
||||
from plugin.registry import registry
|
||||
|
||||
# Ensure we start with a string value
|
||||
@@ -488,16 +453,30 @@ def increment_serial_number(serial):
|
||||
|
||||
# First, let any plugins attempt to increment the serial number
|
||||
for plugin in registry.with_mixin('validation'):
|
||||
result = plugin.increment_serial_number(serial)
|
||||
if result is not None:
|
||||
return str(result)
|
||||
try:
|
||||
if not hasattr(plugin, 'increment_serial_number'):
|
||||
continue
|
||||
|
||||
signature = inspect.signature(plugin.increment_serial_number)
|
||||
|
||||
# Note: 2024-08-21 - The 'part' parameter has been added to the signature
|
||||
if 'part' in signature.parameters:
|
||||
result = plugin.increment_serial_number(serial, part=part)
|
||||
else:
|
||||
result = plugin.increment_serial_number(serial)
|
||||
if result is not None:
|
||||
return str(result)
|
||||
except Exception:
|
||||
log_error(f'{plugin.slug}.increment_serial_number')
|
||||
|
||||
# If we get to here, no plugins were able to "increment" the provided serial value
|
||||
# Attempt to perform increment according to some basic rules
|
||||
return increment(serial)
|
||||
|
||||
|
||||
def extract_serial_numbers(input_string, expected_quantity: int, starting_value=None):
|
||||
def extract_serial_numbers(
|
||||
input_string, expected_quantity: int, starting_value=None, part=None
|
||||
):
|
||||
"""Extract a list of serial numbers from a provided input string.
|
||||
|
||||
The input string can be specified using the following concepts:
|
||||
@@ -517,27 +496,29 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
||||
starting_value: Provide a starting value for the sequence (or None)
|
||||
"""
|
||||
if starting_value is None:
|
||||
starting_value = increment_serial_number(None)
|
||||
starting_value = increment_serial_number(None, part=part)
|
||||
|
||||
try:
|
||||
expected_quantity = int(expected_quantity)
|
||||
except ValueError:
|
||||
raise ValidationError([_('Invalid quantity provided')])
|
||||
|
||||
if input_string:
|
||||
input_string = str(input_string).strip()
|
||||
else:
|
||||
input_string = ''
|
||||
if expected_quantity > 1000:
|
||||
raise ValidationError({
|
||||
'quantity': [_('Cannot serialize more than 1000 items at once')]
|
||||
})
|
||||
|
||||
input_string = str(input_string).strip() if input_string else ''
|
||||
|
||||
if len(input_string) == 0:
|
||||
raise ValidationError([_('Empty serial number string')])
|
||||
|
||||
next_value = increment_serial_number(starting_value)
|
||||
next_value = increment_serial_number(starting_value, part=part)
|
||||
|
||||
# Substitute ~ character with latest value
|
||||
while '~' in input_string and next_value:
|
||||
input_string = input_string.replace('~', str(next_value), 1)
|
||||
next_value = increment_serial_number(next_value)
|
||||
next_value = increment_serial_number(next_value, part=part)
|
||||
|
||||
# Split input string by whitespace or comma (,) characters
|
||||
groups = re.split(r'[\s,]+', input_string)
|
||||
@@ -591,7 +572,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
||||
|
||||
if a == b:
|
||||
# Invalid group
|
||||
add_error(_(f'Invalid group range: {group}'))
|
||||
add_error(_(f'Invalid group: {group}'))
|
||||
continue
|
||||
|
||||
group_items = []
|
||||
@@ -634,7 +615,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
||||
for item in group_items:
|
||||
add_serial(item)
|
||||
else:
|
||||
add_error(_(f'Invalid group range: {group}'))
|
||||
add_error(_(f'Invalid group: {group}'))
|
||||
|
||||
else:
|
||||
# In the case of a different number of hyphens, simply add the entire group
|
||||
@@ -652,14 +633,14 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
||||
sequence_count = max(0, expected_quantity - len(serials))
|
||||
|
||||
if len(items) > 2 or len(items) == 0:
|
||||
add_error(_(f'Invalid group sequence: {group}'))
|
||||
add_error(_(f'Invalid group: {group}'))
|
||||
continue
|
||||
elif len(items) == 2:
|
||||
try:
|
||||
if items[1]:
|
||||
sequence_count = int(items[1]) + 1
|
||||
except ValueError:
|
||||
add_error(_(f'Invalid group sequence: {group}'))
|
||||
add_error(_(f'Invalid group: {group}'))
|
||||
continue
|
||||
|
||||
value = items[0]
|
||||
@@ -678,7 +659,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
|
||||
for item in sequence_items:
|
||||
add_serial(item)
|
||||
else:
|
||||
add_error(_(f'Invalid group sequence: {group}'))
|
||||
add_error(_(f'Invalid group: {group}'))
|
||||
|
||||
else:
|
||||
# At this point, we assume that the 'group' is just a single serial value
|
||||
@@ -799,6 +780,8 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
|
||||
|
||||
If raise_error is True, a ValidationError will be thrown if HTML tags are detected
|
||||
"""
|
||||
value = str(value).strip()
|
||||
|
||||
cleaned = clean(value, strip=True, tags=[], attributes=[])
|
||||
|
||||
# Add escaped characters back in
|
||||
@@ -810,39 +793,91 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
|
||||
# If the length changed, it means that HTML tags were removed!
|
||||
if len(cleaned) != len(value) and raise_error:
|
||||
field = field_name or 'non_field_errors'
|
||||
|
||||
raise ValidationError({field: [_('Remove HTML tags from this value')]})
|
||||
|
||||
return cleaned
|
||||
|
||||
|
||||
def remove_non_printable_characters(
|
||||
value: str, remove_newline=True, remove_ascii=True, remove_unicode=True
|
||||
):
|
||||
def remove_non_printable_characters(value: str, remove_newline=True) -> str:
|
||||
"""Remove non-printable / control characters from the provided string."""
|
||||
cleaned = value
|
||||
|
||||
if remove_ascii:
|
||||
# Remove ASCII control characters
|
||||
# Note that we do not sub out 0x0A (\n) here, it is done separately below
|
||||
cleaned = regex.sub('[\x00-\x09]+', '', cleaned)
|
||||
cleaned = regex.sub('[\x0b-\x1f\x7f]+', '', cleaned)
|
||||
# Remove ASCII control characters
|
||||
# Note that we do not sub out 0x0A (\n) here, it is done separately below
|
||||
regex = re.compile(r'[\u0000-\u0009\u000B-\u001F\u007F-\u009F]')
|
||||
cleaned = regex.sub('', cleaned)
|
||||
|
||||
# Remove Unicode control characters
|
||||
regex = re.compile(r'[\u200E\u200F\u202A-\u202E]')
|
||||
cleaned = regex.sub('', cleaned)
|
||||
|
||||
if remove_newline:
|
||||
cleaned = regex.sub('[\x0a]+', '', cleaned)
|
||||
|
||||
if remove_unicode:
|
||||
# Remove Unicode control characters
|
||||
if remove_newline:
|
||||
cleaned = regex.sub('[^\P{C}]+', '', cleaned)
|
||||
else:
|
||||
# Use 'negative-lookahead' to exclude newline character
|
||||
cleaned = regex.sub('(?![\x0a])[^\P{C}]+', '', cleaned)
|
||||
regex = re.compile(r'[\x0A]')
|
||||
cleaned = regex.sub('', cleaned)
|
||||
|
||||
return cleaned
|
||||
|
||||
|
||||
def hash_barcode(barcode_data):
|
||||
def clean_markdown(value: str) -> str:
|
||||
"""Clean a markdown string.
|
||||
|
||||
This function will remove javascript and other potentially harmful content from the markdown string.
|
||||
"""
|
||||
import markdown
|
||||
|
||||
try:
|
||||
markdownify_settings = settings.MARKDOWNIFY['default']
|
||||
except (AttributeError, KeyError):
|
||||
markdownify_settings = {}
|
||||
|
||||
extensions = markdownify_settings.get('MARKDOWN_EXTENSIONS', [])
|
||||
extension_configs = markdownify_settings.get('MARKDOWN_EXTENSION_CONFIGS', {})
|
||||
|
||||
# Generate raw HTML from provided markdown (without sanitizing)
|
||||
# Note: The 'html' output_format is required to generate self closing tags, e.g. <tag> instead of <tag />
|
||||
html = markdown.markdown(
|
||||
value or '',
|
||||
extensions=extensions,
|
||||
extension_configs=extension_configs,
|
||||
output_format='html',
|
||||
)
|
||||
|
||||
# Bleach settings
|
||||
whitelist_tags = markdownify_settings.get(
|
||||
'WHITELIST_TAGS', bleach.sanitizer.ALLOWED_TAGS
|
||||
)
|
||||
whitelist_attrs = markdownify_settings.get(
|
||||
'WHITELIST_ATTRS', bleach.sanitizer.ALLOWED_ATTRIBUTES
|
||||
)
|
||||
whitelist_styles = markdownify_settings.get(
|
||||
'WHITELIST_STYLES', bleach.css_sanitizer.ALLOWED_CSS_PROPERTIES
|
||||
)
|
||||
whitelist_protocols = markdownify_settings.get(
|
||||
'WHITELIST_PROTOCOLS', bleach.sanitizer.ALLOWED_PROTOCOLS
|
||||
)
|
||||
strip = markdownify_settings.get('STRIP', True)
|
||||
|
||||
css_sanitizer = bleach.css_sanitizer.CSSSanitizer(
|
||||
allowed_css_properties=whitelist_styles
|
||||
)
|
||||
cleaner = bleach.Cleaner(
|
||||
tags=whitelist_tags,
|
||||
attributes=whitelist_attrs,
|
||||
css_sanitizer=css_sanitizer,
|
||||
protocols=whitelist_protocols,
|
||||
strip=strip,
|
||||
)
|
||||
|
||||
# Clean the HTML content (for comparison). This must be the same as the original content
|
||||
clean_html = cleaner.clean(html)
|
||||
|
||||
if html != clean_html:
|
||||
raise ValidationError(_('Data contains prohibited markdown content'))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def hash_barcode(barcode_data: str) -> str:
|
||||
"""Calculate a 'unique' hash for a barcode string.
|
||||
|
||||
This hash is used for comparison / lookup.
|
||||
@@ -861,7 +896,7 @@ def hash_barcode(barcode_data):
|
||||
def hash_file(filename: Union[str, Path], storage: Union[Storage, None] = None):
|
||||
"""Return the MD5 hash of a file."""
|
||||
content = (
|
||||
open(filename, 'rb').read()
|
||||
open(filename, 'rb').read() # noqa: SIM115
|
||||
if storage is None
|
||||
else storage.open(str(filename), 'rb').read()
|
||||
)
|
||||
@@ -899,7 +934,7 @@ def server_timezone() -> str:
|
||||
return settings.TIME_ZONE
|
||||
|
||||
|
||||
def to_local_time(time, target_tz: str = None):
|
||||
def to_local_time(time, target_tz: Optional[str] = None):
|
||||
"""Convert the provided time object to the local timezone.
|
||||
|
||||
Arguments:
|
||||
@@ -926,15 +961,15 @@ def to_local_time(time, target_tz: str = None):
|
||||
|
||||
if not source_tz:
|
||||
# Default to UTC if not provided
|
||||
source_tz = pytz.utc
|
||||
source_tz = ZoneInfo('UTC')
|
||||
|
||||
if not target_tz:
|
||||
target_tz = server_timezone()
|
||||
|
||||
try:
|
||||
target_tz = pytz.timezone(str(target_tz))
|
||||
except pytz.UnknownTimeZoneError:
|
||||
target_tz = pytz.utc
|
||||
target_tz = ZoneInfo(str(target_tz))
|
||||
except ZoneInfoNotFoundError:
|
||||
target_tz = ZoneInfo('UTC')
|
||||
|
||||
target_time = time.replace(tzinfo=source_tz).astimezone(target_tz)
|
||||
|
||||
@@ -981,14 +1016,28 @@ def get_objectreference(
|
||||
ret = {}
|
||||
if url_fnc:
|
||||
ret['link'] = url_fnc()
|
||||
return {'name': str(item), 'model': str(model_cls._meta.verbose_name), **ret}
|
||||
|
||||
return {
|
||||
'name': str(item),
|
||||
'model_name': str(model_cls._meta.verbose_name),
|
||||
'model_type': str(model_cls._meta.model_name),
|
||||
'model_id': getattr(item, 'pk', None),
|
||||
**ret,
|
||||
}
|
||||
|
||||
|
||||
Inheritors_T = TypeVar('Inheritors_T')
|
||||
|
||||
|
||||
def inheritors(cls: type[Inheritors_T]) -> set[type[Inheritors_T]]:
|
||||
"""Return all classes that are subclasses from the supplied cls."""
|
||||
def inheritors(
|
||||
cls: type[Inheritors_T], subclasses: bool = True
|
||||
) -> set[type[Inheritors_T]]:
|
||||
"""Return all classes that are subclasses from the supplied cls.
|
||||
|
||||
Args:
|
||||
cls: The class to search for subclasses
|
||||
subclasses: Include subclasses of subclasses (default = True)
|
||||
"""
|
||||
subcls = set()
|
||||
work = [cls]
|
||||
|
||||
@@ -997,7 +1046,8 @@ def inheritors(cls: type[Inheritors_T]) -> set[type[Inheritors_T]]:
|
||||
for child in parent.__subclasses__():
|
||||
if child not in subcls:
|
||||
subcls.add(child)
|
||||
work.append(child)
|
||||
if subclasses:
|
||||
work.append(child)
|
||||
return subcls
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -62,7 +62,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
|
||||
# If we are importing data, don't send emails
|
||||
return
|
||||
|
||||
if not InvenTree.email.is_email_configured() and not settings.TESTING:
|
||||
if not is_email_configured() and not settings.TESTING:
|
||||
# Email is not configured / enabled
|
||||
return
|
||||
|
||||
@@ -86,4 +86,5 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
|
||||
recipients,
|
||||
fail_silently=False,
|
||||
html_message=html_message,
|
||||
group='notification',
|
||||
)
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
from plugin import registry as plg_registry
|
||||
|
||||
@@ -60,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 = []
|
||||
@@ -73,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:
|
||||
@@ -97,10 +99,44 @@ class ClassProviderMixin:
|
||||
|
||||
@classmethod
|
||||
def get_is_builtin(cls):
|
||||
"""Is this Class build in the Inventree source code?"""
|
||||
"""Is this Class build in the InvenTree source code?"""
|
||||
try:
|
||||
Path(cls.get_provider_file()).relative_to(settings.BASE_DIR)
|
||||
return True
|
||||
except ValueError:
|
||||
# Path(...).relative_to throws an ValueError if its not relative to the InvenTree source base dir
|
||||
return False
|
||||
|
||||
|
||||
def get_shared_class_instance_state_mixin(get_state_key: Callable[[type], str]):
|
||||
"""Get a mixin class that provides shared state for classes across the main application and worker.
|
||||
|
||||
Arguments:
|
||||
get_state_key: A function that returns the key for the shared state when given a class instance.
|
||||
"""
|
||||
|
||||
class SharedClassStateMixinClass:
|
||||
"""Mixin to provide shared state for classes across the main application and worker."""
|
||||
|
||||
def set_shared_state(self, key: str, value: Any):
|
||||
"""Set a shared state value for this machine.
|
||||
|
||||
Arguments:
|
||||
key: The key for the shared state
|
||||
value: The value to set
|
||||
"""
|
||||
cache.set(self._get_key(key), value, timeout=None)
|
||||
|
||||
def get_shared_state(self, key: str, default=None):
|
||||
"""Get a shared state value for this machine.
|
||||
|
||||
Arguments:
|
||||
key: The key for the shared state
|
||||
"""
|
||||
return cache.get(self._get_key(key)) or default
|
||||
|
||||
def _get_key(self, key: str):
|
||||
"""Get the key for this class instance."""
|
||||
return f'{get_state_key(self)}:{key}'
|
||||
|
||||
return SharedClassStateMixinClass
|
||||
|
||||
@@ -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
|
||||
@@ -15,9 +16,6 @@ from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
from PIL import Image
|
||||
|
||||
import InvenTree
|
||||
import InvenTree.helpers_model
|
||||
import InvenTree.version
|
||||
from common.notifications import (
|
||||
InvenTreeNotificationBodies,
|
||||
NotificationBody,
|
||||
@@ -39,8 +37,6 @@ def get_base_url(request=None):
|
||||
3. If settings.SITE_URL is set (e.g. in the Django settings), use that
|
||||
4. If the InvenTree setting INVENTREE_BASE_URL is set, use that
|
||||
"""
|
||||
import common.models
|
||||
|
||||
# Check if a request is provided
|
||||
if request:
|
||||
return request.build_absolute_uri('/')
|
||||
@@ -107,8 +103,6 @@ def download_image_from_url(remote_url, timeout=2.5):
|
||||
ValueError: Server responded with invalid 'Content-Length' value
|
||||
TypeError: Response is not a valid image
|
||||
"""
|
||||
import common.models
|
||||
|
||||
# Check that the provided URL at least looks valid
|
||||
validator = URLValidator()
|
||||
validator(remote_url)
|
||||
@@ -121,10 +115,7 @@ def download_image_from_url(remote_url, timeout=2.5):
|
||||
# Add user specified user-agent to request (if specified)
|
||||
user_agent = get_global_setting('INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT')
|
||||
|
||||
if user_agent:
|
||||
headers = {'User-Agent': user_agent}
|
||||
else:
|
||||
headers = None
|
||||
headers = {'User-Agent': user_agent} if user_agent else None
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
@@ -137,7 +128,7 @@ def download_image_from_url(remote_url, timeout=2.5):
|
||||
# Throw an error if anything goes wrong
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
raise Exception(_('Connection error') + f': {str(exc)}')
|
||||
raise Exception(_('Connection error') + f': {exc!s}')
|
||||
except requests.exceptions.Timeout as exc:
|
||||
raise exc
|
||||
except requests.exceptions.HTTPError:
|
||||
@@ -145,7 +136,7 @@ def download_image_from_url(remote_url, timeout=2.5):
|
||||
_('Server responded with invalid status code') + f': {response.status_code}'
|
||||
)
|
||||
except Exception as exc:
|
||||
raise Exception(_('Exception occurred') + f': {str(exc)}')
|
||||
raise Exception(_('Exception occurred') + f': {exc!s}')
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(
|
||||
@@ -189,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).
|
||||
|
||||
@@ -206,8 +197,6 @@ def render_currency(
|
||||
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
||||
include_symbol: If True, include the currency symbol in the output
|
||||
"""
|
||||
import common.models
|
||||
|
||||
if money in [None, '']:
|
||||
return '-'
|
||||
|
||||
@@ -222,9 +211,6 @@ def render_currency(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if decimal_places is None:
|
||||
decimal_places = get_global_setting('PRICING_DECIMAL_PLACES', 6)
|
||||
|
||||
if min_decimal_places is None:
|
||||
min_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES_MIN', 0)
|
||||
|
||||
@@ -234,17 +220,19 @@ def render_currency(
|
||||
value = Decimal(str(money.amount)).normalize()
|
||||
value = str(value)
|
||||
|
||||
if '.' in value:
|
||||
decimals = len(value.split('.')[-1])
|
||||
|
||||
decimals = max(decimals, min_decimal_places)
|
||||
decimals = min(decimals, decimal_places)
|
||||
|
||||
decimal_places = decimals
|
||||
if decimal_places is not None:
|
||||
# Decimal place count is provided, use it
|
||||
pass
|
||||
elif '.' in value:
|
||||
# If the value has a decimal point, use the number of decimal places in the value
|
||||
decimal_places = len(value.split('.')[-1])
|
||||
else:
|
||||
decimal_places = max(decimal_places, 2)
|
||||
# No decimal point, use 2 as a default
|
||||
decimal_places = 2
|
||||
|
||||
decimal_places = max(decimal_places, max_decimal_places)
|
||||
# Clip the decimal places to the specified range
|
||||
decimal_places = max(decimal_places, min_decimal_places)
|
||||
decimal_places = min(decimal_places, max_decimal_places)
|
||||
|
||||
return format_money(
|
||||
money, decimal_places=decimal_places, include_symbol=include_symbol
|
||||
@@ -252,7 +240,7 @@ def render_currency(
|
||||
|
||||
|
||||
def getModelsWithMixin(mixin_class) -> list:
|
||||
"""Return a list of models that inherit from the given mixin class.
|
||||
"""Return a list of database models that inherit from the given mixin class.
|
||||
|
||||
Args:
|
||||
mixin_class: The mixin class to search for
|
||||
@@ -331,9 +319,7 @@ def notify_users(
|
||||
'instance': instance,
|
||||
'name': content.name.format(**content_context),
|
||||
'message': content.message.format(**content_context),
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(
|
||||
instance.get_absolute_url()
|
||||
),
|
||||
'link': construct_absolute_url(instance.get_absolute_url()),
|
||||
'template': {'subject': content.name.format(**content_context)},
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,14 @@ Additionally, update the following files with the new locale code:
|
||||
|
||||
- /src/frontend/.linguirc file
|
||||
- /src/frontend/src/contexts/LanguageContext.tsx
|
||||
|
||||
(and then run "invoke int.frontend-trans")
|
||||
"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
LOCALES = [
|
||||
('ar', _('Arabic')),
|
||||
('bg', _('Bulgarian')),
|
||||
('cs', _('Czech')),
|
||||
('da', _('Danish')),
|
||||
@@ -23,6 +26,7 @@ LOCALES = [
|
||||
('en', _('English')),
|
||||
('es', _('Spanish')),
|
||||
('es-mx', _('Spanish (Mexican)')),
|
||||
('et', _('Estonian')),
|
||||
('fa', _('Farsi / Persian')),
|
||||
('fi', _('Finnish')),
|
||||
('fr', _('French')),
|
||||
@@ -32,6 +36,7 @@ LOCALES = [
|
||||
('it', _('Italian')),
|
||||
('ja', _('Japanese')),
|
||||
('ko', _('Korean')),
|
||||
('lt', _('Lithuanian')),
|
||||
('lv', _('Latvian')),
|
||||
('nl', _('Dutch')),
|
||||
('no', _('Norwegian')),
|
||||
|
||||
@@ -25,7 +25,7 @@ def send_simple_login_email(user, link):
|
||||
)
|
||||
|
||||
send_mail(
|
||||
_(f'[{site_name}] Log in to the app'),
|
||||
f'[{site_name}] ' + _('Log in to the app'),
|
||||
email_plaintext_message,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[user.email],
|
||||
|
||||
@@ -8,6 +8,7 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""Run the management command."""
|
||||
from plugin.staticfiles import collect_plugins_static_files
|
||||
import plugin.staticfiles
|
||||
|
||||
collect_plugins_static_files()
|
||||
plugin.staticfiles.collect_plugins_static_files()
|
||||
plugin.staticfiles.clear_plugins_static_files()
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Custom management command to export settings definitions.
|
||||
|
||||
This is used to generate a JSON file which contains all of the settings,
|
||||
so that they can be introspected by the InvenTree documentation system.
|
||||
|
||||
This in turn allows settings to be documented in the InvenTree documentation,
|
||||
without having to manually duplicate the information in multiple places.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Extract settings information, and export to a JSON file."""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add custom arguments for this command."""
|
||||
parser.add_argument(
|
||||
'filename', type=str, help='Output filename for settings definitions'
|
||||
)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""Export settings information to a JSON file."""
|
||||
from common.models import InvenTreeSetting, InvenTreeUserSetting
|
||||
|
||||
settings = {'global': {}, 'user': {}}
|
||||
|
||||
# Global settings
|
||||
for key, setting in InvenTreeSetting.SETTINGS.items():
|
||||
settings['global'][key] = {
|
||||
'name': str(setting['name']),
|
||||
'description': str(setting['description']),
|
||||
'default': str(InvenTreeSetting.get_setting_default(key)),
|
||||
'units': str(setting.get('units', '')),
|
||||
}
|
||||
|
||||
# User settings
|
||||
for key, setting in InvenTreeUserSetting.SETTINGS.items():
|
||||
settings['user'][key] = {
|
||||
'name': str(setting['name']),
|
||||
'description': str(setting['description']),
|
||||
'default': str(InvenTreeUserSetting.get_setting_default(key)),
|
||||
'units': str(setting.get('units', '')),
|
||||
}
|
||||
|
||||
filename = kwargs.get('filename', 'inventree_settings.json')
|
||||
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(settings, f, indent=4)
|
||||
|
||||
print(f"Exported InvenTree settings definitions to '{filename}'")
|
||||
@@ -0,0 +1,192 @@
|
||||
"""Custom management command to migrate the old FontAwesome icons."""
|
||||
|
||||
import json
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import models
|
||||
|
||||
from common.icons import validate_icon
|
||||
from part.models import PartCategory
|
||||
from stock.models import StockLocation, StockLocationType
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Generate an icon map from the FontAwesome library to the new icon library."""
|
||||
|
||||
help = """Helper command to migrate the old FontAwesome icons to the new icon library."""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add the arguments."""
|
||||
parser.add_argument(
|
||||
'--output-file',
|
||||
type=str,
|
||||
help='Path to file to write generated icon map to',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--input-file', type=str, help='Path to file to read icon map from'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--include-items',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Include referenced inventree items in the output icon map (optional)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--import-now',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='CAUTION: If this flag is set, the icon map will be imported and the database will be touched',
|
||||
)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""Generate an icon map from the FontAwesome library to the new icon library."""
|
||||
# Check for invalid combinations of arguments
|
||||
if kwargs['output_file'] and kwargs['input_file']:
|
||||
raise CommandError('Cannot specify both --input-file and --output-file')
|
||||
|
||||
if not kwargs['output_file'] and not kwargs['input_file']:
|
||||
raise CommandError('Must specify either --input-file or --output-file')
|
||||
|
||||
if kwargs['include_items'] and not kwargs['output_file']:
|
||||
raise CommandError(
|
||||
'--include-items can only be used with an --output-file specified'
|
||||
)
|
||||
|
||||
if kwargs['output_file'] and kwargs['import_now']:
|
||||
raise CommandError(
|
||||
'--import-now can only be used with an --input-file specified'
|
||||
)
|
||||
|
||||
ICON_MODELS = [
|
||||
(StockLocation, 'custom_icon'),
|
||||
(StockLocationType, 'icon'),
|
||||
(PartCategory, '_icon'),
|
||||
]
|
||||
|
||||
def get_model_items_with_icons(model: models.Model, icon_field: str):
|
||||
"""Return a list of models with icon fields."""
|
||||
return model.objects.exclude(**{f'{icon_field}__isnull': True}).exclude(**{
|
||||
f'{icon_field}__exact': ''
|
||||
})
|
||||
|
||||
# Generate output icon map file
|
||||
if kwargs['output_file']:
|
||||
icons = {}
|
||||
|
||||
for model, icon_name in ICON_MODELS:
|
||||
self.stdout.write(
|
||||
f'Processing model {model.__name__} with icon field {icon_name}'
|
||||
)
|
||||
|
||||
items = get_model_items_with_icons(model, icon_name)
|
||||
|
||||
for item in items:
|
||||
icon = getattr(item, icon_name)
|
||||
|
||||
try:
|
||||
validate_icon(icon)
|
||||
continue # Skip if the icon is already valid
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
if icon not in icons:
|
||||
icons[icon] = {
|
||||
**({'items': []} if kwargs['include_items'] else {}),
|
||||
'new_icon': '',
|
||||
}
|
||||
|
||||
if kwargs['include_items']:
|
||||
icons[icon]['items'].append({
|
||||
'model': model.__name__.lower(),
|
||||
'id': item.id, # type: ignore
|
||||
})
|
||||
|
||||
self.stdout.write(f'Writing icon map for {len(icons.keys())} icons')
|
||||
with open(kwargs['output_file'], 'w', encoding='utf-8') as f:
|
||||
json.dump(icons, f, indent=2)
|
||||
|
||||
self.stdout.write(f'Icon map written to {kwargs["output_file"]}')
|
||||
|
||||
# Import icon map file
|
||||
if kwargs['input_file']:
|
||||
with open(kwargs['input_file'], encoding='utf-8') as f:
|
||||
icons = json.load(f)
|
||||
|
||||
self.stdout.write(f'Loaded icon map for {len(icons.keys())} icons')
|
||||
|
||||
self.stdout.write('Verifying icon map')
|
||||
has_errors = False
|
||||
|
||||
# Verify that all new icons are valid icons
|
||||
for old_icon, data in icons.items():
|
||||
try:
|
||||
validate_icon(data.get('new_icon', ''))
|
||||
except ValidationError:
|
||||
self.stdout.write(
|
||||
f'[ERR] Invalid icon: "{old_icon}" -> "{data.get("new_icon", "")}'
|
||||
)
|
||||
has_errors = True
|
||||
|
||||
# Verify that all required items are provided in the icon map
|
||||
for model, icon_name in ICON_MODELS:
|
||||
self.stdout.write(
|
||||
f'Processing model {model.__name__} with icon field {icon_name}'
|
||||
)
|
||||
items = get_model_items_with_icons(model, icon_name)
|
||||
|
||||
for item in items:
|
||||
icon = getattr(item, icon_name)
|
||||
|
||||
try:
|
||||
validate_icon(icon)
|
||||
continue # Skip if the icon is already valid
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
if icon not in icons:
|
||||
self.stdout.write(
|
||||
f' [ERR] Icon "{icon}" not found in icon map'
|
||||
)
|
||||
has_errors = True
|
||||
|
||||
# If there are errors, stop here
|
||||
if has_errors:
|
||||
self.stdout.write(
|
||||
'[ERR] Icon map has errors, please fix them before continuing with importing'
|
||||
)
|
||||
return
|
||||
|
||||
# Import the icon map into the database if the flag is set
|
||||
if kwargs['import_now']:
|
||||
self.stdout.write('Start importing icons and updating database...')
|
||||
cnt = 0
|
||||
|
||||
for model, icon_name in ICON_MODELS:
|
||||
self.stdout.write(
|
||||
f'Processing model {model.__name__} with icon field {icon_name}'
|
||||
)
|
||||
items = get_model_items_with_icons(model, icon_name)
|
||||
|
||||
for item in items:
|
||||
icon = getattr(item, icon_name)
|
||||
|
||||
try:
|
||||
validate_icon(icon)
|
||||
continue # Skip if the icon is already valid
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
setattr(item, icon_name, icons[icon]['new_icon'])
|
||||
cnt += 1
|
||||
item.save()
|
||||
|
||||
self.stdout.write(
|
||||
f'Icon map successfully imported - changed {cnt} items'
|
||||
)
|
||||
self.stdout.write('Icons are now migrated')
|
||||
else:
|
||||
self.stdout.write('Icon map is valid and ready to be imported')
|
||||
self.stdout.write(
|
||||
'Run the command with --import-now to import the icon map and update the database'
|
||||
)
|
||||
@@ -1,66 +0,0 @@
|
||||
"""Custom management command to prerender files."""
|
||||
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.http.request import HttpRequest
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.translation import override as lang_over
|
||||
|
||||
|
||||
def render_file(file_name, source, target, locales, ctx):
|
||||
"""Renders a file into all provided locales."""
|
||||
for locale in locales:
|
||||
# Enforce lower-case for locale names
|
||||
locale = locale.lower()
|
||||
locale = locale.replace('_', '-')
|
||||
|
||||
target_file = os.path.join(target, locale + '.' + file_name)
|
||||
|
||||
with open(target_file, 'w') as localised_file:
|
||||
with lang_over(locale):
|
||||
rendered = render_to_string(os.path.join(source, file_name), ctx)
|
||||
localised_file.write(rendered)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Django command to prerender files."""
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""Django command to prerender files."""
|
||||
# static directories
|
||||
LC_DIR = settings.LOCALE_PATHS[0]
|
||||
SOURCE_DIR = settings.STATICFILES_I18_SRC
|
||||
TARGET_DIR = settings.STATICFILES_I18_TRG
|
||||
|
||||
# ensure static directory exists
|
||||
if not os.path.exists(TARGET_DIR):
|
||||
os.makedirs(TARGET_DIR, exist_ok=True)
|
||||
|
||||
# collect locales
|
||||
locales = {}
|
||||
for locale in os.listdir(LC_DIR):
|
||||
path = os.path.join(LC_DIR, locale)
|
||||
if os.path.exists(path) and os.path.isdir(path):
|
||||
locales[locale] = locale
|
||||
|
||||
# render!
|
||||
request = HttpRequest()
|
||||
ctx = {}
|
||||
processors = tuple(
|
||||
import_string(path) for path in settings.STATFILES_I18_PROCESSORS
|
||||
)
|
||||
for processor in processors:
|
||||
ctx.update(processor(request))
|
||||
|
||||
for file in os.listdir(SOURCE_DIR):
|
||||
path = os.path.join(SOURCE_DIR, file)
|
||||
if os.path.exists(path) and os.path.isfile(path):
|
||||
render_file(file, SOURCE_DIR, TARGET_DIR, locales, ctx)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'Using multi-level directories is not implemented at this point'
|
||||
) # TODO multilevel dir if needed
|
||||
print(f'Rendered all files in {SOURCE_DIR}')
|
||||
@@ -35,7 +35,7 @@ class Command(BaseCommand):
|
||||
img_paths.append(x.path)
|
||||
|
||||
if len(img_paths) > 0:
|
||||
if all((os.path.exists(path) for path in img_paths)):
|
||||
if all(os.path.exists(path) for path in img_paths):
|
||||
# All images exist - skip further work
|
||||
return
|
||||
|
||||
|
||||
@@ -35,4 +35,4 @@ class Command(BaseCommand):
|
||||
mfa_user[0].staticdevice_set.all().delete()
|
||||
# TOTP tokens
|
||||
mfa_user[0].totpdevice_set.all().delete()
|
||||
print(f'Removed all MFA methods for user {str(mfa_user[0])}')
|
||||
print(f'Removed all MFA methods for user {mfa_user[0]!s}')
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
import logging
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.metadata import SimpleMetadata
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.utils import model_meta
|
||||
|
||||
import common.models
|
||||
@@ -29,6 +33,40 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
so we can perform lookup for ForeignKey related fields.
|
||||
"""
|
||||
|
||||
def determine_actions(self, request, view):
|
||||
"""Determine the 'actions' available to the user for the given view.
|
||||
|
||||
Note that this differs from the standard DRF implementation,
|
||||
in that we also allow annotation for the 'GET' method.
|
||||
|
||||
This allows the client to determine what fields are available,
|
||||
even if they are only for a read (GET) operation.
|
||||
|
||||
See SimpleMetadata.determine_actions for more information.
|
||||
"""
|
||||
actions = {}
|
||||
|
||||
for method in {'PUT', 'POST', 'GET'} & set(view.allowed_methods):
|
||||
view.request = clone_request(request, method)
|
||||
try:
|
||||
# Test global permissions
|
||||
if hasattr(view, 'check_permissions'):
|
||||
view.check_permissions(view.request)
|
||||
# Test object permissions
|
||||
if method == 'PUT' and hasattr(view, 'get_object'):
|
||||
view.get_object()
|
||||
except (exceptions.APIException, PermissionDenied, Http404):
|
||||
pass
|
||||
else:
|
||||
# If user has appropriate permissions for the view, include
|
||||
# appropriate metadata about the fields that should be supplied.
|
||||
serializer = view.get_serializer()
|
||||
actions[method] = self.get_serializer_info(serializer)
|
||||
finally:
|
||||
view.request = request
|
||||
|
||||
return actions
|
||||
|
||||
def determine_metadata(self, request, view):
|
||||
"""Overwrite the metadata to adapt to the request user."""
|
||||
self.request = request
|
||||
@@ -81,6 +119,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
# Map the request method to a permission type
|
||||
rolemap = {
|
||||
'GET': 'view',
|
||||
'POST': 'add',
|
||||
'PUT': 'change',
|
||||
'PATCH': 'change',
|
||||
@@ -102,10 +141,6 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
if 'DELETE' in view.allowed_methods and check(user, table, 'delete'):
|
||||
actions['DELETE'] = {}
|
||||
|
||||
# Add a 'VIEW' action if we are allowed to view
|
||||
if 'GET' in view.allowed_methods and check(user, table, 'view'):
|
||||
actions['GET'] = {}
|
||||
|
||||
metadata['actions'] = actions
|
||||
|
||||
except AttributeError:
|
||||
@@ -115,9 +150,14 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
return metadata
|
||||
|
||||
def override_value(self, field_name, field_value, model_value):
|
||||
def override_value(self, field_name: str, field_key: str, field_value, model_value):
|
||||
"""Override a value on the serializer with a matching value for the model.
|
||||
|
||||
Often, the serializer field will point to an underlying model field,
|
||||
which contains extra information (which is translated already).
|
||||
|
||||
Rather than duplicating this information in the serializer, we can extract it from the model.
|
||||
|
||||
This is used to override the serializer values with model values,
|
||||
if (and *only* if) the model value should take precedence.
|
||||
|
||||
@@ -125,17 +165,28 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
- field_value is None
|
||||
- model_value is callable, and field_value is not (this indicates that the model value is translated)
|
||||
- model_value is not a string, and field_value is a string (this indicates that the model value is translated)
|
||||
|
||||
Arguments:
|
||||
- field_name: The name of the field
|
||||
- field_key: The property key to override
|
||||
- field_value: The value of the field (if available)
|
||||
- model_value: The equivalent value of the model (if available)
|
||||
"""
|
||||
if model_value and not field_value:
|
||||
if field_value is None and model_value is not None:
|
||||
return model_value
|
||||
|
||||
if field_value and not model_value:
|
||||
if model_value is None and field_value is not None:
|
||||
return field_value
|
||||
|
||||
# Callable values will be evaluated later
|
||||
if callable(model_value) and not callable(field_value):
|
||||
return model_value
|
||||
|
||||
if type(model_value) is not str and type(field_value) is str:
|
||||
if callable(field_value) and not callable(model_value):
|
||||
return field_value
|
||||
|
||||
# Prioritize translated text over raw string values
|
||||
if type(field_value) is str and type(model_value) is not str:
|
||||
return model_value
|
||||
|
||||
return field_value
|
||||
@@ -144,6 +195,8 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
"""Override get_serializer_info so that we can add 'default' values to any fields whose Meta.model specifies a default value."""
|
||||
self.serializer = serializer
|
||||
|
||||
request = getattr(self, 'request', None)
|
||||
|
||||
serializer_info = super().get_serializer_info(serializer)
|
||||
|
||||
# Look for any dynamic fields which were not available when the serializer was instantiated
|
||||
@@ -153,12 +206,19 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
# Already know about this one
|
||||
continue
|
||||
|
||||
if hasattr(serializer, field_name):
|
||||
field = getattr(serializer, field_name)
|
||||
if field := getattr(serializer, field_name, None):
|
||||
serializer_info[field_name] = self.get_field_info(field)
|
||||
|
||||
model_class = None
|
||||
|
||||
# Extract read_only_fields and write_only_fields from the Meta class (if available)
|
||||
if meta := getattr(serializer, 'Meta', None):
|
||||
read_only_fields = getattr(meta, 'read_only_fields', [])
|
||||
write_only_fields = getattr(meta, 'write_only_fields', [])
|
||||
else:
|
||||
read_only_fields = []
|
||||
write_only_fields = []
|
||||
|
||||
# Attributes to copy extra attributes from the model to the field (if they don't exist)
|
||||
# Note that the attributes may be named differently on the underlying model!
|
||||
extra_attributes = {
|
||||
@@ -172,16 +232,20 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
model_fields = model_meta.get_field_info(model_class)
|
||||
|
||||
model_default_func = getattr(model_class, 'api_defaults', None)
|
||||
|
||||
if model_default_func:
|
||||
model_default_values = model_class.api_defaults(self.request)
|
||||
if model_default_func := getattr(model_class, 'api_defaults', None):
|
||||
model_default_values = model_default_func(request=request) or {}
|
||||
else:
|
||||
model_default_values = {}
|
||||
|
||||
# Iterate through simple fields
|
||||
for name, field in model_fields.fields.items():
|
||||
if name in serializer_info.keys():
|
||||
if name in serializer_info:
|
||||
if name in read_only_fields:
|
||||
serializer_info[name]['read_only'] = True
|
||||
|
||||
if name in write_only_fields:
|
||||
serializer_info[name]['write_only'] = True
|
||||
|
||||
if field.has_default():
|
||||
default = field.default
|
||||
|
||||
@@ -197,15 +261,17 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
serializer_info[name]['default'] = model_default_values[name]
|
||||
|
||||
for field_key, model_key in extra_attributes.items():
|
||||
field_value = serializer_info[name].get(field_key, None)
|
||||
field_value = getattr(serializer.fields[name], field_key, None)
|
||||
model_value = getattr(field, model_key, None)
|
||||
|
||||
if value := self.override_value(name, field_value, model_value):
|
||||
if value := self.override_value(
|
||||
name, field_key, field_value, model_value
|
||||
):
|
||||
serializer_info[name][field_key] = value
|
||||
|
||||
# Iterate through relations
|
||||
for name, relation in model_fields.relations.items():
|
||||
if name not in serializer_info.keys():
|
||||
if name not in serializer_info:
|
||||
# Skip relation not defined in serializer
|
||||
continue
|
||||
|
||||
@@ -213,6 +279,12 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
# Ignore reverse relations
|
||||
continue
|
||||
|
||||
if name in read_only_fields:
|
||||
serializer_info[name]['read_only'] = True
|
||||
|
||||
if name in write_only_fields:
|
||||
serializer_info[name]['write_only'] = True
|
||||
|
||||
# Extract and provide the "limit_choices_to" filters
|
||||
# This is used to automatically filter AJAX requests
|
||||
serializer_info[name]['filters'] = (
|
||||
@@ -220,10 +292,12 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
)
|
||||
|
||||
for field_key, model_key in extra_attributes.items():
|
||||
field_value = serializer_info[name].get(field_key, None)
|
||||
field_value = getattr(serializer.fields[name], field_key, None)
|
||||
model_value = getattr(relation.model_field, model_key, None)
|
||||
|
||||
if value := self.override_value(name, field_value, model_value):
|
||||
if value := self.override_value(
|
||||
name, field_key, field_value, model_value
|
||||
):
|
||||
serializer_info[name][field_key] = value
|
||||
|
||||
if name in model_default_values:
|
||||
@@ -241,7 +315,8 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
if instance is None and model_class is not None:
|
||||
# Attempt to find the instance based on kwargs lookup
|
||||
kwargs = getattr(self.view, 'kwargs', None)
|
||||
view = getattr(self, 'view', None)
|
||||
kwargs = getattr(view, 'kwargs', None) if view else None
|
||||
|
||||
if kwargs:
|
||||
pk = None
|
||||
@@ -267,12 +342,12 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
instance_filters = instance.api_instance_filters()
|
||||
|
||||
for field_name, field_filters in instance_filters.items():
|
||||
if field_name not in serializer_info.keys():
|
||||
if field_name not in serializer_info:
|
||||
# The field might be missing, but is added later on
|
||||
# This function seems to get called multiple times?
|
||||
continue
|
||||
|
||||
if 'instance_filters' not in serializer_info[field_name].keys():
|
||||
if 'instance_filters' not in serializer_info[field_name]:
|
||||
serializer_info[field_name]['instance_filters'] = {}
|
||||
|
||||
for key, value in field_filters.items():
|
||||
@@ -298,8 +373,10 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
# Force non-nullable fields to read as "required"
|
||||
# (even if there is a default value!)
|
||||
if not field.allow_null and not (
|
||||
hasattr(field, 'allow_blank') and field.allow_blank
|
||||
if (
|
||||
'required' not in field_info
|
||||
and not field.allow_null
|
||||
and not (hasattr(field, 'allow_blank') and field.allow_blank)
|
||||
):
|
||||
field_info['required'] = True
|
||||
|
||||
@@ -321,13 +398,16 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
field_info['type'] = 'related field'
|
||||
field_info['model'] = model._meta.model_name
|
||||
|
||||
# Special case for 'user' model
|
||||
# Special case for special models
|
||||
if field_info['model'] == 'user':
|
||||
field_info['api_url'] = '/api/user/'
|
||||
elif field_info['model'] == 'contenttype':
|
||||
field_info['api_url'] = '/api/contenttype/'
|
||||
else:
|
||||
elif hasattr(model, 'get_api_url'):
|
||||
field_info['api_url'] = model.get_api_url()
|
||||
else:
|
||||
logger.warning("'get_api_url' method not defined for %s", model)
|
||||
field_info['api_url'] = getattr(model, 'api_url', None)
|
||||
|
||||
# Handle custom 'primary key' field
|
||||
field_info['pk_field'] = getattr(field, 'pk_field', 'pk') or 'pk'
|
||||
@@ -336,6 +416,14 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
if field_info['type'] == 'dependent field':
|
||||
field_info['depends_on'] = field.depends_on
|
||||
|
||||
# Extend field info if the field has a get_field_info method
|
||||
if (
|
||||
not field_info.get('read_only')
|
||||
and hasattr(field, 'get_field_info')
|
||||
and callable(field.get_field_info)
|
||||
):
|
||||
field_info = field.get_field_info(field, field_info)
|
||||
|
||||
return field_info
|
||||
|
||||
|
||||
|
||||
@@ -7,12 +7,10 @@ from django.conf import settings
|
||||
from django.contrib.auth.middleware import PersistentRemoteUserMiddleware
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import include, path, resolve, reverse_lazy
|
||||
from django.urls import resolve, reverse_lazy
|
||||
|
||||
from error_report.middleware import ExceptionProcessor
|
||||
|
||||
from common.settings import get_global_setting
|
||||
from InvenTree.urls import frontendpatterns
|
||||
from users.models import ApiToken
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
@@ -35,7 +33,18 @@ def get_token_from_request(request):
|
||||
return None
|
||||
|
||||
|
||||
class AuthRequiredMiddleware(object):
|
||||
# List of target URL endpoints where *do not* want to redirect to
|
||||
urls = [
|
||||
reverse_lazy('account_login'),
|
||||
reverse_lazy('admin:login'),
|
||||
reverse_lazy('admin:logout'),
|
||||
]
|
||||
|
||||
# Do not redirect requests to any of these paths
|
||||
paths_ignore = ['/api/', '/auth/', settings.MEDIA_URL, settings.STATIC_URL]
|
||||
|
||||
|
||||
class AuthRequiredMiddleware:
|
||||
"""Check for user to be authenticated."""
|
||||
|
||||
def __init__(self, get_response):
|
||||
@@ -91,44 +100,22 @@ class AuthRequiredMiddleware(object):
|
||||
|
||||
# Allow static files to be accessed without auth
|
||||
# Important for e.g. login page
|
||||
if request.path_info.startswith('/static/'):
|
||||
authorized = True
|
||||
|
||||
# Unauthorized users can access the login page
|
||||
elif request.path_info.startswith('/accounts/'):
|
||||
authorized = True
|
||||
|
||||
elif (
|
||||
request.path_info.startswith(f'/{settings.FRONTEND_URL_BASE}/')
|
||||
or request.path_info.startswith('/assets/')
|
||||
or request.path_info == f'/{settings.FRONTEND_URL_BASE}'
|
||||
if (
|
||||
request.path_info.startswith('/static/')
|
||||
or request.path_info.startswith('/accounts/')
|
||||
or (
|
||||
request.path_info.startswith(f'/{settings.FRONTEND_URL_BASE}/')
|
||||
or request.path_info.startswith('/assets/')
|
||||
or request.path_info == f'/{settings.FRONTEND_URL_BASE}'
|
||||
)
|
||||
or self.check_token(request)
|
||||
):
|
||||
authorized = True
|
||||
|
||||
elif self.check_token(request):
|
||||
authorized = True
|
||||
|
||||
# No authorization was found for the request
|
||||
if not authorized:
|
||||
path = request.path_info
|
||||
|
||||
# List of URL endpoints we *do not* want to redirect to
|
||||
urls = [
|
||||
reverse_lazy('account_login'),
|
||||
reverse_lazy('account_logout'),
|
||||
reverse_lazy('admin:login'),
|
||||
reverse_lazy('admin:logout'),
|
||||
]
|
||||
|
||||
# Do not redirect requests to any of these paths
|
||||
paths_ignore = [
|
||||
'/api/',
|
||||
'/auth/',
|
||||
'/js/',
|
||||
settings.MEDIA_URL,
|
||||
settings.STATIC_URL,
|
||||
]
|
||||
|
||||
if path not in urls and not any(
|
||||
path.startswith(p) for p in paths_ignore
|
||||
):
|
||||
|
||||
@@ -6,7 +6,11 @@ from rest_framework import generics, mixins, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from InvenTree.fields import InvenTreeNotesField
|
||||
from InvenTree.helpers import remove_non_printable_characters, strip_html_tags
|
||||
from InvenTree.helpers import (
|
||||
clean_markdown,
|
||||
remove_non_printable_characters,
|
||||
strip_html_tags,
|
||||
)
|
||||
|
||||
|
||||
class CleanMixin:
|
||||
@@ -53,22 +57,24 @@ class CleanMixin:
|
||||
Ref: https://github.com/mozilla/bleach/issues/192
|
||||
|
||||
"""
|
||||
cleaned = strip_html_tags(data, field_name=field)
|
||||
cleaned = data
|
||||
|
||||
# By default, newline characters are removed
|
||||
remove_newline = True
|
||||
is_markdown = False
|
||||
|
||||
try:
|
||||
if hasattr(self, 'serializer_class'):
|
||||
model = self.serializer_class.Meta.model
|
||||
field = model._meta.get_field(field)
|
||||
field_base = model._meta.get_field(field)
|
||||
|
||||
# The following field types allow newline characters
|
||||
allow_newline = [InvenTreeNotesField]
|
||||
allow_newline = [(InvenTreeNotesField, True)]
|
||||
|
||||
for field_type in allow_newline:
|
||||
if issubclass(type(field), field_type):
|
||||
if issubclass(type(field_base), field_type[0]):
|
||||
remove_newline = False
|
||||
is_markdown = field_type[1]
|
||||
break
|
||||
|
||||
except AttributeError:
|
||||
@@ -80,6 +86,11 @@ class CleanMixin:
|
||||
cleaned, remove_newline=remove_newline
|
||||
)
|
||||
|
||||
cleaned = strip_html_tags(cleaned, field_name=field)
|
||||
|
||||
if is_markdown:
|
||||
cleaned = clean_markdown(cleaned)
|
||||
|
||||
return cleaned
|
||||
|
||||
def clean_data(self, data: dict) -> dict:
|
||||
@@ -128,14 +139,10 @@ class CreateAPI(CleanMixin, generics.CreateAPIView):
|
||||
class RetrieveAPI(generics.RetrieveAPIView):
|
||||
"""View for retrieve API."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RetrieveUpdateAPI(CleanMixin, generics.RetrieveUpdateAPIView):
|
||||
"""View for retrieve and update API."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CustomDestroyModelMixin:
|
||||
"""This mixin was created pass the kwargs from the API to the models."""
|
||||
@@ -184,5 +191,9 @@ class RetrieveUpdateDestroyAPI(CleanMixin, generics.RetrieveUpdateDestroyAPIView
|
||||
"""View for retrieve, update and destroy API."""
|
||||
|
||||
|
||||
class RetrieveDestroyAPI(generics.RetrieveDestroyAPIView):
|
||||
"""View for retrieve and destroy API."""
|
||||
|
||||
|
||||
class UpdateAPI(CleanMixin, generics.UpdateAPIView):
|
||||
"""View for update API."""
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from string import Formatter
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_q.models import Task
|
||||
from error_report.models import Error
|
||||
from mptt.exceptions import InvalidMove
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
@@ -95,7 +96,7 @@ class PluginValidationMixin(DiffMixin):
|
||||
return
|
||||
except ValidationError as exc:
|
||||
raise exc
|
||||
except Exception as exc:
|
||||
except Exception:
|
||||
# Log the exception to the database
|
||||
import InvenTree.exceptions
|
||||
|
||||
@@ -127,10 +128,18 @@ class PluginValidationMixin(DiffMixin):
|
||||
|
||||
Note: Each plugin may raise a ValidationError to prevent deletion.
|
||||
"""
|
||||
from InvenTree.exceptions import log_error
|
||||
from plugin.registry import registry
|
||||
|
||||
for plugin in registry.with_mixin('validation'):
|
||||
plugin.validate_model_deletion(self)
|
||||
try:
|
||||
plugin.validate_model_deletion(self)
|
||||
except ValidationError as e:
|
||||
# Plugin might raise a ValidationError to prevent deletion
|
||||
raise e
|
||||
except Exception:
|
||||
log_error('plugin.validate_model_deletion')
|
||||
continue
|
||||
|
||||
super().delete()
|
||||
|
||||
@@ -216,12 +225,15 @@ class MetadataMixin(models.Model):
|
||||
self.save()
|
||||
|
||||
|
||||
class DataImportMixin(object):
|
||||
class DataImportMixin:
|
||||
"""Model mixin class which provides support for 'data import' functionality.
|
||||
|
||||
Models which implement this mixin should provide information on the fields available for import
|
||||
"""
|
||||
|
||||
# TODO: This mixin should be removed after https://github.com/inventree/InvenTree/pull/6911 is implemented
|
||||
# TODO: This approach to data import functionality is *outdated*
|
||||
|
||||
# Define a map of fields available for import
|
||||
IMPORT_FIELDS = {}
|
||||
|
||||
@@ -312,8 +324,9 @@ class ReferenceIndexingMixin(models.Model):
|
||||
|
||||
- Returns a python dict object which contains the context data for formatting the reference string.
|
||||
- The default implementation provides some default context information
|
||||
- The '?' key is required to accept our wildcard-with-default syntax {?:default}
|
||||
"""
|
||||
return {'ref': cls.get_next_reference(), 'date': datetime.now()}
|
||||
return {'ref': cls.get_next_reference(), 'date': datetime.now(), '?': '?'}
|
||||
|
||||
@classmethod
|
||||
def get_most_recent_item(cls):
|
||||
@@ -360,8 +373,18 @@ class ReferenceIndexingMixin(models.Model):
|
||||
@classmethod
|
||||
def generate_reference(cls):
|
||||
"""Generate the next 'reference' field based on specified pattern."""
|
||||
fmt = cls.get_reference_pattern()
|
||||
|
||||
# Based on https://stackoverflow.com/a/57570269/14488558
|
||||
class ReferenceFormatter(Formatter):
|
||||
def format_field(self, value, format_spec):
|
||||
if isinstance(value, str) and value == '?':
|
||||
value = format_spec
|
||||
format_spec = ''
|
||||
return super().format_field(value, format_spec)
|
||||
|
||||
ref_ptn = cls.get_reference_pattern()
|
||||
ctx = cls.get_reference_context()
|
||||
fmt = ReferenceFormatter()
|
||||
|
||||
reference = None
|
||||
|
||||
@@ -369,7 +392,7 @@ class ReferenceIndexingMixin(models.Model):
|
||||
|
||||
while reference is None:
|
||||
try:
|
||||
ref = fmt.format(**ctx)
|
||||
ref = fmt.format(ref_ptn, **ctx)
|
||||
|
||||
if ref in attempts:
|
||||
# We are stuck in a loop!
|
||||
@@ -389,10 +412,7 @@ class ReferenceIndexingMixin(models.Model):
|
||||
except Exception:
|
||||
# If anything goes wrong, return the most recent reference
|
||||
recent = cls.get_most_recent_item()
|
||||
if recent:
|
||||
reference = recent.reference
|
||||
else:
|
||||
reference = ''
|
||||
reference = recent.reference if recent else ''
|
||||
|
||||
return reference
|
||||
|
||||
@@ -409,14 +429,14 @@ class ReferenceIndexingMixin(models.Model):
|
||||
})
|
||||
|
||||
# Check that only 'allowed' keys are provided
|
||||
for key in info.keys():
|
||||
if key not in ctx.keys():
|
||||
for key in info:
|
||||
if key not in ctx:
|
||||
raise ValidationError({
|
||||
'value': _('Unknown format key specified') + f": '{key}'"
|
||||
})
|
||||
|
||||
# Check that the 'ref' variable is specified
|
||||
if 'ref' not in info.keys():
|
||||
if 'ref' not in info:
|
||||
raise ValidationError({
|
||||
'value': _('Missing required format key') + ": 'ref'"
|
||||
})
|
||||
@@ -441,7 +461,7 @@ class ReferenceIndexingMixin(models.Model):
|
||||
)
|
||||
|
||||
# Check that the reference field can be rebuild
|
||||
cls.rebuild_reference_field(value, validate=True)
|
||||
return cls.rebuild_reference_field(value, validate=True)
|
||||
|
||||
@classmethod
|
||||
def rebuild_reference_field(cls, reference, validate=False):
|
||||
@@ -574,6 +594,9 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
|
||||
# e.g. for StockLocation, this value is 'location'
|
||||
ITEM_PARENT_KEY = None
|
||||
|
||||
# Extra fields to include in the get_path result. E.g. icon
|
||||
EXTRA_PATH_FIELDS = []
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model properties."""
|
||||
|
||||
@@ -779,7 +802,7 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
|
||||
on_delete=models.DO_NOTHING,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('parent'),
|
||||
verbose_name='parent',
|
||||
related_name='children',
|
||||
)
|
||||
|
||||
@@ -855,7 +878,7 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
|
||||
Returns:
|
||||
List of category names from the top level to this category
|
||||
"""
|
||||
return self.parentpath + [self]
|
||||
return [*self.parentpath, self]
|
||||
|
||||
def get_path(self):
|
||||
"""Return a list of element in the item tree.
|
||||
@@ -867,7 +890,14 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
|
||||
name: <name>,
|
||||
}
|
||||
"""
|
||||
return [{'pk': item.pk, 'name': item.name} for item in self.path]
|
||||
return [
|
||||
{
|
||||
'pk': item.pk,
|
||||
'name': item.name,
|
||||
**{k: getattr(item, k, None) for k in self.EXTRA_PATH_FIELDS},
|
||||
}
|
||||
for item in self.path
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
"""String representation of a category is the full path to that category."""
|
||||
@@ -931,6 +961,8 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
|
||||
- barcode_data : Raw data associated with an assigned barcode
|
||||
- barcode_hash : A 'hash' of the assigned barcode data used to improve matching
|
||||
|
||||
The barcode_model_type_code() classmethod must be implemented in the model class.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
@@ -961,11 +993,25 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
# By default, use the name of the class
|
||||
return cls.__name__.lower()
|
||||
|
||||
@classmethod
|
||||
def barcode_model_type_code(cls):
|
||||
r"""Return a 'short' code for the model type.
|
||||
|
||||
This is used to generate a efficient QR code for the model type.
|
||||
It is expected to match this pattern: [0-9A-Z $%*+-.\/:]{2}
|
||||
|
||||
Note: Due to the shape constrains (45**2=2025 different allowed codes)
|
||||
this needs to be explicitly implemented in the model class to avoid collisions.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'barcode_model_type_code() must be implemented in the model class'
|
||||
)
|
||||
|
||||
def format_barcode(self, **kwargs):
|
||||
"""Return a JSON string for formatting a QR code for this model instance."""
|
||||
return InvenTree.helpers.MakeBarcode(
|
||||
self.__class__.barcode_model_type(), self.pk, **kwargs
|
||||
)
|
||||
from plugin.base.barcodes.helper import generate_barcode
|
||||
|
||||
return generate_barcode(self)
|
||||
|
||||
def format_matched_response(self):
|
||||
"""Format a standard response for a matched barcode."""
|
||||
@@ -983,7 +1029,7 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
@property
|
||||
def barcode(self):
|
||||
"""Format a minimal barcode string (e.g. for label printing)."""
|
||||
return self.format_barcode(brief=True)
|
||||
return self.format_barcode()
|
||||
|
||||
@classmethod
|
||||
def lookup_barcode(cls, barcode_hash):
|
||||
@@ -1027,6 +1073,71 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
self.save()
|
||||
|
||||
|
||||
def notify_staff_users_of_error(instance, label: str, context: dict):
|
||||
"""Helper function to notify staff users of an error."""
|
||||
import common.models
|
||||
import common.notifications
|
||||
|
||||
try:
|
||||
# Get all staff users
|
||||
staff_users = get_user_model().objects.filter(is_staff=True)
|
||||
|
||||
target_users = []
|
||||
|
||||
# Send a notification to each staff user (unless they have disabled error notifications)
|
||||
for user in staff_users:
|
||||
if common.models.InvenTreeUserSetting.get_setting(
|
||||
'NOTIFICATION_ERROR_REPORT', True, user=user
|
||||
):
|
||||
target_users.append(user)
|
||||
|
||||
if len(target_users) > 0:
|
||||
common.notifications.trigger_notification(
|
||||
instance,
|
||||
label,
|
||||
context=context,
|
||||
targets=target_users,
|
||||
delivery_methods={common.notifications.UIMessageNotification},
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
# We do not want to throw an exception while reporting an exception!
|
||||
logger.error(exc)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Task, dispatch_uid='failure_post_save_notification')
|
||||
def after_failed_task(sender, instance: Task, created: bool, **kwargs):
|
||||
"""Callback when a new task failure log is generated."""
|
||||
from django.conf import settings
|
||||
|
||||
max_attempts = int(settings.Q_CLUSTER.get('max_attempts', 5))
|
||||
n = instance.attempt_count
|
||||
|
||||
# Only notify once the maximum number of attempts has been reached
|
||||
if not instance.success and n >= max_attempts:
|
||||
try:
|
||||
url = InvenTree.helpers_model.construct_absolute_url(
|
||||
reverse(
|
||||
'admin:django_q_failure_change', kwargs={'object_id': instance.pk}
|
||||
)
|
||||
)
|
||||
except (ValueError, NoReverseMatch):
|
||||
url = ''
|
||||
|
||||
notify_staff_users_of_error(
|
||||
instance,
|
||||
'inventree.task_failure',
|
||||
{
|
||||
'failure': instance,
|
||||
'name': _('Task Failure'),
|
||||
'message': _(
|
||||
f"Background worker task '{instance.func}' failed after {n} attempts"
|
||||
),
|
||||
'link': url,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Error, dispatch_uid='error_post_save_notification')
|
||||
def after_error_logged(sender, instance: Error, created: bool, **kwargs):
|
||||
"""Callback when a server error is logged.
|
||||
@@ -1035,41 +1146,21 @@ def after_error_logged(sender, instance: Error, created: bool, **kwargs):
|
||||
"""
|
||||
if created:
|
||||
try:
|
||||
import common.models
|
||||
import common.notifications
|
||||
|
||||
users = get_user_model().objects.filter(is_staff=True)
|
||||
|
||||
link = InvenTree.helpers_model.construct_absolute_url(
|
||||
url = InvenTree.helpers_model.construct_absolute_url(
|
||||
reverse(
|
||||
'admin:error_report_error_change', kwargs={'object_id': instance.pk}
|
||||
)
|
||||
)
|
||||
except NoReverseMatch:
|
||||
url = ''
|
||||
|
||||
context = {
|
||||
notify_staff_users_of_error(
|
||||
instance,
|
||||
'inventree.error_log',
|
||||
{
|
||||
'error': instance,
|
||||
'name': _('Server Error'),
|
||||
'message': _('An error has been logged by the server.'),
|
||||
'link': link,
|
||||
}
|
||||
|
||||
target_users = []
|
||||
|
||||
for user in users:
|
||||
if common.models.InvenTreeUserSetting.get_setting(
|
||||
'NOTIFICATION_ERROR_REPORT', True, user=user
|
||||
):
|
||||
target_users.append(user)
|
||||
|
||||
if len(target_users) > 0:
|
||||
common.notifications.trigger_notification(
|
||||
instance,
|
||||
'inventree.error_log',
|
||||
context=context,
|
||||
targets=target_users,
|
||||
delivery_methods={common.notifications.UIMessageNotification},
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
"""We do not want to throw an exception while reporting an exception"""
|
||||
logger.error(exc) # noqa: LOG005
|
||||
'link': url,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -79,6 +79,9 @@ class RolePermission(permissions.BasePermission):
|
||||
# Extract the model name associated with this request
|
||||
model = get_model_for_view(view)
|
||||
|
||||
if model is None:
|
||||
return True
|
||||
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
|
||||
@@ -99,14 +102,24 @@ class IsSuperuser(permissions.IsAdminUser):
|
||||
return bool(request.user and request.user.is_superuser)
|
||||
|
||||
|
||||
class IsSuperuserOrReadOnly(permissions.IsAdminUser):
|
||||
"""Allow read-only access to any user, but write access is restricted to superuser users."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Check if the user is a superuser."""
|
||||
return bool(
|
||||
(request.user and request.user.is_superuser)
|
||||
or request.method in permissions.SAFE_METHODS
|
||||
)
|
||||
|
||||
|
||||
class IsStaffOrReadOnly(permissions.IsAdminUser):
|
||||
"""Allows read-only access to any user, but write access is restricted to staff users."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Check if the user is a superuser."""
|
||||
return bool(
|
||||
request.user
|
||||
and request.user.is_staff
|
||||
(request.user and request.user.is_staff)
|
||||
or request.method in permissions.SAFE_METHODS
|
||||
)
|
||||
|
||||
|
||||
@@ -11,43 +11,36 @@ def isInTestMode():
|
||||
|
||||
def isImportingData():
|
||||
"""Returns True if the database is currently importing (or exporting) data, e.g. 'loaddata' command is performed."""
|
||||
return any((x in sys.argv for x in ['flush', 'loaddata', 'dumpdata']))
|
||||
return any(x in sys.argv for x in ['flush', 'loaddata', 'dumpdata'])
|
||||
|
||||
|
||||
def isRunningMigrations():
|
||||
"""Return True if the database is currently running migrations."""
|
||||
return any(
|
||||
(
|
||||
x in sys.argv
|
||||
for x in ['migrate', 'makemigrations', 'showmigrations', 'runmigrations']
|
||||
)
|
||||
x in sys.argv
|
||||
for x in ['migrate', 'makemigrations', 'showmigrations', 'runmigrations']
|
||||
)
|
||||
|
||||
|
||||
def isRebuildingData():
|
||||
"""Return true if any of the rebuilding commands are being executed."""
|
||||
return any(
|
||||
(
|
||||
x in sys.argv
|
||||
for x in ['prerender', 'rebuild_models', 'rebuild_thumbnails', 'rebuild']
|
||||
)
|
||||
x in sys.argv for x in ['rebuild_models', 'rebuild_thumbnails', 'rebuild']
|
||||
)
|
||||
|
||||
|
||||
def isRunningBackup():
|
||||
"""Return true if any of the backup commands are being executed."""
|
||||
return any(
|
||||
(
|
||||
x in sys.argv
|
||||
for x in [
|
||||
'backup',
|
||||
'restore',
|
||||
'dbbackup',
|
||||
'dbresotore',
|
||||
'mediabackup',
|
||||
'mediarestore',
|
||||
]
|
||||
)
|
||||
x in sys.argv
|
||||
for x in [
|
||||
'backup',
|
||||
'restore',
|
||||
'dbbackup',
|
||||
'dbresotore',
|
||||
'mediabackup',
|
||||
'mediarestore',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -64,10 +57,7 @@ def isInServerThread():
|
||||
if 'runserver' in sys.argv:
|
||||
return True
|
||||
|
||||
if 'gunicorn' in sys.argv[0]:
|
||||
return True
|
||||
|
||||
return False
|
||||
return 'gunicorn' in sys.argv[0]
|
||||
|
||||
|
||||
def isInMainThread():
|
||||
@@ -115,6 +105,7 @@ def canAppAccessDatabase(
|
||||
'makemessages',
|
||||
'compilemessages',
|
||||
'spectactular',
|
||||
'collectstatic',
|
||||
]
|
||||
|
||||
if not allow_shell:
|
||||
@@ -125,13 +116,9 @@ def canAppAccessDatabase(
|
||||
excluded_commands.append('test')
|
||||
|
||||
if not allow_plugins:
|
||||
excluded_commands.extend(['collectstatic', 'collectplugins'])
|
||||
excluded_commands.extend(['collectplugins'])
|
||||
|
||||
for cmd in excluded_commands:
|
||||
if cmd in sys.argv:
|
||||
return False
|
||||
|
||||
return True
|
||||
return all(cmd not in sys.argv for cmd in excluded_commands)
|
||||
|
||||
|
||||
def isPluginRegistryLoaded():
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.http import Http404
|
||||
|
||||
import rest_framework.exceptions
|
||||
import sentry_sdk
|
||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
|
||||
import InvenTree.version
|
||||
@@ -27,6 +28,7 @@ def sentry_ignore_errors():
|
||||
"""
|
||||
return [
|
||||
Http404,
|
||||
MissingRate,
|
||||
ValidationError,
|
||||
rest_framework.exceptions.AuthenticationFailed,
|
||||
rest_framework.exceptions.NotAuthenticated,
|
||||
|
||||
@@ -89,7 +89,7 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
|
||||
)
|
||||
|
||||
if allow_blank:
|
||||
choices = [('', '---------')] + choices
|
||||
choices = [('', '---------'), *choices]
|
||||
|
||||
kwargs['choices'] = choices
|
||||
|
||||
@@ -379,7 +379,7 @@ class InvenTreeTaggitSerializer(TaggitSerializer):
|
||||
|
||||
tag_object = super().update(instance, validated_data)
|
||||
|
||||
for key in to_be_tagged.keys():
|
||||
for key in to_be_tagged:
|
||||
# re-add the tagmanager
|
||||
new_tagobject = tag_object.__class__.objects.get(id=tag_object.id)
|
||||
setattr(tag_object, key, getattr(new_tagobject, key))
|
||||
@@ -390,8 +390,6 @@ class InvenTreeTaggitSerializer(TaggitSerializer):
|
||||
class InvenTreeTagModelSerializer(InvenTreeTaggitSerializer, InvenTreeModelSerializer):
|
||||
"""Combination of InvenTreeTaggitSerializer and InvenTreeModelSerializer."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserSerializer(InvenTreeModelSerializer):
|
||||
"""Serializer for a User."""
|
||||
@@ -402,10 +400,24 @@ class UserSerializer(InvenTreeModelSerializer):
|
||||
model = User
|
||||
fields = ['pk', 'username', 'first_name', 'last_name', 'email']
|
||||
|
||||
read_only_fields = ['username']
|
||||
read_only_fields = ['username', 'email']
|
||||
|
||||
username = serializers.CharField(label=_('Username'), help_text=_('Username'))
|
||||
|
||||
first_name = serializers.CharField(
|
||||
label=_('First Name'), help_text=_('First name of the user'), allow_blank=True
|
||||
)
|
||||
|
||||
last_name = serializers.CharField(
|
||||
label=_('Last Name'), help_text=_('Last name of the user'), allow_blank=True
|
||||
)
|
||||
|
||||
email = serializers.EmailField(
|
||||
label=_('Email'), help_text=_('Email address of the user'), allow_blank=True
|
||||
)
|
||||
|
||||
|
||||
class ExendedUserSerializer(UserSerializer):
|
||||
class ExtendedUserSerializer(UserSerializer):
|
||||
"""Serializer for a User with a bit more info."""
|
||||
|
||||
from users.serializers import GroupSerializer
|
||||
@@ -415,14 +427,27 @@ class ExendedUserSerializer(UserSerializer):
|
||||
class Meta(UserSerializer.Meta):
|
||||
"""Metaclass defines serializer fields."""
|
||||
|
||||
fields = UserSerializer.Meta.fields + [
|
||||
fields = [
|
||||
*UserSerializer.Meta.fields,
|
||||
'groups',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
'is_active',
|
||||
]
|
||||
|
||||
read_only_fields = UserSerializer.Meta.read_only_fields + ['groups']
|
||||
read_only_fields = [*UserSerializer.Meta.read_only_fields, 'groups']
|
||||
|
||||
is_staff = serializers.BooleanField(
|
||||
label=_('Staff'), help_text=_('Does this user have staff permissions')
|
||||
)
|
||||
|
||||
is_superuser = serializers.BooleanField(
|
||||
label=_('Superuser'), help_text=_('Is this user a superuser')
|
||||
)
|
||||
|
||||
is_active = serializers.BooleanField(
|
||||
label=_('Active'), help_text=_('Is this user account active')
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Expanded validation for changing user role."""
|
||||
@@ -444,9 +469,33 @@ class ExendedUserSerializer(UserSerializer):
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class UserCreateSerializer(ExendedUserSerializer):
|
||||
class MeUserSerializer(ExtendedUserSerializer):
|
||||
"""API serializer specifically for the 'me' endpoint."""
|
||||
|
||||
class Meta(ExtendedUserSerializer.Meta):
|
||||
"""Metaclass options.
|
||||
|
||||
Extends the ExtendedUserSerializer.Meta options,
|
||||
but ensures that certain fields are read-only.
|
||||
"""
|
||||
|
||||
read_only_fields = [
|
||||
*ExtendedUserSerializer.Meta.read_only_fields,
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
]
|
||||
|
||||
|
||||
class UserCreateSerializer(ExtendedUserSerializer):
|
||||
"""Serializer for creating a new User."""
|
||||
|
||||
class Meta(ExtendedUserSerializer.Meta):
|
||||
"""Metaclass options for the UserCreateSerializer."""
|
||||
|
||||
# Prevent creation of users with superuser or staff permissions
|
||||
read_only_fields = ['groups', 'is_staff', 'is_superuser']
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Expanded valiadation for auth."""
|
||||
# Check that the user trying to create a new user is a superuser
|
||||
@@ -461,6 +510,7 @@ class UserCreateSerializer(ExendedUserSerializer):
|
||||
def create(self, validated_data):
|
||||
"""Send an e email to the user after creation."""
|
||||
from InvenTree.helpers_model import get_base_url
|
||||
from InvenTree.tasks import email_user, offload_task
|
||||
|
||||
base_url = get_base_url()
|
||||
|
||||
@@ -478,8 +528,12 @@ class UserCreateSerializer(ExendedUserSerializer):
|
||||
if base_url:
|
||||
message += f'\n\nURL: {base_url}'
|
||||
|
||||
subject = _('Welcome to InvenTree')
|
||||
|
||||
# Send the user an onboarding email (from current site)
|
||||
instance.email_user(subject=_('Welcome to InvenTree'), message=message)
|
||||
offload_task(
|
||||
email_user, instance.pk, str(subject), str(message), force_async=True
|
||||
)
|
||||
|
||||
return instance
|
||||
|
||||
@@ -575,7 +629,7 @@ class DataFileUploadSerializer(serializers.Serializer):
|
||||
accepted_file_types = ['xls', 'xlsx', 'csv', 'tsv', 'xml']
|
||||
|
||||
if ext not in accepted_file_types:
|
||||
raise serializers.ValidationError(_('Unsupported file type'))
|
||||
raise serializers.ValidationError(_('Unsupported file format'))
|
||||
|
||||
# Impose a 50MB limit on uploaded BOM files
|
||||
max_upload_file_size = 50 * 1024 * 1024
|
||||
@@ -683,7 +737,6 @@ class DataFileUploadSerializer(serializers.Serializer):
|
||||
|
||||
def save(self):
|
||||
"""Empty overwrite for save."""
|
||||
...
|
||||
|
||||
|
||||
class DataFileExtractSerializer(serializers.Serializer):
|
||||
@@ -785,11 +838,10 @@ class DataFileExtractSerializer(serializers.Serializer):
|
||||
required = field.get('required', False)
|
||||
|
||||
# Check for missing required columns
|
||||
if required:
|
||||
if name not in self.columns:
|
||||
raise serializers.ValidationError(
|
||||
_(f"Missing required column: '{name}'")
|
||||
)
|
||||
if required and name not in self.columns:
|
||||
raise serializers.ValidationError(
|
||||
_(f"Missing required column: '{name}'")
|
||||
)
|
||||
|
||||
for col in self.columns:
|
||||
if not col:
|
||||
@@ -803,7 +855,6 @@ class DataFileExtractSerializer(serializers.Serializer):
|
||||
|
||||
def save(self):
|
||||
"""No "save" action for this serializer."""
|
||||
pass
|
||||
|
||||
|
||||
class NotesFieldMixin:
|
||||
@@ -835,7 +886,7 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
|
||||
|
||||
remote_image = serializers.URLField(
|
||||
required=False,
|
||||
allow_blank=False,
|
||||
allow_blank=True,
|
||||
write_only=True,
|
||||
label=_('Remote Image'),
|
||||
help_text=_('URL of remote image file'),
|
||||
@@ -861,8 +912,8 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
|
||||
|
||||
try:
|
||||
self.remote_image_file = download_image_from_url(url)
|
||||
except Exception as exc:
|
||||
except Exception:
|
||||
self.remote_image_file = None
|
||||
raise ValidationError(str(exc))
|
||||
raise ValidationError(_('Failed to download image from remote URL'))
|
||||
|
||||
return url
|
||||
|
||||
@@ -18,10 +18,10 @@ import django.conf.locale
|
||||
import django.core.exceptions
|
||||
from django.core.validators import URLValidator
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import pytz
|
||||
import structlog
|
||||
from dotenv import load_dotenv
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from InvenTree.cache import get_cache_config, is_global_cache_enabled
|
||||
from InvenTree.config import get_boolean_setting, get_custom_file, get_setting
|
||||
@@ -33,7 +33,8 @@ from . import config, locales
|
||||
|
||||
checkMinPythonVersion()
|
||||
|
||||
INVENTREE_NEWS_URL = 'https://inventree.org/news/feed.atom'
|
||||
INVENTREE_BASE_URL = 'https://inventree.org'
|
||||
INVENTREE_NEWS_URL = f'{INVENTREE_BASE_URL}/news/feed.atom'
|
||||
|
||||
# Determine if we are running in "test" mode e.g. "manage.py test"
|
||||
TESTING = 'test' in sys.argv or 'TESTING' in os.environ
|
||||
@@ -78,43 +79,87 @@ if version_file.exists():
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = get_boolean_setting('INVENTREE_DEBUG', 'debug', True)
|
||||
|
||||
ENABLE_CLASSIC_FRONTEND = get_boolean_setting(
|
||||
'INVENTREE_CLASSIC_FRONTEND', 'classic_frontend', True
|
||||
)
|
||||
|
||||
# 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()
|
||||
@@ -134,30 +179,50 @@ STATIC_URL = '/static/'
|
||||
# Web URL endpoint for served media files
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
# Are plugins enabled?
|
||||
PLUGINS_ENABLED = get_boolean_setting(
|
||||
'INVENTREE_PLUGINS_ENABLED', 'plugins_enabled', False
|
||||
)
|
||||
|
||||
PLUGINS_INSTALL_DISABLED = get_boolean_setting(
|
||||
'INVENTREE_PLUGIN_NOINSTALL', 'plugin_noinstall', False
|
||||
)
|
||||
|
||||
PLUGIN_FILE = config.get_plugin_file()
|
||||
|
||||
# Plugin test settings
|
||||
PLUGIN_TESTING = get_setting(
|
||||
'INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING
|
||||
) # Are plugins being tested?
|
||||
|
||||
PLUGIN_TESTING_SETUP = get_setting(
|
||||
'INVENTREE_PLUGIN_TESTING_SETUP', 'PLUGIN_TESTING_SETUP', False
|
||||
) # Load plugins from setup hooks in testing?
|
||||
|
||||
PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
|
||||
PLUGIN_TESTING_EVENTS_ASYNC = False # Flag if events are tested asynchronously
|
||||
|
||||
PLUGIN_RETRY = get_setting(
|
||||
'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
|
||||
) # How often should plugin loading be tried?
|
||||
|
||||
# Hash of the plugin file (will be updated on each change)
|
||||
PLUGIN_FILE_HASH = ''
|
||||
|
||||
STATICFILES_DIRS = []
|
||||
|
||||
# Translated Template settings
|
||||
STATICFILES_I18_PREFIX = 'i18n'
|
||||
STATICFILES_I18_SRC = BASE_DIR.joinpath('templates', 'js', 'translated')
|
||||
STATICFILES_I18_TRG = BASE_DIR.joinpath('InvenTree', 'static_i18n')
|
||||
|
||||
# Create the target directory if it does not exist
|
||||
if not STATICFILES_I18_TRG.exists():
|
||||
STATICFILES_I18_TRG.mkdir(parents=True)
|
||||
|
||||
STATICFILES_DIRS.append(STATICFILES_I18_TRG)
|
||||
STATICFILES_I18_TRG = STATICFILES_I18_TRG.joinpath(STATICFILES_I18_PREFIX)
|
||||
|
||||
# Append directory for compiled react files if debug server is running
|
||||
if DEBUG and 'collectstatic' not in sys.argv:
|
||||
web_dir = BASE_DIR.joinpath('..', 'web', 'static').absolute()
|
||||
if web_dir.exists():
|
||||
STATICFILES_DIRS.append(web_dir)
|
||||
|
||||
STATFILES_I18_PROCESSORS = ['InvenTree.context.status_codes']
|
||||
# Append directory for sample plugin static content (if in debug mode)
|
||||
if PLUGINS_ENABLED:
|
||||
print('Adding plugin sample static content')
|
||||
STATICFILES_DIRS.append(BASE_DIR.joinpath('plugin', 'samples', 'static'))
|
||||
|
||||
# Color Themes Directory
|
||||
STATIC_COLOR_THEMES_DIR = STATIC_ROOT.joinpath('css', 'color-themes').resolve()
|
||||
print('-', STATICFILES_DIRS[-1])
|
||||
|
||||
# Database backup options
|
||||
# Ref: https://django-dbbackup.readthedocs.io/en/master/configuration.html
|
||||
@@ -170,10 +235,11 @@ DBBACKUP_STORAGE = get_setting(
|
||||
|
||||
# Default backup configuration
|
||||
DBBACKUP_STORAGE_OPTIONS = get_setting(
|
||||
'INVENTREE_BACKUP_OPTIONS', 'backup_options', None
|
||||
'INVENTREE_BACKUP_OPTIONS',
|
||||
'backup_options',
|
||||
default_value={'location': config.get_backup_dir()},
|
||||
typecast=dict,
|
||||
)
|
||||
if DBBACKUP_STORAGE_OPTIONS is None:
|
||||
DBBACKUP_STORAGE_OPTIONS = {'location': config.get_backup_dir()}
|
||||
|
||||
INVENTREE_ADMIN_ENABLED = get_boolean_setting(
|
||||
'INVENTREE_ADMIN_ENABLED', config_key='admin_enabled', default_value=True
|
||||
@@ -198,6 +264,7 @@ INSTALLED_APPS = [
|
||||
'stock.apps.StockConfig',
|
||||
'users.apps.UsersConfig',
|
||||
'machine.apps.MachineConfig',
|
||||
'importer.apps.ImporterConfig',
|
||||
'web',
|
||||
'generic',
|
||||
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
|
||||
@@ -216,7 +283,6 @@ INSTALLED_APPS = [
|
||||
'django_filters', # Extended filter functionality
|
||||
'rest_framework', # DRF (Django Rest Framework)
|
||||
'corsheaders', # Cross-origin Resource Sharing for DRF
|
||||
'crispy_forms', # Improved form rendering
|
||||
'import_export', # Import / export tables to file
|
||||
'django_cleanup.apps.CleanupConfig', # Automatically delete orphaned MEDIA files
|
||||
'mptt', # Modified Preorder Tree Traversal
|
||||
@@ -225,10 +291,10 @@ INSTALLED_APPS = [
|
||||
'djmoney.contrib.exchange', # django-money exchange rates
|
||||
'error_report', # Error reporting in the admin interface
|
||||
'django_q',
|
||||
'formtools', # Form wizard tools
|
||||
'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.headless', # APIs for auth
|
||||
@@ -262,6 +328,7 @@ MIDDLEWARE = CONFIG.get(
|
||||
'InvenTree.middleware.AuthRequiredMiddleware',
|
||||
'maintenance_mode.middleware.MaintenanceModeMiddleware',
|
||||
'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting
|
||||
'django_structlog.middlewares.RequestMiddleware', # Structured logging
|
||||
],
|
||||
)
|
||||
|
||||
@@ -280,7 +347,7 @@ QUERYCOUNT = {
|
||||
'MIN_TIME_TO_LOG': 0.1,
|
||||
'MIN_QUERY_COUNT_TO_LOG': 25,
|
||||
},
|
||||
'IGNORE_REQUEST_PATTERNS': ['^(?!\/(api)?(plugin)?\/).*'],
|
||||
'IGNORE_REQUEST_PATTERNS': [r'^(?!\/(api)?(plugin)?\/).*'],
|
||||
'IGNORE_SQL_PATTERNS': [],
|
||||
'DISPLAY_DUPLICATES': 1,
|
||||
'RESPONSE_HEADER': 'X-Django-Query-Count',
|
||||
@@ -297,9 +364,9 @@ if (
|
||||
and INVENTREE_ADMIN_ENABLED
|
||||
and not TESTING
|
||||
and get_boolean_setting('INVENTREE_DEBUG_SHELL', 'debug_shell', False)
|
||||
): # noqa
|
||||
):
|
||||
try:
|
||||
import django_admin_shell
|
||||
import django_admin_shell # noqa: F401
|
||||
|
||||
INSTALLED_APPS.append('django_admin_shell')
|
||||
ADMIN_SHELL_ENABLE = True
|
||||
@@ -323,8 +390,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')
|
||||
|
||||
@@ -375,7 +442,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(
|
||||
@@ -402,12 +469,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'
|
||||
)
|
||||
@@ -441,10 +534,6 @@ TEMPLATES = [
|
||||
'django.template.context_processors.i18n',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
# Custom InvenTree context processors
|
||||
'InvenTree.context.health_status',
|
||||
'InvenTree.context.status_codes',
|
||||
'InvenTree.context.user_roles',
|
||||
],
|
||||
'loaders': [
|
||||
(
|
||||
@@ -527,6 +616,9 @@ for key in db_keys:
|
||||
# Check that required database configuration options are specified
|
||||
required_keys = ['ENGINE', 'NAME']
|
||||
|
||||
# Ensure all database keys are upper case
|
||||
db_config = {key.upper(): value for key, value in db_config.items()}
|
||||
|
||||
for key in required_keys:
|
||||
if key not in db_config: # pragma: no cover
|
||||
error_msg = f'Missing required database configuration value {key}'
|
||||
@@ -784,13 +876,20 @@ _q_worker_timeout = int(
|
||||
get_setting('INVENTREE_BACKGROUND_TIMEOUT', 'background.timeout', 90)
|
||||
)
|
||||
|
||||
|
||||
# Prevent running multiple background workers if global cache is disabled
|
||||
# This is to prevent scheduling conflicts due to the lack of a shared cache
|
||||
BACKGROUND_WORKER_COUNT = (
|
||||
int(get_setting('INVENTREE_BACKGROUND_WORKERS', 'background.workers', 4))
|
||||
if GLOBAL_CACHE_ENABLED
|
||||
else 1
|
||||
)
|
||||
|
||||
# django-q background worker configuration
|
||||
Q_CLUSTER = {
|
||||
'name': 'InvenTree',
|
||||
'label': 'Background Tasks',
|
||||
'workers': int(
|
||||
get_setting('INVENTREE_BACKGROUND_WORKERS', 'background.workers', 4)
|
||||
),
|
||||
'workers': BACKGROUND_WORKER_COUNT,
|
||||
'timeout': _q_worker_timeout,
|
||||
'retry': max(120, _q_worker_timeout + 30),
|
||||
'max_attempts': int(
|
||||
@@ -907,24 +1006,18 @@ TIME_ZONE = get_setting('INVENTREE_TIMEZONE', 'timezone', 'UTC')
|
||||
|
||||
# Check that the timezone is valid
|
||||
try:
|
||||
pytz.timezone(TIME_ZONE)
|
||||
except pytz.exceptions.UnknownTimeZoneError: # pragma: no cover
|
||||
ZoneInfo(TIME_ZONE)
|
||||
except ZoneInfoNotFoundError: # pragma: no cover
|
||||
raise ValueError(f"Specified timezone '{TIME_ZONE}' is not valid")
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
# Do not use native timezone support in "test" mode
|
||||
# It generates a *lot* of cruft in the logs
|
||||
if not TESTING:
|
||||
USE_TZ = True # pragma: no cover
|
||||
else:
|
||||
USE_TZ = False
|
||||
USE_TZ = bool(not TESTING)
|
||||
|
||||
DATE_INPUT_FORMATS = ['%Y-%m-%d']
|
||||
|
||||
# crispy forms use the bootstrap templates
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap4'
|
||||
|
||||
# Use database transactions when importing / exporting data
|
||||
IMPORT_EXPORT_USE_TRANSACTIONS = True
|
||||
|
||||
@@ -1024,26 +1117,40 @@ if (
|
||||
sys.exit(-1)
|
||||
|
||||
COOKIE_MODE = (
|
||||
str(get_setting('INVENTREE_COOKIE_SAMESITE', 'cookie.samesite', 'None'))
|
||||
str(get_setting('INVENTREE_COOKIE_SAMESITE', 'cookie.samesite', 'False'))
|
||||
.lower()
|
||||
.strip()
|
||||
)
|
||||
|
||||
valid_cookie_modes = {'lax': 'Lax', 'strict': 'Strict', 'none': None, 'null': None}
|
||||
# Valid modes (as per the django settings documentation)
|
||||
valid_cookie_modes = ['lax', 'strict', 'none']
|
||||
|
||||
if COOKIE_MODE not in valid_cookie_modes.keys():
|
||||
logger.error('Invalid cookie samesite mode: %s', COOKIE_MODE)
|
||||
sys.exit(-1)
|
||||
|
||||
COOKIE_MODE = valid_cookie_modes[COOKIE_MODE.lower()]
|
||||
if not DEBUG and not TESTING and COOKIE_MODE in valid_cookie_modes:
|
||||
# Set the cookie mode (in production mode only)
|
||||
COOKIE_MODE = COOKIE_MODE.capitalize()
|
||||
else:
|
||||
# Default to False, as per the Django settings
|
||||
COOKIE_MODE = False
|
||||
|
||||
# Additional CSRF settings
|
||||
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
|
||||
CSRF_COOKIE_NAME = 'csrftoken'
|
||||
|
||||
CSRF_COOKIE_SAMESITE = COOKIE_MODE
|
||||
SESSION_COOKIE_SAMESITE = COOKIE_MODE
|
||||
SESSION_COOKIE_SECURE = get_boolean_setting(
|
||||
'INVENTREE_SESSION_COOKIE_SECURE', 'cookie.secure', False
|
||||
|
||||
"""Set the SESSION_COOKIE_SECURE value based on the following rules:
|
||||
- False if the server is running in DEBUG mode
|
||||
- True if samesite cookie setting is set to 'None'
|
||||
- Otherwise, use the value specified in the configuration file (or env var)
|
||||
"""
|
||||
SESSION_COOKIE_SECURE = (
|
||||
False
|
||||
if DEBUG
|
||||
else (
|
||||
SESSION_COOKIE_SAMESITE == 'None'
|
||||
or get_boolean_setting('INVENTREE_SESSION_COOKIE_SECURE', 'cookie.secure', True)
|
||||
)
|
||||
)
|
||||
|
||||
USE_X_FORWARDED_HOST = get_boolean_setting(
|
||||
@@ -1167,8 +1274,8 @@ REMOVE_SUCCESS_URL = 'settings'
|
||||
|
||||
# override forms / adapters
|
||||
ACCOUNT_FORMS = {
|
||||
'login': 'InvenTree.forms.CustomLoginForm',
|
||||
'signup': 'InvenTree.forms.CustomSignupForm',
|
||||
'login': 'InvenTree.auth_overrides.CustomLoginForm',
|
||||
'signup': 'InvenTree.auth_overrides.CustomSignupForm',
|
||||
'add_email': 'allauth.account.forms.AddEmailForm',
|
||||
'change_password': 'allauth.account.forms.ChangePasswordForm',
|
||||
'set_password': 'allauth.account.forms.SetPasswordForm',
|
||||
@@ -1177,16 +1284,17 @@ ACCOUNT_FORMS = {
|
||||
'disconnect': 'allauth.socialaccount.forms.DisconnectForm',
|
||||
}
|
||||
|
||||
SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter'
|
||||
ACCOUNT_ADAPTER = 'InvenTree.forms.CustomAccountAdapter'
|
||||
SOCIALACCOUNT_ADAPTER = 'InvenTree.auth_overrides.CustomSocialAccountAdapter'
|
||||
ACCOUNT_ADAPTER = 'InvenTree.auth_overrides.CustomAccountAdapter'
|
||||
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
|
||||
|
||||
HEADLESS_FRONTEND_URLS = {
|
||||
'account_confirm_email': 'https://app.project.org/account/verify-email/{key}',
|
||||
'account_reset_password_from_key': 'https://app.org/account/password/reset/key/{key}',
|
||||
'account_confirm_email': 'https://app.project.org/account/verify-email/{key}', # noqa: RUF027
|
||||
'account_reset_password_from_key': 'https://app.org/account/password/reset/key/{key}', # noqa: RUF027
|
||||
'account_signup': 'https://app.org/account/signup',
|
||||
}
|
||||
HEADLESS_ONLY = not ENABLE_CLASSIC_FRONTEND
|
||||
HEADLESS_ONLY = True
|
||||
MFA_ENABLED = get_boolean_setting('INVENTREE_MFA_ENABLED', 'mfa_enabled', True)
|
||||
|
||||
# Markdownify configuration
|
||||
# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html
|
||||
@@ -1201,23 +1309,29 @@ MARKDOWNIFY = {
|
||||
'abbr',
|
||||
'b',
|
||||
'blockquote',
|
||||
'code',
|
||||
'em',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'hr',
|
||||
'i',
|
||||
'img',
|
||||
'li',
|
||||
'ol',
|
||||
'p',
|
||||
'pre',
|
||||
's',
|
||||
'strong',
|
||||
'ul',
|
||||
'table',
|
||||
'thead',
|
||||
'tbody',
|
||||
'th',
|
||||
'tr',
|
||||
'td',
|
||||
'ul',
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -1229,32 +1343,12 @@ IGNORED_ERRORS = [Http404, django.core.exceptions.PermissionDenied]
|
||||
MAINTENANCE_MODE_RETRY_AFTER = 10
|
||||
MAINTENANCE_MODE_STATE_BACKEND = 'InvenTree.backends.InvenTreeMaintenanceModeBackend'
|
||||
|
||||
# Are plugins enabled?
|
||||
PLUGINS_ENABLED = get_boolean_setting(
|
||||
'INVENTREE_PLUGINS_ENABLED', 'plugins_enabled', False
|
||||
)
|
||||
PLUGINS_INSTALL_DISABLED = get_boolean_setting(
|
||||
'INVENTREE_PLUGIN_NOINSTALL', 'plugin_noinstall', False
|
||||
)
|
||||
|
||||
PLUGIN_FILE = config.get_plugin_file()
|
||||
|
||||
# Plugin test settings
|
||||
PLUGIN_TESTING = get_setting(
|
||||
'INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING
|
||||
) # Are plugins being tested?
|
||||
PLUGIN_TESTING_SETUP = get_setting(
|
||||
'INVENTREE_PLUGIN_TESTING_SETUP', 'PLUGIN_TESTING_SETUP', False
|
||||
) # Load plugins from setup hooks in testing?
|
||||
PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
|
||||
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?
|
||||
|
||||
# Flag to allow table events during testing
|
||||
TESTING_TABLE_EVENTS = False
|
||||
|
||||
# Flag to allow pricing recalculations during testing
|
||||
TESTING_PRICING = False
|
||||
|
||||
# User interface customization values
|
||||
CUSTOM_LOGO = get_custom_file(
|
||||
'INVENTREE_CUSTOM_LOGO', 'customize.logo', 'custom logo', lookup_media=True
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
"""Helper functions for Single Sign On functionality."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from allauth.socialaccount.models import SocialAccount, SocialLogin
|
||||
|
||||
from common.settings import get_global_setting
|
||||
from InvenTree.helpers import str2bool
|
||||
|
||||
@@ -53,12 +60,21 @@ def check_provider(provider):
|
||||
return True
|
||||
|
||||
|
||||
def login_enabled() -> bool:
|
||||
def provider_display_name(provider):
|
||||
"""Return the 'display name' for the given provider."""
|
||||
if app := get_provider_app(provider):
|
||||
return app.name
|
||||
|
||||
# Fallback value if app not found
|
||||
return provider.name
|
||||
|
||||
|
||||
def sso_login_enabled() -> bool:
|
||||
"""Return True if SSO login is enabled."""
|
||||
return str2bool(get_global_setting('LOGIN_ENABLE_SSO'))
|
||||
|
||||
|
||||
def registration_enabled() -> bool:
|
||||
def sso_registration_enabled() -> bool:
|
||||
"""Return True if SSO registration is enabled."""
|
||||
return str2bool(get_global_setting('LOGIN_ENABLE_SSO_REG'))
|
||||
|
||||
@@ -66,3 +82,55 @@ def registration_enabled() -> bool:
|
||||
def auto_registration_enabled() -> bool:
|
||||
"""Return True if SSO auto-registration is enabled."""
|
||||
return str2bool(get_global_setting('LOGIN_SIGNUP_SSO_AUTO'))
|
||||
|
||||
|
||||
def ensure_sso_groups(sender, sociallogin: SocialLogin, **kwargs):
|
||||
"""Sync groups from IdP each time a SSO user logs on.
|
||||
|
||||
This event listener is registered in the apps ready method.
|
||||
"""
|
||||
if not get_global_setting('LOGIN_ENABLE_SSO_GROUP_SYNC'):
|
||||
return
|
||||
|
||||
group_key = get_global_setting('SSO_GROUP_KEY')
|
||||
group_map = json.loads(get_global_setting('SSO_GROUP_MAP'))
|
||||
# map SSO groups to InvenTree groups
|
||||
group_names = []
|
||||
for sso_group in sociallogin.account.extra_data.get(group_key, []):
|
||||
if mapped_name := group_map.get(sso_group):
|
||||
group_names.append(mapped_name)
|
||||
|
||||
# ensure user has groups
|
||||
user = sociallogin.account.user
|
||||
for group_name in group_names:
|
||||
try:
|
||||
user.groups.get(name=group_name)
|
||||
except Group.DoesNotExist:
|
||||
# user not in group yet
|
||||
try:
|
||||
group = Group.objects.get(name=group_name)
|
||||
except Group.DoesNotExist:
|
||||
logger.info(f'Creating group {group_name} as it did not exist')
|
||||
group = Group(name=group_name)
|
||||
group.save()
|
||||
logger.info(f'Adding group {group_name} to user {user}')
|
||||
user.groups.add(group)
|
||||
|
||||
# remove groups not listed by SSO if not disabled
|
||||
if get_global_setting('SSO_REMOVE_GROUPS'):
|
||||
for group in user.groups.all():
|
||||
if group.name not in group_names:
|
||||
logger.info(f'Removing group {group.name} from {user}')
|
||||
user.groups.remove(group)
|
||||
|
||||
|
||||
@receiver(post_save, sender=SocialAccount)
|
||||
def on_social_account_created(sender, instance: SocialAccount, created: bool, **kwargs):
|
||||
"""Sync SSO groups when new SocialAccount is added.
|
||||
|
||||
Since the allauth `social_account_added` signal is not sent for some reason, this
|
||||
signal is simulated using post_save signals. The issue has been reported as
|
||||
https://github.com/pennersr/django-allauth/issues/3834
|
||||
"""
|
||||
if created:
|
||||
ensure_sso_groups(None, SocialLogin(account=instance))
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
/**
|
||||
* @author zhixin wen <wenzhixin2010@gmail.com>
|
||||
* version: 1.18.3
|
||||
* https://github.com/wenzhixin/bootstrap-table/
|
||||
*/
|
||||
.bootstrap-table .fixed-table-toolbar::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .bs-bars,
|
||||
.bootstrap-table .fixed-table-toolbar .search,
|
||||
.bootstrap-table .fixed-table-toolbar .columns {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group {
|
||||
display: inline-block;
|
||||
margin-left: -1px !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group > .btn {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group:first-child > .btn {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group:last-child > .btn {
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu {
|
||||
text-align: left;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
-ms-overflow-style: scrollbar;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns label {
|
||||
display: block;
|
||||
padding: 3px 20px;
|
||||
clear: both;
|
||||
font-weight: normal;
|
||||
line-height: 1.428571429;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns-left {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns-right {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container {
|
||||
position: relative;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table {
|
||||
width: 100%;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table th,
|
||||
.bootstrap-table .fixed-table-container .table td {
|
||||
vertical-align: middle;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th {
|
||||
vertical-align: bottom;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th:focus {
|
||||
outline: 0 solid transparent;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th.detail {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th .th-inner {
|
||||
padding: 0.75rem;
|
||||
vertical-align: bottom;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th .sortable {
|
||||
cursor: pointer;
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
padding-right: 30px !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th .both {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC");
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th .asc {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==");
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th .desc {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ");
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table tbody tr.selected td {
|
||||
background-color: rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table tbody tr .card-view {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
min-width: 30%;
|
||||
width: auto !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table .bs-checkbox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table .bs-checkbox label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type="radio"],
|
||||
.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type="checkbox"] {
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table.table-sm .th-inner {
|
||||
padding: 0.3rem;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer) {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container.fixed-height.has-card-view {
|
||||
border-top: 1px solid #dee2e6;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border {
|
||||
border-left: 1px solid #dee2e6;
|
||||
border-right: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container.fixed-height .table thead th {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th {
|
||||
border-bottom: 1px solid #32383e;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-header {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading {
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
transition: visibility 0s, opacity 0.15s ease-in-out;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before {
|
||||
content: "";
|
||||
animation-duration: 1.5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: LOADING;
|
||||
background: #212529;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
height: 5px;
|
||||
margin: 0 4px;
|
||||
opacity: 0;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark {
|
||||
background: #212529;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-footer {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination-detail,
|
||||
.bootstrap-table .fixed-table-pagination > .pagination {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination-detail .pagination-info {
|
||||
line-height: 34px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination-detail .page-list {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination-detail .page-list .btn-group {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination-detail .page-list .btn-group .dropdown-menu {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination ul.pagination {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.page-intermediate a {
|
||||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.page-intermediate a::before {
|
||||
content: '\2B05';
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.page-intermediate a::after {
|
||||
content: '\27A1';
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.disabled a {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.bootstrap-table.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1050;
|
||||
width: 100% !important;
|
||||
background: #fff;
|
||||
height: calc(100vh);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.bootstrap-table.bootstrap4 .pagination-lg .page-link, .bootstrap-table.bootstrap5 .pagination-lg .page-link {
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.bootstrap-table.bootstrap5 .float-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.bootstrap-table.bootstrap5 .float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* calculate scrollbar width */
|
||||
div.fixed-table-scroll-inner {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
div.fixed-table-scroll-outer {
|
||||
top: 0;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes LOADING {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
-10
File diff suppressed because one or more lines are too long
src/backend/InvenTree/InvenTree/static/bootstrap-table/extensions/addrbar/bootstrap-table-addrbar.js
Vendored
-1779
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
-1211
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
Vendored
-2546
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
-1237
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
-1256
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
-869
@@ -1,869 +0,0 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery')) :
|
||||
typeof define === 'function' && define.amd ? define(['jquery'], factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.jQuery));
|
||||
}(this, (function ($) { 'use strict';
|
||||
|
||||
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
||||
|
||||
var $__default = /*#__PURE__*/_interopDefaultLegacy($);
|
||||
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
}
|
||||
|
||||
function _defineProperties(target, props) {
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var descriptor = props[i];
|
||||
descriptor.enumerable = descriptor.enumerable || false;
|
||||
descriptor.configurable = true;
|
||||
if ("value" in descriptor) descriptor.writable = true;
|
||||
Object.defineProperty(target, descriptor.key, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
function _createClass(Constructor, protoProps, staticProps) {
|
||||
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
|
||||
if (staticProps) _defineProperties(Constructor, staticProps);
|
||||
return Constructor;
|
||||
}
|
||||
|
||||
function _inherits(subClass, superClass) {
|
||||
if (typeof superClass !== "function" && superClass !== null) {
|
||||
throw new TypeError("Super expression must either be null or a function");
|
||||
}
|
||||
|
||||
subClass.prototype = Object.create(superClass && superClass.prototype, {
|
||||
constructor: {
|
||||
value: subClass,
|
||||
writable: true,
|
||||
configurable: true
|
||||
}
|
||||
});
|
||||
if (superClass) _setPrototypeOf(subClass, superClass);
|
||||
}
|
||||
|
||||
function _getPrototypeOf(o) {
|
||||
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
|
||||
return o.__proto__ || Object.getPrototypeOf(o);
|
||||
};
|
||||
return _getPrototypeOf(o);
|
||||
}
|
||||
|
||||
function _setPrototypeOf(o, p) {
|
||||
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
|
||||
o.__proto__ = p;
|
||||
return o;
|
||||
};
|
||||
|
||||
return _setPrototypeOf(o, p);
|
||||
}
|
||||
|
||||
function _isNativeReflectConstruct() {
|
||||
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
|
||||
if (Reflect.construct.sham) return false;
|
||||
if (typeof Proxy === "function") return true;
|
||||
|
||||
try {
|
||||
Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function _assertThisInitialized(self) {
|
||||
if (self === void 0) {
|
||||
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
function _possibleConstructorReturn(self, call) {
|
||||
if (call && (typeof call === "object" || typeof call === "function")) {
|
||||
return call;
|
||||
}
|
||||
|
||||
return _assertThisInitialized(self);
|
||||
}
|
||||
|
||||
function _createSuper(Derived) {
|
||||
var hasNativeReflectConstruct = _isNativeReflectConstruct();
|
||||
|
||||
return function _createSuperInternal() {
|
||||
var Super = _getPrototypeOf(Derived),
|
||||
result;
|
||||
|
||||
if (hasNativeReflectConstruct) {
|
||||
var NewTarget = _getPrototypeOf(this).constructor;
|
||||
|
||||
result = Reflect.construct(Super, arguments, NewTarget);
|
||||
} else {
|
||||
result = Super.apply(this, arguments);
|
||||
}
|
||||
|
||||
return _possibleConstructorReturn(this, result);
|
||||
};
|
||||
}
|
||||
|
||||
function _superPropBase(object, property) {
|
||||
while (!Object.prototype.hasOwnProperty.call(object, property)) {
|
||||
object = _getPrototypeOf(object);
|
||||
if (object === null) break;
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
function _get(target, property, receiver) {
|
||||
if (typeof Reflect !== "undefined" && Reflect.get) {
|
||||
_get = Reflect.get;
|
||||
} else {
|
||||
_get = function _get(target, property, receiver) {
|
||||
var base = _superPropBase(target, property);
|
||||
|
||||
if (!base) return;
|
||||
var desc = Object.getOwnPropertyDescriptor(base, property);
|
||||
|
||||
if (desc.get) {
|
||||
return desc.get.call(receiver);
|
||||
}
|
||||
|
||||
return desc.value;
|
||||
};
|
||||
}
|
||||
|
||||
return _get(target, property, receiver || target);
|
||||
}
|
||||
|
||||
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
||||
|
||||
function createCommonjsModule(fn, module) {
|
||||
return module = { exports: {} }, fn(module, module.exports), module.exports;
|
||||
}
|
||||
|
||||
var check = function (it) {
|
||||
return it && it.Math == Math && it;
|
||||
};
|
||||
|
||||
// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028
|
||||
var global_1 =
|
||||
/* global globalThis -- safe */
|
||||
check(typeof globalThis == 'object' && globalThis) ||
|
||||
check(typeof window == 'object' && window) ||
|
||||
check(typeof self == 'object' && self) ||
|
||||
check(typeof commonjsGlobal == 'object' && commonjsGlobal) ||
|
||||
// eslint-disable-next-line no-new-func -- fallback
|
||||
(function () { return this; })() || Function('return this')();
|
||||
|
||||
var fails = function (exec) {
|
||||
try {
|
||||
return !!exec();
|
||||
} catch (error) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Detect IE8's incomplete defineProperty implementation
|
||||
var descriptors = !fails(function () {
|
||||
return Object.defineProperty({}, 1, { get: function () { return 7; } })[1] != 7;
|
||||
});
|
||||
|
||||
var nativePropertyIsEnumerable = {}.propertyIsEnumerable;
|
||||
var getOwnPropertyDescriptor$1 = Object.getOwnPropertyDescriptor;
|
||||
|
||||
// Nashorn ~ JDK8 bug
|
||||
var NASHORN_BUG = getOwnPropertyDescriptor$1 && !nativePropertyIsEnumerable.call({ 1: 2 }, 1);
|
||||
|
||||
// `Object.prototype.propertyIsEnumerable` method implementation
|
||||
// https://tc39.es/ecma262/#sec-object.prototype.propertyisenumerable
|
||||
var f$4 = NASHORN_BUG ? function propertyIsEnumerable(V) {
|
||||
var descriptor = getOwnPropertyDescriptor$1(this, V);
|
||||
return !!descriptor && descriptor.enumerable;
|
||||
} : nativePropertyIsEnumerable;
|
||||
|
||||
var objectPropertyIsEnumerable = {
|
||||
f: f$4
|
||||
};
|
||||
|
||||
var createPropertyDescriptor = function (bitmap, value) {
|
||||
return {
|
||||
enumerable: !(bitmap & 1),
|
||||
configurable: !(bitmap & 2),
|
||||
writable: !(bitmap & 4),
|
||||
value: value
|
||||
};
|
||||
};
|
||||
|
||||
var toString = {}.toString;
|
||||
|
||||
var classofRaw = function (it) {
|
||||
return toString.call(it).slice(8, -1);
|
||||
};
|
||||
|
||||
var split = ''.split;
|
||||
|
||||
// fallback for non-array-like ES3 and non-enumerable old V8 strings
|
||||
var indexedObject = fails(function () {
|
||||
// throws an error in rhino, see https://github.com/mozilla/rhino/issues/346
|
||||
// eslint-disable-next-line no-prototype-builtins -- safe
|
||||
return !Object('z').propertyIsEnumerable(0);
|
||||
}) ? function (it) {
|
||||
return classofRaw(it) == 'String' ? split.call(it, '') : Object(it);
|
||||
} : Object;
|
||||
|
||||
// `RequireObjectCoercible` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-requireobjectcoercible
|
||||
var requireObjectCoercible = function (it) {
|
||||
if (it == undefined) throw TypeError("Can't call method on " + it);
|
||||
return it;
|
||||
};
|
||||
|
||||
// toObject with fallback for non-array-like ES3 strings
|
||||
|
||||
|
||||
|
||||
var toIndexedObject = function (it) {
|
||||
return indexedObject(requireObjectCoercible(it));
|
||||
};
|
||||
|
||||
var isObject = function (it) {
|
||||
return typeof it === 'object' ? it !== null : typeof it === 'function';
|
||||
};
|
||||
|
||||
// `ToPrimitive` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-toprimitive
|
||||
// instead of the ES6 spec version, we didn't implement @@toPrimitive case
|
||||
// and the second argument - flag - preferred type is a string
|
||||
var toPrimitive = function (input, PREFERRED_STRING) {
|
||||
if (!isObject(input)) return input;
|
||||
var fn, val;
|
||||
if (PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val;
|
||||
if (typeof (fn = input.valueOf) == 'function' && !isObject(val = fn.call(input))) return val;
|
||||
if (!PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val;
|
||||
throw TypeError("Can't convert object to primitive value");
|
||||
};
|
||||
|
||||
var hasOwnProperty = {}.hasOwnProperty;
|
||||
|
||||
var has$1 = function (it, key) {
|
||||
return hasOwnProperty.call(it, key);
|
||||
};
|
||||
|
||||
var document = global_1.document;
|
||||
// typeof document.createElement is 'object' in old IE
|
||||
var EXISTS = isObject(document) && isObject(document.createElement);
|
||||
|
||||
var documentCreateElement = function (it) {
|
||||
return EXISTS ? document.createElement(it) : {};
|
||||
};
|
||||
|
||||
// Thank's IE8 for his funny defineProperty
|
||||
var ie8DomDefine = !descriptors && !fails(function () {
|
||||
return Object.defineProperty(documentCreateElement('div'), 'a', {
|
||||
get: function () { return 7; }
|
||||
}).a != 7;
|
||||
});
|
||||
|
||||
var nativeGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
||||
|
||||
// `Object.getOwnPropertyDescriptor` method
|
||||
// https://tc39.es/ecma262/#sec-object.getownpropertydescriptor
|
||||
var f$3 = descriptors ? nativeGetOwnPropertyDescriptor : function getOwnPropertyDescriptor(O, P) {
|
||||
O = toIndexedObject(O);
|
||||
P = toPrimitive(P, true);
|
||||
if (ie8DomDefine) try {
|
||||
return nativeGetOwnPropertyDescriptor(O, P);
|
||||
} catch (error) { /* empty */ }
|
||||
if (has$1(O, P)) return createPropertyDescriptor(!objectPropertyIsEnumerable.f.call(O, P), O[P]);
|
||||
};
|
||||
|
||||
var objectGetOwnPropertyDescriptor = {
|
||||
f: f$3
|
||||
};
|
||||
|
||||
var anObject = function (it) {
|
||||
if (!isObject(it)) {
|
||||
throw TypeError(String(it) + ' is not an object');
|
||||
} return it;
|
||||
};
|
||||
|
||||
var nativeDefineProperty = Object.defineProperty;
|
||||
|
||||
// `Object.defineProperty` method
|
||||
// https://tc39.es/ecma262/#sec-object.defineproperty
|
||||
var f$2 = descriptors ? nativeDefineProperty : function defineProperty(O, P, Attributes) {
|
||||
anObject(O);
|
||||
P = toPrimitive(P, true);
|
||||
anObject(Attributes);
|
||||
if (ie8DomDefine) try {
|
||||
return nativeDefineProperty(O, P, Attributes);
|
||||
} catch (error) { /* empty */ }
|
||||
if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported');
|
||||
if ('value' in Attributes) O[P] = Attributes.value;
|
||||
return O;
|
||||
};
|
||||
|
||||
var objectDefineProperty = {
|
||||
f: f$2
|
||||
};
|
||||
|
||||
var createNonEnumerableProperty = descriptors ? function (object, key, value) {
|
||||
return objectDefineProperty.f(object, key, createPropertyDescriptor(1, value));
|
||||
} : function (object, key, value) {
|
||||
object[key] = value;
|
||||
return object;
|
||||
};
|
||||
|
||||
var setGlobal = function (key, value) {
|
||||
try {
|
||||
createNonEnumerableProperty(global_1, key, value);
|
||||
} catch (error) {
|
||||
global_1[key] = value;
|
||||
} return value;
|
||||
};
|
||||
|
||||
var SHARED = '__core-js_shared__';
|
||||
var store$1 = global_1[SHARED] || setGlobal(SHARED, {});
|
||||
|
||||
var sharedStore = store$1;
|
||||
|
||||
var functionToString = Function.toString;
|
||||
|
||||
// this helper broken in `3.4.1-3.4.4`, so we can't use `shared` helper
|
||||
if (typeof sharedStore.inspectSource != 'function') {
|
||||
sharedStore.inspectSource = function (it) {
|
||||
return functionToString.call(it);
|
||||
};
|
||||
}
|
||||
|
||||
var inspectSource = sharedStore.inspectSource;
|
||||
|
||||
var WeakMap$1 = global_1.WeakMap;
|
||||
|
||||
var nativeWeakMap = typeof WeakMap$1 === 'function' && /native code/.test(inspectSource(WeakMap$1));
|
||||
|
||||
var shared = createCommonjsModule(function (module) {
|
||||
(module.exports = function (key, value) {
|
||||
return sharedStore[key] || (sharedStore[key] = value !== undefined ? value : {});
|
||||
})('versions', []).push({
|
||||
version: '3.9.1',
|
||||
mode: 'global',
|
||||
copyright: '© 2021 Denis Pushkarev (zloirock.ru)'
|
||||
});
|
||||
});
|
||||
|
||||
var id = 0;
|
||||
var postfix = Math.random();
|
||||
|
||||
var uid = function (key) {
|
||||
return 'Symbol(' + String(key === undefined ? '' : key) + ')_' + (++id + postfix).toString(36);
|
||||
};
|
||||
|
||||
var keys = shared('keys');
|
||||
|
||||
var sharedKey = function (key) {
|
||||
return keys[key] || (keys[key] = uid(key));
|
||||
};
|
||||
|
||||
var hiddenKeys$1 = {};
|
||||
|
||||
var WeakMap = global_1.WeakMap;
|
||||
var set, get, has;
|
||||
|
||||
var enforce = function (it) {
|
||||
return has(it) ? get(it) : set(it, {});
|
||||
};
|
||||
|
||||
var getterFor = function (TYPE) {
|
||||
return function (it) {
|
||||
var state;
|
||||
if (!isObject(it) || (state = get(it)).type !== TYPE) {
|
||||
throw TypeError('Incompatible receiver, ' + TYPE + ' required');
|
||||
} return state;
|
||||
};
|
||||
};
|
||||
|
||||
if (nativeWeakMap) {
|
||||
var store = sharedStore.state || (sharedStore.state = new WeakMap());
|
||||
var wmget = store.get;
|
||||
var wmhas = store.has;
|
||||
var wmset = store.set;
|
||||
set = function (it, metadata) {
|
||||
metadata.facade = it;
|
||||
wmset.call(store, it, metadata);
|
||||
return metadata;
|
||||
};
|
||||
get = function (it) {
|
||||
return wmget.call(store, it) || {};
|
||||
};
|
||||
has = function (it) {
|
||||
return wmhas.call(store, it);
|
||||
};
|
||||
} else {
|
||||
var STATE = sharedKey('state');
|
||||
hiddenKeys$1[STATE] = true;
|
||||
set = function (it, metadata) {
|
||||
metadata.facade = it;
|
||||
createNonEnumerableProperty(it, STATE, metadata);
|
||||
return metadata;
|
||||
};
|
||||
get = function (it) {
|
||||
return has$1(it, STATE) ? it[STATE] : {};
|
||||
};
|
||||
has = function (it) {
|
||||
return has$1(it, STATE);
|
||||
};
|
||||
}
|
||||
|
||||
var internalState = {
|
||||
set: set,
|
||||
get: get,
|
||||
has: has,
|
||||
enforce: enforce,
|
||||
getterFor: getterFor
|
||||
};
|
||||
|
||||
var redefine = createCommonjsModule(function (module) {
|
||||
var getInternalState = internalState.get;
|
||||
var enforceInternalState = internalState.enforce;
|
||||
var TEMPLATE = String(String).split('String');
|
||||
|
||||
(module.exports = function (O, key, value, options) {
|
||||
var unsafe = options ? !!options.unsafe : false;
|
||||
var simple = options ? !!options.enumerable : false;
|
||||
var noTargetGet = options ? !!options.noTargetGet : false;
|
||||
var state;
|
||||
if (typeof value == 'function') {
|
||||
if (typeof key == 'string' && !has$1(value, 'name')) {
|
||||
createNonEnumerableProperty(value, 'name', key);
|
||||
}
|
||||
state = enforceInternalState(value);
|
||||
if (!state.source) {
|
||||
state.source = TEMPLATE.join(typeof key == 'string' ? key : '');
|
||||
}
|
||||
}
|
||||
if (O === global_1) {
|
||||
if (simple) O[key] = value;
|
||||
else setGlobal(key, value);
|
||||
return;
|
||||
} else if (!unsafe) {
|
||||
delete O[key];
|
||||
} else if (!noTargetGet && O[key]) {
|
||||
simple = true;
|
||||
}
|
||||
if (simple) O[key] = value;
|
||||
else createNonEnumerableProperty(O, key, value);
|
||||
// add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative
|
||||
})(Function.prototype, 'toString', function toString() {
|
||||
return typeof this == 'function' && getInternalState(this).source || inspectSource(this);
|
||||
});
|
||||
});
|
||||
|
||||
var path = global_1;
|
||||
|
||||
var aFunction = function (variable) {
|
||||
return typeof variable == 'function' ? variable : undefined;
|
||||
};
|
||||
|
||||
var getBuiltIn = function (namespace, method) {
|
||||
return arguments.length < 2 ? aFunction(path[namespace]) || aFunction(global_1[namespace])
|
||||
: path[namespace] && path[namespace][method] || global_1[namespace] && global_1[namespace][method];
|
||||
};
|
||||
|
||||
var ceil = Math.ceil;
|
||||
var floor = Math.floor;
|
||||
|
||||
// `ToInteger` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-tointeger
|
||||
var toInteger = function (argument) {
|
||||
return isNaN(argument = +argument) ? 0 : (argument > 0 ? floor : ceil)(argument);
|
||||
};
|
||||
|
||||
var min$1 = Math.min;
|
||||
|
||||
// `ToLength` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-tolength
|
||||
var toLength = function (argument) {
|
||||
return argument > 0 ? min$1(toInteger(argument), 0x1FFFFFFFFFFFFF) : 0; // 2 ** 53 - 1 == 9007199254740991
|
||||
};
|
||||
|
||||
var max = Math.max;
|
||||
var min = Math.min;
|
||||
|
||||
// Helper for a popular repeating case of the spec:
|
||||
// Let integer be ? ToInteger(index).
|
||||
// If integer < 0, let result be max((length + integer), 0); else let result be min(integer, length).
|
||||
var toAbsoluteIndex = function (index, length) {
|
||||
var integer = toInteger(index);
|
||||
return integer < 0 ? max(integer + length, 0) : min(integer, length);
|
||||
};
|
||||
|
||||
// `Array.prototype.{ indexOf, includes }` methods implementation
|
||||
var createMethod = function (IS_INCLUDES) {
|
||||
return function ($this, el, fromIndex) {
|
||||
var O = toIndexedObject($this);
|
||||
var length = toLength(O.length);
|
||||
var index = toAbsoluteIndex(fromIndex, length);
|
||||
var value;
|
||||
// Array#includes uses SameValueZero equality algorithm
|
||||
// eslint-disable-next-line no-self-compare -- NaN check
|
||||
if (IS_INCLUDES && el != el) while (length > index) {
|
||||
value = O[index++];
|
||||
// eslint-disable-next-line no-self-compare -- NaN check
|
||||
if (value != value) return true;
|
||||
// Array#indexOf ignores holes, Array#includes - not
|
||||
} else for (;length > index; index++) {
|
||||
if ((IS_INCLUDES || index in O) && O[index] === el) return IS_INCLUDES || index || 0;
|
||||
} return !IS_INCLUDES && -1;
|
||||
};
|
||||
};
|
||||
|
||||
var arrayIncludes = {
|
||||
// `Array.prototype.includes` method
|
||||
// https://tc39.es/ecma262/#sec-array.prototype.includes
|
||||
includes: createMethod(true),
|
||||
// `Array.prototype.indexOf` method
|
||||
// https://tc39.es/ecma262/#sec-array.prototype.indexof
|
||||
indexOf: createMethod(false)
|
||||
};
|
||||
|
||||
var indexOf = arrayIncludes.indexOf;
|
||||
|
||||
|
||||
var objectKeysInternal = function (object, names) {
|
||||
var O = toIndexedObject(object);
|
||||
var i = 0;
|
||||
var result = [];
|
||||
var key;
|
||||
for (key in O) !has$1(hiddenKeys$1, key) && has$1(O, key) && result.push(key);
|
||||
// Don't enum bug & hidden keys
|
||||
while (names.length > i) if (has$1(O, key = names[i++])) {
|
||||
~indexOf(result, key) || result.push(key);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// IE8- don't enum bug keys
|
||||
var enumBugKeys = [
|
||||
'constructor',
|
||||
'hasOwnProperty',
|
||||
'isPrototypeOf',
|
||||
'propertyIsEnumerable',
|
||||
'toLocaleString',
|
||||
'toString',
|
||||
'valueOf'
|
||||
];
|
||||
|
||||
var hiddenKeys = enumBugKeys.concat('length', 'prototype');
|
||||
|
||||
// `Object.getOwnPropertyNames` method
|
||||
// https://tc39.es/ecma262/#sec-object.getownpropertynames
|
||||
var f$1 = Object.getOwnPropertyNames || function getOwnPropertyNames(O) {
|
||||
return objectKeysInternal(O, hiddenKeys);
|
||||
};
|
||||
|
||||
var objectGetOwnPropertyNames = {
|
||||
f: f$1
|
||||
};
|
||||
|
||||
var f = Object.getOwnPropertySymbols;
|
||||
|
||||
var objectGetOwnPropertySymbols = {
|
||||
f: f
|
||||
};
|
||||
|
||||
// all object keys, includes non-enumerable and symbols
|
||||
var ownKeys = getBuiltIn('Reflect', 'ownKeys') || function ownKeys(it) {
|
||||
var keys = objectGetOwnPropertyNames.f(anObject(it));
|
||||
var getOwnPropertySymbols = objectGetOwnPropertySymbols.f;
|
||||
return getOwnPropertySymbols ? keys.concat(getOwnPropertySymbols(it)) : keys;
|
||||
};
|
||||
|
||||
var copyConstructorProperties = function (target, source) {
|
||||
var keys = ownKeys(source);
|
||||
var defineProperty = objectDefineProperty.f;
|
||||
var getOwnPropertyDescriptor = objectGetOwnPropertyDescriptor.f;
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
if (!has$1(target, key)) defineProperty(target, key, getOwnPropertyDescriptor(source, key));
|
||||
}
|
||||
};
|
||||
|
||||
var replacement = /#|\.prototype\./;
|
||||
|
||||
var isForced = function (feature, detection) {
|
||||
var value = data[normalize(feature)];
|
||||
return value == POLYFILL ? true
|
||||
: value == NATIVE ? false
|
||||
: typeof detection == 'function' ? fails(detection)
|
||||
: !!detection;
|
||||
};
|
||||
|
||||
var normalize = isForced.normalize = function (string) {
|
||||
return String(string).replace(replacement, '.').toLowerCase();
|
||||
};
|
||||
|
||||
var data = isForced.data = {};
|
||||
var NATIVE = isForced.NATIVE = 'N';
|
||||
var POLYFILL = isForced.POLYFILL = 'P';
|
||||
|
||||
var isForced_1 = isForced;
|
||||
|
||||
var getOwnPropertyDescriptor = objectGetOwnPropertyDescriptor.f;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
options.target - name of the target object
|
||||
options.global - target is the global object
|
||||
options.stat - export as static methods of target
|
||||
options.proto - export as prototype methods of target
|
||||
options.real - real prototype method for the `pure` version
|
||||
options.forced - export even if the native feature is available
|
||||
options.bind - bind methods to the target, required for the `pure` version
|
||||
options.wrap - wrap constructors to preventing global pollution, required for the `pure` version
|
||||
options.unsafe - use the simple assignment of property instead of delete + defineProperty
|
||||
options.sham - add a flag to not completely full polyfills
|
||||
options.enumerable - export as enumerable property
|
||||
options.noTargetGet - prevent calling a getter on target
|
||||
*/
|
||||
var _export = function (options, source) {
|
||||
var TARGET = options.target;
|
||||
var GLOBAL = options.global;
|
||||
var STATIC = options.stat;
|
||||
var FORCED, target, key, targetProperty, sourceProperty, descriptor;
|
||||
if (GLOBAL) {
|
||||
target = global_1;
|
||||
} else if (STATIC) {
|
||||
target = global_1[TARGET] || setGlobal(TARGET, {});
|
||||
} else {
|
||||
target = (global_1[TARGET] || {}).prototype;
|
||||
}
|
||||
if (target) for (key in source) {
|
||||
sourceProperty = source[key];
|
||||
if (options.noTargetGet) {
|
||||
descriptor = getOwnPropertyDescriptor(target, key);
|
||||
targetProperty = descriptor && descriptor.value;
|
||||
} else targetProperty = target[key];
|
||||
FORCED = isForced_1(GLOBAL ? key : TARGET + (STATIC ? '.' : '#') + key, options.forced);
|
||||
// contained in target
|
||||
if (!FORCED && targetProperty !== undefined) {
|
||||
if (typeof sourceProperty === typeof targetProperty) continue;
|
||||
copyConstructorProperties(sourceProperty, targetProperty);
|
||||
}
|
||||
// add a flag to not completely full polyfills
|
||||
if (options.sham || (targetProperty && targetProperty.sham)) {
|
||||
createNonEnumerableProperty(sourceProperty, 'sham', true);
|
||||
}
|
||||
// extend global
|
||||
redefine(target, key, sourceProperty, options);
|
||||
}
|
||||
};
|
||||
|
||||
// `IsArray` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-isarray
|
||||
var isArray = Array.isArray || function isArray(arg) {
|
||||
return classofRaw(arg) == 'Array';
|
||||
};
|
||||
|
||||
// `ToObject` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-toobject
|
||||
var toObject = function (argument) {
|
||||
return Object(requireObjectCoercible(argument));
|
||||
};
|
||||
|
||||
var createProperty = function (object, key, value) {
|
||||
var propertyKey = toPrimitive(key);
|
||||
if (propertyKey in object) objectDefineProperty.f(object, propertyKey, createPropertyDescriptor(0, value));
|
||||
else object[propertyKey] = value;
|
||||
};
|
||||
|
||||
var engineIsNode = classofRaw(global_1.process) == 'process';
|
||||
|
||||
var engineUserAgent = getBuiltIn('navigator', 'userAgent') || '';
|
||||
|
||||
var process = global_1.process;
|
||||
var versions = process && process.versions;
|
||||
var v8 = versions && versions.v8;
|
||||
var match, version;
|
||||
|
||||
if (v8) {
|
||||
match = v8.split('.');
|
||||
version = match[0] + match[1];
|
||||
} else if (engineUserAgent) {
|
||||
match = engineUserAgent.match(/Edge\/(\d+)/);
|
||||
if (!match || match[1] >= 74) {
|
||||
match = engineUserAgent.match(/Chrome\/(\d+)/);
|
||||
if (match) version = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
var engineV8Version = version && +version;
|
||||
|
||||
var nativeSymbol = !!Object.getOwnPropertySymbols && !fails(function () {
|
||||
/* global Symbol -- required for testing */
|
||||
return !Symbol.sham &&
|
||||
// Chrome 38 Symbol has incorrect toString conversion
|
||||
// Chrome 38-40 symbols are not inherited from DOM collections prototypes to instances
|
||||
(engineIsNode ? engineV8Version === 38 : engineV8Version > 37 && engineV8Version < 41);
|
||||
});
|
||||
|
||||
var useSymbolAsUid = nativeSymbol
|
||||
/* global Symbol -- safe */
|
||||
&& !Symbol.sham
|
||||
&& typeof Symbol.iterator == 'symbol';
|
||||
|
||||
var WellKnownSymbolsStore = shared('wks');
|
||||
var Symbol$1 = global_1.Symbol;
|
||||
var createWellKnownSymbol = useSymbolAsUid ? Symbol$1 : Symbol$1 && Symbol$1.withoutSetter || uid;
|
||||
|
||||
var wellKnownSymbol = function (name) {
|
||||
if (!has$1(WellKnownSymbolsStore, name) || !(nativeSymbol || typeof WellKnownSymbolsStore[name] == 'string')) {
|
||||
if (nativeSymbol && has$1(Symbol$1, name)) {
|
||||
WellKnownSymbolsStore[name] = Symbol$1[name];
|
||||
} else {
|
||||
WellKnownSymbolsStore[name] = createWellKnownSymbol('Symbol.' + name);
|
||||
}
|
||||
} return WellKnownSymbolsStore[name];
|
||||
};
|
||||
|
||||
var SPECIES$1 = wellKnownSymbol('species');
|
||||
|
||||
// `ArraySpeciesCreate` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-arrayspeciescreate
|
||||
var arraySpeciesCreate = function (originalArray, length) {
|
||||
var C;
|
||||
if (isArray(originalArray)) {
|
||||
C = originalArray.constructor;
|
||||
// cross-realm fallback
|
||||
if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined;
|
||||
else if (isObject(C)) {
|
||||
C = C[SPECIES$1];
|
||||
if (C === null) C = undefined;
|
||||
}
|
||||
} return new (C === undefined ? Array : C)(length === 0 ? 0 : length);
|
||||
};
|
||||
|
||||
var SPECIES = wellKnownSymbol('species');
|
||||
|
||||
var arrayMethodHasSpeciesSupport = function (METHOD_NAME) {
|
||||
// We can't use this feature detection in V8 since it causes
|
||||
// deoptimization and serious performance degradation
|
||||
// https://github.com/zloirock/core-js/issues/677
|
||||
return engineV8Version >= 51 || !fails(function () {
|
||||
var array = [];
|
||||
var constructor = array.constructor = {};
|
||||
constructor[SPECIES] = function () {
|
||||
return { foo: 1 };
|
||||
};
|
||||
return array[METHOD_NAME](Boolean).foo !== 1;
|
||||
});
|
||||
};
|
||||
|
||||
var IS_CONCAT_SPREADABLE = wellKnownSymbol('isConcatSpreadable');
|
||||
var MAX_SAFE_INTEGER = 0x1FFFFFFFFFFFFF;
|
||||
var MAXIMUM_ALLOWED_INDEX_EXCEEDED = 'Maximum allowed index exceeded';
|
||||
|
||||
// We can't use this feature detection in V8 since it causes
|
||||
// deoptimization and serious performance degradation
|
||||
// https://github.com/zloirock/core-js/issues/679
|
||||
var IS_CONCAT_SPREADABLE_SUPPORT = engineV8Version >= 51 || !fails(function () {
|
||||
var array = [];
|
||||
array[IS_CONCAT_SPREADABLE] = false;
|
||||
return array.concat()[0] !== array;
|
||||
});
|
||||
|
||||
var SPECIES_SUPPORT = arrayMethodHasSpeciesSupport('concat');
|
||||
|
||||
var isConcatSpreadable = function (O) {
|
||||
if (!isObject(O)) return false;
|
||||
var spreadable = O[IS_CONCAT_SPREADABLE];
|
||||
return spreadable !== undefined ? !!spreadable : isArray(O);
|
||||
};
|
||||
|
||||
var FORCED = !IS_CONCAT_SPREADABLE_SUPPORT || !SPECIES_SUPPORT;
|
||||
|
||||
// `Array.prototype.concat` method
|
||||
// https://tc39.es/ecma262/#sec-array.prototype.concat
|
||||
// with adding support of @@isConcatSpreadable and @@species
|
||||
_export({ target: 'Array', proto: true, forced: FORCED }, {
|
||||
// eslint-disable-next-line no-unused-vars -- required for `.length`
|
||||
concat: function concat(arg) {
|
||||
var O = toObject(this);
|
||||
var A = arraySpeciesCreate(O, 0);
|
||||
var n = 0;
|
||||
var i, k, length, len, E;
|
||||
for (i = -1, length = arguments.length; i < length; i++) {
|
||||
E = i === -1 ? O : arguments[i];
|
||||
if (isConcatSpreadable(E)) {
|
||||
len = toLength(E.length);
|
||||
if (n + len > MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED);
|
||||
for (k = 0; k < len; k++, n++) if (k in E) createProperty(A, n, E[k]);
|
||||
} else {
|
||||
if (n >= MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED);
|
||||
createProperty(A, n++, E);
|
||||
}
|
||||
}
|
||||
A.length = n;
|
||||
return A;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* When using server-side processing, the default mode of operation for
|
||||
* bootstrap-table is to simply throw away any data that currently exists in the
|
||||
* table and make a request to the server to get the first page of data to
|
||||
* display. This is fine for an empty table, but if you already have the first
|
||||
* page of data displayed in the plain HTML, it is a waste of resources. As
|
||||
* such, you can use data-defer-url instead of data-url to allow you to instruct
|
||||
* bootstrap-table to not make that initial request, rather it will use the data
|
||||
* already on the page.
|
||||
*
|
||||
* @author: Ruben Suarez
|
||||
* @webSite: http://rubensa.eu.org
|
||||
* @update zhixin wen <wenzhixin2010@gmail.com>
|
||||
*/
|
||||
|
||||
$__default['default'].extend($__default['default'].fn.bootstrapTable.defaults, {
|
||||
deferUrl: undefined
|
||||
});
|
||||
|
||||
$__default['default'].BootstrapTable = /*#__PURE__*/function (_$$BootstrapTable) {
|
||||
_inherits(_class, _$$BootstrapTable);
|
||||
|
||||
var _super = _createSuper(_class);
|
||||
|
||||
function _class() {
|
||||
_classCallCheck(this, _class);
|
||||
|
||||
return _super.apply(this, arguments);
|
||||
}
|
||||
|
||||
_createClass(_class, [{
|
||||
key: "init",
|
||||
value: function init() {
|
||||
var _get2;
|
||||
|
||||
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
||||
args[_key] = arguments[_key];
|
||||
}
|
||||
|
||||
(_get2 = _get(_getPrototypeOf(_class.prototype), "init", this)).call.apply(_get2, [this].concat(args));
|
||||
|
||||
if (this.options.deferUrl) {
|
||||
this.options.url = this.options.deferUrl;
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
return _class;
|
||||
}($__default['default'].BootstrapTable);
|
||||
|
||||
})));
|
||||
-10
File diff suppressed because one or more lines are too long
-1904
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
Vendored
-2232
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
-13
@@ -1,13 +0,0 @@
|
||||
@charset "UTF-8";
|
||||
/**
|
||||
* @author: Dennis Hernández
|
||||
* @webSite: http://djhvscf.github.io/Blog
|
||||
* @version: v2.1.1
|
||||
*/
|
||||
.no-filter-control {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.filter-control {
|
||||
margin: 0 2px 2px 2px;
|
||||
}
|
||||
-3140
File diff suppressed because it is too large
Load Diff
-10
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation)
|
||||
*
|
||||
* @version v1.18.3
|
||||
* @homepage https://bootstrap-table.com
|
||||
* @author wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@charset "UTF-8";.no-filter-control{height:34px}.filter-control{margin:0 2px 2px 2px}
|
||||
-10
File diff suppressed because one or more lines are too long
-2434
File diff suppressed because it is too large
Load Diff
Vendored
-10
File diff suppressed because one or more lines are too long
-25
@@ -1,25 +0,0 @@
|
||||
.fixed-columns,
|
||||
.fixed-columns-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.fixed-columns {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.fixed-columns .fixed-table-body {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.fixed-columns-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.fixed-columns-right .fixed-table-body {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
-1610
File diff suppressed because it is too large
Load Diff
-10
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation)
|
||||
*
|
||||
* @version v1.18.3
|
||||
* @homepage https://bootstrap-table.com
|
||||
* @author wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
.fixed-columns,.fixed-columns-right{position:absolute;top:0;height:100%;background-color:#fff;box-sizing:border-box;z-index:1}.fixed-columns{left:0}.fixed-columns .fixed-table-body{overflow:hidden!important}.fixed-columns-right{right:0}.fixed-columns-right .fixed-table-body{overflow-x:hidden!important}
|
||||
-10
File diff suppressed because one or more lines are too long
-8
@@ -1,8 +0,0 @@
|
||||
.bootstrap-table .table > tbody > tr.groupBy.expanded,
|
||||
.bootstrap-table .table > tbody > tr.groupBy.collapsed {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bootstrap-table .table > tbody > tr.hidden {
|
||||
display: none;
|
||||
}
|
||||
-1604
File diff suppressed because it is too large
Load Diff
-10
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation)
|
||||
*
|
||||
* @version v1.18.3
|
||||
* @homepage https://bootstrap-table.com
|
||||
* @author wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
.bootstrap-table .table>tbody>tr.groupBy.collapsed,.bootstrap-table .table>tbody>tr.groupBy.expanded{cursor:pointer}.bootstrap-table .table>tbody>tr.hidden{display:none}
|
||||
-10
File diff suppressed because one or more lines are too long
-159
@@ -1,159 +0,0 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery')) :
|
||||
typeof define === 'function' && define.amd ? define(['jquery'], factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.jQuery));
|
||||
}(this, (function ($) { 'use strict';
|
||||
|
||||
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
||||
|
||||
var $__default = /*#__PURE__*/_interopDefaultLegacy($);
|
||||
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
}
|
||||
|
||||
function _defineProperties(target, props) {
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var descriptor = props[i];
|
||||
descriptor.enumerable = descriptor.enumerable || false;
|
||||
descriptor.configurable = true;
|
||||
if ("value" in descriptor) descriptor.writable = true;
|
||||
Object.defineProperty(target, descriptor.key, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
function _createClass(Constructor, protoProps, staticProps) {
|
||||
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
|
||||
if (staticProps) _defineProperties(Constructor, staticProps);
|
||||
return Constructor;
|
||||
}
|
||||
|
||||
function _inherits(subClass, superClass) {
|
||||
if (typeof superClass !== "function" && superClass !== null) {
|
||||
throw new TypeError("Super expression must either be null or a function");
|
||||
}
|
||||
|
||||
subClass.prototype = Object.create(superClass && superClass.prototype, {
|
||||
constructor: {
|
||||
value: subClass,
|
||||
writable: true,
|
||||
configurable: true
|
||||
}
|
||||
});
|
||||
if (superClass) _setPrototypeOf(subClass, superClass);
|
||||
}
|
||||
|
||||
function _getPrototypeOf(o) {
|
||||
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
|
||||
return o.__proto__ || Object.getPrototypeOf(o);
|
||||
};
|
||||
return _getPrototypeOf(o);
|
||||
}
|
||||
|
||||
function _setPrototypeOf(o, p) {
|
||||
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
|
||||
o.__proto__ = p;
|
||||
return o;
|
||||
};
|
||||
|
||||
return _setPrototypeOf(o, p);
|
||||
}
|
||||
|
||||
function _isNativeReflectConstruct() {
|
||||
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
|
||||
if (Reflect.construct.sham) return false;
|
||||
if (typeof Proxy === "function") return true;
|
||||
|
||||
try {
|
||||
Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function _assertThisInitialized(self) {
|
||||
if (self === void 0) {
|
||||
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
function _possibleConstructorReturn(self, call) {
|
||||
if (call && (typeof call === "object" || typeof call === "function")) {
|
||||
return call;
|
||||
}
|
||||
|
||||
return _assertThisInitialized(self);
|
||||
}
|
||||
|
||||
function _createSuper(Derived) {
|
||||
var hasNativeReflectConstruct = _isNativeReflectConstruct();
|
||||
|
||||
return function _createSuperInternal() {
|
||||
var Super = _getPrototypeOf(Derived),
|
||||
result;
|
||||
|
||||
if (hasNativeReflectConstruct) {
|
||||
var NewTarget = _getPrototypeOf(this).constructor;
|
||||
|
||||
result = Reflect.construct(Super, arguments, NewTarget);
|
||||
} else {
|
||||
result = Super.apply(this, arguments);
|
||||
}
|
||||
|
||||
return _possibleConstructorReturn(this, result);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @author: Jewway
|
||||
* @update zhixin wen <wenzhixin2010@gmail.com>
|
||||
*/
|
||||
|
||||
$__default['default'].fn.bootstrapTable.methods.push('changeTitle');
|
||||
$__default['default'].fn.bootstrapTable.methods.push('changeLocale');
|
||||
|
||||
$__default['default'].BootstrapTable = /*#__PURE__*/function (_$$BootstrapTable) {
|
||||
_inherits(_class, _$$BootstrapTable);
|
||||
|
||||
var _super = _createSuper(_class);
|
||||
|
||||
function _class() {
|
||||
_classCallCheck(this, _class);
|
||||
|
||||
return _super.apply(this, arguments);
|
||||
}
|
||||
|
||||
_createClass(_class, [{
|
||||
key: "changeTitle",
|
||||
value: function changeTitle(locale) {
|
||||
$__default['default'].each(this.options.columns, function (idx, columnList) {
|
||||
$__default['default'].each(columnList, function (idx, column) {
|
||||
if (column.field) {
|
||||
column.title = locale[column.field];
|
||||
}
|
||||
});
|
||||
});
|
||||
this.initHeader();
|
||||
this.initBody();
|
||||
this.initToolbar();
|
||||
}
|
||||
}, {
|
||||
key: "changeLocale",
|
||||
value: function changeLocale(localeId) {
|
||||
this.options.locale = localeId;
|
||||
this.initLocale();
|
||||
this.initPagination();
|
||||
this.initBody();
|
||||
this.initToolbar();
|
||||
}
|
||||
}]);
|
||||
|
||||
return _class;
|
||||
}($__default['default'].BootstrapTable);
|
||||
|
||||
})));
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation)
|
||||
*
|
||||
* @version v1.18.3
|
||||
* @homepage https://bootstrap-table.com
|
||||
* @author wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).jQuery)}(this,(function(t){"use strict";function e(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var n=e(t);function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function r(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}function i(t){return(i=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}function u(t,e){return(u=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function f(t,e){return!e||"object"!=typeof e&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function c(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}}();return function(){var n,o=i(t);if(e){var r=i(this).constructor;n=Reflect.construct(o,arguments,r)}else n=o.apply(this,arguments);return f(this,n)}}n.default.fn.bootstrapTable.methods.push("changeTitle"),n.default.fn.bootstrapTable.methods.push("changeLocale"),n.default.BootstrapTable=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&u(t,e)}(l,t);var e,i,f,a=c(l);function l(){return o(this,l),a.apply(this,arguments)}return e=l,(i=[{key:"changeTitle",value:function(t){n.default.each(this.options.columns,(function(e,o){n.default.each(o,(function(e,n){n.field&&(n.title=t[n.field])}))})),this.initHeader(),this.initBody(),this.initToolbar()}},{key:"changeLocale",value:function(t){this.options.locale=t,this.initLocale(),this.initPagination(),this.initBody(),this.initToolbar()}}])&&r(e.prototype,i),f&&r(e,f),l}(n.default.BootstrapTable)}));
|
||||
-1472
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
Vendored
-1295
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
-1792
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
-11
@@ -1,11 +0,0 @@
|
||||
.bootstrap-table.bootstrap3 .fixed-table-pagination > .pagination ul.pagination,
|
||||
.bootstrap-table.bootstrap3 .fixed-table-pagination > .pagination .page-jump-to {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination .page-jump-to input {
|
||||
width: 70px;
|
||||
margin-left: 5px;
|
||||
text-align: center;
|
||||
float: left;
|
||||
}
|
||||
-1123
File diff suppressed because it is too large
Load Diff
-10
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation)
|
||||
*
|
||||
* @version v1.18.3
|
||||
* @homepage https://bootstrap-table.com
|
||||
* @author wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
.bootstrap-table.bootstrap3 .fixed-table-pagination>.pagination .page-jump-to,.bootstrap-table.bootstrap3 .fixed-table-pagination>.pagination ul.pagination{display:inline}.bootstrap-table .fixed-table-pagination>.pagination .page-jump-to input{width:70px;margin-left:5px;text-align:center;float:left}
|
||||
-10
File diff suppressed because one or more lines are too long
-1192
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
Vendored
-1646
File diff suppressed because it is too large
Load Diff
src/backend/InvenTree/InvenTree/static/bootstrap-table/extensions/print/bootstrap-table-print.min.js
Vendored
-10
File diff suppressed because one or more lines are too long
-1477
File diff suppressed because it is too large
Load Diff
-10
File diff suppressed because one or more lines are too long
-14
@@ -1,14 +0,0 @@
|
||||
.reorder_rows_onDragClass td {
|
||||
background-color: #eee;
|
||||
-webkit-box-shadow: 11px 5px 12px 2px #333, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset;
|
||||
-webkit-box-shadow: 6px 3px 5px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset;
|
||||
-moz-box-shadow: 6px 4px 5px 1px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset;
|
||||
-box-shadow: 6px 4px 5px 1px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset;
|
||||
}
|
||||
|
||||
.reorder_rows_onDragClass td:last-child {
|
||||
-webkit-box-shadow: 8px 7px 12px 0 #333, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset;
|
||||
-webkit-box-shadow: 1px 8px 6px -4px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset;
|
||||
-moz-box-shadow: 0 9px 4px -4px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset, -1px 0 0 #ccc inset;
|
||||
-box-shadow: 0 9px 4px -4px #555, 0 1px 0 #ccc inset, 0 -1px 0 #ccc inset, -1px 0 0 #ccc inset;
|
||||
}
|
||||
-1040
File diff suppressed because it is too large
Load Diff
-10
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation)
|
||||
*
|
||||
* @version v1.18.3
|
||||
* @homepage https://bootstrap-table.com
|
||||
* @author wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
.reorder_rows_onDragClass td{background-color:#eee;-webkit-box-shadow:11px 5px 12px 2px #333,0 1px 0 #ccc inset,0 -1px 0 #ccc inset;-webkit-box-shadow:6px 3px 5px #555,0 1px 0 #ccc inset,0 -1px 0 #ccc inset;-moz-box-shadow:6px 4px 5px 1px #555,0 1px 0 #ccc inset,0 -1px 0 #ccc inset;-box-shadow:6px 4px 5px 1px #555,0 1px 0 #ccc inset,0 -1px 0 #ccc inset}.reorder_rows_onDragClass td:last-child{-webkit-box-shadow:8px 7px 12px 0 #333,0 1px 0 #ccc inset,0 -1px 0 #ccc inset;-webkit-box-shadow:1px 8px 6px -4px #555,0 1px 0 #ccc inset,0 -1px 0 #ccc inset;-moz-box-shadow:0 9px 4px -4px #555,0 1px 0 #ccc inset,0 -1px 0 #ccc inset,-1px 0 0 #ccc inset;-box-shadow:0 9px 4px -4px #555,0 1px 0 #ccc inset,0 -1px 0 #ccc inset,-1px 0 0 #ccc inset}
|
||||
-10
File diff suppressed because one or more lines are too long
-919
@@ -1,919 +0,0 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery')) :
|
||||
typeof define === 'function' && define.amd ? define(['jquery'], factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.jQuery));
|
||||
}(this, (function ($) { 'use strict';
|
||||
|
||||
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
||||
|
||||
var $__default = /*#__PURE__*/_interopDefaultLegacy($);
|
||||
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
}
|
||||
|
||||
function _defineProperties(target, props) {
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var descriptor = props[i];
|
||||
descriptor.enumerable = descriptor.enumerable || false;
|
||||
descriptor.configurable = true;
|
||||
if ("value" in descriptor) descriptor.writable = true;
|
||||
Object.defineProperty(target, descriptor.key, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
function _createClass(Constructor, protoProps, staticProps) {
|
||||
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
|
||||
if (staticProps) _defineProperties(Constructor, staticProps);
|
||||
return Constructor;
|
||||
}
|
||||
|
||||
function _inherits(subClass, superClass) {
|
||||
if (typeof superClass !== "function" && superClass !== null) {
|
||||
throw new TypeError("Super expression must either be null or a function");
|
||||
}
|
||||
|
||||
subClass.prototype = Object.create(superClass && superClass.prototype, {
|
||||
constructor: {
|
||||
value: subClass,
|
||||
writable: true,
|
||||
configurable: true
|
||||
}
|
||||
});
|
||||
if (superClass) _setPrototypeOf(subClass, superClass);
|
||||
}
|
||||
|
||||
function _getPrototypeOf(o) {
|
||||
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
|
||||
return o.__proto__ || Object.getPrototypeOf(o);
|
||||
};
|
||||
return _getPrototypeOf(o);
|
||||
}
|
||||
|
||||
function _setPrototypeOf(o, p) {
|
||||
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
|
||||
o.__proto__ = p;
|
||||
return o;
|
||||
};
|
||||
|
||||
return _setPrototypeOf(o, p);
|
||||
}
|
||||
|
||||
function _isNativeReflectConstruct() {
|
||||
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
|
||||
if (Reflect.construct.sham) return false;
|
||||
if (typeof Proxy === "function") return true;
|
||||
|
||||
try {
|
||||
Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function _assertThisInitialized(self) {
|
||||
if (self === void 0) {
|
||||
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
function _possibleConstructorReturn(self, call) {
|
||||
if (call && (typeof call === "object" || typeof call === "function")) {
|
||||
return call;
|
||||
}
|
||||
|
||||
return _assertThisInitialized(self);
|
||||
}
|
||||
|
||||
function _createSuper(Derived) {
|
||||
var hasNativeReflectConstruct = _isNativeReflectConstruct();
|
||||
|
||||
return function _createSuperInternal() {
|
||||
var Super = _getPrototypeOf(Derived),
|
||||
result;
|
||||
|
||||
if (hasNativeReflectConstruct) {
|
||||
var NewTarget = _getPrototypeOf(this).constructor;
|
||||
|
||||
result = Reflect.construct(Super, arguments, NewTarget);
|
||||
} else {
|
||||
result = Super.apply(this, arguments);
|
||||
}
|
||||
|
||||
return _possibleConstructorReturn(this, result);
|
||||
};
|
||||
}
|
||||
|
||||
function _superPropBase(object, property) {
|
||||
while (!Object.prototype.hasOwnProperty.call(object, property)) {
|
||||
object = _getPrototypeOf(object);
|
||||
if (object === null) break;
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
function _get(target, property, receiver) {
|
||||
if (typeof Reflect !== "undefined" && Reflect.get) {
|
||||
_get = Reflect.get;
|
||||
} else {
|
||||
_get = function _get(target, property, receiver) {
|
||||
var base = _superPropBase(target, property);
|
||||
|
||||
if (!base) return;
|
||||
var desc = Object.getOwnPropertyDescriptor(base, property);
|
||||
|
||||
if (desc.get) {
|
||||
return desc.get.call(receiver);
|
||||
}
|
||||
|
||||
return desc.value;
|
||||
};
|
||||
}
|
||||
|
||||
return _get(target, property, receiver || target);
|
||||
}
|
||||
|
||||
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
||||
|
||||
function createCommonjsModule(fn, module) {
|
||||
return module = { exports: {} }, fn(module, module.exports), module.exports;
|
||||
}
|
||||
|
||||
var check = function (it) {
|
||||
return it && it.Math == Math && it;
|
||||
};
|
||||
|
||||
// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028
|
||||
var global_1 =
|
||||
/* global globalThis -- safe */
|
||||
check(typeof globalThis == 'object' && globalThis) ||
|
||||
check(typeof window == 'object' && window) ||
|
||||
check(typeof self == 'object' && self) ||
|
||||
check(typeof commonjsGlobal == 'object' && commonjsGlobal) ||
|
||||
// eslint-disable-next-line no-new-func -- fallback
|
||||
(function () { return this; })() || Function('return this')();
|
||||
|
||||
var fails = function (exec) {
|
||||
try {
|
||||
return !!exec();
|
||||
} catch (error) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Detect IE8's incomplete defineProperty implementation
|
||||
var descriptors = !fails(function () {
|
||||
return Object.defineProperty({}, 1, { get: function () { return 7; } })[1] != 7;
|
||||
});
|
||||
|
||||
var nativePropertyIsEnumerable = {}.propertyIsEnumerable;
|
||||
var getOwnPropertyDescriptor$1 = Object.getOwnPropertyDescriptor;
|
||||
|
||||
// Nashorn ~ JDK8 bug
|
||||
var NASHORN_BUG = getOwnPropertyDescriptor$1 && !nativePropertyIsEnumerable.call({ 1: 2 }, 1);
|
||||
|
||||
// `Object.prototype.propertyIsEnumerable` method implementation
|
||||
// https://tc39.es/ecma262/#sec-object.prototype.propertyisenumerable
|
||||
var f$4 = NASHORN_BUG ? function propertyIsEnumerable(V) {
|
||||
var descriptor = getOwnPropertyDescriptor$1(this, V);
|
||||
return !!descriptor && descriptor.enumerable;
|
||||
} : nativePropertyIsEnumerable;
|
||||
|
||||
var objectPropertyIsEnumerable = {
|
||||
f: f$4
|
||||
};
|
||||
|
||||
var createPropertyDescriptor = function (bitmap, value) {
|
||||
return {
|
||||
enumerable: !(bitmap & 1),
|
||||
configurable: !(bitmap & 2),
|
||||
writable: !(bitmap & 4),
|
||||
value: value
|
||||
};
|
||||
};
|
||||
|
||||
var toString = {}.toString;
|
||||
|
||||
var classofRaw = function (it) {
|
||||
return toString.call(it).slice(8, -1);
|
||||
};
|
||||
|
||||
var split = ''.split;
|
||||
|
||||
// fallback for non-array-like ES3 and non-enumerable old V8 strings
|
||||
var indexedObject = fails(function () {
|
||||
// throws an error in rhino, see https://github.com/mozilla/rhino/issues/346
|
||||
// eslint-disable-next-line no-prototype-builtins -- safe
|
||||
return !Object('z').propertyIsEnumerable(0);
|
||||
}) ? function (it) {
|
||||
return classofRaw(it) == 'String' ? split.call(it, '') : Object(it);
|
||||
} : Object;
|
||||
|
||||
// `RequireObjectCoercible` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-requireobjectcoercible
|
||||
var requireObjectCoercible = function (it) {
|
||||
if (it == undefined) throw TypeError("Can't call method on " + it);
|
||||
return it;
|
||||
};
|
||||
|
||||
// toObject with fallback for non-array-like ES3 strings
|
||||
|
||||
|
||||
|
||||
var toIndexedObject = function (it) {
|
||||
return indexedObject(requireObjectCoercible(it));
|
||||
};
|
||||
|
||||
var isObject = function (it) {
|
||||
return typeof it === 'object' ? it !== null : typeof it === 'function';
|
||||
};
|
||||
|
||||
// `ToPrimitive` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-toprimitive
|
||||
// instead of the ES6 spec version, we didn't implement @@toPrimitive case
|
||||
// and the second argument - flag - preferred type is a string
|
||||
var toPrimitive = function (input, PREFERRED_STRING) {
|
||||
if (!isObject(input)) return input;
|
||||
var fn, val;
|
||||
if (PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val;
|
||||
if (typeof (fn = input.valueOf) == 'function' && !isObject(val = fn.call(input))) return val;
|
||||
if (!PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val;
|
||||
throw TypeError("Can't convert object to primitive value");
|
||||
};
|
||||
|
||||
var hasOwnProperty = {}.hasOwnProperty;
|
||||
|
||||
var has$1 = function (it, key) {
|
||||
return hasOwnProperty.call(it, key);
|
||||
};
|
||||
|
||||
var document = global_1.document;
|
||||
// typeof document.createElement is 'object' in old IE
|
||||
var EXISTS = isObject(document) && isObject(document.createElement);
|
||||
|
||||
var documentCreateElement = function (it) {
|
||||
return EXISTS ? document.createElement(it) : {};
|
||||
};
|
||||
|
||||
// Thank's IE8 for his funny defineProperty
|
||||
var ie8DomDefine = !descriptors && !fails(function () {
|
||||
return Object.defineProperty(documentCreateElement('div'), 'a', {
|
||||
get: function () { return 7; }
|
||||
}).a != 7;
|
||||
});
|
||||
|
||||
var nativeGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
||||
|
||||
// `Object.getOwnPropertyDescriptor` method
|
||||
// https://tc39.es/ecma262/#sec-object.getownpropertydescriptor
|
||||
var f$3 = descriptors ? nativeGetOwnPropertyDescriptor : function getOwnPropertyDescriptor(O, P) {
|
||||
O = toIndexedObject(O);
|
||||
P = toPrimitive(P, true);
|
||||
if (ie8DomDefine) try {
|
||||
return nativeGetOwnPropertyDescriptor(O, P);
|
||||
} catch (error) { /* empty */ }
|
||||
if (has$1(O, P)) return createPropertyDescriptor(!objectPropertyIsEnumerable.f.call(O, P), O[P]);
|
||||
};
|
||||
|
||||
var objectGetOwnPropertyDescriptor = {
|
||||
f: f$3
|
||||
};
|
||||
|
||||
var anObject = function (it) {
|
||||
if (!isObject(it)) {
|
||||
throw TypeError(String(it) + ' is not an object');
|
||||
} return it;
|
||||
};
|
||||
|
||||
var nativeDefineProperty = Object.defineProperty;
|
||||
|
||||
// `Object.defineProperty` method
|
||||
// https://tc39.es/ecma262/#sec-object.defineproperty
|
||||
var f$2 = descriptors ? nativeDefineProperty : function defineProperty(O, P, Attributes) {
|
||||
anObject(O);
|
||||
P = toPrimitive(P, true);
|
||||
anObject(Attributes);
|
||||
if (ie8DomDefine) try {
|
||||
return nativeDefineProperty(O, P, Attributes);
|
||||
} catch (error) { /* empty */ }
|
||||
if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported');
|
||||
if ('value' in Attributes) O[P] = Attributes.value;
|
||||
return O;
|
||||
};
|
||||
|
||||
var objectDefineProperty = {
|
||||
f: f$2
|
||||
};
|
||||
|
||||
var createNonEnumerableProperty = descriptors ? function (object, key, value) {
|
||||
return objectDefineProperty.f(object, key, createPropertyDescriptor(1, value));
|
||||
} : function (object, key, value) {
|
||||
object[key] = value;
|
||||
return object;
|
||||
};
|
||||
|
||||
var setGlobal = function (key, value) {
|
||||
try {
|
||||
createNonEnumerableProperty(global_1, key, value);
|
||||
} catch (error) {
|
||||
global_1[key] = value;
|
||||
} return value;
|
||||
};
|
||||
|
||||
var SHARED = '__core-js_shared__';
|
||||
var store$1 = global_1[SHARED] || setGlobal(SHARED, {});
|
||||
|
||||
var sharedStore = store$1;
|
||||
|
||||
var functionToString = Function.toString;
|
||||
|
||||
// this helper broken in `3.4.1-3.4.4`, so we can't use `shared` helper
|
||||
if (typeof sharedStore.inspectSource != 'function') {
|
||||
sharedStore.inspectSource = function (it) {
|
||||
return functionToString.call(it);
|
||||
};
|
||||
}
|
||||
|
||||
var inspectSource = sharedStore.inspectSource;
|
||||
|
||||
var WeakMap$1 = global_1.WeakMap;
|
||||
|
||||
var nativeWeakMap = typeof WeakMap$1 === 'function' && /native code/.test(inspectSource(WeakMap$1));
|
||||
|
||||
var shared = createCommonjsModule(function (module) {
|
||||
(module.exports = function (key, value) {
|
||||
return sharedStore[key] || (sharedStore[key] = value !== undefined ? value : {});
|
||||
})('versions', []).push({
|
||||
version: '3.9.1',
|
||||
mode: 'global',
|
||||
copyright: '© 2021 Denis Pushkarev (zloirock.ru)'
|
||||
});
|
||||
});
|
||||
|
||||
var id = 0;
|
||||
var postfix = Math.random();
|
||||
|
||||
var uid = function (key) {
|
||||
return 'Symbol(' + String(key === undefined ? '' : key) + ')_' + (++id + postfix).toString(36);
|
||||
};
|
||||
|
||||
var keys = shared('keys');
|
||||
|
||||
var sharedKey = function (key) {
|
||||
return keys[key] || (keys[key] = uid(key));
|
||||
};
|
||||
|
||||
var hiddenKeys$1 = {};
|
||||
|
||||
var WeakMap = global_1.WeakMap;
|
||||
var set, get, has;
|
||||
|
||||
var enforce = function (it) {
|
||||
return has(it) ? get(it) : set(it, {});
|
||||
};
|
||||
|
||||
var getterFor = function (TYPE) {
|
||||
return function (it) {
|
||||
var state;
|
||||
if (!isObject(it) || (state = get(it)).type !== TYPE) {
|
||||
throw TypeError('Incompatible receiver, ' + TYPE + ' required');
|
||||
} return state;
|
||||
};
|
||||
};
|
||||
|
||||
if (nativeWeakMap) {
|
||||
var store = sharedStore.state || (sharedStore.state = new WeakMap());
|
||||
var wmget = store.get;
|
||||
var wmhas = store.has;
|
||||
var wmset = store.set;
|
||||
set = function (it, metadata) {
|
||||
metadata.facade = it;
|
||||
wmset.call(store, it, metadata);
|
||||
return metadata;
|
||||
};
|
||||
get = function (it) {
|
||||
return wmget.call(store, it) || {};
|
||||
};
|
||||
has = function (it) {
|
||||
return wmhas.call(store, it);
|
||||
};
|
||||
} else {
|
||||
var STATE = sharedKey('state');
|
||||
hiddenKeys$1[STATE] = true;
|
||||
set = function (it, metadata) {
|
||||
metadata.facade = it;
|
||||
createNonEnumerableProperty(it, STATE, metadata);
|
||||
return metadata;
|
||||
};
|
||||
get = function (it) {
|
||||
return has$1(it, STATE) ? it[STATE] : {};
|
||||
};
|
||||
has = function (it) {
|
||||
return has$1(it, STATE);
|
||||
};
|
||||
}
|
||||
|
||||
var internalState = {
|
||||
set: set,
|
||||
get: get,
|
||||
has: has,
|
||||
enforce: enforce,
|
||||
getterFor: getterFor
|
||||
};
|
||||
|
||||
var redefine = createCommonjsModule(function (module) {
|
||||
var getInternalState = internalState.get;
|
||||
var enforceInternalState = internalState.enforce;
|
||||
var TEMPLATE = String(String).split('String');
|
||||
|
||||
(module.exports = function (O, key, value, options) {
|
||||
var unsafe = options ? !!options.unsafe : false;
|
||||
var simple = options ? !!options.enumerable : false;
|
||||
var noTargetGet = options ? !!options.noTargetGet : false;
|
||||
var state;
|
||||
if (typeof value == 'function') {
|
||||
if (typeof key == 'string' && !has$1(value, 'name')) {
|
||||
createNonEnumerableProperty(value, 'name', key);
|
||||
}
|
||||
state = enforceInternalState(value);
|
||||
if (!state.source) {
|
||||
state.source = TEMPLATE.join(typeof key == 'string' ? key : '');
|
||||
}
|
||||
}
|
||||
if (O === global_1) {
|
||||
if (simple) O[key] = value;
|
||||
else setGlobal(key, value);
|
||||
return;
|
||||
} else if (!unsafe) {
|
||||
delete O[key];
|
||||
} else if (!noTargetGet && O[key]) {
|
||||
simple = true;
|
||||
}
|
||||
if (simple) O[key] = value;
|
||||
else createNonEnumerableProperty(O, key, value);
|
||||
// add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative
|
||||
})(Function.prototype, 'toString', function toString() {
|
||||
return typeof this == 'function' && getInternalState(this).source || inspectSource(this);
|
||||
});
|
||||
});
|
||||
|
||||
var path = global_1;
|
||||
|
||||
var aFunction = function (variable) {
|
||||
return typeof variable == 'function' ? variable : undefined;
|
||||
};
|
||||
|
||||
var getBuiltIn = function (namespace, method) {
|
||||
return arguments.length < 2 ? aFunction(path[namespace]) || aFunction(global_1[namespace])
|
||||
: path[namespace] && path[namespace][method] || global_1[namespace] && global_1[namespace][method];
|
||||
};
|
||||
|
||||
var ceil = Math.ceil;
|
||||
var floor = Math.floor;
|
||||
|
||||
// `ToInteger` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-tointeger
|
||||
var toInteger = function (argument) {
|
||||
return isNaN(argument = +argument) ? 0 : (argument > 0 ? floor : ceil)(argument);
|
||||
};
|
||||
|
||||
var min$1 = Math.min;
|
||||
|
||||
// `ToLength` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-tolength
|
||||
var toLength = function (argument) {
|
||||
return argument > 0 ? min$1(toInteger(argument), 0x1FFFFFFFFFFFFF) : 0; // 2 ** 53 - 1 == 9007199254740991
|
||||
};
|
||||
|
||||
var max = Math.max;
|
||||
var min = Math.min;
|
||||
|
||||
// Helper for a popular repeating case of the spec:
|
||||
// Let integer be ? ToInteger(index).
|
||||
// If integer < 0, let result be max((length + integer), 0); else let result be min(integer, length).
|
||||
var toAbsoluteIndex = function (index, length) {
|
||||
var integer = toInteger(index);
|
||||
return integer < 0 ? max(integer + length, 0) : min(integer, length);
|
||||
};
|
||||
|
||||
// `Array.prototype.{ indexOf, includes }` methods implementation
|
||||
var createMethod = function (IS_INCLUDES) {
|
||||
return function ($this, el, fromIndex) {
|
||||
var O = toIndexedObject($this);
|
||||
var length = toLength(O.length);
|
||||
var index = toAbsoluteIndex(fromIndex, length);
|
||||
var value;
|
||||
// Array#includes uses SameValueZero equality algorithm
|
||||
// eslint-disable-next-line no-self-compare -- NaN check
|
||||
if (IS_INCLUDES && el != el) while (length > index) {
|
||||
value = O[index++];
|
||||
// eslint-disable-next-line no-self-compare -- NaN check
|
||||
if (value != value) return true;
|
||||
// Array#indexOf ignores holes, Array#includes - not
|
||||
} else for (;length > index; index++) {
|
||||
if ((IS_INCLUDES || index in O) && O[index] === el) return IS_INCLUDES || index || 0;
|
||||
} return !IS_INCLUDES && -1;
|
||||
};
|
||||
};
|
||||
|
||||
var arrayIncludes = {
|
||||
// `Array.prototype.includes` method
|
||||
// https://tc39.es/ecma262/#sec-array.prototype.includes
|
||||
includes: createMethod(true),
|
||||
// `Array.prototype.indexOf` method
|
||||
// https://tc39.es/ecma262/#sec-array.prototype.indexof
|
||||
indexOf: createMethod(false)
|
||||
};
|
||||
|
||||
var indexOf = arrayIncludes.indexOf;
|
||||
|
||||
|
||||
var objectKeysInternal = function (object, names) {
|
||||
var O = toIndexedObject(object);
|
||||
var i = 0;
|
||||
var result = [];
|
||||
var key;
|
||||
for (key in O) !has$1(hiddenKeys$1, key) && has$1(O, key) && result.push(key);
|
||||
// Don't enum bug & hidden keys
|
||||
while (names.length > i) if (has$1(O, key = names[i++])) {
|
||||
~indexOf(result, key) || result.push(key);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// IE8- don't enum bug keys
|
||||
var enumBugKeys = [
|
||||
'constructor',
|
||||
'hasOwnProperty',
|
||||
'isPrototypeOf',
|
||||
'propertyIsEnumerable',
|
||||
'toLocaleString',
|
||||
'toString',
|
||||
'valueOf'
|
||||
];
|
||||
|
||||
var hiddenKeys = enumBugKeys.concat('length', 'prototype');
|
||||
|
||||
// `Object.getOwnPropertyNames` method
|
||||
// https://tc39.es/ecma262/#sec-object.getownpropertynames
|
||||
var f$1 = Object.getOwnPropertyNames || function getOwnPropertyNames(O) {
|
||||
return objectKeysInternal(O, hiddenKeys);
|
||||
};
|
||||
|
||||
var objectGetOwnPropertyNames = {
|
||||
f: f$1
|
||||
};
|
||||
|
||||
var f = Object.getOwnPropertySymbols;
|
||||
|
||||
var objectGetOwnPropertySymbols = {
|
||||
f: f
|
||||
};
|
||||
|
||||
// all object keys, includes non-enumerable and symbols
|
||||
var ownKeys = getBuiltIn('Reflect', 'ownKeys') || function ownKeys(it) {
|
||||
var keys = objectGetOwnPropertyNames.f(anObject(it));
|
||||
var getOwnPropertySymbols = objectGetOwnPropertySymbols.f;
|
||||
return getOwnPropertySymbols ? keys.concat(getOwnPropertySymbols(it)) : keys;
|
||||
};
|
||||
|
||||
var copyConstructorProperties = function (target, source) {
|
||||
var keys = ownKeys(source);
|
||||
var defineProperty = objectDefineProperty.f;
|
||||
var getOwnPropertyDescriptor = objectGetOwnPropertyDescriptor.f;
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
if (!has$1(target, key)) defineProperty(target, key, getOwnPropertyDescriptor(source, key));
|
||||
}
|
||||
};
|
||||
|
||||
var replacement = /#|\.prototype\./;
|
||||
|
||||
var isForced = function (feature, detection) {
|
||||
var value = data[normalize(feature)];
|
||||
return value == POLYFILL ? true
|
||||
: value == NATIVE ? false
|
||||
: typeof detection == 'function' ? fails(detection)
|
||||
: !!detection;
|
||||
};
|
||||
|
||||
var normalize = isForced.normalize = function (string) {
|
||||
return String(string).replace(replacement, '.').toLowerCase();
|
||||
};
|
||||
|
||||
var data = isForced.data = {};
|
||||
var NATIVE = isForced.NATIVE = 'N';
|
||||
var POLYFILL = isForced.POLYFILL = 'P';
|
||||
|
||||
var isForced_1 = isForced;
|
||||
|
||||
var getOwnPropertyDescriptor = objectGetOwnPropertyDescriptor.f;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
options.target - name of the target object
|
||||
options.global - target is the global object
|
||||
options.stat - export as static methods of target
|
||||
options.proto - export as prototype methods of target
|
||||
options.real - real prototype method for the `pure` version
|
||||
options.forced - export even if the native feature is available
|
||||
options.bind - bind methods to the target, required for the `pure` version
|
||||
options.wrap - wrap constructors to preventing global pollution, required for the `pure` version
|
||||
options.unsafe - use the simple assignment of property instead of delete + defineProperty
|
||||
options.sham - add a flag to not completely full polyfills
|
||||
options.enumerable - export as enumerable property
|
||||
options.noTargetGet - prevent calling a getter on target
|
||||
*/
|
||||
var _export = function (options, source) {
|
||||
var TARGET = options.target;
|
||||
var GLOBAL = options.global;
|
||||
var STATIC = options.stat;
|
||||
var FORCED, target, key, targetProperty, sourceProperty, descriptor;
|
||||
if (GLOBAL) {
|
||||
target = global_1;
|
||||
} else if (STATIC) {
|
||||
target = global_1[TARGET] || setGlobal(TARGET, {});
|
||||
} else {
|
||||
target = (global_1[TARGET] || {}).prototype;
|
||||
}
|
||||
if (target) for (key in source) {
|
||||
sourceProperty = source[key];
|
||||
if (options.noTargetGet) {
|
||||
descriptor = getOwnPropertyDescriptor(target, key);
|
||||
targetProperty = descriptor && descriptor.value;
|
||||
} else targetProperty = target[key];
|
||||
FORCED = isForced_1(GLOBAL ? key : TARGET + (STATIC ? '.' : '#') + key, options.forced);
|
||||
// contained in target
|
||||
if (!FORCED && targetProperty !== undefined) {
|
||||
if (typeof sourceProperty === typeof targetProperty) continue;
|
||||
copyConstructorProperties(sourceProperty, targetProperty);
|
||||
}
|
||||
// add a flag to not completely full polyfills
|
||||
if (options.sham || (targetProperty && targetProperty.sham)) {
|
||||
createNonEnumerableProperty(sourceProperty, 'sham', true);
|
||||
}
|
||||
// extend global
|
||||
redefine(target, key, sourceProperty, options);
|
||||
}
|
||||
};
|
||||
|
||||
// `IsArray` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-isarray
|
||||
var isArray = Array.isArray || function isArray(arg) {
|
||||
return classofRaw(arg) == 'Array';
|
||||
};
|
||||
|
||||
// `ToObject` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-toobject
|
||||
var toObject = function (argument) {
|
||||
return Object(requireObjectCoercible(argument));
|
||||
};
|
||||
|
||||
var createProperty = function (object, key, value) {
|
||||
var propertyKey = toPrimitive(key);
|
||||
if (propertyKey in object) objectDefineProperty.f(object, propertyKey, createPropertyDescriptor(0, value));
|
||||
else object[propertyKey] = value;
|
||||
};
|
||||
|
||||
var engineIsNode = classofRaw(global_1.process) == 'process';
|
||||
|
||||
var engineUserAgent = getBuiltIn('navigator', 'userAgent') || '';
|
||||
|
||||
var process = global_1.process;
|
||||
var versions = process && process.versions;
|
||||
var v8 = versions && versions.v8;
|
||||
var match, version;
|
||||
|
||||
if (v8) {
|
||||
match = v8.split('.');
|
||||
version = match[0] + match[1];
|
||||
} else if (engineUserAgent) {
|
||||
match = engineUserAgent.match(/Edge\/(\d+)/);
|
||||
if (!match || match[1] >= 74) {
|
||||
match = engineUserAgent.match(/Chrome\/(\d+)/);
|
||||
if (match) version = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
var engineV8Version = version && +version;
|
||||
|
||||
var nativeSymbol = !!Object.getOwnPropertySymbols && !fails(function () {
|
||||
/* global Symbol -- required for testing */
|
||||
return !Symbol.sham &&
|
||||
// Chrome 38 Symbol has incorrect toString conversion
|
||||
// Chrome 38-40 symbols are not inherited from DOM collections prototypes to instances
|
||||
(engineIsNode ? engineV8Version === 38 : engineV8Version > 37 && engineV8Version < 41);
|
||||
});
|
||||
|
||||
var useSymbolAsUid = nativeSymbol
|
||||
/* global Symbol -- safe */
|
||||
&& !Symbol.sham
|
||||
&& typeof Symbol.iterator == 'symbol';
|
||||
|
||||
var WellKnownSymbolsStore = shared('wks');
|
||||
var Symbol$1 = global_1.Symbol;
|
||||
var createWellKnownSymbol = useSymbolAsUid ? Symbol$1 : Symbol$1 && Symbol$1.withoutSetter || uid;
|
||||
|
||||
var wellKnownSymbol = function (name) {
|
||||
if (!has$1(WellKnownSymbolsStore, name) || !(nativeSymbol || typeof WellKnownSymbolsStore[name] == 'string')) {
|
||||
if (nativeSymbol && has$1(Symbol$1, name)) {
|
||||
WellKnownSymbolsStore[name] = Symbol$1[name];
|
||||
} else {
|
||||
WellKnownSymbolsStore[name] = createWellKnownSymbol('Symbol.' + name);
|
||||
}
|
||||
} return WellKnownSymbolsStore[name];
|
||||
};
|
||||
|
||||
var SPECIES$1 = wellKnownSymbol('species');
|
||||
|
||||
// `ArraySpeciesCreate` abstract operation
|
||||
// https://tc39.es/ecma262/#sec-arrayspeciescreate
|
||||
var arraySpeciesCreate = function (originalArray, length) {
|
||||
var C;
|
||||
if (isArray(originalArray)) {
|
||||
C = originalArray.constructor;
|
||||
// cross-realm fallback
|
||||
if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined;
|
||||
else if (isObject(C)) {
|
||||
C = C[SPECIES$1];
|
||||
if (C === null) C = undefined;
|
||||
}
|
||||
} return new (C === undefined ? Array : C)(length === 0 ? 0 : length);
|
||||
};
|
||||
|
||||
var SPECIES = wellKnownSymbol('species');
|
||||
|
||||
var arrayMethodHasSpeciesSupport = function (METHOD_NAME) {
|
||||
// We can't use this feature detection in V8 since it causes
|
||||
// deoptimization and serious performance degradation
|
||||
// https://github.com/zloirock/core-js/issues/677
|
||||
return engineV8Version >= 51 || !fails(function () {
|
||||
var array = [];
|
||||
var constructor = array.constructor = {};
|
||||
constructor[SPECIES] = function () {
|
||||
return { foo: 1 };
|
||||
};
|
||||
return array[METHOD_NAME](Boolean).foo !== 1;
|
||||
});
|
||||
};
|
||||
|
||||
var IS_CONCAT_SPREADABLE = wellKnownSymbol('isConcatSpreadable');
|
||||
var MAX_SAFE_INTEGER = 0x1FFFFFFFFFFFFF;
|
||||
var MAXIMUM_ALLOWED_INDEX_EXCEEDED = 'Maximum allowed index exceeded';
|
||||
|
||||
// We can't use this feature detection in V8 since it causes
|
||||
// deoptimization and serious performance degradation
|
||||
// https://github.com/zloirock/core-js/issues/679
|
||||
var IS_CONCAT_SPREADABLE_SUPPORT = engineV8Version >= 51 || !fails(function () {
|
||||
var array = [];
|
||||
array[IS_CONCAT_SPREADABLE] = false;
|
||||
return array.concat()[0] !== array;
|
||||
});
|
||||
|
||||
var SPECIES_SUPPORT = arrayMethodHasSpeciesSupport('concat');
|
||||
|
||||
var isConcatSpreadable = function (O) {
|
||||
if (!isObject(O)) return false;
|
||||
var spreadable = O[IS_CONCAT_SPREADABLE];
|
||||
return spreadable !== undefined ? !!spreadable : isArray(O);
|
||||
};
|
||||
|
||||
var FORCED = !IS_CONCAT_SPREADABLE_SUPPORT || !SPECIES_SUPPORT;
|
||||
|
||||
// `Array.prototype.concat` method
|
||||
// https://tc39.es/ecma262/#sec-array.prototype.concat
|
||||
// with adding support of @@isConcatSpreadable and @@species
|
||||
_export({ target: 'Array', proto: true, forced: FORCED }, {
|
||||
// eslint-disable-next-line no-unused-vars -- required for `.length`
|
||||
concat: function concat(arg) {
|
||||
var O = toObject(this);
|
||||
var A = arraySpeciesCreate(O, 0);
|
||||
var n = 0;
|
||||
var i, k, length, len, E;
|
||||
for (i = -1, length = arguments.length; i < length; i++) {
|
||||
E = i === -1 ? O : arguments[i];
|
||||
if (isConcatSpreadable(E)) {
|
||||
len = toLength(E.length);
|
||||
if (n + len > MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED);
|
||||
for (k = 0; k < len; k++, n++) if (k in E) createProperty(A, n, E[k]);
|
||||
} else {
|
||||
if (n >= MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED);
|
||||
createProperty(A, n++, E);
|
||||
}
|
||||
}
|
||||
A.length = n;
|
||||
return A;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @author: Dennis Hernández
|
||||
* @webSite: http://djhvscf.github.io/Blog
|
||||
* @version: v2.0.0
|
||||
*/
|
||||
|
||||
var isInit = function isInit(that) {
|
||||
return that.$el.data('resizableColumns') !== undefined;
|
||||
};
|
||||
|
||||
var initResizable = function initResizable(that) {
|
||||
if (that.options.resizable && !that.options.cardView && !isInit(that) && that.$el.is(':visible')) {
|
||||
that.$el.resizableColumns({
|
||||
store: window.store
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var destroy = function destroy(that) {
|
||||
if (isInit(that)) {
|
||||
that.$el.data('resizableColumns').destroy();
|
||||
}
|
||||
};
|
||||
|
||||
var reInitResizable = function reInitResizable(that) {
|
||||
destroy(that);
|
||||
initResizable(that);
|
||||
};
|
||||
|
||||
$__default['default'].extend($__default['default'].fn.bootstrapTable.defaults, {
|
||||
resizable: false
|
||||
});
|
||||
|
||||
$__default['default'].BootstrapTable = /*#__PURE__*/function (_$$BootstrapTable) {
|
||||
_inherits(_class, _$$BootstrapTable);
|
||||
|
||||
var _super = _createSuper(_class);
|
||||
|
||||
function _class() {
|
||||
_classCallCheck(this, _class);
|
||||
|
||||
return _super.apply(this, arguments);
|
||||
}
|
||||
|
||||
_createClass(_class, [{
|
||||
key: "initBody",
|
||||
value: function initBody() {
|
||||
var _get2,
|
||||
_this = this;
|
||||
|
||||
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
||||
args[_key] = arguments[_key];
|
||||
}
|
||||
|
||||
(_get2 = _get(_getPrototypeOf(_class.prototype), "initBody", this)).call.apply(_get2, [this].concat(args));
|
||||
|
||||
this.$el.off('column-switch.bs.table page-change.bs.table').on('column-switch.bs.table page-change.bs.table', function () {
|
||||
reInitResizable(_this);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: "toggleView",
|
||||
value: function toggleView() {
|
||||
var _get3;
|
||||
|
||||
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
|
||||
args[_key2] = arguments[_key2];
|
||||
}
|
||||
|
||||
(_get3 = _get(_getPrototypeOf(_class.prototype), "toggleView", this)).call.apply(_get3, [this].concat(args));
|
||||
|
||||
if (this.options.resizable && this.options.cardView) {
|
||||
// Destroy the plugin
|
||||
destroy(this);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "resetView",
|
||||
value: function resetView() {
|
||||
var _get4,
|
||||
_this2 = this;
|
||||
|
||||
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
|
||||
args[_key3] = arguments[_key3];
|
||||
}
|
||||
|
||||
(_get4 = _get(_getPrototypeOf(_class.prototype), "resetView", this)).call.apply(_get4, [this].concat(args));
|
||||
|
||||
if (this.options.resizable) {
|
||||
// because in fitHeader function, we use setTimeout(func, 100);
|
||||
setTimeout(function () {
|
||||
initResizable(_this2);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
return _class;
|
||||
}($__default['default'].BootstrapTable);
|
||||
|
||||
})));
|
||||
-10
File diff suppressed because one or more lines are too long
-21
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* @author vincent loh <vincent.ml@gmail.com>
|
||||
* @update zhixin wen <wenzhixin2010@gmail.com>
|
||||
*/
|
||||
.fix-sticky {
|
||||
position: fixed !important;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.fix-sticky table thead {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.fix-sticky table thead.thead-light {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.fix-sticky table thead.thead-dark {
|
||||
background: #212529;
|
||||
}
|
||||
-1251
File diff suppressed because it is too large
Load Diff
-10
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation)
|
||||
*
|
||||
* @version v1.18.3
|
||||
* @homepage https://bootstrap-table.com
|
||||
* @author wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
.fix-sticky{position:fixed!important;overflow:hidden;z-index:100}.fix-sticky table thead{background:#fff}.fix-sticky table thead.thead-light{background:#e9ecef}.fix-sticky table thead.thead-dark{background:#212529}
|
||||
-10
File diff suppressed because one or more lines are too long
src/backend/InvenTree/InvenTree/static/bootstrap-table/extensions/toolbar/bootstrap-table-toolbar.js
Vendored
-2037
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user