2
0
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:
Matthias Mair
2024-12-22 23:42:44 +01:00
3201 changed files with 573426 additions and 709553 deletions
+7 -5
View File
@@ -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)
+86 -50
View File
@@ -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."""
+294 -2
View File
@@ -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)
+16 -4
View File
@@ -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]
@@ -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.')
+1 -1
View File
@@ -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}
+8 -11
View File
@@ -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
+1 -4
View File
@@ -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
+9 -6
View File
@@ -4,6 +4,7 @@ import sys
from decimal import Decimal
from django import forms
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
@@ -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
-8
View File
@@ -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
+1 -4
View File
@@ -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,
+13 -11
View File
@@ -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:
+140 -90
View File
@@ -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
@@ -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}')
+116 -28
View File
@@ -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
+22 -35
View File
@@ -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
):
+20 -9
View File
@@ -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."""
+144 -53
View File
@@ -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,
},
)
+15 -2
View File
@@ -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
)
+17 -30
View File
@@ -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,
+72 -21
View File
@@ -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
+201 -107
View File
@@ -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
+70 -2
View File
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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);
})));
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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;
}
@@ -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}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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;
}
@@ -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}
File diff suppressed because one or more lines are too long
@@ -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;
}
@@ -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}
File diff suppressed because one or more lines are too long
@@ -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);
})));
@@ -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)}));
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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;
}
@@ -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}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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;
}
@@ -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}
File diff suppressed because one or more lines are too long
@@ -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);
})));
File diff suppressed because one or more lines are too long
@@ -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;
}
@@ -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}
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More