From 125a493dc6baed50a834dbbc3963212796a421d1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Feb 2024 11:37:23 -0500 Subject: [PATCH 1/4] Changelog for #14438, #15042, #15087, #15131, #15238 --- docs/release-notes/version-4.0.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index 9bf0a4db8..4bae93fa8 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -13,6 +13,10 @@ The NetBox user interface has been completely refreshed and updated. +#### Dynamic REST API Fields ([#15087](https://github.com/netbox-community/netbox/issues/15087)) + +The REST API now supports specifying which fields to include in the response data. + ### Enhancements * [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3 @@ -22,6 +26,9 @@ The NetBox user interface has been completely refreshed and updated. * [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12 * [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI * [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI +* [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects +* [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets +* [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations ### Other Changes @@ -34,5 +41,6 @@ The NetBox user interface has been completely refreshed and updated. * [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin` * [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`) * [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class +* [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features * [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices * [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class From 709eac6b9838fe1129f1371ab793d25bc656913c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Feb 2024 08:53:45 -0500 Subject: [PATCH 2/4] Closes #15292: Remove obsolete device_role attribute from Device model --- docs/customization/custom-scripts.md | 2 +- docs/models/dcim/device.md | 4 ++-- netbox/dcim/api/serializers.py | 32 ++++++++++++---------------- netbox/dcim/models/devices.py | 14 ------------ netbox/dcim/tests/test_cablepaths.py | 2 +- netbox/dcim/tests/test_models.py | 24 --------------------- 6 files changed, 18 insertions(+), 60 deletions(-) diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index 671f3ab17..bdc3f9104 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -476,7 +476,7 @@ class NewBranchScript(Script): name=f'{site.slug}-switch{i}', site=site, status=DeviceStatusChoices.STATUS_PLANNED, - device_role=switch_role + role=switch_role ) switch.full_clean() switch.save() diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md index c9f05cd93..8b38d7c89 100644 --- a/docs/models/dcim/device.md +++ b/docs/models/dcim/device.md @@ -18,9 +18,9 @@ When a device has one or more interfaces with IP addresses assigned, a primary I The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant. -### Device Role +### Role -The functional [role](./devicerole.md) assigned to this device. +The functional [device role](./devicerole.md) assigned to this device. ### Device Type diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 1bf4969e2..8fbe9fd04 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -702,7 +702,6 @@ class DeviceSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') device_type = NestedDeviceTypeSerializer() role = NestedDeviceRoleSerializer() - device_role = NestedDeviceRoleSerializer(read_only=True, help_text='Deprecated in v3.6 in favor of `role`.') tenant = NestedTenantSerializer(required=False, allow_null=True, default=None) platform = NestedPlatformSerializer(required=False, allow_null=True) site = NestedSiteSerializer() @@ -744,13 +743,13 @@ class DeviceSerializer(NetBoxModelSerializer): class Meta: model = Device fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', - 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', - 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', - 'device_bay_count', 'module_bay_count', 'inventory_item_count', + 'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', + 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow', + 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', + 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', + 'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count', + 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count', + 'module_bay_count', 'inventory_item_count', ] brief_fields = ('id', 'url', 'display', 'name', 'description') @@ -765,22 +764,19 @@ class DeviceSerializer(NetBoxModelSerializer): data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data return data - def get_device_role(self, obj): - return obj.role - class DeviceWithConfigContextSerializer(DeviceSerializer): config_context = serializers.SerializerMethodField(read_only=True) class Meta(DeviceSerializer.Meta): fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context', - 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count', - 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count', - 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count', + 'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', + 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow', + 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', + 'vc_priority', 'description', 'comments', 'config_template', 'config_context', 'local_context_data', 'tags', + 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', + 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', + 'device_bay_count', 'module_bay_count', 'inventory_item_count', ] @extend_schema_field(serializers.JSONField(allow_null=True)) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 5e773364a..c75757fa7 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -815,20 +815,6 @@ class Device( def get_absolute_url(self): return reverse('dcim:device', args=[self.pk]) - @property - def device_role(self): - """ - For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device. - """ - return self.role - - @device_role.setter - def device_role(self, value): - """ - For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device. - """ - self.role = value - def clean(self): super().clean() diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index a827939f7..49a71022e 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -2156,7 +2156,7 @@ class CablePathTestCase(TestCase): device = Device.objects.create( site=self.site, device_type=self.device.device_type, - device_role=self.device.device_role, + role=self.device.role, name='Test mid-span Device' ) interface1 = Interface.objects.create(device=self.device, name='Interface 1') diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index d56bf0741..8eb057020 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -533,30 +533,6 @@ class DeviceTestCase(TestCase): device2.full_clean() device2.save() - def test_old_device_role_field(self): - """ - Ensure that the old device role field sets the value in the new role field. - """ - - # Test getter method - device = Device( - site=Site.objects.first(), - device_type=DeviceType.objects.first(), - role=DeviceRole.objects.first(), - name='Test Device 1', - device_role=DeviceRole.objects.first() - ) - device.full_clean() - device.save() - - self.assertEqual(device.role, device.device_role) - - # Test setter method - device.device_role = DeviceRole.objects.last() - device.full_clean() - device.save() - self.assertEqual(device.role, device.device_role) - class CableTestCase(TestCase): From c6a3fc2407fe598cca2bb71a7a124d30615256c2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 08:29:53 -0500 Subject: [PATCH 3/4] #12795: Introduce a custom Group model (#15304) * Rename sequences & indexes after renaming users table * Migrate from auth.Group to a custom group model * Delete original groups from auth_group table * Update object & multi-object custom fields referencing the Group model * Fix ContentType resolution * Clean up obsolete logic for view/serializer resolution --- netbox/netbox/authentication.py | 4 +- netbox/netbox/navigation/menu.py | 6 +- netbox/netbox/tests/test_authentication.py | 3 +- netbox/templates/users/group.html | 2 +- netbox/templates/users/objectpermission.html | 2 +- netbox/templates/users/user.html | 2 +- netbox/users/api/nested_serializers.py | 3 +- netbox/users/api/serializers.py | 3 +- netbox/users/api/views.py | 10 +-- netbox/users/filtersets.py | 3 +- netbox/users/forms/bulk_import.py | 2 +- netbox/users/forms/filtersets.py | 5 +- netbox/users/forms/model_forms.py | 7 +- netbox/users/graphql/schema.py | 6 +- netbox/users/graphql/types.py | 2 +- .../users/migrations/0005_alter_user_table.py | 20 ++++- .../migrations/0006_custom_group_model.py | 80 +++++++++++++++++ netbox/users/models.py | 86 ++++++++++++------- netbox/users/tables.py | 8 +- netbox/users/tests/test_api.py | 3 +- netbox/users/tests/test_filtersets.py | 3 +- netbox/users/tests/test_views.py | 3 +- netbox/users/urls.py | 10 +-- netbox/users/views.py | 20 ++--- netbox/utilities/api.py | 16 +--- netbox/utilities/utils.py | 18 +--- 26 files changed, 208 insertions(+), 119 deletions(-) create mode 100644 netbox/users/migrations/0006_custom_group_model.py diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py index 10555b373..c70c68bc0 100644 --- a/netbox/netbox/authentication.py +++ b/netbox/netbox/authentication.py @@ -4,13 +4,13 @@ from collections import defaultdict from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend -from django.contrib.auth.models import Group, AnonymousUser +from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ImproperlyConfigured from django.db.models import Q from django.utils.translation import gettext_lazy as _ from users.constants import CONSTRAINT_TOKEN_USER -from users.models import ObjectPermission +from users.models import Group, ObjectPermission from utilities.permissions import ( permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct, ) diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 2dba76e72..621bd4f5d 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -392,19 +392,19 @@ ADMIN_MENU = Menu( ), # Proxy model for auth.Group MenuItem( - link=f'users:netboxgroup_list', + link=f'users:group_list', link_text=_('Groups'), permissions=[f'auth.view_group'], staff_only=True, buttons=( MenuItemButton( - link=f'users:netboxgroup_add', + link=f'users:group_add', title='Add', icon_class='mdi mdi-plus-thick', permissions=[f'auth.add_group'] ), MenuItemButton( - link=f'users:netboxgroup_import', + link=f'users:group_import', title='Import', icon_class='mdi mdi-upload', permissions=[f'auth.add_group'] diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py index 1804087d1..6a894edcd 100644 --- a/netbox/netbox/tests/test_authentication.py +++ b/netbox/netbox/tests/test_authentication.py @@ -2,7 +2,6 @@ import datetime from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.test import Client from django.test.utils import override_settings @@ -12,7 +11,7 @@ from rest_framework.test import APIClient from dcim.models import Site from ipam.models import Prefix -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token from utilities.testing import TestCase from utilities.testing.api import APITestCase diff --git a/netbox/templates/users/group.html b/netbox/templates/users/group.html index 27b4707fb..d3f02af12 100644 --- a/netbox/templates/users/group.html +++ b/netbox/templates/users/group.html @@ -24,7 +24,7 @@
{% trans "Users" %}
- {% for user in object.user_set.all %} + {% for user in object.users.all %} {{ user }} {% empty %}
{% trans "None" %}
diff --git a/netbox/templates/users/objectpermission.html b/netbox/templates/users/objectpermission.html index 9a222ba80..3e8b71327 100644 --- a/netbox/templates/users/objectpermission.html +++ b/netbox/templates/users/objectpermission.html @@ -82,7 +82,7 @@
{% trans "Assigned Groups" %}
{% for group in object.groups.all %} - {{ group }} + {{ group }} {% empty %}
{% trans "None" %}
{% endfor %} diff --git a/netbox/templates/users/user.html b/netbox/templates/users/user.html index 3b08f98d9..0dd12fb52 100644 --- a/netbox/templates/users/user.html +++ b/netbox/templates/users/user.html @@ -53,7 +53,7 @@
{% trans "Assigned Groups" %}
{% for group in object.groups.all %} - {{ group }} + {{ group }} {% empty %}
{% trans "None" %}
{% endfor %} diff --git a/netbox/users/api/nested_serializers.py b/netbox/users/api/nested_serializers.py index 5e15fa41a..552c24906 100644 --- a/netbox/users/api/nested_serializers.py +++ b/netbox/users/api/nested_serializers.py @@ -1,5 +1,4 @@ from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes @@ -7,7 +6,7 @@ from rest_framework import serializers from netbox.api.fields import ContentTypeField from netbox.api.serializers import WritableNestedSerializer -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token __all__ = [ 'NestedGroupSerializer', diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 0eef61dc8..b9bd55e75 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -1,7 +1,6 @@ from django.conf import settings from django.contrib.auth import authenticate from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes @@ -10,7 +9,7 @@ from rest_framework.exceptions import AuthenticationFailed, PermissionDenied from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField from netbox.api.serializers import ValidatedModelSerializer -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token from .nested_serializers import * diff --git a/netbox/users/api/views.py b/netbox/users/api/views.py index 895600822..412bccf59 100644 --- a/netbox/users/api/views.py +++ b/netbox/users/api/views.py @@ -1,11 +1,9 @@ import logging -from django.contrib.auth import authenticate + from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.db.models import Count -from drf_spectacular.utils import extend_schema from drf_spectacular.types import OpenApiTypes -from rest_framework.exceptions import AuthenticationFailed +from drf_spectacular.utils import extend_schema from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.routers import APIRootView @@ -15,7 +13,7 @@ from rest_framework.viewsets import ViewSet from netbox.api.viewsets import NetBoxModelViewSet from users import filtersets -from users.models import ObjectPermission, Token, UserConfig +from users.models import Group, ObjectPermission, Token, UserConfig from utilities.querysets import RestrictedQuerySet from utilities.utils import deepmerge from . import serializers @@ -40,7 +38,7 @@ class UserViewSet(NetBoxModelViewSet): class GroupViewSet(NetBoxModelViewSet): - queryset = RestrictedQuerySet(model=Group).annotate(user_count=Count('user')).order_by('name') + queryset = Group.objects.annotate(user_count=Count('user')) serializer_class = serializers.GroupSerializer filterset_class = filtersets.GroupFilterSet diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index 0f590e012..5dbca7738 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -1,11 +1,10 @@ import django_filters from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.db.models import Q from django.utils.translation import gettext as _ from netbox.filtersets import BaseFilterSet -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token __all__ = ( 'GroupFilterSet', diff --git a/netbox/users/forms/bulk_import.py b/netbox/users/forms/bulk_import.py index 055998c69..cbaa1ad76 100644 --- a/netbox/users/forms/bulk_import.py +++ b/netbox/users/forms/bulk_import.py @@ -14,7 +14,7 @@ __all__ = ( class GroupImportForm(CSVModelForm): class Meta: - model = NetBoxGroup + model = Group fields = ( 'name', ) diff --git a/netbox/users/forms/filtersets.py b/netbox/users/forms/filtersets.py index c127e2144..23bbe45e1 100644 --- a/netbox/users/forms/filtersets.py +++ b/netbox/users/forms/filtersets.py @@ -1,11 +1,10 @@ from django import forms from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.utils.translation import gettext_lazy as _ from netbox.forms import NetBoxModelFilterSetForm from netbox.forms.mixins import SavedFiltersMixin -from users.models import NetBoxGroup, User, ObjectPermission, Token +from users.models import Group, ObjectPermission, Token, User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm from utilities.forms.fields import DynamicModelMultipleChoiceField from utilities.forms.widgets import DateTimePicker @@ -19,7 +18,7 @@ __all__ = ( class GroupFilterForm(NetBoxModelFilterSetForm): - model = NetBoxGroup + model = Group fieldsets = ( (None, ('q', 'filter_id',)), ) diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 8875dc7f0..2a024bf47 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -1,7 +1,6 @@ from django import forms from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.forms import SimpleArrayField from django.core.exceptions import FieldError @@ -253,7 +252,7 @@ class GroupForm(forms.ModelForm): ) class Meta: - model = NetBoxGroup + model = Group fields = [ 'name', 'users', 'object_permissions', ] @@ -263,14 +262,14 @@ class GroupForm(forms.ModelForm): # Populate assigned users and permissions if self.instance.pk: - self.fields['users'].initial = self.instance.user_set.values_list('id', flat=True) + self.fields['users'].initial = self.instance.users.values_list('id', flat=True) self.fields['object_permissions'].initial = self.instance.object_permissions.values_list('id', flat=True) def save(self, *args, **kwargs): instance = super().save(*args, **kwargs) # Update assigned users and permissions - instance.user_set.set(self.cleaned_data['users']) + instance.users.set(self.cleaned_data['users']) instance.object_permissions.set(self.cleaned_data['object_permissions']) return instance diff --git a/netbox/users/graphql/schema.py b/netbox/users/graphql/schema.py index f033a535a..84ae0c975 100644 --- a/netbox/users/graphql/schema.py +++ b/netbox/users/graphql/schema.py @@ -1,10 +1,10 @@ import graphene - from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group + from netbox.graphql.fields import ObjectField, ObjectListField -from .types import * +from users.models import Group from utilities.graphql_optimizer import gql_query_optimizer +from .types import * class UsersQuery(graphene.ObjectType): diff --git a/netbox/users/graphql/types.py b/netbox/users/graphql/types.py index 4254f1791..58d211028 100644 --- a/netbox/users/graphql/types.py +++ b/netbox/users/graphql/types.py @@ -1,8 +1,8 @@ from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from graphene_django import DjangoObjectType from users import filtersets +from users.models import Group from utilities.querysets import RestrictedQuerySet __all__ = ( diff --git a/netbox/users/migrations/0005_alter_user_table.py b/netbox/users/migrations/0005_alter_user_table.py index 6c4a815dd..e07db6875 100644 --- a/netbox/users/migrations/0005_alter_user_table.py +++ b/netbox/users/migrations/0005_alter_user_table.py @@ -1,5 +1,3 @@ -# Generated by Django 5.0.1 on 2024-01-31 23:18 - from django.db import migrations @@ -27,12 +25,26 @@ class Migration(migrations.Migration): ] operations = [ - # 0001_squashed had model with db_table=auth_user - now we switch it - # to None to use the default Django resolution (users.user) + # The User table was originally created as 'auth_user'. Now we nullify the model's + # db_table option, so that it defaults to the app & model name (users_user). This + # causes the database table to be renamed. migrations.AlterModelTable( name='user', table=None, ), + + # Rename auth_user_* sequences + migrations.RunSQL("ALTER TABLE auth_user_groups_id_seq RENAME TO users_user_groups_id_seq"), + migrations.RunSQL("ALTER TABLE auth_user_id_seq RENAME TO users_user_id_seq"), + migrations.RunSQL("ALTER TABLE auth_user_user_permissions_id_seq RENAME TO users_user_user_permissions_id_seq"), + + # Rename auth_user_* indexes + migrations.RunSQL("ALTER INDEX auth_user_pkey RENAME TO users_user_pkey"), + # Hash is deterministic; generated via schema_editor._create_index_name() + migrations.RunSQL("ALTER INDEX auth_user_username_6821ab7c_like RENAME TO users_user_username_06e46fe6_like"), + migrations.RunSQL("ALTER INDEX auth_user_username_key RENAME TO users_user_username_key"), + + # Update ContentTypes migrations.RunPython( code=update_content_types, reverse_code=migrations.RunPython.noop diff --git a/netbox/users/migrations/0006_custom_group_model.py b/netbox/users/migrations/0006_custom_group_model.py new file mode 100644 index 000000000..282da3ce0 --- /dev/null +++ b/netbox/users/migrations/0006_custom_group_model.py @@ -0,0 +1,80 @@ +import users.models +from django.db import migrations, models + + +def update_custom_fields(apps, schema_editor): + """ + Update any CustomFields referencing the old Group model to use the new model. + """ + ContentType = apps.get_model('contenttypes', 'ContentType') + CustomField = apps.get_model('extras', 'CustomField') + Group = apps.get_model('users', 'Group') + + if old_ct := ContentType.objects.filter(app_label='users', model='netboxgroup').first(): + new_ct = ContentType.objects.get_for_model(Group) + CustomField.objects.filter(object_type=old_ct).update(object_type=new_ct) + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_alter_user_table'), + ] + + operations = [ + # Create the new Group model & table + migrations.CreateModel( + name='Group', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=150, unique=True)), + ('description', models.CharField(blank=True, max_length=200)), + ('permissions', models.ManyToManyField(blank=True, related_name='groups', related_query_name='group', to='auth.permission')), + ], + options={ + 'verbose_name': 'group', + 'verbose_name_plural': 'groups', + }, + managers=[ + ('objects', users.models.NetBoxGroupManager()), + ], + ), + + # Copy existing groups from the old table into the new one + migrations.RunSQL( + "INSERT INTO users_group (SELECT id, name, '' AS description FROM auth_group)" + ), + + # Update the sequence for group ID values + migrations.RunSQL( + "SELECT setval('users_group_id_seq', (SELECT MAX(id) FROM users_group))" + ), + + # Update the "groups" M2M fields on User & ObjectPermission + migrations.AlterField( + model_name='user', + name='groups', + field=models.ManyToManyField(blank=True, related_name='users', related_query_name='user', to='users.group'), + ), + migrations.AlterField( + model_name='objectpermission', + name='groups', + field=models.ManyToManyField(blank=True, related_name='object_permissions', to='users.group'), + ), + + # Delete groups from the old table + migrations.RunSQL( + "DELETE from auth_group" + ), + + # Update custom fields + migrations.RunPython( + code=update_custom_fields, + reverse_code=migrations.RunPython.noop + ), + + # Delete the proxy model + migrations.DeleteModel( + name='NetBoxGroup', + ), + ] diff --git a/netbox/users/models.py b/netbox/users/models.py index 5e817be0b..19d6013c7 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -4,7 +4,12 @@ import os from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import ( - AbstractUser, Group, GroupManager, User as DjangoUser, UserManager as DjangoUserManager + AbstractUser, + Group as DjangoGroup, + GroupManager, + Permission, + User as DjangoUser, + UserManager as DjangoUserManager ) from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError @@ -25,7 +30,7 @@ from utilities.utils import flatten_dict from .constants import * __all__ = ( - 'NetBoxGroup', + 'Group', 'ObjectPermission', 'Token', 'User', @@ -33,22 +38,61 @@ __all__ = ( ) -# -# Proxies for Django's User and Group models -# +class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)): + pass + + +class Group(models.Model): + name = models.CharField( + verbose_name=_('name'), + max_length=150, + unique=True + ) + description = models.CharField( + verbose_name=_('description'), + max_length=200, + blank=True + ) + + # Replicate legacy Django permissions support from stock Group model + # to ensure authentication backend compatibility + permissions = models.ManyToManyField( + Permission, + verbose_name=_("permissions"), + blank=True, + related_name='groups', + related_query_name='group' + ) + + objects = NetBoxGroupManager() + + class Meta: + verbose_name = _('group') + verbose_name_plural = _('groups') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('users:group', args=[self.pk]) + + def natural_key(self): + return (self.name,) + class UserManager(DjangoUserManager.from_queryset(RestrictedQuerySet)): pass -class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)): - pass - - class User(AbstractUser): - """ - Proxy contrib.auth.models.User for the UI - """ + groups = models.ManyToManyField( + to='users.Group', + verbose_name=_('groups'), + blank=True, + related_name='users', + related_query_name='user' + ) + objects = UserManager() class Meta: @@ -68,22 +112,6 @@ class User(AbstractUser): raise ValidationError(_("A user with this username already exists.")) -class NetBoxGroup(Group): - """ - Proxy contrib.auth.models.User for the UI - """ - objects = NetBoxGroupManager() - - class Meta: - proxy = True - ordering = ('name',) - verbose_name = _('group') - verbose_name_plural = _('groups') - - def get_absolute_url(self): - return reverse('users:netboxgroup', args=[self.pk]) - - # # User preferences # @@ -360,7 +388,7 @@ class ObjectPermission(models.Model): related_name='object_permissions' ) groups = models.ManyToManyField( - to=Group, + to='users.Group', blank=True, related_name='object_permissions' ) diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 781660817..813d729c9 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext as _ from account.tables import UserTokenTable from netbox.tables import NetBoxTable, columns -from users.models import NetBoxGroup, User, ObjectPermission, Token +from users.models import Group, ObjectPermission, Token, User __all__ = ( 'GroupTable', @@ -33,7 +33,7 @@ class UserTable(NetBoxTable): ) groups = columns.ManyToManyColumn( verbose_name=_('Groups'), - linkify_item=('users:netboxgroup', {'pk': tables.A('pk')}) + linkify_item=('users:group', {'pk': tables.A('pk')}) ) is_active = columns.BooleanColumn( verbose_name=_('Is Active'), @@ -67,7 +67,7 @@ class GroupTable(NetBoxTable): ) class Meta(NetBoxTable.Meta): - model = NetBoxGroup + model = Group fields = ( 'pk', 'id', 'name', 'users_count', ) @@ -107,7 +107,7 @@ class ObjectPermissionTable(NetBoxTable): ) groups = columns.ManyToManyColumn( verbose_name=_('Groups'), - linkify_item=('users:netboxgroup', {'pk': tables.A('pk')}) + linkify_item=('users:group', {'pk': tables.A('pk')}) ) actions = columns.ActionsColumn( actions=('edit', 'delete'), diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 40a3edf31..51fc21c97 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -1,9 +1,8 @@ from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.urls import reverse -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token from utilities.testing import APIViewTestCases, APITestCase, create_test_user from utilities.utils import deepmerge diff --git a/netbox/users/tests/test_filtersets.py b/netbox/users/tests/test_filtersets.py index 38a0df813..5d373628f 100644 --- a/netbox/users/tests/test_filtersets.py +++ b/netbox/users/tests/test_filtersets.py @@ -1,13 +1,12 @@ import datetime from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.test import TestCase from django.utils.timezone import make_aware from users import filtersets -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token from utilities.testing import BaseFilterSetTests User = get_user_model() diff --git a/netbox/users/tests/test_views.py b/netbox/users/tests/test_views.py index 259b7a857..27d2aeab1 100644 --- a/netbox/users/tests/test_views.py +++ b/netbox/users/tests/test_views.py @@ -1,4 +1,3 @@ -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from users.models import * @@ -70,7 +69,7 @@ class GroupTestCase( ViewTestCases.BulkImportObjectsViewTestCase, ViewTestCases.BulkDeleteObjectsViewTestCase, ): - model = NetBoxGroup + model = Group maxDiff = None @classmethod diff --git a/netbox/users/urls.py b/netbox/users/urls.py index 486a0c771..adfeba378 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -23,11 +23,11 @@ urlpatterns = [ path('users//', include(get_model_urls('users', 'user'))), # Groups - path('groups/', views.GroupListView.as_view(), name='netboxgroup_list'), - path('groups/add/', views.GroupEditView.as_view(), name='netboxgroup_add'), - path('groups/import/', views.GroupBulkImportView.as_view(), name='netboxgroup_import'), - path('groups/delete/', views.GroupBulkDeleteView.as_view(), name='netboxgroup_bulk_delete'), - path('groups//', include(get_model_urls('users', 'netboxgroup'))), + path('groups/', views.GroupListView.as_view(), name='group_list'), + path('groups/add/', views.GroupEditView.as_view(), name='group_add'), + path('groups/import/', views.GroupBulkImportView.as_view(), name='group_import'), + path('groups/delete/', views.GroupBulkDeleteView.as_view(), name='group_bulk_delete'), + path('groups//', include(get_model_urls('users', 'group'))), # Permissions path('permissions/', views.ObjectPermissionListView.as_view(), name='objectpermission_list'), diff --git a/netbox/users/views.py b/netbox/users/views.py index 324125604..662e5e573 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -5,7 +5,7 @@ from extras.tables import ObjectChangeTable from netbox.views import generic from utilities.views import register_model_view from . import filtersets, forms, tables -from .models import NetBoxGroup, User, ObjectPermission, Token +from .models import Group, User, ObjectPermission, Token # @@ -110,36 +110,36 @@ class UserBulkDeleteView(generic.BulkDeleteView): # class GroupListView(generic.ObjectListView): - queryset = NetBoxGroup.objects.annotate(users_count=Count('user')) + queryset = Group.objects.annotate(users_count=Count('user')) filterset = filtersets.GroupFilterSet filterset_form = forms.GroupFilterForm table = tables.GroupTable -@register_model_view(NetBoxGroup) +@register_model_view(Group) class GroupView(generic.ObjectView): - queryset = NetBoxGroup.objects.all() + queryset = Group.objects.all() template_name = 'users/group.html' -@register_model_view(NetBoxGroup, 'edit') +@register_model_view(Group, 'edit') class GroupEditView(generic.ObjectEditView): - queryset = NetBoxGroup.objects.all() + queryset = Group.objects.all() form = forms.GroupForm -@register_model_view(NetBoxGroup, 'delete') +@register_model_view(Group, 'delete') class GroupDeleteView(generic.ObjectDeleteView): - queryset = NetBoxGroup.objects.all() + queryset = Group.objects.all() class GroupBulkImportView(generic.BulkImportView): - queryset = NetBoxGroup.objects.all() + queryset = Group.objects.all() model_form = forms.GroupImportForm class GroupBulkDeleteView(generic.BulkDeleteView): - queryset = NetBoxGroup.objects.annotate(users_count=Count('user')) + queryset = Group.objects.annotate(users_count=Count('user')) filterset = filtersets.GroupFilterSet table = tables.GroupTable diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index a13e62bfd..25a350c81 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -31,23 +31,13 @@ def get_serializer_for_model(model, prefix=''): """ Dynamically resolve and return the appropriate serializer for a model. """ - app_name, model_name = model._meta.label.split('.') - # Serializers for Django's auth models are in the users app - if app_name == 'auth': - app_name = 'users' - # Account for changes using Proxy model - if app_name == 'users': - if model_name == 'NetBoxUser': - model_name = 'User' - elif model_name == 'NetBoxGroup': - model_name = 'Group' - - serializer_name = f'{app_name}.api.serializers.{prefix}{model_name}Serializer' + app_label, model_name = model._meta.label.split('.') + serializer_name = f'{app_label}.api.serializers.{prefix}{model_name}Serializer' try: return dynamic_import(serializer_name) except AttributeError: raise SerializerNotFound( - f"Could not determine serializer for {app_name}.{model_name} with prefix '{prefix}'" + f"Could not determine serializer for {app_label}.{model_name} with prefix '{prefix}'" ) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index bd03ae4b8..5a25b4465 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -1,11 +1,12 @@ import datetime import decimal import json -import nh3 import re from decimal import Decimal from itertools import count, groupby +from urllib.parse import urlencode +import nh3 from django.contrib.contenttypes.models import ContentType from django.core import serializers from django.db.models import Count, ManyToOneRel, OuterRef, Subquery @@ -23,7 +24,6 @@ from dcim.choices import CableLengthUnitChoices, WeightUnitChoices from extras.utils import is_taggable from netbox.config import get_config from netbox.plugins import PluginConfig -from urllib.parse import urlencode from utilities.constants import HTTP_REQUEST_META_SAFE_COPY from .constants import HTML_ALLOWED_ATTRIBUTES, HTML_ALLOWED_TAGS @@ -48,26 +48,16 @@ def get_viewname(model, action=None, rest_api=False): model_name = model._meta.model_name if rest_api: + viewname = f'{app_label}-api:{model_name}' if is_plugin: - viewname = f'plugins-api:{app_label}-api:{model_name}' - else: - # Alter the app_label for group and user model_name to point to users app - if app_label == 'auth' and model_name in ['group', 'user']: - app_label = 'users' - if app_label == 'users' and model._meta.proxy and model_name in ['netboxuser', 'netboxgroup']: - model_name = model._meta.proxy_for_model._meta.model_name - - viewname = f'{app_label}-api:{model_name}' - # Append the action, if any + viewname = f'plugins-api:{viewname}' if action: viewname = f'{viewname}-{action}' else: viewname = f'{app_label}:{model_name}' - # Prepend the plugins namespace if this is a plugin model if is_plugin: viewname = f'plugins:{viewname}' - # Append the action, if any if action: viewname = f'{viewname}_{action}' From 239d21870b00a3336549806773a25d888039fba4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 15:55:01 -0500 Subject: [PATCH 4/4] Closes #14871: Complete work on UI cleanup (#15341) * Fix left padding of login button in top menu * Relocate "add" buttons for embedded object tables * Remove unused data template block & getNetboxData() utility function * Remove bottom margin from last

element in rendered Markdown inside a table cell * Prevent TomSelect from initializing on ',copyClassesToDropdown:!1,placeholder:null,hidePlaceholder:null,shouldLoad:function(di){return di.length>0},render:{}};let _n=di=>typeof di=="undefined"||di===null?null:Os(di),Os=di=>typeof di=="boolean"?di?"1":"0":di+"",Ms=di=>(di+"").replace(/&/g,"&").replace(//g,">").replace(/"/g,"""),kl=(di,oi)=>oi>0?setTimeout(di,oi):(di.call(null),null),_o=(di,oi)=>{var li;return function(ui,mi){var yi=this;li&&(yi.loading=Math.max(yi.loading-1,0),clearTimeout(li)),li=setTimeout(function(){li=null,yi.loadedSearches[ui]=!0,di.call(yi,ui,mi)},oi)}},yo=(di,oi,li)=>{var ui,mi=di.trigger,yi={};di.trigger=function(){var _i=arguments[0];if(oi.indexOf(_i)!==-1)yi[_i]=arguments;else return mi.apply(di,arguments)},li.apply(di,[]),di.trigger=mi;for(ui of oi)ui in yi&&mi.apply(di,yi[ui])},Jr=di=>({start:di.selectionStart||0,length:(di.selectionEnd||0)-(di.selectionStart||0)}),xn=(di,oi=!1)=>{di&&(di.preventDefault(),oi&&di.stopPropagation())},Sn=(di,oi,li,ui)=>{di.addEventListener(oi,li,ui)},pi=(di,oi)=>{if(!oi||!oi[di])return!1;var li=(oi.altKey?1:0)+(oi.ctrlKey?1:0)+(oi.shiftKey?1:0)+(oi.metaKey?1:0);return li===1},gi=(di,oi)=>{let li=di.getAttribute("id");return li||(di.setAttribute("id",oi),oi)},Ei=di=>di.replace(/[\\"']/g,"\\$&"),xi=(di,oi)=>{oi&&di.append(oi)};function Ni(di,oi){var li=Object.assign({},Da,oi),ui=li.dataAttr,mi=li.labelField,yi=li.valueField,_i=li.disabledField,Si=li.optgroupField,Ai=li.optgroupLabelField,Ci=li.optgroupValueField,zi=di.tagName.toLowerCase(),Hi=di.getAttribute("placeholder")||di.getAttribute("data-placeholder");if(!Hi&&!li.allowEmptyOption){let bn=di.querySelector('option[value=""]');bn&&(Hi=bn.textContent)}var Zi={placeholder:Hi,options:[],optgroups:[],items:[],maxItems:null},Pn=()=>{var bn,Hn=Zi.options,Ln={},an=1;let Fn=0;var rs=yn=>{var An=Object.assign({},yn.dataset),mn=ui&&An[ui];return typeof mn=="string"&&mn.length&&(An=Object.assign(An,JSON.parse(mn))),An},Ca=(yn,An)=>{var mn=_n(yn.value);if(mn!=null&&!(!mn&&!li.allowEmptyOption)){if(Ln.hasOwnProperty(mn)){if(An){var wr=Ln[mn][Si];wr?Array.isArray(wr)?wr.push(An):Ln[mn][Si]=[wr,An]:Ln[mn][Si]=An}}else{var Nn=rs(yn);Nn[mi]=Nn[mi]||yn.textContent,Nn[yi]=Nn[yi]||mn,Nn[_i]=Nn[_i]||yn.disabled,Nn[Si]=Nn[Si]||An,Nn.$option=yn,Nn.$order=Nn.$order||++Fn,Ln[mn]=Nn,Hn.push(Nn)}yn.selected&&Zi.items.push(mn)}},bo=yn=>{var An,mn;mn=rs(yn),mn[Ai]=mn[Ai]||yn.getAttribute("label")||"",mn[Ci]=mn[Ci]||an++,mn[_i]=mn[_i]||yn.disabled,mn.$order=mn.$order||++Fn,Zi.optgroups.push(mn),An=mn[Ci],Ii(yn.children,wr=>{Ca(wr,An)})};Zi.maxItems=di.hasAttribute("multiple")?null:1,Ii(di.children,yn=>{bn=yn.tagName.toLowerCase(),bn==="optgroup"?bo(yn):bn==="option"&&Ca(yn)})},Xi=()=>{let bn=di.getAttribute(ui);if(bn)Zi.options=JSON.parse(bn),Ii(Zi.options,Ln=>{Zi.items.push(Ln[yi])});else{var Hn=di.value.trim()||"";if(!li.allowEmptyOption&&!Hn.length)return;let Ln=Hn.split(li.delimiter);Ii(Ln,an=>{let Fn={};Fn[mi]=an,Fn[yi]=an,Zi.options.push(Fn)}),Zi.items=Ln}};return zi==="select"?Pn():Xi(),Object.assign({},Da,Zi,oi)}var Vi=0;class tn extends ei(ti){constructor(oi,li){super();this.control_input=void 0,this.wrapper=void 0,this.dropdown=void 0,this.control=void 0,this.dropdown_content=void 0,this.focus_node=void 0,this.order=0,this.settings=void 0,this.input=void 0,this.tabIndex=void 0,this.is_select_tag=void 0,this.rtl=void 0,this.inputId=void 0,this._destroy=void 0,this.sifter=void 0,this.isOpen=!1,this.isDisabled=!1,this.isReadOnly=!1,this.isRequired=void 0,this.isInvalid=!1,this.isValid=!0,this.isLocked=!1,this.isFocused=!1,this.isInputHidden=!1,this.isSetup=!1,this.ignoreFocus=!1,this.ignoreHover=!1,this.hasOptions=!1,this.currentResults=void 0,this.lastValue="",this.caretPos=0,this.loading=0,this.loadedSearches={},this.activeOption=null,this.activeItems=[],this.optgroups={},this.options={},this.userOptions={},this.items=[],this.refreshTimeout=null,Vi++;var ui,mi=Ri(oi);if(mi.tomselect)throw new Error("Tom Select already initialized on this element");mi.tomselect=this;var yi=window.getComputedStyle&&window.getComputedStyle(mi,null);ui=yi.getPropertyValue("direction");let _i=Ni(mi,li);this.settings=_i,this.input=mi,this.tabIndex=mi.tabIndex||0,this.is_select_tag=mi.tagName.toLowerCase()==="select",this.rtl=/rtl/i.test(ui),this.inputId=gi(mi,"tomselect-"+Vi),this.isRequired=mi.required,this.sifter=new en(this.options,{diacritics:_i.diacritics}),_i.mode=_i.mode||(_i.maxItems===1?"single":"multi"),typeof _i.hideSelected!="boolean"&&(_i.hideSelected=_i.mode==="multi"),typeof _i.hidePlaceholder!="boolean"&&(_i.hidePlaceholder=_i.mode!=="multi");var Si=_i.createFilter;typeof Si!="function"&&(typeof Si=="string"&&(Si=new RegExp(Si)),Si instanceof RegExp?_i.createFilter=Hn=>Si.test(Hn):_i.createFilter=Hn=>this.settings.duplicates||!this.options[Hn]),this.initializePlugins(_i.plugins),this.setupCallbacks(),this.setupTemplates();let Ai=Ri("

"),Ci=Ri("
"),zi=this._render("dropdown"),Hi=Ri('
'),Zi=this.input.getAttribute("class")||"",Pn=_i.mode;var Xi;if(Xn(Ai,_i.wrapperClass,Zi,Pn),Xn(Ci,_i.controlClass),xi(Ai,Ci),Xn(zi,_i.dropdownClass,Pn),_i.copyClassesToDropdown&&Xn(zi,Zi),Xn(Hi,_i.dropdownContentClass),xi(zi,Hi),Ri(_i.dropdownParent||Ai).appendChild(zi),fn(_i.controlInput)){Xi=Ri(_i.controlInput);var bn=["autocorrect","autocapitalize","autocomplete","spellcheck"];Mi(bn,Hn=>{mi.getAttribute(Hn)&&Cn(Xi,{[Hn]:mi.getAttribute(Hn)})}),Xi.tabIndex=-1,Ci.appendChild(Xi),this.focus_node=Xi}else _i.controlInput?(Xi=Ri(_i.controlInput),this.focus_node=Xi):(Xi=Ri(""),this.focus_node=Ci);this.wrapper=Ai,this.dropdown=zi,this.dropdown_content=Hi,this.control=Ci,this.control_input=Xi,this.setup()}setup(){let oi=this,li=oi.settings,ui=oi.control_input,mi=oi.dropdown,yi=oi.dropdown_content,_i=oi.wrapper,Si=oi.control,Ai=oi.input,Ci=oi.focus_node,zi={passive:!0},Hi=oi.inputId+"-ts-dropdown";Cn(yi,{id:Hi}),Cn(Ci,{role:"combobox","aria-haspopup":"listbox","aria-expanded":"false","aria-controls":Hi});let Zi=gi(Ci,oi.inputId+"-ts-control"),Pn="label[for='"+On(oi.inputId)+"']",Xi=document.querySelector(Pn),bn=oi.focus.bind(oi);if(Xi){Sn(Xi,"click",bn),Cn(Xi,{for:Zi});let an=gi(Xi,oi.inputId+"-ts-label");Cn(Ci,{"aria-labelledby":an}),Cn(yi,{"aria-labelledby":an})}if(_i.style.width=Ai.style.width,oi.plugins.names.length){let an="plugin-"+oi.plugins.names.join(" plugin-");Xn([_i,mi],an)}(li.maxItems===null||li.maxItems>1)&&oi.is_select_tag&&Cn(Ai,{multiple:"multiple"}),li.placeholder&&Cn(ui,{placeholder:li.placeholder}),!li.splitOn&&li.delimiter&&(li.splitOn=new RegExp("\\s*"+ci(li.delimiter)+"+\\s*")),li.load&&li.loadThrottle&&(li.load=_o(li.load,li.loadThrottle)),Sn(mi,"mousemove",()=>{oi.ignoreHover=!1}),Sn(mi,"mouseenter",an=>{var Fn=Ts(an.target,"[data-selectable]",mi);Fn&&oi.onOptionHover(an,Fn)},{capture:!0}),Sn(mi,"click",an=>{let Fn=Ts(an.target,"[data-selectable]");Fn&&(oi.onOptionSelect(an,Fn),xn(an,!0))}),Sn(Si,"click",an=>{var Fn=Ts(an.target,"[data-ts-item]",Si);if(Fn&&oi.onItemSelect(an,Fn)){xn(an,!0);return}ui.value==""&&(oi.onClick(),xn(an,!0))}),Sn(Ci,"keydown",an=>oi.onKeyDown(an)),Sn(ui,"keypress",an=>oi.onKeyPress(an)),Sn(ui,"input",an=>oi.onInput(an)),Sn(Ci,"blur",an=>oi.onBlur(an)),Sn(Ci,"focus",an=>oi.onFocus(an)),Sn(ui,"paste",an=>oi.onPaste(an));let Hn=an=>{let Fn=an.composedPath()[0];if(!_i.contains(Fn)&&!mi.contains(Fn)){oi.isFocused&&oi.blur(),oi.inputState();return}Fn==ui&&oi.isOpen?an.stopPropagation():xn(an,!0)},Ln=()=>{oi.isOpen&&oi.positionDropdown()};Sn(document,"mousedown",Hn),Sn(window,"scroll",Ln,zi),Sn(window,"resize",Ln,zi),this._destroy=()=>{document.removeEventListener("mousedown",Hn),window.removeEventListener("scroll",Ln),window.removeEventListener("resize",Ln),Xi&&Xi.removeEventListener("click",bn)},this.revertSettings={innerHTML:Ai.innerHTML,tabIndex:Ai.tabIndex},Ai.tabIndex=-1,Ai.insertAdjacentElement("afterend",oi.wrapper),oi.sync(!1),li.items=[],delete li.optgroups,delete li.options,Sn(Ai,"invalid",()=>{oi.isValid&&(oi.isValid=!1,oi.isInvalid=!0,oi.refreshState())}),oi.updateOriginalInput(),oi.refreshItems(),oi.close(!1),oi.inputState(),oi.isSetup=!0,Ai.disabled?oi.disable():Ai.readOnly?oi.setReadOnly(!0):oi.enable(),oi.on("change",this.onChange),Xn(Ai,"tomselected","ts-hidden-accessible"),oi.trigger("initialize"),li.preload===!0&&oi.preload()}setupOptions(oi=[],li=[]){this.addOptions(oi),Mi(li,ui=>{this.registerOptionGroup(ui)})}setupTemplates(){var oi=this,li=oi.settings.labelField,ui=oi.settings.optgroupLabelField,mi={optgroup:yi=>{let _i=document.createElement("div");return _i.className="optgroup",_i.appendChild(yi.options),_i},optgroup_header:(yi,_i)=>'
'+_i(yi[ui])+"
",option:(yi,_i)=>"
"+_i(yi[li])+"
",item:(yi,_i)=>"
"+_i(yi[li])+"
",option_create:(yi,_i)=>'
Add '+_i(yi.input)+"
",no_results:()=>'
No results found
',loading:()=>'
',not_loading:()=>{},dropdown:()=>"
"};oi.settings.render=Object.assign({},mi,oi.settings.render)}setupCallbacks(){var oi,li,ui={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",item_select:"onItemSelect",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"};for(oi in ui)li=this.settings[ui[oi]],li&&this.on(oi,li)}sync(oi=!0){let li=this,ui=oi?Ni(li.input,{delimiter:li.settings.delimiter}):li.settings;li.setupOptions(ui.options,ui.optgroups),li.setValue(ui.items||[],!0),li.lastQuery=null}onClick(){var oi=this;if(oi.activeItems.length>0){oi.clearActiveItems(),oi.focus();return}oi.isFocused&&oi.isOpen?oi.blur():oi.focus()}onMouseDown(){}onChange(){In(this.input,"input"),In(this.input,"change")}onPaste(oi){var li=this;if(li.isInputHidden||li.isLocked){xn(oi);return}!li.settings.splitOn||setTimeout(()=>{var ui=li.inputValue();if(!!ui.match(li.settings.splitOn)){var mi=ui.trim().split(li.settings.splitOn);Mi(mi,yi=>{_n(yi)&&(this.options[yi]?li.addItem(yi):li.createItem(yi))})}},0)}onKeyPress(oi){var li=this;if(li.isLocked){xn(oi);return}var ui=String.fromCharCode(oi.keyCode||oi.which);if(li.settings.create&&li.settings.mode==="multi"&&ui===li.settings.delimiter){li.createItem(),xn(oi);return}}onKeyDown(oi){var li=this;if(li.ignoreHover=!0,li.isLocked){oi.keyCode!==Xo&&xn(oi);return}switch(oi.keyCode){case Ys:if(pi(vo,oi)&&li.control_input.value==""){xn(oi),li.selectAll();return}break;case ds:li.isOpen&&(xn(oi,!0),li.close()),li.clearActiveItems();return;case go:if(!li.isOpen&&li.hasOptions)li.open();else if(li.activeOption){let ui=li.getAdjacent(li.activeOption,1);ui&&li.setActiveOption(ui)}xn(oi);return;case mo:if(li.activeOption){let ui=li.getAdjacent(li.activeOption,-1);ui&&li.setActiveOption(ui)}xn(oi);return;case Cs:li.canSelect(li.activeOption)?(li.onOptionSelect(oi,li.activeOption),xn(oi)):(li.settings.create&&li.createItem()||document.activeElement==li.control_input&&li.isOpen)&&xn(oi);return;case Xr:li.advanceSelection(-1,oi);return;case As:li.advanceSelection(1,oi);return;case Xo:li.settings.selectOnTab&&(li.canSelect(li.activeOption)&&(li.onOptionSelect(oi,li.activeOption),xn(oi)),li.settings.create&&li.createItem()&&xn(oi));return;case Ko:case Ll:li.deleteSelection(oi);return}li.isInputHidden&&!pi(vo,oi)&&xn(oi)}onInput(oi){if(this.isLocked)return;let li=this.inputValue();if(this.lastValue!==li){if(this.lastValue=li,li==""){this._onInput();return}this.refreshTimeout&&clearTimeout(this.refreshTimeout),this.refreshTimeout=kl(()=>{this.refreshTimeout=null,this._onInput()},this.settings.refreshThrottle)}}_onInput(){let oi=this.lastValue;this.settings.shouldLoad.call(this,oi)&&this.load(oi),this.refreshOptions(),this.trigger("type",oi)}onOptionHover(oi,li){this.ignoreHover||this.setActiveOption(li,!1)}onFocus(oi){var li=this,ui=li.isFocused;if(li.isDisabled||li.isReadOnly){li.blur(),xn(oi);return}li.ignoreFocus||(li.isFocused=!0,li.settings.preload==="focus"&&li.preload(),ui||li.trigger("focus"),li.activeItems.length||(li.inputState(),li.refreshOptions(!!li.settings.openOnFocus)),li.refreshState())}onBlur(oi){if(document.hasFocus()!==!1){var li=this;if(!!li.isFocused){li.isFocused=!1,li.ignoreFocus=!1;var ui=()=>{li.close(),li.setActiveItem(),li.setCaret(li.items.length),li.trigger("blur")};li.settings.create&&li.settings.createOnBlur?li.createItem(null,ui):ui()}}}onOptionSelect(oi,li){var ui,mi=this;li.parentElement&&li.parentElement.matches("[data-disabled]")||(li.classList.contains("create")?mi.createItem(null,()=>{mi.settings.closeAfterSelect&&mi.close()}):(ui=li.dataset.value,typeof ui!="undefined"&&(mi.lastQuery=null,mi.addItem(ui),mi.settings.closeAfterSelect&&mi.close(),!mi.settings.hideSelected&&oi.type&&/click/.test(oi.type)&&mi.setActiveOption(li))))}canSelect(oi){return!!(this.isOpen&&oi&&this.dropdown_content.contains(oi))}onItemSelect(oi,li){var ui=this;return!ui.isLocked&&ui.settings.mode==="multi"?(xn(oi),ui.setActiveItem(li,oi),!0):!1}canLoad(oi){return!(!this.settings.load||this.loadedSearches.hasOwnProperty(oi))}load(oi){let li=this;if(!li.canLoad(oi))return;Xn(li.wrapper,li.settings.loadingClass),li.loading++;let ui=li.loadCallback.bind(li);li.settings.load.call(li,oi,ui)}loadCallback(oi,li){let ui=this;ui.loading=Math.max(ui.loading-1,0),ui.lastQuery=null,ui.clearActiveOption(),ui.setupOptions(oi,li),ui.refreshOptions(ui.isFocused&&!ui.isInputHidden),ui.loading||Er(ui.wrapper,ui.settings.loadingClass),ui.trigger("load",oi,li)}preload(){var oi=this.wrapper.classList;oi.contains("preloaded")||(oi.add("preloaded"),this.load(""))}setTextboxValue(oi=""){var li=this.control_input,ui=li.value!==oi;ui&&(li.value=oi,In(li,"update"),this.lastValue=oi)}getValue(){return this.is_select_tag&&this.input.hasAttribute("multiple")?this.items:this.items.join(this.settings.delimiter)}setValue(oi,li){var ui=li?[]:["change"];yo(this,ui,()=>{this.clear(li),this.addItems(oi,li)})}setMaxItems(oi){oi===0&&(oi=null),this.settings.maxItems=oi,this.refreshState()}setActiveItem(oi,li){var ui=this,mi,yi,_i,Si,Ai,Ci;if(ui.settings.mode!=="single"){if(!oi){ui.clearActiveItems(),ui.isFocused&&ui.inputState();return}if(mi=li&&li.type.toLowerCase(),mi==="click"&&pi("shiftKey",li)&&ui.activeItems.length){for(Ci=ui.getLastActive(),_i=Array.prototype.indexOf.call(ui.control.children,Ci),Si=Array.prototype.indexOf.call(ui.control.children,oi),_i>Si&&(Ai=_i,_i=Si,Si=Ai),yi=_i;yi<=Si;yi++)oi=ui.control.children[yi],ui.activeItems.indexOf(oi)===-1&&ui.setActiveItemClass(oi);xn(li)}else mi==="click"&&pi(vo,li)||mi==="keydown"&&pi("shiftKey",li)?oi.classList.contains("active")?ui.removeActiveItem(oi):ui.setActiveItemClass(oi):(ui.clearActiveItems(),ui.setActiveItemClass(oi));ui.inputState(),ui.isFocused||ui.focus()}}setActiveItemClass(oi){let li=this,ui=li.control.querySelector(".last-active");ui&&Er(ui,"last-active"),Xn(oi,"active last-active"),li.trigger("item_select",oi),li.activeItems.indexOf(oi)==-1&&li.activeItems.push(oi)}removeActiveItem(oi){var li=this.activeItems.indexOf(oi);this.activeItems.splice(li,1),Er(oi,"active")}clearActiveItems(){Er(this.activeItems,"active"),this.activeItems=[]}setActiveOption(oi,li=!0){oi!==this.activeOption&&(this.clearActiveOption(),!!oi&&(this.activeOption=oi,Cn(this.focus_node,{"aria-activedescendant":oi.getAttribute("id")}),Cn(oi,{"aria-selected":"true"}),Xn(oi,"active"),li&&this.scrollToOption(oi)))}scrollToOption(oi,li){if(!oi)return;let ui=this.dropdown_content,mi=ui.clientHeight,yi=ui.scrollTop||0,_i=oi.offsetHeight,Si=oi.getBoundingClientRect().top-ui.getBoundingClientRect().top+yi;Si+_i>mi+yi?this.scroll(Si-mi+_i,li):Si{oi.setActiveItemClass(ui)}))}inputState(){var oi=this;!oi.control.contains(oi.control_input)||(Cn(oi.control_input,{placeholder:oi.settings.placeholder}),oi.activeItems.length>0||!oi.isFocused&&oi.settings.hidePlaceholder&&oi.items.length>0?(oi.setTextboxValue(),oi.isInputHidden=!0):(oi.settings.hidePlaceholder&&oi.items.length>0&&Cn(oi.control_input,{placeholder:""}),oi.isInputHidden=!1),oi.wrapper.classList.toggle("input-hidden",oi.isInputHidden))}inputValue(){return this.control_input.value.trim()}focus(){var oi=this;oi.isDisabled||oi.isReadOnly||(oi.ignoreFocus=!0,oi.control_input.offsetWidth?oi.control_input.focus():oi.focus_node.focus(),setTimeout(()=>{oi.ignoreFocus=!1,oi.onFocus()},0))}blur(){this.focus_node.blur(),this.onBlur()}getScoreFunction(oi){return this.sifter.getScoreFunction(oi,this.getSearchOptions())}getSearchOptions(){var oi=this.settings,li=oi.sortField;return typeof oi.sortField=="string"&&(li=[{field:oi.sortField}]),{fields:oi.searchField,conjunction:oi.searchConjunction,sort:li,nesting:oi.nesting}}search(oi){var li,ui,mi=this,yi=this.getSearchOptions();if(mi.settings.score&&(ui=mi.settings.score.call(mi,oi),typeof ui!="function"))throw new Error('Tom Select "score" setting must be a function that returns a function');return oi!==mi.lastQuery?(mi.lastQuery=oi,li=mi.sifter.search(oi,Object.assign(yi,{score:ui})),mi.currentResults=li):li=Object.assign({},mi.currentResults),mi.settings.hideSelected&&(li.items=li.items.filter(_i=>{let Si=_n(_i.id);return!(Si&&mi.items.indexOf(Si)!==-1)})),li}refreshOptions(oi=!0){var li,ui,mi,yi,_i,Si,Ai,Ci,zi,Hi;let Zi={},Pn=[];var Xi=this,bn=Xi.inputValue();let Hn=bn===Xi.lastQuery||bn==""&&Xi.lastQuery==null;var Ln=Xi.search(bn),an=null,Fn=Xi.settings.shouldOpen||!1,rs=Xi.dropdown_content;Hn&&(an=Xi.activeOption,an&&(zi=an.closest("[data-group]"))),yi=Ln.items.length,typeof Xi.settings.maxOptions=="number"&&(yi=Math.min(yi,Xi.settings.maxOptions)),yi>0&&(Fn=!0);let Ca=(yn,An)=>{let mn=Zi[yn];if(mn!==void 0){let Nn=Pn[mn];if(Nn!==void 0)return[mn,Nn.fragment]}let wr=document.createDocumentFragment();return mn=Pn.length,Pn.push({fragment:wr,order:An,optgroup:yn}),[mn,wr]};for(li=0;li0&&(Nn=Nn.cloneNode(!0),Cn(Nn,{id:mn.$id+"-clone-"+ui,"aria-selected":null}),Nn.classList.add("ts-cloned"),Er(Nn,"active"),Xi.activeOption&&Xi.activeOption.dataset.value==An&&zi&&zi.dataset.group===_i.toString()&&(an=Nn)),dh.appendChild(Nn),_i!=""&&(Zi[_i]=uh)}}Xi.settings.lockOptgroupOrder&&Pn.sort((yn,An)=>yn.order-An.order),Ai=document.createDocumentFragment(),Mi(Pn,yn=>{let An=yn.fragment,mn=yn.optgroup;if(!An||!An.children.length)return;let wr=Xi.optgroups[mn];if(wr!==void 0){let Nn=document.createDocumentFragment(),Aa=Xi.render("optgroup_header",wr);xi(Nn,Aa),xi(Nn,An);let Oa=Xi.render("optgroup",{group:wr,options:Nn});xi(Ai,Oa)}else xi(Ai,An)}),rs.innerHTML="",xi(rs,Ai),Xi.settings.highlight&&(Ds(rs),Ln.query.length&&Ln.tokens.length&&Mi(Ln.tokens,yn=>{Kr(rs,yn.regex)}));var bo=yn=>{let An=Xi.render(yn,{input:bn});return An&&(Fn=!0,rs.insertBefore(An,rs.firstChild)),An};if(Xi.loading?bo("loading"):Xi.settings.shouldLoad.call(Xi,bn)?Ln.items.length===0&&bo("no_results"):bo("not_loading"),Ci=Xi.canCreate(bn),Ci&&(Hi=bo("option_create")),Xi.hasOptions=Ln.items.length>0||Ci,Fn){if(Ln.items.length>0){if(!an&&Xi.settings.mode==="single"&&Xi.items[0]!=null&&(an=Xi.getOption(Xi.items[0])),!rs.contains(an)){let yn=0;Hi&&!Xi.settings.addPrecedence&&(yn=1),an=Xi.selectable()[yn]}}else Hi&&(an=Hi);oi&&!Xi.isOpen&&(Xi.open(),Xi.scrollToOption(an,"auto")),Xi.setActiveOption(an)}else Xi.clearActiveOption(),oi&&Xi.isOpen&&Xi.close(!1)}selectable(){return this.dropdown_content.querySelectorAll("[data-selectable]")}addOption(oi,li=!1){let ui=this;if(Array.isArray(oi))return ui.addOptions(oi,li),!1;let mi=_n(oi[ui.settings.valueField]);return mi===null||ui.options.hasOwnProperty(mi)?!1:(oi.$order=oi.$order||++ui.order,oi.$id=ui.inputId+"-opt-"+oi.$order,ui.options[mi]=oi,ui.lastQuery=null,li&&(ui.userOptions[mi]=li,ui.trigger("option_add",mi,oi)),mi)}addOptions(oi,li=!1){Mi(oi,ui=>{this.addOption(ui,li)})}registerOption(oi){return this.addOption(oi)}registerOptionGroup(oi){var li=_n(oi[this.settings.optgroupValueField]);return li===null?!1:(oi.$order=oi.$order||++this.order,this.optgroups[li]=oi,li)}addOptionGroup(oi,li){var ui;li[this.settings.optgroupValueField]=oi,(ui=this.registerOptionGroup(li))&&this.trigger("optgroup_add",ui,li)}removeOptionGroup(oi){this.optgroups.hasOwnProperty(oi)&&(delete this.optgroups[oi],this.clearCache(),this.trigger("optgroup_remove",oi))}clearOptionGroups(){this.optgroups={},this.clearCache(),this.trigger("optgroup_clear")}updateOption(oi,li){let ui=this;var mi,yi;let _i=_n(oi),Si=_n(li[ui.settings.valueField]);if(_i===null)return;let Ai=ui.options[_i];if(Ai==null)return;if(typeof Si!="string")throw new Error("Value must be set in option data");let Ci=ui.getOption(_i),zi=ui.getItem(_i);if(li.$order=li.$order||Ai.$order,delete ui.options[_i],ui.uncacheValue(Si),ui.options[Si]=li,Ci){if(ui.dropdown_content.contains(Ci)){let Hi=ui._render("option",li);ns(Ci,Hi),ui.activeOption===Ci&&ui.setActiveOption(Hi)}Ci.remove()}zi&&(yi=ui.items.indexOf(_i),yi!==-1&&ui.items.splice(yi,1,Si),mi=ui._render("item",li),zi.classList.contains("active")&&Xn(mi,"active"),ns(zi,mi)),ui.lastQuery=null}removeOption(oi,li){let ui=this;oi=Os(oi),ui.uncacheValue(oi),delete ui.userOptions[oi],delete ui.options[oi],ui.lastQuery=null,ui.trigger("option_remove",oi),ui.removeItem(oi,li)}clearOptions(oi){let li=(oi||this.clearFilter).bind(this);this.loadedSearches={},this.userOptions={},this.clearCache();let ui={};Mi(this.options,(mi,yi)=>{li(mi,yi)&&(ui[yi]=mi)}),this.options=this.sifter.items=ui,this.lastQuery=null,this.trigger("option_clear")}clearFilter(oi,li){return this.items.indexOf(li)>=0}getOption(oi,li=!1){let ui=_n(oi);if(ui===null)return null;let mi=this.options[ui];if(mi!=null){if(mi.$div)return mi.$div;if(li)return this._render("option",mi)}return null}getAdjacent(oi,li,ui="option"){var mi=this,yi;if(!oi)return null;ui=="item"?yi=mi.controlChildren():yi=mi.dropdown_content.querySelectorAll("[data-selectable]");for(let _i=0;_i0?yi[_i+1]:yi[_i-1];return null}getItem(oi){if(typeof oi=="object")return oi;var li=_n(oi);return li!==null?this.control.querySelector(`[data-value="${Ei(li)}"]`):null}addItems(oi,li){var ui=this,mi=Array.isArray(oi)?oi:[oi];mi=mi.filter(_i=>ui.items.indexOf(_i)===-1);let yi=mi[mi.length-1];mi.forEach(_i=>{ui.isPending=_i!==yi,ui.addItem(_i,li)})}addItem(oi,li){var ui=li?[]:["change","dropdown_close"];yo(this,ui,()=>{var mi,yi;let _i=this,Si=_i.settings.mode,Ai=_n(oi);if(!(Ai&&_i.items.indexOf(Ai)!==-1&&(Si==="single"&&_i.close(),Si==="single"||!_i.settings.duplicates))&&!(Ai===null||!_i.options.hasOwnProperty(Ai))&&(Si==="single"&&_i.clear(li),!(Si==="multi"&&_i.isFull()))){if(mi=_i._render("item",_i.options[Ai]),_i.control.contains(mi)&&(mi=mi.cloneNode(!0)),yi=_i.isFull(),_i.items.splice(_i.caretPos,0,Ai),_i.insertAtCaret(mi),_i.isSetup){if(!_i.isPending&&_i.settings.hideSelected){let Ci=_i.getOption(Ai),zi=_i.getAdjacent(Ci,1);zi&&_i.setActiveOption(zi)}!_i.isPending&&!_i.settings.closeAfterSelect&&_i.refreshOptions(_i.isFocused&&Si!=="single"),_i.settings.closeAfterSelect!=!1&&_i.isFull()?_i.close():_i.isPending||_i.positionDropdown(),_i.trigger("item_add",Ai,mi),_i.isPending||_i.updateOriginalInput({silent:li})}(!_i.isPending||!yi&&_i.isFull())&&(_i.inputState(),_i.refreshState())}})}removeItem(oi=null,li){let ui=this;if(oi=ui.getItem(oi),!oi)return;var mi,yi;let _i=oi.dataset.value;mi=is(oi),oi.remove(),oi.classList.contains("active")&&(yi=ui.activeItems.indexOf(oi),ui.activeItems.splice(yi,1),Er(oi,"active")),ui.items.splice(mi,1),ui.lastQuery=null,!ui.settings.persist&&ui.userOptions.hasOwnProperty(_i)&&ui.removeOption(_i,li),mi{}){arguments.length===3&&(li=arguments[2]),typeof li!="function"&&(li=()=>{});var ui=this,mi=ui.caretPos,yi;if(oi=oi||ui.inputValue(),!ui.canCreate(oi))return li(),!1;ui.lock();var _i=!1,Si=Ai=>{if(ui.unlock(),!Ai||typeof Ai!="object")return li();var Ci=_n(Ai[ui.settings.valueField]);if(typeof Ci!="string")return li();ui.setTextboxValue(),ui.addOption(Ai,!0),ui.setCaret(mi),ui.addItem(Ci),li(Ai),_i=!0};return typeof ui.settings.create=="function"?yi=ui.settings.create.call(this,oi,Si):yi={[ui.settings.labelField]:oi,[ui.settings.valueField]:oi},_i||Si(yi),!0}refreshItems(){var oi=this;oi.lastQuery=null,oi.isSetup&&oi.addItems(oi.items),oi.updateOriginalInput(),oi.refreshState()}refreshState(){let oi=this;oi.refreshValidityState();let li=oi.isFull(),ui=oi.isLocked;oi.wrapper.classList.toggle("rtl",oi.rtl);let mi=oi.wrapper.classList;mi.toggle("focus",oi.isFocused),mi.toggle("disabled",oi.isDisabled),mi.toggle("readonly",oi.isReadOnly),mi.toggle("required",oi.isRequired),mi.toggle("invalid",!oi.isValid),mi.toggle("locked",ui),mi.toggle("full",li),mi.toggle("input-active",oi.isFocused&&!oi.isInputHidden),mi.toggle("dropdown-active",oi.isOpen),mi.toggle("has-options",po(oi.options)),mi.toggle("has-items",oi.items.length>0)}refreshValidityState(){var oi=this;!oi.input.validity||(oi.isValid=oi.input.validity.valid,oi.isInvalid=!oi.isValid)}isFull(){return this.settings.maxItems!==null&&this.items.length>=this.settings.maxItems}updateOriginalInput(oi={}){let li=this;var ui,mi;let yi=li.input.querySelector('option[value=""]');if(li.is_select_tag){let Ai=function(Ci,zi,Hi){return Ci||(Ci=Ri('")),Ci!=yi&&li.input.append(Ci),_i.push(Ci),(Ci!=yi||Si>0)&&(Ci.selected=!0),Ci},_i=[],Si=li.input.querySelectorAll("option:checked").length;li.input.querySelectorAll("option:checked").forEach(Ci=>{Ci.selected=!1}),li.items.length==0&&li.settings.mode=="single"?Ai(yi,"",""):li.items.forEach(Ci=>{if(ui=li.options[Ci],mi=ui[li.settings.labelField]||"",_i.includes(ui.$option)){let zi=li.input.querySelector(`option[value="${Ei(Ci)}"]:not(:checked)`);Ai(zi,Ci,mi)}else ui.$option=Ai(ui.$option,Ci,mi)})}else li.input.value=li.getValue();li.isSetup&&(oi.silent||li.trigger("change",li.getValue()))}open(){var oi=this;oi.isLocked||oi.isOpen||oi.settings.mode==="multi"&&oi.isFull()||(oi.isOpen=!0,Cn(oi.focus_node,{"aria-expanded":"true"}),oi.refreshState(),br(oi.dropdown,{visibility:"hidden",display:"block"}),oi.positionDropdown(),br(oi.dropdown,{visibility:"visible",display:"block"}),oi.focus(),oi.trigger("dropdown_open",oi.dropdown))}close(oi=!0){var li=this,ui=li.isOpen;oi&&(li.setTextboxValue(),li.settings.mode==="single"&&li.items.length&&li.inputState()),li.isOpen=!1,Cn(li.focus_node,{"aria-expanded":"false"}),br(li.dropdown,{display:"none"}),li.settings.hideSelected&&li.clearActiveOption(),li.refreshState(),ui&&li.trigger("dropdown_close",li.dropdown)}positionDropdown(){if(this.settings.dropdownParent==="body"){var oi=this.control,li=oi.getBoundingClientRect(),ui=oi.offsetHeight+li.top+window.scrollY,mi=li.left+window.scrollX;br(this.dropdown,{width:li.width+"px",top:ui+"px",left:mi+"px"})}}clear(oi){var li=this;if(!!li.items.length){var ui=li.controlChildren();Mi(ui,mi=>{li.removeItem(mi,!0)}),li.inputState(),oi||li.updateOriginalInput(),li.trigger("clear")}}insertAtCaret(oi){let li=this,ui=li.caretPos,mi=li.control;mi.insertBefore(oi,mi.children[ui]||null),li.setCaret(ui+1)}deleteSelection(oi){var li,ui,mi,yi,_i=this;li=oi&&oi.keyCode===Ko?-1:1,ui=Jr(_i.control_input);let Si=[];if(_i.activeItems.length)yi=Ws(_i.activeItems,li),mi=is(yi),li>0&&mi++,Mi(_i.activeItems,Ai=>Si.push(Ai));else if((_i.isFocused||_i.settings.mode==="single")&&_i.items.length){let Ai=_i.controlChildren(),Ci;li<0&&ui.start===0&&ui.length===0?Ci=Ai[_i.caretPos-1]:li>0&&ui.start===_i.inputValue().length&&(Ci=Ai[_i.caretPos]),Ci!==void 0&&Si.push(Ci)}if(!_i.shouldDelete(Si,oi))return!1;for(xn(oi,!0),typeof mi!="undefined"&&_i.setCaret(mi);Si.length;)_i.removeItem(Si.pop());return _i.inputState(),_i.positionDropdown(),_i.refreshOptions(!1),!0}shouldDelete(oi,li){let ui=oi.map(mi=>mi.dataset.value);return!(!ui.length||typeof this.settings.onDelete=="function"&&this.settings.onDelete(ui,li)===!1)}advanceSelection(oi,li){var ui,mi,yi=this;yi.rtl&&(oi*=-1),!yi.inputValue().length&&(pi(vo,li)||pi("shiftKey",li)?(ui=yi.getLastActive(oi),ui?ui.classList.contains("active")?mi=yi.getAdjacent(ui,oi,"item"):mi=ui:oi>0?mi=yi.control_input.nextElementSibling:mi=yi.control_input.previousElementSibling,mi&&(mi.classList.contains("active")&&yi.removeActiveItem(ui),yi.setActiveItemClass(mi))):yi.moveCaret(oi))}moveCaret(oi){}getLastActive(oi){let li=this.control.querySelector(".last-active");if(li)return li;var ui=this.control.querySelectorAll(".active");if(ui)return Ws(ui,oi)}setCaret(oi){this.caretPos=this.items.length}controlChildren(){return Array.from(this.control.querySelectorAll("[data-ts-item]"))}lock(){this.setLocked(!0)}unlock(){this.setLocked(!1)}setLocked(oi=this.isReadOnly||this.isDisabled){this.isLocked=oi,this.refreshState()}disable(){this.setDisabled(!0),this.close()}enable(){this.setDisabled(!1)}setDisabled(oi){this.focus_node.tabIndex=oi?-1:this.tabIndex,this.isDisabled=oi,this.input.disabled=oi,this.control_input.disabled=oi,this.setLocked()}setReadOnly(oi){this.isReadOnly=oi,this.input.readOnly=oi,this.control_input.readOnly=oi,this.setLocked()}destroy(){var oi=this,li=oi.revertSettings;oi.trigger("destroy"),oi.off(),oi.wrapper.remove(),oi.dropdown.remove(),oi.input.innerHTML=li.innerHTML,oi.input.tabIndex=li.tabIndex,Er(oi.input,"tomselected","ts-hidden-accessible"),oi._destroy(),delete oi.input.tomselect}render(oi,li){var ui,mi;let yi=this;if(typeof this.settings.render[oi]!="function"||(mi=yi.settings.render[oi].call(this,li,Ms),!mi))return null;if(mi=Ri(mi),oi==="option"||oi==="option_create"?li[yi.settings.disabledField]?Cn(mi,{"aria-disabled":"true"}):Cn(mi,{"data-selectable":""}):oi==="optgroup"&&(ui=li.group[yi.settings.optgroupValueField],Cn(mi,{"data-group":ui}),li.group[yi.settings.disabledField]&&Cn(mi,{"data-disabled":""})),oi==="option"||oi==="item"){let _i=Os(li[yi.settings.valueField]);Cn(mi,{"data-value":_i}),oi==="item"?(Xn(mi,yi.settings.itemClass),Cn(mi,{"data-ts-item":""})):(Xn(mi,yi.settings.optionClass),Cn(mi,{role:"option",id:li.$id}),li.$div=mi,yi.options[_i]=li)}return mi}_render(oi,li){let ui=this.render(oi,li);if(ui==null)throw"HTMLElement expected";return ui}clearCache(){Mi(this.options,oi=>{oi.$div&&(oi.$div.remove(),delete oi.$div)})}uncacheValue(oi){let li=this.getOption(oi);li&&li.remove()}canCreate(oi){return this.settings.create&&oi.length>0&&this.settings.createFilter.call(this,oi)}hook(oi,li,ui){var mi=this,yi=mi[li];mi[li]=function(){var _i,Si;return oi==="after"&&(_i=yi.apply(mi,arguments)),Si=ui.apply(mi,arguments),oi==="instead"?Si:(oi==="before"&&(_i=yi.apply(mi,arguments)),_i)}}}function Qi(){Sn(this.input,"change",()=>{this.sync()})}function hn(di){var oi=this,li=oi.onOptionSelect;oi.settings.hideSelected=!1;let ui=Object.assign({className:"tomselect-checkbox",checkedClassNames:void 0,uncheckedClassNames:void 0},di);var mi=function(Si,Ai){Ai?(Si.checked=!0,ui.uncheckedClassNames&&Si.classList.remove(...ui.uncheckedClassNames),ui.checkedClassNames&&Si.classList.add(...ui.checkedClassNames)):(Si.checked=!1,ui.checkedClassNames&&Si.classList.remove(...ui.checkedClassNames),ui.uncheckedClassNames&&Si.classList.add(...ui.uncheckedClassNames))},yi=function(Si){setTimeout(()=>{var Ai=Si.querySelector("input."+ui.className);Ai instanceof HTMLInputElement&&mi(Ai,Si.classList.contains("selected"))},1)};oi.hook("after","setupTemplates",()=>{var _i=oi.settings.render.option;oi.settings.render.option=(Si,Ai)=>{var Ci=Ri(_i.call(oi,Si,Ai)),zi=document.createElement("input");ui.className&&zi.classList.add(ui.className),zi.addEventListener("click",function(Zi){xn(Zi)}),zi.type="checkbox";let Hi=_n(Si[oi.settings.valueField]);return mi(zi,!!(Hi&&oi.items.indexOf(Hi)>-1)),Ci.prepend(zi),Ci}}),oi.on("item_remove",_i=>{var Si=oi.getOption(_i);Si&&(Si.classList.remove("selected"),yi(Si))}),oi.on("item_add",_i=>{var Si=oi.getOption(_i);Si&&yi(Si)}),oi.hook("instead","onOptionSelect",(_i,Si)=>{if(Si.classList.contains("selected")){Si.classList.remove("selected"),oi.removeItem(Si.dataset.value),oi.refreshOptions(),xn(_i,!0);return}li.call(oi,_i,Si),yi(Si)})}function Ki(di){let oi=this,li=Object.assign({className:"clear-button",title:"Clear All",html:ui=>`
`},di);oi.on("initialize",()=>{var ui=Ri(li.html(li));ui.addEventListener("click",mi=>{oi.isLocked||(oi.clear(),oi.settings.mode==="single"&&oi.settings.allowEmptyOption&&oi.addItem(""),mi.preventDefault(),mi.stopPropagation())}),oi.control.appendChild(ui)})}let cn=(di,oi)=>{var li;(li=di.parentNode)==null||li.insertBefore(oi,di.nextSibling)},Mn=(di,oi)=>{var li;(li=di.parentNode)==null||li.insertBefore(oi,di)},Hr=(di,oi)=>{do{var li;if(oi=(li=oi)==null?void 0:li.previousElementSibling,di==oi)return!0}while(oi&&oi.previousElementSibling);return!1};function Cr(){var di=this;if(di.settings.mode!=="multi")return;var oi=di.lock,li=di.unlock;let ui=!0,mi;di.hook("after","setupTemplates",()=>{var yi=di.settings.render.item;di.settings.render.item=(_i,Si)=>{let Ai=Ri(yi.call(di,_i,Si));Cn(Ai,{draggable:"true"});let Ci=bn=>{ui||xn(bn),bn.stopPropagation()},zi=bn=>{mi=Ai,setTimeout(()=>{Ai.classList.add("ts-dragging")},0)},Hi=bn=>{bn.preventDefault(),Ai.classList.add("ts-drag-over"),Pn(Ai,mi)},Zi=()=>{Ai.classList.remove("ts-drag-over")},Pn=(bn,Hn)=>{Hn!==void 0&&(Hr(Hn,Ai)?cn(bn,Hn):Mn(bn,Hn))},Xi=()=>{var bn;document.querySelectorAll(".ts-drag-over").forEach(Ln=>Ln.classList.remove("ts-drag-over")),(bn=mi)==null||bn.classList.remove("ts-dragging"),mi=void 0;var Hn=[];di.control.querySelectorAll("[data-value]").forEach(Ln=>{if(Ln.dataset.value){let an=Ln.dataset.value;an&&Hn.push(an)}}),di.setValue(Hn)};return Sn(Ai,"mousedown",Ci),Sn(Ai,"dragstart",zi),Sn(Ai,"dragenter",Hi),Sn(Ai,"dragover",Hi),Sn(Ai,"dragleave",Zi),Sn(Ai,"dragend",Xi),Ai}}),di.hook("instead","lock",()=>(ui=!1,oi.call(di))),di.hook("instead","unlock",()=>(ui=!0,li.call(di)))}function Gs(di){let oi=this,li=Object.assign({title:"Untitled",headerClass:"dropdown-header",titleRowClass:"dropdown-header-title",labelClass:"dropdown-header-label",closeClass:"dropdown-header-close",html:ui=>'
'+ui.title+'×
'},di);oi.on("initialize",()=>{var ui=Ri(li.html(li)),mi=ui.querySelector("."+li.closeClass);mi&&mi.addEventListener("click",yi=>{xn(yi,!0),oi.close()}),oi.dropdown.insertBefore(ui,oi.dropdown.firstChild)})}function Ls(){var di=this;di.hook("instead","setCaret",oi=>{di.settings.mode==="single"||!di.control.contains(di.control_input)?oi=di.items.length:(oi=Math.max(0,Math.min(di.items.length,oi)),oi!=di.caretPos&&!di.isPending&&di.controlChildren().forEach((li,ui)=>{ui{if(!di.isFocused)return;let li=di.getLastActive(oi);if(li){let ui=is(li);di.setCaret(oi>0?ui+1:ui),di.setActiveItem(),Er(li,"last-active")}else di.setCaret(di.caretPos+oi)})}function Il(){let di=this;di.settings.shouldOpen=!0,di.hook("before","setup",()=>{di.focus_node=di.control,Xn(di.control_input,"dropdown-input");let oi=Ri('