diff --git a/.github/ISSUE_TEMPLATE/01-feature_request.yaml b/.github/ISSUE_TEMPLATE/01-feature_request.yaml index ab0044ecd..10d7180e4 100644 --- a/.github/ISSUE_TEMPLATE/01-feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/01-feature_request.yaml @@ -15,7 +15,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v4.5.1 + placeholder: v4.5.2 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/02-bug_report.yaml b/.github/ISSUE_TEMPLATE/02-bug_report.yaml index 6ee0a2c6d..0e733b613 100644 --- a/.github/ISSUE_TEMPLATE/02-bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/02-bug_report.yaml @@ -27,7 +27,7 @@ body: attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v4.5.1 + placeholder: v4.5.2 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/03-performance.yaml b/.github/ISSUE_TEMPLATE/03-performance.yaml index b13190766..f0286871e 100644 --- a/.github/ISSUE_TEMPLATE/03-performance.yaml +++ b/.github/ISSUE_TEMPLATE/03-performance.yaml @@ -8,7 +8,7 @@ body: attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v4.5.1 + placeholder: v4.5.2 validations: required: true - type: dropdown diff --git a/base_requirements.txt b/base_requirements.txt index 3518fa884..ad04f764b 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -85,7 +85,7 @@ drf-spectacular-sidecar feedparser # WSGI HTTP server -# https://docs.gunicorn.org/en/latest/news.html +# https://gunicorn.org/news/ gunicorn # Platform-agnostic template rendering engine diff --git a/contrib/openapi.json b/contrib/openapi.json index a73d5dd01..2275a0d2c 100644 --- a/contrib/openapi.json +++ b/contrib/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "NetBox REST API", - "version": "4.5.1", + "version": "4.5.2", "license": { "name": "Apache v2 License" } @@ -1854,6 +1854,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -5696,6 +5748,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -7507,6 +7611,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -9612,6 +9768,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -10806,6 +11014,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -12279,6 +12539,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -15193,6 +15505,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -16486,6 +16850,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -19428,6 +19844,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -21145,6 +21613,97 @@ }, "description": "Search" }, + { + "in": "query", + "name": "queue_name", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "queue_name__empty", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "queue_name__ic", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "queue_name__ie", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "queue_name__iew", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "queue_name__iregex", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "queue_name__isw", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "queue_name__n", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "queue_name__nic", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "queue_name__nie", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "queue_name__niew", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "queue_name__nisw", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "queue_name__regex", + "schema": { + "type": "string" + } + }, { "in": "query", "name": "scheduled", @@ -24711,6 +25270,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -28924,6 +29535,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -33023,6 +33686,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -36661,6 +37376,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -38334,6 +39101,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -40719,6 +41538,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -44792,6 +45663,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -50079,6 +51002,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -55390,6 +56365,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -58836,6 +59863,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -62577,6 +63656,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -64845,6 +65976,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -66782,6 +67965,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -68085,6 +69320,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -71352,6 +72639,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -72977,6 +74316,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -74445,6 +75836,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -76294,6 +77737,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -78085,6 +79580,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -79985,6 +81532,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -85158,6 +86757,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -87122,6 +88773,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -91024,6 +92727,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -92594,6 +94349,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -94427,6 +96234,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -96524,6 +98383,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -99885,6 +101796,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -105122,6 +107085,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -106980,6 +108995,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -108451,6 +110518,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -110295,6 +112414,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -112335,6 +114506,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -113848,6 +116071,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -116281,6 +118556,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -117893,6 +120220,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -120153,6 +122532,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -121599,6 +124030,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -123357,6 +125840,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -125954,6 +128489,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -127659,6 +130246,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -129517,6 +132156,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -134033,6 +136724,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -138372,6 +141115,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -140167,6 +142962,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -141496,6 +144343,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -142885,6 +145784,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -144454,6 +147405,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -147315,6 +150318,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -148998,6 +152053,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -150753,6 +153860,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -152703,6 +155862,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -154868,6 +158079,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -156161,6 +159424,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -157747,6 +161062,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -158991,6 +162358,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -160517,6 +163936,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -162017,6 +165488,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -163642,6 +167165,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -166077,6 +169652,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -168221,6 +171848,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -170993,6 +174672,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -172340,6 +176071,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -174134,6 +177917,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -175626,6 +179461,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -177097,6 +180984,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -185137,6 +189076,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -186430,6 +190421,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -187901,6 +191944,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -190161,6 +194256,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -191553,6 +195700,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -193563,6 +197762,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -196098,6 +200349,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -198201,6 +202504,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -199476,6 +203831,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -201065,6 +205472,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -202633,6 +207092,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -205537,6 +210048,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -207334,6 +211897,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -210204,6 +214819,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -211744,6 +216411,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -213581,6 +218300,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -216080,6 +220851,58 @@ "explode": true, "style": "form" }, + { + "in": "query", + "name": "owner_group", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Owner Group (name)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "owner_group_id__n", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "description": "Owner Group (ID)", + "explode": true, + "style": "form" + }, { "in": "query", "name": "owner_id", @@ -237637,6 +242460,11 @@ "type": "string", "format": "uuid" }, + "queue_name": { + "type": "string", + "description": "Name of the queue in which this job was enqueued", + "maxLength": 100 + }, "log_entries": { "type": "array", "items": {} diff --git a/docs/administration/netbox-shell.md b/docs/administration/netbox-shell.md index a74c5114b..2de363771 100644 --- a/docs/administration/netbox-shell.md +++ b/docs/administration/netbox-shell.md @@ -3,29 +3,41 @@ NetBox includes a Python management shell within which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command: ``` -./manage.py nbshell +cd /opt/netbox +source /opt/netbox/venv/bin/activate +python3 netbox/manage.py nbshell ``` -This will launch a lightly customized version of [the built-in Django shell](https://docs.djangoproject.com/en/stable/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.) +This will launch a lightly customized version of [the built-in Django shell](https://docs.djangoproject.com/en/stable/ref/django-admin/#shell) with all relevant NetBox models preloaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.) ``` -$ ./manage.py nbshell +(venv) $ python3 netbox/manage.py nbshell ### NetBox interactive shell (localhost) -### Python 3.7.10 | Django 3.2.5 | NetBox 3.0 -### lsmodels() will show available models. Use help() for more info. +### Python v3.12.3 | Django v5.2.10 | NetBox Community v4.5.1 +### lsapps() & lsmodels() will show available models. Use help() for more info. ``` The function `lsmodels()` will print a list of all available NetBox models: ``` >>> lsmodels() -DCIM: - ConsolePort - ConsolePortTemplate - ConsoleServerPort - ConsoleServerPortTemplate - Device ... +DCIM: + dcim.Cable + dcim.CableTermination + dcim.ConsolePort + dcim.ConsolePortTemplate + dcim.ConsoleServerPort + dcim.ConsoleServerPortTemplate + dcim.Device + ... +``` + +To exit the NetBox shell, type `exit()` or press `Ctrl+D`. + +``` +>>> exit() +(venv) $ ``` !!! warning @@ -114,7 +126,7 @@ Reverse relationships can be traversed as well. For example, the following will >>> Device.objects.filter(interfaces__name="em0") ``` -Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the later of which is case-insensitive). +Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the latter of which is case-insensitive). ``` >>> Device.objects.filter(name__icontains="testdevice") diff --git a/docs/configuration/data-validation.md b/docs/configuration/data-validation.md index 9988f6e0b..2f00814f3 100644 --- a/docs/configuration/data-validation.md +++ b/docs/configuration/data-validation.md @@ -8,7 +8,7 @@ This is a mapping of models to [custom validators](../customization/custom-valid ```python CUSTOM_VALIDATORS = { - "dcim.site": [ + "dcim.Site": [ { "name": { "min_length": 5, @@ -17,12 +17,15 @@ CUSTOM_VALIDATORS = { }, "my_plugin.validators.Validator1" ], - "dcim.device": [ + "dcim.Device": [ "my_plugin.validators.Validator1" ] } ``` +!!! info "Case-Insensitive Model Names" + Model identifiers are case-insensitive. Both `dcim.site` and `dcim.Site` are valid and equivalent. + --- ## FIELD_CHOICES @@ -53,6 +56,9 @@ FIELD_CHOICES = { } ``` +!!! info "Case-Insensitive Field Identifiers" + Field identifiers are case-insensitive. Both `dcim.Site.status` and `dcim.site.status` are valid and equivalent. + The following model fields support configurable choices: * `circuits.Circuit.status` @@ -98,7 +104,7 @@ This is a mapping of models to [custom validators](../customization/custom-valid ```python PROTECTION_RULES = { - "dcim.site": [ + "dcim.Site": [ { "status": { "eq": "decommissioning" @@ -108,3 +114,6 @@ PROTECTION_RULES = { ] } ``` + +!!! info "Case-Insensitive Model Names" + Model identifiers are case-insensitive. Both `dcim.site` and `dcim.Site` are valid and equivalent. diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 7fa554a58..e3e2d061c 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -15,7 +15,7 @@ Some configuration parameters may alternatively be defined either in `configurat ## Dynamic Configuration Parameters -Some configuration parameters are primarily controlled via NetBox's admin interface (under Admin > Extras > Configuration Revisions). These are noted where applicable in the documentation. These settings may also be overridden in `configuration.py` to prevent them from being modified via the UI. A complete list of supported parameters is provided below: +Some configuration parameters are primarily controlled via NetBox's admin interface (under Admin > System > Configuration History). These are noted where applicable in the documentation. These settings may also be overridden in `configuration.py` to prevent them from being modified via the UI. A complete list of supported parameters is provided below: * [`ALLOWED_URL_SCHEMES`](./security.md#allowed_url_schemes) * [`BANNER_BOTTOM`](./miscellaneous.md#banner_bottom) diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index 90d7ea382..eedb4e5a6 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -144,7 +144,7 @@ Then, compile these portable (`.po`) files for use in the application: * Update the version number and published date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable. * Copy the version number from `release.yaml` to `pyproject.toml` in the project root. -* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`. +* Update the example version numbers in the feature request, bug report, and performance templates under `.github/ISSUE_TEMPLATES/`. * Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release. !!! tip diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md index 0d74eea05..41751fc10 100644 --- a/docs/installation/1-postgresql.md +++ b/docs/installation/1-postgresql.md @@ -51,14 +51,14 @@ You can verify that authentication works by executing the `psql` command and pas ```no-highlight $ psql --username netbox --password --host localhost netbox -Password for user netbox: -psql (12.5 (Ubuntu 12.5-0ubuntu0.20.04.1)) -SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) +Password: +psql (16.11 (Ubuntu 16.11-0ubuntu0.24.04.1)) +SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off) Type "help" for help. netbox=> \conninfo You are connected to database "netbox" as user "netbox" on host "localhost" (address "127.0.0.1") at port "5432". -SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) +SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off) netbox=> \q ``` diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index fd9b21f50..20ff6070f 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -36,7 +36,7 @@ sudo ln -s /opt/netbox-X.Y.Z/ /opt/netbox ``` !!! note - It is recommended to install NetBox in a directory named for its version number. For example, NetBox v3.0.0 would be installed into `/opt/netbox-3.0.0`, and a symlink from `/opt/netbox/` would point to this location. (You can verify this configuration with the command `ls -l /opt | grep netbox`.) This allows for future releases to be installed in parallel without interrupting the current installation. When changing to the new release, only the symlink needs to be updated. + It is recommended to install NetBox in a directory named for its version number. For example, NetBox v4.0.0 would be installed into `/opt/netbox-4.0.0`, and a symlink from `/opt/netbox/` would point to this location. (You can verify this configuration with the command `ls -l /opt | grep netbox`.) This allows for future releases to be installed in parallel without interrupting the current installation. When changing to the new release, only the symlink needs to be updated. ### Option B: Clone the Git Repository @@ -63,12 +63,12 @@ This command should generate output similar to the following: ``` Cloning into '.'... -remote: Enumerating objects: 996, done. -remote: Counting objects: 100% (996/996), done. -remote: Compressing objects: 100% (935/935), done. -remote: Total 996 (delta 148), reused 386 (delta 34), pack-reused 0 -Receiving objects: 100% (996/996), 4.26 MiB | 9.81 MiB/s, done. -Resolving deltas: 100% (148/148), done. +remote: Enumerating objects: 148317, done. +remote: Counting objects: 100% (183/183), done. +remote: Compressing objects: 100% (115/115), done. +remote: Total 148317 (delta 127), reused 68 (delta 68), pack-reused 148134 (from 3) +Receiving objects: 100% (148317/148317), 165.12 MiB | 28.71 MiB/s, done. +Resolving deltas: 100% (116428/116428), done. ``` Finally, check out the tag for the desired release. You can find these on our [releases page](https://github.com/netbox-community/netbox/releases). Replace `vX.Y.Z` with your selected release tag below. @@ -102,7 +102,8 @@ sudo cp configuration_example.py configuration.py Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations: * `ALLOWED_HOSTS` -* `DATABASES` (or `DATABASE`) +* `API_TOKEN_PEPPERS` +* `DATABASES` * `REDIS` * `SECRET_KEY` @@ -158,7 +159,7 @@ DATABASES = { ### REDIS -Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-parameters.md#redis) for more detail on individual parameters. +Redis is an in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-parameters.md#redis) for more detail on individual parameters. Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID. @@ -252,7 +253,7 @@ Once NetBox has been configured, we're ready to proceed with the actual installa sudo /opt/netbox/upgrade.sh ``` -Note that **Python 3.12 or later is required** for NetBox v4.5 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.) +Note that **Python 3.12 or later is required** for NetBox v4.5 and later releases. If the default Python installation on your server is set to a lesser version, pass the path to the supported installation as an environment variable named `PYTHON`. (Note that the environment variable must be passed _after_ the `sudo` command.) ```no-highlight sudo PYTHON=/usr/bin/python3.12 /opt/netbox/upgrade.sh @@ -295,13 +296,12 @@ python3 manage.py runserver 0.0.0.0:8000 --insecure If successful, you should see output similar to the following: ```no-highlight -Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). -August 30, 2021 - 18:02:23 -Django version 3.2.6, using settings 'netbox.settings' -Starting development server at http://127.0.0.1:8000/ +January 26, 2026 - 17:00:00 +Django version 5.2.10, using settings 'netbox.settings' +Starting development server at http://0.0.0.0:8000/ Quit the server with CONTROL-C. ``` diff --git a/docs/installation/4a-gunicorn.md b/docs/installation/4a-gunicorn.md index 91bbcd0e5..7170f8c64 100644 --- a/docs/installation/4a-gunicorn.md +++ b/docs/installation/4a-gunicorn.md @@ -43,16 +43,22 @@ You should see output similar to the following: ```no-highlight ● netbox.service - NetBox WSGI Service - Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled) - Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago + Loaded: loaded (/etc/systemd/system/netbox.service; enabled; preset: enabled) + Active: active (running) since Mon 2026-01-26 11:00:00 CST; 7s ago Docs: https://docs.netbox.dev/ - Main PID: 1140492 (gunicorn) - Tasks: 19 (limit: 4683) - Memory: 666.2M + Main PID: 7283 (gunicorn) + Tasks: 6 (limit: 4545) + Memory: 556.1M (peak: 556.3M) + CPU: 3.387s CGroup: /system.slice/netbox.service - ├─1140492 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va> - ├─1140513 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va> - ├─1140514 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /va> + ├─7283 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox> + ├─7285 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox> + ├─7286 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox> + ├─7287 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox> + ├─7288 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox> + └─7289 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox> + +Jan 26 11:00:00 netbox systemd[1]: Started netbox.service - NetBox WSGI Service. ... ``` diff --git a/docs/installation/5-http-server.md b/docs/installation/5-http-server.md index 7496d3bf4..c75a4fafc 100644 --- a/docs/installation/5-http-server.md +++ b/docs/installation/5-http-server.md @@ -3,7 +3,7 @@ This documentation provides example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](https://httpd.apache.org/docs/current/), though any HTTP server which supports WSGI should be compatible. !!! info - For the sake of brevity, only Ubuntu 20.04 instructions are provided here. These tasks are not unique to NetBox and should carry over to other distributions with minimal changes. Please consult your distribution's documentation for assistance if needed. + For the sake of brevity, only Ubuntu 24.04 instructions are provided here. These tasks are not unique to NetBox and should carry over to other distributions with minimal changes. Please consult your distribution's documentation for assistance if needed. ## Obtain an SSL Certificate diff --git a/docs/installation/index.md b/docs/installation/index.md index 73bf1220c..7ef8dd0d4 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -12,12 +12,12 @@ -The installation instructions provided here have been tested to work on Ubuntu 22.04. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors. +The installation instructions provided here have been tested to work on Ubuntu 24.04. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors. The following sections detail how to set up a new instance of NetBox: 1. [PostgreSQL database](1-postgresql.md) -1. [Redis](2-redis.md) +2. [Redis](2-redis.md) 3. [NetBox components](3-netbox.md) 4. [Gunicorn](4a-gunicorn.md) or [uWSGI](4b-uwsgi.md) 5. [HTTP server](5-http-server.md) diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index ce5282b04..5110def57 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -65,7 +65,7 @@ Download and extract the latest version: ```no-highlight # Set $NEWVER to the NetBox version being installed -NEWVER=3.5.0 +NEWVER=4.5.0 wget https://github.com/netbox-community/netbox/archive/v$NEWVER.tar.gz sudo tar -xzf v$NEWVER.tar.gz -C /opt sudo ln -sfn /opt/netbox-$NEWVER/ /opt/netbox @@ -75,7 +75,7 @@ Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if pres ```no-highlight # Set $OLDVER to the NetBox version currently installed -OLDVER=3.4.9 +OLDVER=4.4.10 sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/ sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/ sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ @@ -116,7 +116,7 @@ Check out the desired release by specifying its tag. For example: ``` cd /opt/netbox && \ sudo git fetch --tags && \ -sudo git checkout v4.2.7 +sudo git checkout v4.5.0 ``` ## 4. Run the Upgrade Script @@ -128,7 +128,7 @@ sudo ./upgrade.sh ``` !!! warning - If the default version of Python is not at least 3.10, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example: + If the default version of Python is not **at least 3.12**, you'll need to pass the path to a supported Python version as an environment variable when calling the upgrade script. For example: ```no-highlight sudo PYTHON=/usr/bin/python3.12 ./upgrade.sh diff --git a/docs/integrations/graphql-api.md b/docs/integrations/graphql-api.md index 39309671c..a05fcbc53 100644 --- a/docs/integrations/graphql-api.md +++ b/docs/integrations/graphql-api.md @@ -133,23 +133,67 @@ The field "class_type" is an easy way to distinguish what type of object it is w ## Pagination -Queries can be paginated by specifying pagination in the query and supplying an offset and optionaly a limit in the query. If no limit is given, a default of 100 is used. Queries are not paginated unless requested in the query. An example paginated query is shown below: +The GraphQL API supports two types of pagination. Offset-based pagination operates using an offset relative to the first record in a set, specified by the `offset` parameter. For example, the response to a request specifying an offset of 100 will contain the 101st and later matching records. Offset-based pagination feels very natural, but its performance can suffer when dealing with large data sets due to the overhead involved in calculating the relative offset. + +The alternative approach is cursor-based pagination, which operates using absolute (rather than relative) primary key values. (These are the numeric IDs assigned to each object in the database.) When using cursor-based pagination, the response will contain records with a primary key greater than or equal to the specified start value, up to the maximum number of results. This strategy requires keeping track of the last seen primary key from each response when paginating through data, but is extremely performant. The cursor is specified by passing the starting object ID via the `start` parameter. + +To ensure consistent ordering, objects will always be ordered by their primary keys when cursor-based pagination is used. + +!!! note "Cursor-based pagination was introduced in NetBox v4.5.2." + +Both pagination strategies support passing an optional `limit` parameter. In both approaches, this specifies the maximum number of objects to include in the response. If no limit is specified, a default value of 100 is used. + +### Offset Pagination + +The first page will have an `offset` of zero, or the `offset` parameter will be omitted: ``` query { - device_list(pagination: { offset: 0, limit: 20 }) { + device_list(pagination: {offset: 0, limit: 20}) { id } } ``` +The second page will have an offset equal to the size of the first page. If the number of records is less than the specified limit, there are no more records to process. For example, if a request specifies a `limit` of 20 but returns only 13 records, we can conclude that this is the final page of records. + +``` +query { + device_list(pagination: {offset: 20, limit: 20}) { + id + } +} +``` + +### Cursor Pagination + +Set the `start` value to zero to fetch the first page. Note that if the `start` parameter is omitted, offset-based pagination will be used by default. + +``` +query { + device_list(pagination: {start: 0, limit: 20}) { + id + } +} +``` + +To determine the `start` value for the next page, add 1 to the primary key (`id`) of the last record in the previous page. + +For example, if the ID of the last record in the previous response was 123, we would specify a `start` value of 124: + +``` +query { + device_list(pagination: {start: 124, limit: 20}) { + id + } +} +``` + +This will return up to 20 records with an ID greater than or equal to 124. + ## Authentication -NetBox's GraphQL API uses the same API authentication tokens as its REST API. Authentication tokens are included with requests by attaching an `Authorization` HTTP header in the following form: - -``` -Authorization: Token $TOKEN -``` +NetBox's GraphQL API uses the same API authentication tokens as its REST API. See the [REST API authentication](./rest-api.md#authentication) documentation for further detail. ## Disabling the GraphQL API diff --git a/docs/integrations/rest-api.md b/docs/integrations/rest-api.md index 66a95d924..bac984a08 100644 --- a/docs/integrations/rest-api.md +++ b/docs/integrations/rest-api.md @@ -215,9 +215,51 @@ http://netbox/api/ipam/ip-addresses/ \ If we wanted to assign this IP address to a virtual machine interface instead, we would have set `assigned_object_type` to `virtualization.vminterface` and updated the object ID appropriately. -### Brief Format +### Specifying Fields -Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. As an example, the default (complete) format of a prefix looks like this: +A REST API response will include all available fields for the object type by default. If you wish to return only a subset of the available fields, you can append `?fields=` to the URL followed by a comma-separated list of field names. For example, the following request will return only the `id`, `name`, `status`, and `region` fields for each site in the response. + +``` +GET /api/dcim/sites/?fields=id,name,status,region +``` + +```json +{ + "id": 1, + "name": "DM-NYC", + "status": { + "value": "active", + "label": "Active" + }, + "region": { + "id": 43, + "url": "http://netbox:8000/api/dcim/regions/43/", + "display": "New York", + "name": "New York", + "slug": "us-ny", + "description": "", + "site_count": 0, + "_depth": 2 + } +} +``` + +Similarly, you can opt to omit only specific fields by passing the `omit` parameter: + +``` +GET /api/dcim/sites/?omit=circuit_count,device_count,virtualmachine_count +``` + +!!! note "The `omit` parameter was introduced in NetBox v4.5.2." + +Strategic use of the `fields` and `omit` parameters can drastically improve REST API performance, as the exclusion of fields which reference related objects reduces the number and complexity of underlying database queries needed to generate the response. + +!!! note + The `fields` and `omit` parameters should be considered mutually exclusive. If both are passed, `fields` takes precedence. + +#### Brief Format + +Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. It's also more convenient than listing out individual fields via the `fields` or `omit` parameters. As an example, the default (complete) format of a prefix looks like this: ```no-highlight GET /api/ipam/prefixes/13980/ @@ -270,10 +312,10 @@ GET /api/ipam/prefixes/13980/ } ``` -The brief format is much more terse: +The brief format includes only a few fields: ```no-highlight -GET /api/ipam/prefixes/13980/?brief=1 +GET /api/ipam/prefixes/13980/?brief=true ``` ```json diff --git a/docs/release-notes/version-4.5.md b/docs/release-notes/version-4.5.md index 9372009c0..cda2d878e 100644 --- a/docs/release-notes/version-4.5.md +++ b/docs/release-notes/version-4.5.md @@ -1,5 +1,53 @@ # NetBox v4.5 +## v4.5.2 (2026-02-03) + +### Enhancements + +* [#15801](https://github.com/netbox-community/netbox/issues/15801) - Add link peer and connection columns to the VLAN device interfaces table +* [#19221](https://github.com/netbox-community/netbox/issues/19221) - Truncate long image attachment filenames in the UI +* [#19869](https://github.com/netbox-community/netbox/issues/19869) - Display peer connections for LAG member interfaces +* [#20052](https://github.com/netbox-community/netbox/issues/20052) - Increase logging level of error message when a custom script fails to load +* [#20172](https://github.com/netbox-community/netbox/issues/20172) - Add `cabled` filter for interfaces in GraphQL API +* [#21081](https://github.com/netbox-community/netbox/issues/21081) - Add owner group table columns & filters across all supported object list views +* [#21088](https://github.com/netbox-community/netbox/issues/21088) - Add max depth and max length dropdowns for child prefix views +* [#21110](https://github.com/netbox-community/netbox/issues/21110) - Support cursor-based pagination in GraphQL API +* [#21201](https://github.com/netbox-community/netbox/issues/21201) - Pre-populate GenericForeignKey form fields when cloning +* [#21209](https://github.com/netbox-community/netbox/issues/21209) - Ignore case sensitivity for configuration parameters which specify an app label and model name +* [#21228](https://github.com/netbox-community/netbox/issues/21228) - Support image attachments for rack types +* [#21244](https://github.com/netbox-community/netbox/issues/21244) - Enable omitting specific fields from REST API responses with `?omit=` parameter + +### Performance Improvements + +* [#21249](https://github.com/netbox-community/netbox/issues/21249) - Avoid extraneous user query when no event rules are present +* [#21259](https://github.com/netbox-community/netbox/issues/21259) - Cache ObjectType lookups for the duration of a request +* [#21260](https://github.com/netbox-community/netbox/issues/21260) - Defer object serialization for events pipeline processing +* [#21263](https://github.com/netbox-community/netbox/issues/21263) - Prefetch related objects after creating/updating objects via REST API +* [#21300](https://github.com/netbox-community/netbox/issues/21300) - Cache custom field lookups for the duration of a request +* [#21302](https://github.com/netbox-community/netbox/issues/21302) - Avoid redundant uniqueness checks in ValidatedModelSerializer +* [#21303](https://github.com/netbox-community/netbox/issues/21303) - Cache post-change snapshot on each instance after serialization +* [#21327](https://github.com/netbox-community/netbox/issues/21327) - Always leverage `get_by_natural_key()` to resolve ContentTypes + +### Bug Fixes + +* [#20212](https://github.com/netbox-community/netbox/issues/20212) - Fix support for image attachment thumbnails when using S3 storage +* [#20383](https://github.com/netbox-community/netbox/issues/20383) - When editing a device, clearing the assigned unit should also clear the rack face selection +* [#20902](https://github.com/netbox-community/netbox/issues/20902) - Avoid `SyncError` exception when Git URL contains an embedded username +* [#20977](https://github.com/netbox-community/netbox/issues/20977) - "Run again" button should respect script variable defaults +* [#21115](https://github.com/netbox-community/netbox/issues/21115) - Include `attribute_data` in ModuleType YAML export +* [#21129](https://github.com/netbox-community/netbox/issues/21129) - Store queue name on the Job model to ensure deletion of associated RQ task when a non-default queue is used +* [#21168](https://github.com/netbox-community/netbox/issues/21168) - Fix Application Service cloning to preserve parent object +* [#21173](https://github.com/netbox-community/netbox/issues/21173) - Ensure all plugin menu items are registered regardless of initialization order +* [#21176](https://github.com/netbox-community/netbox/issues/21176) - Remove checkboxes from IP ranges in mixed-type tables +* [#21202](https://github.com/netbox-community/netbox/issues/21202) - Fix scoped form cloning clearing the `scope` field when `scope_type` changes +* [#21214](https://github.com/netbox-community/netbox/issues/21214) - Clean up AutoSyncRecord when detaching from DataSource +* [#21242](https://github.com/netbox-community/netbox/issues/21242) - Navigation menu items for authentication should not require `staff_only` permission +* [#21254](https://github.com/netbox-community/netbox/issues/21254) - Fix `AttributeError` exception when checking for latest release +* [#21262](https://github.com/netbox-community/netbox/issues/21262) - Assigned scope should be replicated when cloning a prefix +* [#21269](https://github.com/netbox-community/netbox/issues/21269) - Fix replication of front/rear port assignments from the module type when installing a module + +--- + ## v4.5.1 (2026-01-20) ### Enhancements diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index c71f5c65c..ec754f697 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -34,9 +34,10 @@ __all__ = ( class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm): model = Provider fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), FieldSet('asn_id', name=_('ASN')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) region_id = DynamicModelMultipleChoiceField( @@ -69,8 +70,9 @@ class ProviderFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm): class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm): model = ProviderAccount fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('provider_id', 'account', name=_('Attributes')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) provider_id = DynamicModelMultipleChoiceField( @@ -88,8 +90,9 @@ class ProviderAccountFilterForm(ContactModelFilterForm, PrimaryModelFilterSetFor class ProviderNetworkFilterForm(PrimaryModelFilterSetForm): model = ProviderNetwork fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('provider_id', 'service_id', name=_('Attributes')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) provider_id = DynamicModelMultipleChoiceField( queryset=Provider.objects.all(), @@ -107,8 +110,9 @@ class ProviderNetworkFilterForm(PrimaryModelFilterSetForm): class CircuitTypeFilterForm(OrganizationalModelFilterSetForm): model = CircuitType fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('color', name=_('Attributes')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) tag = TagFilterField(model) @@ -121,7 +125,7 @@ class CircuitTypeFilterForm(OrganizationalModelFilterSetForm): class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm): model = Circuit fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')), FieldSet( 'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit', @@ -129,6 +133,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelF ), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id') @@ -274,8 +279,9 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm): class CircuitGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm): model = CircuitGroup fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) tag = TagFilterField(model) @@ -312,8 +318,9 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm): class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm): model = VirtualCircuitType fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('color', name=_('Attributes')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) tag = TagFilterField(model) @@ -326,10 +333,11 @@ class VirtualCircuitTypeFilterForm(OrganizationalModelFilterSetForm): class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm): model = VirtualCircuit fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')), FieldSet('type_id', 'status', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id') provider_id = DynamicModelMultipleChoiceField( diff --git a/netbox/core/api/serializers_/jobs.py b/netbox/core/api/serializers_/jobs.py index 26726ebdd..5693a8099 100644 --- a/netbox/core/api/serializers_/jobs.py +++ b/netbox/core/api/serializers_/jobs.py @@ -31,7 +31,8 @@ class JobSerializer(BaseModelSerializer): model = Job fields = [ 'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'object', 'name', 'status', 'created', - 'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries', + 'scheduled', 'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'queue_name', + 'log_entries', ] brief_fields = ('url', 'created', 'completed', 'user', 'status') diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py index 9ba1d5dfd..86e16afa2 100644 --- a/netbox/core/data_backends.py +++ b/netbox/core/data_backends.py @@ -21,11 +21,24 @@ __all__ = ( 'GitBackend', 'LocalBackend', 'S3Backend', + 'url_has_embedded_credentials', ) logger = logging.getLogger('netbox.data_backends') +def url_has_embedded_credentials(url): + """ + Check if a URL contains embedded credentials (username in the URL). + + URLs like 'https://user@bitbucket.org/...' have embedded credentials. + This is used to avoid passing explicit credentials to dulwich when the + URL already contains them, which would cause authentication conflicts. + """ + parsed = urlparse(url) + return bool(parsed.username) + + @register_data_backend() class LocalBackend(DataBackend): name = 'local' @@ -102,7 +115,9 @@ class GitBackend(DataBackend): clone_args['pool_manager'] = ProxyPoolManager(self.socks_proxy) if self.url_scheme in ('http', 'https'): - if self.params.get('username'): + # Only pass explicit credentials if URL doesn't already contain embedded username + # to avoid credential conflicts (see #20902) + if not url_has_embedded_credentials(self.url) and self.params.get('username'): clone_args.update( { "username": self.params.get('username'), diff --git a/netbox/core/filtersets.py b/netbox/core/filtersets.py index a531c051e..13f046b5b 100644 --- a/netbox/core/filtersets.py +++ b/netbox/core/filtersets.py @@ -129,10 +129,14 @@ class JobFilterSet(BaseFilterSet): choices=JobStatusChoices, null_value=None ) + queue_name = django_filters.CharFilter() class Meta: model = Job - fields = ('id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id') + fields = ( + 'id', 'object_type', 'object_type_id', 'object_id', 'name', 'interval', 'status', 'user', 'job_id', + 'queue_name', + ) def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py index 69e6a4fbb..d0e78507c 100644 --- a/netbox/core/forms/filtersets.py +++ b/netbox/core/forms/filtersets.py @@ -26,8 +26,9 @@ __all__ = ( class DataSourceFilterForm(PrimaryModelFilterSetForm): model = DataSource fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('type', 'status', 'enabled', 'sync_interval', name=_('Data Source')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) type = forms.MultipleChoiceField( label=_('Type'), @@ -71,7 +72,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm): model = Job fieldsets = ( FieldSet('q', 'filter_id'), - FieldSet('object_type_id', 'status', name=_('Attributes')), + FieldSet('object_type_id', 'status', 'queue_name', name=_('Attributes')), FieldSet( 'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before', 'started__after', 'completed__before', 'completed__after', 'user', name=_('Creation') @@ -87,6 +88,10 @@ class JobFilterForm(SavedFiltersMixin, FilterForm): choices=JobStatusChoices, required=False ) + queue_name = forms.CharField( + label=_('Queue'), + required=False + ) created__after = forms.DateTimeField( label=_('Created after'), required=False, diff --git a/netbox/core/migrations/0021_job_queue_name.py b/netbox/core/migrations/0021_job_queue_name.py new file mode 100644 index 000000000..1525f9e7d --- /dev/null +++ b/netbox/core/migrations/0021_job_queue_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.9 on 2026-01-27 00:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0020_owner'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='queue_name', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py index 8a6bf6a1d..4427bb7ff 100644 --- a/netbox/core/models/jobs.py +++ b/netbox/core/models/jobs.py @@ -112,6 +112,12 @@ class Job(models.Model): verbose_name=_('job ID'), unique=True ) + queue_name = models.CharField( + verbose_name=_('queue name'), + max_length=100, + blank=True, + help_text=_('Name of the queue in which this job was enqueued') + ) log_entries = ArrayField( verbose_name=_('log entries'), base_field=models.JSONField( @@ -179,11 +185,15 @@ class Job(models.Model): return f"{int(minutes)} minutes, {seconds:.2f} seconds" def delete(self, *args, **kwargs): + # Use the stored queue name, or fall back to get_queue_for_model for legacy jobs + rq_queue_name = self.queue_name or get_queue_for_model(self.object_type.model if self.object_type else None) + rq_job_id = str(self.job_id) + super().delete(*args, **kwargs) - rq_queue_name = get_queue_for_model(self.object_type.model if self.object_type else None) + # Cancel the RQ job using the stored queue name queue = django_rq.get_queue(rq_queue_name) - job = queue.fetch_job(str(self.job_id)) + job = queue.fetch_job(rq_job_id) if job: try: @@ -288,7 +298,8 @@ class Job(models.Model): scheduled=schedule_at, interval=interval, user=user, - job_id=uuid.uuid4() + job_id=uuid.uuid4(), + queue_name=rq_queue_name ) job.full_clean() job.save() diff --git a/netbox/core/models/object_types.py b/netbox/core/models/object_types.py index 21fbde3b3..1daeb2c4b 100644 --- a/netbox/core/models/object_types.py +++ b/netbox/core/models/object_types.py @@ -9,6 +9,7 @@ from django.db import connection, models from django.db.models import Q from django.utils.translation import gettext as _ +from netbox.context import query_cache from netbox.plugins import PluginConfig from netbox.registry import registry from utilities.string import title @@ -70,6 +71,12 @@ class ObjectTypeManager(models.Manager): """ from netbox.models.features import get_model_features, model_is_public + # Check the request cache before hitting the database + cache = query_cache.get() + if cache is not None: + if ot := cache['object_types'].get((model._meta.model, for_concrete_model)): + return ot + # TODO: Remove this in NetBox v5.0 # If the ObjectType table has not yet been provisioned (e.g. because we're in a pre-v4.4 migration), # fall back to ContentType. @@ -96,6 +103,10 @@ class ObjectTypeManager(models.Manager): features=get_model_features(model), )[0] + # Populate the request cache to avoid redundant lookups + if cache is not None: + cache['object_types'][(model._meta.model, for_concrete_model)] = ot + return ot def get_for_models(self, *models, for_concrete_models=True): diff --git a/netbox/core/signals.py b/netbox/core/signals.py index d918d2389..6316cfd01 100644 --- a/netbox/core/signals.py +++ b/netbox/core/signals.py @@ -18,6 +18,7 @@ from extras.events import enqueue_event from extras.models import Tag from extras.utils import run_validators from netbox.config import get_config +from utilities.data import get_config_value_ci from netbox.context import current_request, events_queue from netbox.models.features import ChangeLoggingMixin, get_model_features, model_is_public from utilities.exceptions import AbortRequest @@ -168,7 +169,7 @@ def handle_deleted_object(sender, instance, **kwargs): # to queueing any events for the object being deleted, in case a validation error is # raised, causing the deletion to fail. model_name = f'{sender._meta.app_label}.{sender._meta.model_name}' - validators = get_config().PROTECTION_RULES.get(model_name, []) + validators = get_config_value_ci(get_config().PROTECTION_RULES, model_name, default=[]) try: run_validators(instance, validators) except ValidationError as e: diff --git a/netbox/core/tables/jobs.py b/netbox/core/tables/jobs.py index 00032057f..3720baa39 100644 --- a/netbox/core/tables/jobs.py +++ b/netbox/core/tables/jobs.py @@ -42,6 +42,9 @@ class JobTable(NetBoxTable): completed = columns.DateTimeColumn( verbose_name=_('Completed'), ) + queue_name = tables.Column( + verbose_name=_('Queue'), + ) log_entries = tables.Column( verbose_name=_('Log Entries'), ) @@ -53,7 +56,7 @@ class JobTable(NetBoxTable): model = Job fields = ( 'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started', - 'completed', 'user', 'error', 'job_id', + 'completed', 'user', 'queue_name', 'log_entries', 'error', 'job_id', ) default_columns = ( 'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user', diff --git a/netbox/core/tests/test_data_backends.py b/netbox/core/tests/test_data_backends.py new file mode 100644 index 000000000..8020eb76c --- /dev/null +++ b/netbox/core/tests/test_data_backends.py @@ -0,0 +1,116 @@ +from unittest import skipIf +from unittest.mock import patch + +from django.test import TestCase + +from core.data_backends import url_has_embedded_credentials + +try: + import dulwich # noqa: F401 + DULWICH_AVAILABLE = True +except ImportError: + DULWICH_AVAILABLE = False + + +class URLEmbeddedCredentialsTests(TestCase): + def test_url_with_embedded_username(self): + self.assertTrue(url_has_embedded_credentials('https://myuser@bitbucket.org/workspace/repo.git')) + + def test_url_without_embedded_username(self): + self.assertFalse(url_has_embedded_credentials('https://bitbucket.org/workspace/repo.git')) + + def test_url_with_username_and_password(self): + self.assertTrue(url_has_embedded_credentials('https://user:pass@bitbucket.org/workspace/repo.git')) + + def test_various_providers_with_embedded_username(self): + urls = [ + 'https://user@bitbucket.org/workspace/repo.git', + 'https://user@github.com/owner/repo.git', + 'https://deploy-key@gitlab.com/group/project.git', + 'http://user@internal-git.example.com/repo.git', + ] + for url in urls: + with self.subTest(url=url): + self.assertTrue(url_has_embedded_credentials(url)) + + def test_various_providers_without_embedded_username(self): + """Various Git providers without embedded usernames.""" + urls = [ + 'https://bitbucket.org/workspace/repo.git', + 'https://github.com/owner/repo.git', + 'https://gitlab.com/group/project.git', + 'http://internal-git.example.com/repo.git', + ] + for url in urls: + with self.subTest(url=url): + self.assertFalse(url_has_embedded_credentials(url)) + + def test_ssh_url(self): + # git@host:path format doesn't parse as having a username in the traditional sense + self.assertFalse(url_has_embedded_credentials('git@github.com:owner/repo.git')) + + def test_file_url(self): + self.assertFalse(url_has_embedded_credentials('file:///path/to/repo')) + + +@skipIf(not DULWICH_AVAILABLE, "dulwich is not installed") +class GitBackendCredentialIntegrationTests(TestCase): + """ + Integration tests that verify GitBackend correctly applies credential logic. + + These tests require dulwich to be installed and verify the full integration + of the credential handling in GitBackend.fetch(). + """ + + def _get_clone_kwargs(self, url, **params): + from core.data_backends import GitBackend + + backend = GitBackend(url=url, **params) + + with patch('dulwich.porcelain.clone') as mock_clone, \ + patch('dulwich.porcelain.NoneStream'): + try: + with backend.fetch(): + pass + except Exception: + pass + + if mock_clone.called: + return mock_clone.call_args.kwargs + return {} + + def test_url_with_embedded_username_skips_explicit_credentials(self): + kwargs = self._get_clone_kwargs( + url='https://myuser@bitbucket.org/workspace/repo.git', + username='myuser', + password='my-api-key' + ) + + self.assertEqual(kwargs.get('username'), None) + self.assertEqual(kwargs.get('password'), None) + + def test_url_without_embedded_username_passes_explicit_credentials(self): + kwargs = self._get_clone_kwargs( + url='https://bitbucket.org/workspace/repo.git', + username='myuser', + password='my-api-key' + ) + + self.assertEqual(kwargs.get('username'), 'myuser') + self.assertEqual(kwargs.get('password'), 'my-api-key') + + def test_url_with_embedded_username_no_explicit_credentials(self): + kwargs = self._get_clone_kwargs( + url='https://myuser@bitbucket.org/workspace/repo.git' + ) + + self.assertEqual(kwargs.get('username'), None) + self.assertEqual(kwargs.get('password'), None) + + def test_public_repo_no_credentials(self): + kwargs = self._get_clone_kwargs( + url='https://github.com/public/repo.git' + ) + + self.assertEqual(kwargs.get('username'), None) + self.assertEqual(kwargs.get('password'), None) diff --git a/netbox/core/tests/test_models.py b/netbox/core/tests/test_models.py index 28225c7a6..3a1c2acc7 100644 --- a/netbox/core/tests/test_models.py +++ b/netbox/core/tests/test_models.py @@ -1,8 +1,10 @@ +from unittest.mock import patch, MagicMock + from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.test import TestCase -from core.models import DataSource, ObjectType +from core.models import DataSource, Job, ObjectType from core.choices import ObjectChangeActionChoices from dcim.models import Site, Location, Device from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED @@ -200,3 +202,38 @@ class ObjectTypeTest(TestCase): bookmarks_ots = ObjectType.objects.with_feature('bookmarks') self.assertIn(ObjectType.objects.get_by_natural_key('dcim', 'site'), bookmarks_ots) self.assertNotIn(ObjectType.objects.get_by_natural_key('dcim', 'cabletermination'), bookmarks_ots) + + +class JobTest(TestCase): + + @patch('core.models.jobs.django_rq.get_queue') + def test_delete_cancels_job_from_correct_queue(self, mock_get_queue): + """ + Test that when a job is deleted, it's canceled from the correct queue. + """ + mock_queue = MagicMock() + mock_rq_job = MagicMock() + mock_queue.fetch_job.return_value = mock_rq_job + mock_get_queue.return_value = mock_queue + + def dummy_func(**kwargs): + pass + + # Enqueue a job with a custom queue name + custom_queue = 'my_custom_queue' + job = Job.enqueue( + func=dummy_func, + name='Test Job', + queue_name=custom_queue + ) + + # Reset mock to clear enqueue call + mock_get_queue.reset_mock() + + # Delete the job + job.delete() + + # Verify the correct queue was used for cancellation + mock_get_queue.assert_called_with(custom_queue) + mock_queue.fetch_job.assert_called_with(str(job.job_id)) + mock_rq_job.cancel.assert_called_once() diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 63f59c6c8..2c539e896 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -12,11 +12,12 @@ from netbox.forms import ( NestedGroupModelFilterSetForm, NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm, ) +from netbox.forms.mixins import OwnerFilterMixin from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from tenancy.models import Tenant -from users.models import Owner, User +from users.models import User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice -from utilities.forms.fields import ColorField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.rendering import FieldSet from utilities.forms.widgets import NumberWithOptions from virtualization.models import Cluster, ClusterGroup, VirtualMachine @@ -70,11 +71,11 @@ __all__ = ( 'SiteFilterForm', 'SiteGroupFilterForm', 'VirtualChassisFilterForm', - 'VirtualDeviceContextFilterForm' + 'VirtualDeviceContextFilterForm', ) -class DeviceComponentFilterForm(NetBoxModelFilterSetForm): +class DeviceComponentFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm): name = forms.CharField( label=_('Name'), required=False @@ -157,18 +158,14 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm): required=False, label=_('Device Status'), ) - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm): model = Region fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('parent_id', name=_('Region')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')) ) parent_id = DynamicModelMultipleChoiceField( @@ -182,8 +179,9 @@ class RegionFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm): class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm): model = SiteGroup fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('parent_id', name=_('Site Group')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')) ) parent_id = DynamicModelMultipleChoiceField( @@ -197,9 +195,10 @@ class SiteGroupFilterForm(ContactModelFilterForm, NestedGroupModelFilterSetForm) class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilterSetForm): model = Site fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('status', 'region_id', 'group_id', 'asn_id', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) selector_fields = ('filter_id', 'q', 'region_id', 'group_id') @@ -229,9 +228,10 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, PrimaryModelFilt class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupModelFilterSetForm): model = Location fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', 'facility', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) region_id = DynamicModelMultipleChoiceField( @@ -277,7 +277,8 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NestedGroupM class RackRoleFilterForm(OrganizationalModelFilterSetForm): model = RackRole fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) tag = TagFilterField(model) @@ -328,10 +329,11 @@ class RackBaseFilterForm(PrimaryModelFilterSetForm): class RackTypeFilterForm(RackBaseFilterForm): model = RackType fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('manufacturer_id', 'form_factor', 'width', 'u_height', 'rack_count', name=_('Rack Type')), FieldSet('starting_unit', 'desc_units', name=_('Numbering')), FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) selector_fields = ('filter_id', 'q', 'manufacturer_id') manufacturer_id = DynamicModelMultipleChoiceField( @@ -350,13 +352,14 @@ class RackTypeFilterForm(RackBaseFilterForm): class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm): model = Rack fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')), - FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')), FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Hardware')), FieldSet('starting_unit', 'desc_units', name=_('Numbering')), FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')), + FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id') @@ -433,9 +436,10 @@ class RackElevationFilterForm(RackFilterForm): FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'id', name=_('Location')), FieldSet('status', 'role_id', name=_('Function')), FieldSet('type', 'width', 'serial', 'asset_tag', name=_('Hardware')), - FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), - FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')), + FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), + FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), @@ -451,10 +455,11 @@ class RackElevationFilterForm(RackFilterForm): class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = RackReservation fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('status', 'user_id', name=_('Reservation')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -509,7 +514,8 @@ class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSetForm): model = Manufacturer fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')) ) tag = TagFilterField(model) @@ -518,7 +524,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, OrganizationalModelFilterSe class DeviceTypeFilterForm(PrimaryModelFilterSetForm): model = DeviceType fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet( 'manufacturer_id', 'default_platform_id', 'part_number', 'device_count', 'subdevice_role', 'airflow', name=_('Hardware') @@ -529,6 +535,7 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm): 'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items', name=_('Components') ), FieldSet('weight', 'weight_unit', name=_('Weight')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) selector_fields = ('filter_id', 'q', 'manufacturer_id') manufacturer_id = DynamicModelMultipleChoiceField( @@ -652,7 +659,8 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm): class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm): model = ModuleTypeProfile fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) selector_fields = ('filter_id', 'q') tag = TagFilterField(model) @@ -661,7 +669,7 @@ class ModuleTypeProfileFilterForm(PrimaryModelFilterSetForm): class ModuleTypeFilterForm(PrimaryModelFilterSetForm): model = ModuleType fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet( 'profile_id', 'manufacturer_id', 'part_number', 'module_count', 'airflow', name=_('Hardware') @@ -671,6 +679,7 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm): 'pass_through_ports', name=_('Components') ), FieldSet('weight', 'weight_unit', name=_('Weight')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) selector_fields = ('filter_id', 'q', 'manufacturer_id') profile_id = DynamicModelMultipleChoiceField( @@ -754,8 +763,9 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm): class DeviceRoleFilterForm(NestedGroupModelFilterSetForm): model = DeviceRole fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), - FieldSet('parent_id', 'config_template_id', name=_('Device Role')) + FieldSet('q', 'filter_id', 'tag'), + FieldSet('parent_id', 'config_template_id', name=_('Device Role')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) config_template_id = DynamicModelMultipleChoiceField( queryset=ConfigTemplate.objects.all(), @@ -773,8 +783,9 @@ class DeviceRoleFilterForm(NestedGroupModelFilterSetForm): class PlatformFilterForm(NestedGroupModelFilterSetForm): model = Platform fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), - FieldSet('manufacturer_id', 'parent_id', 'config_template_id', name=_('Platform')) + FieldSet('q', 'filter_id', 'tag'), + FieldSet('manufacturer_id', 'parent_id', 'config_template_id', name=_('Platform')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) selector_fields = ('filter_id', 'q', 'manufacturer_id') parent_id = DynamicModelMultipleChoiceField( @@ -803,11 +814,12 @@ class DeviceFilterForm( ): model = Device fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_('Operation')), FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_('Hardware')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), FieldSet( 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', @@ -996,9 +1008,10 @@ class DeviceFilterForm( class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = VirtualDeviceContext fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('device', 'status', 'has_primary_ip', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) device = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), @@ -1023,9 +1036,10 @@ class VirtualDeviceContextFilterForm(TenancyFilterForm, PrimaryModelFilterSetFor class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = Module fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')), FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) device_id = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), @@ -1106,9 +1120,10 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryM class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = VirtualChassis fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), - FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -1135,10 +1150,11 @@ class VirtualChassisFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = Cable fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')), FieldSet('type', 'status', 'profile', 'color', 'length', 'length_unit', 'unterminated', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -1224,8 +1240,9 @@ class CableFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm): model = PowerPanel fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) selector_fields = ('filter_id', 'q', 'site_id', 'location_id') @@ -1263,10 +1280,11 @@ class PowerPanelFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm): class PowerFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = PowerFeed fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id', name=_('Location')), - FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', name=_('Attributes')), + FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -1390,7 +1408,7 @@ class PathEndpointFilterForm(CabledFilterForm): class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = ConsolePort fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1398,6 +1416,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): name=_('Device') ), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) type = forms.MultipleChoiceField( label=_('Type'), @@ -1429,7 +1448,7 @@ class ConsolePortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm): class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = ConsoleServerPort fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1437,6 +1456,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF name=_('Device') ), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) type = forms.MultipleChoiceField( label=_('Type'), @@ -1468,7 +1488,7 @@ class ConsoleServerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterFo class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = PowerPort fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('name', 'label', 'type', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1476,6 +1496,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): name=_('Device') ), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) type = forms.MultipleChoiceField( label=_('Type'), @@ -1502,7 +1523,7 @@ class PowerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm): class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = PowerOutlet fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('name', 'label', 'type', 'color', 'status', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1510,6 +1531,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): name=_('Device') ), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) type = forms.MultipleChoiceField( label=_('Type'), @@ -1545,7 +1567,7 @@ class PowerOutletTemplateFilterForm(ModularDeviceComponentTemplateFilterForm): class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = Interface fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only', name=_('Attributes')), FieldSet('vrf_id', 'l2vpn_id', 'mac_address', 'wwn', name=_('Addressing')), FieldSet('poe_mode', 'poe_type', name=_('PoE')), @@ -1558,6 +1580,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): name=_('Device') ), FieldSet('cabled', 'connected', 'occupied', name=_('Connection')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) selector_fields = ('filter_id', 'q', 'device_id') vdc_id = DynamicModelMultipleChoiceField( @@ -1716,7 +1739,7 @@ class InterfaceTemplateFilterForm(ModularDeviceComponentTemplateFilterForm): class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('name', 'label', 'type', 'color', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1724,6 +1747,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): name=_('Device') ), FieldSet('cabled', 'occupied', name=_('Cable')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) model = FrontPort type = forms.MultipleChoiceField( @@ -1759,7 +1783,7 @@ class FrontPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm): class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): model = RearPort fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('name', 'label', 'type', 'color', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( @@ -1767,6 +1791,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): name=_('Device') ), FieldSet('cabled', 'occupied', name=_('Cable')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) type = forms.MultipleChoiceField( label=_('Type'), @@ -1801,13 +1826,14 @@ class RearPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm): class ModuleBayFilterForm(DeviceComponentFilterForm): model = ModuleBay fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('name', 'label', 'position', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') ), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) tag = TagFilterField(model) position = forms.CharField( @@ -1832,13 +1858,14 @@ class ModuleBayTemplateFilterForm(ModularDeviceComponentTemplateFilterForm): class DeviceBayFilterForm(DeviceComponentFilterForm): model = DeviceBay fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('name', 'label', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet( 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') ), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) tag = TagFilterField(model) @@ -1855,7 +1882,7 @@ class DeviceBayTemplateFilterForm(DeviceComponentTemplateFilterForm): class InventoryItemFilterForm(DeviceComponentFilterForm): model = InventoryItem fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet( 'name', 'label', 'status', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered', name=_('Attributes') @@ -1865,6 +1892,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): 'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device') ), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) role_id = DynamicModelMultipleChoiceField( queryset=InventoryItemRole.objects.all(), @@ -1925,7 +1953,8 @@ class InventoryItemTemplateFilterForm(DeviceComponentTemplateFilterForm): class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm): model = InventoryItemRole fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) tag = TagFilterField(model) @@ -1937,9 +1966,10 @@ class InventoryItemRoleFilterForm(OrganizationalModelFilterSetForm): class MACAddressFilterForm(PrimaryModelFilterSetForm): model = MACAddress fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('mac_address', name=_('Attributes')), FieldSet('device_id', 'virtual_machine_id', 'assigned', 'primary', name=_('Assignments')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) selector_fields = ('filter_id', 'q', 'device_id', 'virtual_machine_id') mac_address = forms.CharField( diff --git a/netbox/dcim/forms/mixins.py b/netbox/dcim/forms/mixins.py index af618eb83..30314274d 100644 --- a/netbox/dcim/forms/mixins.py +++ b/netbox/dcim/forms/mixins.py @@ -75,7 +75,7 @@ class ScopedForm(forms.Form): except ObjectDoesNotExist: pass - if self.instance and scope_type_id != self.instance.scope_type_id: + if self.instance and self.instance.pk and scope_type_id != self.instance.scope_type_id: self.initial['scope'] = None else: diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 8ebc95ae1..13d5f512b 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -20,7 +20,9 @@ from utilities.forms.fields import ( DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, ) from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups -from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK +from utilities.forms.widgets import ( + APISelect, ClearableFileInput, ClearableSelect, HTMXSelect, NumberWithOptions, SelectWithPK, +) from utilities.jsonschema import JSONSchemaProperty from virtualization.models import Cluster, VMInterface from wireless.models import WirelessLAN, WirelessLANGroup @@ -592,6 +594,14 @@ class DeviceForm(TenancyForm, PrimaryModelForm): }, ) ) + face = forms.ChoiceField( + label=_('Face'), + choices=add_blank_choice(DeviceFaceChoices), + required=False, + widget=ClearableSelect( + requires_fields=['rack'] + ) + ) device_type = DynamicModelChoiceField( label=_('Device type'), queryset=DeviceType.objects.all(), diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index 8e0818326..2354ee5c5 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _ from dcim.models import * from netbox.forms import NetBoxModelForm +from netbox.forms.mixins import OwnerMixin from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField from utilities.forms.rendering import FieldSet, TabbedGroups from utilities.forms.widgets import APISelect @@ -271,7 +272,7 @@ class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm # Virtual chassis # -class VirtualChassisCreateForm(NetBoxModelForm): +class VirtualChassisCreateForm(OwnerMixin, NetBoxModelForm): region = DynamicModelChoiceField( label=_('Region'), queryset=Region.objects.all(), diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py index 9e5915146..edf6127c2 100644 --- a/netbox/dcim/graphql/filters.py +++ b/netbox/dcim/graphql/filters.py @@ -550,6 +550,10 @@ class InterfaceFilter( strawberry_django.filter_field() ) + @strawberry_django.filter_field + def cabled(self, value: bool, prefix: str): + return Q(**{f'{prefix}cable__isnull': (not value)}) + @strawberry_django.filter_field def connected(self, queryset, value: bool, prefix: str): if value is True: @@ -889,7 +893,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM @strawberry_django.filter_type(models.RackType, lookups=True) -class RackTypeFilter(RackFilterMixin, WeightFilterMixin, PrimaryModelFilter): +class RackTypeFilter(ImageAttachmentFilterMixin, RackFilterMixin, WeightFilterMixin, PrimaryModelFilter): form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = ( strawberry_django.filter_field() ) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 1132a0ca9..ac704c28e 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -734,7 +734,7 @@ class PowerPortTemplateType(ModularComponentTemplateType): filters=RackTypeFilter, pagination=True ) -class RackTypeType(PrimaryObjectType): +class RackTypeType(ImageAttachmentsMixin, PrimaryObjectType): rack_count: BigInt manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] diff --git a/netbox/dcim/models/modules.py b/netbox/dcim/models/modules.py index 97ffeccbe..d718cc08c 100644 --- a/netbox/dcim/models/modules.py +++ b/netbox/dcim/models/modules.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ from jsonschema.exceptions import ValidationError as JSONValidationError from dcim.choices import * -from dcim.utils import update_interface_bridges +from dcim.utils import create_port_mappings, update_interface_bridges from extras.models import ConfigContextModel, CustomField from netbox.models import PrimaryModel from netbox.models.features import ImageAttachmentsMixin @@ -155,6 +155,8 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): 'description': self.description, 'weight': float(self.weight) if self.weight is not None else None, 'weight_unit': self.weight_unit, + 'airflow': self.airflow, + 'attribute_data': self.attribute_data, 'comments': self.comments, } @@ -359,5 +361,7 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel): update_fields=update_fields ) + # Replicate any front/rear port mappings from the ModuleType + create_port_mappings(self.device, self.module_type, self) # Interface bridges have to be set after interface instantiation update_interface_bridges(self.device, self.module_type.interfacetemplates, self) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index d7afb7896..c4eaa7e20 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -122,7 +122,7 @@ class RackBase(WeightMixin, PrimaryModel): abstract = True -class RackType(RackBase): +class RackType(ImageAttachmentsMixin, RackBase): """ Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. Each Rack is assigned to a Site and (optionally) a Location. @@ -373,7 +373,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, TrackingModelMixin, RackBase): super().clean() # Validate location/site assignment - if self.site and self.location and self.location.site != self.site: + if self.site_id and self.location_id and self.location.site_id != self.site_id: raise ValidationError(_("Assigned location must belong to parent site ({site}).").format(site=self.site)) # Validate outer dimensions and unit diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index fe34896ce..f5eabdec0 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -27,6 +27,7 @@ __all__ = ( 'DeviceTable', 'FrontPortTable', 'InterfaceTable', + 'InterfaceLAGMemberTable', 'InventoryItemRoleTable', 'InventoryItemTable', 'MACAddressTable', @@ -695,6 +696,33 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description') +class InterfaceLAGMemberTable(PathEndpointTable, NetBoxTable): + parent = tables.Column( + verbose_name=_('Parent'), + accessor=Accessor('device'), + linkify=True, + ) + name = tables.Column( + verbose_name=_('Name'), + linkify=True, + order_by=('_name',), + ) + connection = columns.TemplateColumn( + accessor='connected_endpoints', + template_code=INTERFACE_LAG_MEMBERS_LINKTERMINATION, + verbose_name=_('Peer'), + orderable=False, + ) + tags = columns.TagColumn( + url_name='dcim:interface_list' + ) + + class Meta(NetBoxTable.Meta): + model = models.Interface + fields = ('pk', 'parent', 'name', 'type', 'connection') + default_columns = ('pk', 'parent', 'name', 'type', 'connection') + + class DeviceInterfaceTable(InterfaceTable): name = tables.TemplateColumn( verbose_name=_('Name'), diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index cfd0f63c5..12b192d83 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -24,6 +24,24 @@ INTERFACE_LINKTERMINATION = """ {% else %}""" + LINKTERMINATION + """{% endif %} """ +INTERFACE_LAG_MEMBERS_LINKTERMINATION = """ +{% for termination in value %} + {% if termination.parent_object %} + {{ termination.parent_object }} + + {% endif %} + {{ termination }} + {% if termination.lag %} + + {{ termination.lag }} + (LAG) + {% endif %} + {% if not forloop.last %}
{% endif %} +{% empty %} + {{ ''|placeholder }} +{% endfor %} +""" + CABLE_LENGTH = """ {% load helpers %} {% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %} diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index c4b9e37bb..6e843015e 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -875,6 +875,142 @@ class ModuleBayTestCase(TestCase): self.assertIsNone(bay2.parent) self.assertIsNone(bay2.module) + def test_module_installation_creates_port_mappings(self): + """ + Test that installing a module with front/rear port templates correctly + creates PortMapping instances for the device. + """ + device = Device.objects.first() + manufacturer = Manufacturer.objects.first() + module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 1') + + # Create a module type with a rear port template + module_type_with_mappings = ModuleType.objects.create( + manufacturer=manufacturer, + model='Module Type With Mappings', + ) + + # Create a rear port template with 12 positions (splice) + rear_port_template = RearPortTemplate.objects.create( + module_type=module_type_with_mappings, + name='Rear Port 1', + type=PortTypeChoices.TYPE_SPLICE, + positions=12, + ) + + # Create 12 front port templates mapped to the rear port + front_port_templates = [] + for i in range(1, 13): + front_port_template = FrontPortTemplate.objects.create( + module_type=module_type_with_mappings, + name=f'port {i}', + type=PortTypeChoices.TYPE_LC, + positions=1, + ) + front_port_templates.append(front_port_template) + + # Create port template mapping + PortTemplateMapping.objects.create( + device_type=None, + module_type=module_type_with_mappings, + front_port=front_port_template, + front_port_position=1, + rear_port=rear_port_template, + rear_port_position=i, + ) + + # Install the module + module = Module.objects.create( + device=device, + module_bay=module_bay, + module_type=module_type_with_mappings, + status=ModuleStatusChoices.STATUS_ACTIVE, + ) + + # Verify that front ports were created + front_ports = FrontPort.objects.filter(device=device, module=module) + self.assertEqual(front_ports.count(), 12) + + # Verify that the rear port was created + rear_ports = RearPort.objects.filter(device=device, module=module) + self.assertEqual(rear_ports.count(), 1) + rear_port = rear_ports.first() + self.assertEqual(rear_port.positions, 12) + + # Verify that port mappings were created + port_mappings = PortMapping.objects.filter(front_port__module=module) + self.assertEqual(port_mappings.count(), 12) + + # Verify each mapping is correct + for i, front_port_template in enumerate(front_port_templates, start=1): + front_port = FrontPort.objects.get( + device=device, + name=front_port_template.name, + module=module, + ) + + # Check that a mapping exists for this front port + mapping = PortMapping.objects.get( + device=device, + front_port=front_port, + front_port_position=1, + ) + + self.assertEqual(mapping.rear_port, rear_port) + self.assertEqual(mapping.front_port_position, 1) + self.assertEqual(mapping.rear_port_position, i) + + def test_module_installation_without_mappings(self): + """ + Test that installing a module without port template mappings + doesn't create any PortMapping instances. + """ + device = Device.objects.first() + manufacturer = Manufacturer.objects.first() + module_bay = ModuleBay.objects.create(device=device, name='Test Bay PortMapping 2') + + # Create a module type without any port template mappings + module_type_no_mappings = ModuleType.objects.create( + manufacturer=manufacturer, + model='Module Type Without Mappings', + ) + + # Create a rear port template + RearPortTemplate.objects.create( + module_type=module_type_no_mappings, + name='Rear Port 1', + type=PortTypeChoices.TYPE_SPLICE, + positions=12, + ) + + # Create front port templates but DO NOT create PortTemplateMapping rows + for i in range(1, 13): + FrontPortTemplate.objects.create( + module_type=module_type_no_mappings, + name=f'port {i}', + type=PortTypeChoices.TYPE_LC, + positions=1, + ) + + # Install the module + module = Module.objects.create( + device=device, + module_bay=module_bay, + module_type=module_type_no_mappings, + status=ModuleStatusChoices.STATUS_ACTIVE, + ) + + # Verify no port mappings were created for this module + port_mappings = PortMapping.objects.filter( + device=device, + front_port__module=module, + front_port_position=1, + ) + self.assertEqual(port_mappings.count(), 0) + self.assertEqual(FrontPort.objects.filter(module=module).count(), 12) + self.assertEqual(RearPort.objects.filter(module=module).count(), 1) + self.assertEqual(PortMapping.objects.filter(front_port__module=module).count(), 0) + class CableTestCase(TestCase): diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py index 4b9d0fb5c..5dcc20035 100644 --- a/netbox/dcim/utils.py +++ b/netbox/dcim/utils.py @@ -85,13 +85,13 @@ def update_interface_bridges(device, interface_templates, module=None): interface.save() -def create_port_mappings(device, device_type, module=None): +def create_port_mappings(device, device_or_module_type, module=None): """ - Replicate all front/rear port mappings from a DeviceType to the given device. + Replicate all front/rear port mappings from a DeviceType or ModuleType to the given device. """ from dcim.models import FrontPort, PortMapping, RearPort - templates = device_type.port_mappings.prefetch_related('front_port', 'rear_port') + templates = device_or_module_type.port_mappings.prefetch_related('front_port', 'rear_port') # Cache front & rear ports for efficient lookups by name front_ports = { diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index d12bd9f1c..f920a0bb3 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -880,6 +880,7 @@ class RackTypeView(GetRelatedModelsMixin, generic.ObjectView): panels.RackWeightPanel(title=_('Weight'), exclude=['total_weight']), CustomFieldsPanel(), RelatedObjectsPanel(), + ImageAttachmentsPanel(), ], ) @@ -3135,6 +3136,14 @@ class InterfaceView(generic.ObjectView): ) child_interfaces_table.configure(request) + # Get LAG interfaces + lag_interfaces = Interface.objects.restrict(request.user, 'view').filter(lag=instance) + lag_interfaces_table = tables.InterfaceLAGMemberTable( + lag_interfaces, + orderable=False + ) + lag_interfaces_table.configure(request) + # Get assigned VLANs and annotate whether each is tagged or untagged vlans = [] if instance.untagged_vlan is not None: @@ -3164,6 +3173,7 @@ class InterfaceView(generic.ObjectView): 'bridge_interfaces': bridge_interfaces, 'bridge_interfaces_table': bridge_interfaces_table, 'child_interfaces_table': child_interfaces_table, + 'lag_interfaces_table': lag_interfaces_table, 'vlan_table': vlan_table, 'vlan_translation_table': vlan_translation_table, } diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 578ab8c4b..1ef4d1e1d 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -4,7 +4,6 @@ from drf_spectacular.utils import extend_schema_field from rest_framework.fields import Field from rest_framework.serializers import ValidationError -from core.models import ObjectType from extras.choices import CustomFieldTypeChoices from extras.constants import CUSTOMFIELD_EMPTY_VALUES from extras.models import CustomField @@ -24,13 +23,9 @@ class CustomFieldDefaultValues: def __call__(self, serializer_field): self.model = serializer_field.parent.Meta.model - # Retrieve the CustomFields for the parent model - object_type = ObjectType.objects.get_for_model(self.model) - fields = CustomField.objects.filter(object_types=object_type) - - # Populate the default value for each CustomField + # Populate the default value for each CustomField on the model value = {} - for field in fields: + for field in CustomField.objects.get_for_model(self.model): if field.default is not None: value[field.name] = field.default else: @@ -47,8 +42,7 @@ class CustomFieldsDataField(Field): Cache CustomFields assigned to this model to avoid redundant database queries """ if not hasattr(self, '_custom_fields'): - object_type = ObjectType.objects.get_for_model(self.parent.Meta.model) - self._custom_fields = CustomField.objects.filter(object_types=object_type) + self._custom_fields = CustomField.objects.get_for_model(self.parent.Meta.model) return self._custom_fields def to_representation(self, obj): diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index 935e48051..39bfcb13d 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -75,10 +75,11 @@ def get_bookmarks_object_type_choices(): def get_models_from_content_types(content_types): """ Return a list of models corresponding to the given content types, identified by natural key. + Accepts both lowercase (e.g. "dcim.site") and PascalCase (e.g. "dcim.Site") model names. """ models = [] for content_type_id in content_types: - app_label, model_name = content_type_id.split('.') + app_label, model_name = content_type_id.lower().split('.') try: content_type = ObjectType.objects.get_by_natural_key(app_label, model_name) if content_type.model_class(): diff --git a/netbox/extras/events.py b/netbox/extras/events.py index 6f4854ff3..392071877 100644 --- a/netbox/extras/events.py +++ b/netbox/extras/events.py @@ -1,5 +1,5 @@ import logging -from collections import defaultdict +from collections import UserDict, defaultdict from django.conf import settings from django.utils import timezone @@ -12,7 +12,6 @@ from core.models import ObjectType from netbox.config import get_config from netbox.constants import RQ_QUEUE_DEFAULT from netbox.models.features import has_feature -from users.models import User from utilities.api import get_serializer_for_model from utilities.request import copy_safe_request from utilities.rqworker import get_rq_retry @@ -23,6 +22,21 @@ from .models import EventRule logger = logging.getLogger('netbox.events_processor') +class EventContext(UserDict): + """ + A custom dictionary that automatically serializes its associated object on demand. + """ + + # We're emulating a dictionary here (rather than using a custom class) because prior to NetBox v4.5.2, events were + # queued as dictionaries for processing by handles in EVENTS_PIPELINE. We need to avoid introducing any breaking + # changes until a suitable minor release. + def __getitem__(self, item): + if item == 'data' and 'data' not in self: + data = serialize_for_event(self['object']) + self.__setitem__('data', data) + return super().__getitem__(item) + + def serialize_for_event(instance): """ Return a serialized representation of the given instance suitable for use in a queued event. @@ -37,18 +51,26 @@ def serialize_for_event(instance): def get_snapshots(instance, event_type): - snapshots = { - 'prechange': getattr(instance, '_prechange_snapshot', None), - 'postchange': None, - } - if event_type != OBJECT_DELETED: - # Use model's serialize_object() method if defined; fall back to serialize_object() utility function - if hasattr(instance, 'serialize_object'): - snapshots['postchange'] = instance.serialize_object() - else: - snapshots['postchange'] = serialize_object(instance) + """ + Return a dictionary of pre- and post-change snapshots for the given instance. + """ + if event_type == OBJECT_DELETED: + # Post-change snapshot must be empty for deleted objects + postchange_snapshot = None + elif hasattr(instance, '_postchange_snapshot'): + # Use the cached post-change snapshot if one is available + postchange_snapshot = instance._postchange_snapshot + elif hasattr(instance, 'serialize_object'): + # Use model's serialize_object() method if defined + postchange_snapshot = instance.serialize_object() + else: + # Fall back to the serialize_object() utility function + postchange_snapshot = serialize_object(instance) - return snapshots + return { + 'prechange': getattr(instance, '_prechange_snapshot', None), + 'postchange': postchange_snapshot, + } def enqueue_event(queue, instance, request, event_type): @@ -66,37 +88,42 @@ def enqueue_event(queue, instance, request, event_type): assert instance.pk is not None key = f'{app_label}.{model_name}:{instance.pk}' if key in queue: - queue[key]['data'] = serialize_for_event(instance) queue[key]['snapshots']['postchange'] = get_snapshots(instance, event_type)['postchange'] # If the object is being deleted, update any prior "update" event to "delete" if event_type == OBJECT_DELETED: queue[key]['event_type'] = event_type else: - queue[key] = { - 'object_type': ObjectType.objects.get_for_model(instance), - 'object_id': instance.pk, - 'event_type': event_type, - 'data': serialize_for_event(instance), - 'snapshots': get_snapshots(instance, event_type), - 'request': request, + queue[key] = EventContext( + object_type=ObjectType.objects.get_for_model(instance), + object_id=instance.pk, + object=instance, + event_type=event_type, + snapshots=get_snapshots(instance, event_type), + request=request, + user=request.user, # Legacy request attributes for backward compatibility - 'username': request.user.username, - 'request_id': request.id, - } + username=request.user.username, + request_id=request.id, + ) + # Force serialization of objects prior to them actually being deleted + if event_type == OBJECT_DELETED: + queue[key]['data'] = serialize_for_event(instance) -def process_event_rules(event_rules, object_type, event_type, data, username=None, snapshots=None, request=None): - user = User.objects.get(username=username) if username else None +def process_event_rules(event_rules, object_type, event): + """ + Process a list of EventRules against an event. + """ for event_rule in event_rules: # Evaluate event rule conditions (if any) - if not event_rule.eval_conditions(data): + if not event_rule.eval_conditions(event['data']): continue # Compile event data event_data = event_rule.action_data or {} - event_data.update(data) + event_data.update(event['data']) # Webhooks if event_rule.action_type == EventRuleActionChoices.WEBHOOK: @@ -109,25 +136,20 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non params = { "event_rule": event_rule, "object_type": object_type, - "event_type": event_type, + "event_type": event['event_type'], "data": event_data, - "snapshots": snapshots, + "snapshots": event.get('snapshots'), "timestamp": timezone.now().isoformat(), - "username": username, + "username": event['username'], "retry": get_rq_retry() } - if snapshots: - params["snapshots"] = snapshots - if request: + if 'request' in event: # Exclude FILES - webhooks don't need uploaded files, # which can cause pickle errors with Pillow. - params["request"] = copy_safe_request(request, include_files=False) + params['request'] = copy_safe_request(event['request'], include_files=False) # Enqueue the task - rq_queue.enqueue( - "extras.webhooks.send_webhook", - **params - ) + rq_queue.enqueue('extras.webhooks.send_webhook', **params) # Scripts elif event_rule.action_type == EventRuleActionChoices.SCRIPT: @@ -139,16 +161,16 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non params = { "instance": event_rule.action_object, "name": script.name, - "user": user, + "user": event['user'], "data": event_data } - if snapshots: - params["snapshots"] = snapshots - if request: - params["request"] = copy_safe_request(request) - ScriptJob.enqueue( - **params - ) + if 'snapshots' in event: + params['snapshots'] = event['snapshots'] + if 'request' in event: + params['request'] = copy_safe_request(event['request']) + + # Enqueue the job + ScriptJob.enqueue(**params) # Notification groups elif event_rule.action_type == EventRuleActionChoices.NOTIFICATION: @@ -157,7 +179,7 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non object_type=object_type, object_id=event_data['id'], object_repr=event_data.get('display'), - event_type=event_type + event_type=event['event_type'] ) else: @@ -169,6 +191,8 @@ def process_event_rules(event_rules, object_type, event_type, data, username=Non def process_event_queue(events): """ Flush a list of object representation to RQ for EventRule processing. + + This is the default processor listed in EVENTS_PIPELINE. """ events_cache = defaultdict(dict) @@ -188,11 +212,7 @@ def process_event_queue(events): process_event_rules( event_rules=event_rules, object_type=object_type, - event_type=event['event_type'], - data=event['data'], - username=event['username'], - snapshots=event['snapshots'], - request=event['request'], + event=event, ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 0a3036597..7ff458c0f 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -7,13 +7,12 @@ from extras.choices import * from extras.models import * from netbox.events import get_event_type_choices from netbox.forms import NetBoxModelFilterSetForm, PrimaryModelFilterSetForm -from netbox.forms.mixins import SavedFiltersMixin +from netbox.forms.mixins import OwnerFilterMixin, SavedFiltersMixin from tenancy.models import Tenant, TenantGroup -from users.models import Group, Owner, User +from users.models import Group, User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms.fields import ( - ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - TagFilterField, + ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField, ) from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DateTimePicker @@ -39,7 +38,7 @@ __all__ = ( ) -class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): +class CustomFieldFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm): model = CustomField fieldsets = ( FieldSet('q', 'filter_id'), @@ -47,6 +46,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): FieldSet('choice_set_id', 'related_object_type_id', name=_('Type Options')), FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')), FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) object_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('custom_fields'), @@ -119,18 +119,14 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): label=_('Validation regex'), required=False ) - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) -class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): +class CustomFieldChoiceSetFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm): model = CustomFieldChoiceSet fieldsets = ( FieldSet('q', 'filter_id'), FieldSet('base_choices', 'choice', name=_('Choices')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) base_choices = forms.MultipleChoiceField( choices=CustomFieldChoiceSetBaseChoices, @@ -139,18 +135,14 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): choice = forms.CharField( required=False ) - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) -class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): +class CustomLinkFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm): model = CustomLink fieldsets = ( FieldSet('q', 'filter_id'), FieldSet('object_type_id', 'enabled', 'new_window', 'weight', name=_('Attributes')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) object_type_id = ContentTypeMultipleChoiceField( label=_('Object types'), @@ -175,19 +167,15 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): label=_('Weight'), required=False ) - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) -class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): +class ExportTemplateFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm): model = ExportTemplate fieldsets = ( FieldSet('q', 'filter_id', 'object_type_id'), FieldSet('data_source_id', 'data_file_id', name=_('Data')), FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) data_source_id = DynamicModelMultipleChoiceField( queryset=DataSource.objects.all(), @@ -226,11 +214,6 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): @@ -250,11 +233,12 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): ) -class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): +class SavedFilterFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm): model = SavedFilter fieldsets = ( FieldSet('q', 'filter_id'), FieldSet('object_type_id', 'enabled', 'shared', 'weight', name=_('Attributes')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) object_type_id = ContentTypeMultipleChoiceField( label=_('Object types'), @@ -279,11 +263,6 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): label=_('Weight'), required=False ) - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) class TableConfigFilterForm(SavedFiltersMixin, FilterForm): @@ -317,11 +296,12 @@ class TableConfigFilterForm(SavedFiltersMixin, FilterForm): ) -class WebhookFilterForm(NetBoxModelFilterSetForm): +class WebhookFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm): model = Webhook fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('payload_url', 'http_method', 'http_content_type', name=_('Attributes')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) http_content_type = forms.CharField( label=_('HTTP content type'), @@ -336,19 +316,15 @@ class WebhookFilterForm(NetBoxModelFilterSetForm): required=False, label=_('HTTP method') ) - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) tag = TagFilterField(model) -class EventRuleFilterForm(NetBoxModelFilterSetForm): +class EventRuleFilterForm(OwnerFilterMixin, NetBoxModelFilterSetForm): model = EventRule fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('object_type_id', 'event_type', 'action_type', 'enabled', name=_('Attributes')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) object_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('event_rules'), @@ -372,16 +348,16 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) tag = TagFilterField(model) -class TagFilterForm(SavedFiltersMixin, FilterForm): +class TagFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm): model = Tag + fieldsets = ( + FieldSet('q', 'filter_id'), + FieldSet('content_type_id', 'for_object_type_id', name=_('Attributes')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), + ) content_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('tags'), required=False, @@ -392,11 +368,6 @@ class TagFilterForm(SavedFiltersMixin, FilterForm): required=False, label=_('Allowed object type') ) - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm): @@ -404,6 +375,7 @@ class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm): fieldsets = ( FieldSet('q', 'filter_id'), FieldSet('data_source_id', 'data_file_id', name=_('Data')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) data_source_id = DynamicModelMultipleChoiceField( queryset=DataSource.objects.all(), @@ -420,16 +392,17 @@ class ConfigContextProfileFilterForm(PrimaryModelFilterSetForm): ) -class ConfigContextFilterForm(SavedFiltersMixin, FilterForm): +class ConfigContextFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm): model = ConfigContext fieldsets = ( FieldSet('q', 'filter_id', 'tag_id'), - FieldSet('profile', name=_('Config Context')), + FieldSet('profile_id', name=_('Config Context')), FieldSet('data_source_id', 'data_file_id', name=_('Data')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')), FieldSet('device_type_id', 'platform_id', 'device_role_id', name=_('Device')), FieldSet('cluster_type_id', 'cluster_group_id', 'cluster_id', name=_('Cluster')), - FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')) + FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) profile_id = DynamicModelMultipleChoiceField( queryset=ConfigContextProfile.objects.all(), @@ -514,19 +487,15 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm): required=False, label=_('Tags') ) - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) -class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm): +class ConfigTemplateFilterForm(OwnerFilterMixin, SavedFiltersMixin, FilterForm): model = ConfigTemplate fieldsets = ( FieldSet('q', 'filter_id', 'tag'), FieldSet('data_source_id', 'data_file_id', 'auto_sync_enabled', name=_('Data')), - FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering')) + FieldSet('mime_type', 'file_name', 'file_extension', 'as_attachment', name=_('Rendering')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) data_source_id = DynamicModelMultipleChoiceField( queryset=DataSource.objects.all(), @@ -568,11 +537,6 @@ class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) class LocalConfigContextFilterForm(forms.Form): diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 68151c80b..b0252962e 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -178,6 +178,13 @@ class CustomFieldChoiceSetForm(ChangelogMessageMixin, OwnerMixin, forms.ModelFor ) + ' choice1:First Choice') ) + fieldsets = ( + FieldSet( + 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', + name=_('Custom Field Choice Set') + ), + ) + class Meta: model = CustomFieldChoiceSet fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', 'owner') diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index a25e7c04d..a29036821 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -19,6 +19,7 @@ from django.utils.translation import gettext_lazy as _ from core.models import ObjectType from extras.choices import * from extras.data import CHOICE_SETS +from netbox.context import query_cache from netbox.models import ChangeLoggedModel from netbox.models.features import CloningMixin, ExportTemplatesMixin from netbox.models.mixins import OwnerMixin @@ -58,8 +59,20 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): """ Return all CustomFields assigned to the given model. """ + # Check the request cache before hitting the database + cache = query_cache.get() + if cache is not None: + if custom_fields := cache['custom_fields'].get(model._meta.model): + return custom_fields + content_type = ObjectType.objects.get_for_model(model._meta.concrete_model) - return self.get_queryset().filter(object_types=content_type) + custom_fields = self.get_queryset().filter(object_types=content_type) + + # Populate the request cache to avoid redundant lookups + if cache is not None: + cache['custom_fields'][model._meta.model] = custom_fields + + return custom_fields def get_defaults_for_model(self, model): """ diff --git a/netbox/extras/models/scripts.py b/netbox/extras/models/scripts.py index 944492d76..3d71d8308 100644 --- a/netbox/extras/models/scripts.py +++ b/netbox/extras/models/scripts.py @@ -137,7 +137,7 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile): module = self.get_module() except Exception as e: self.error = e - logger.debug(f"Failed to load script: {self.python_name} error: {e}") + logger.error(f"Failed to load script: {self.python_name} error: {e}") module = None scripts = {} diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index ad9e5bcc4..3ba7792a1 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -61,7 +61,7 @@ class ScriptVariable: self.field_attrs['label'] = label if description: self.field_attrs['help_text'] = description - if default: + if default is not None: self.field_attrs['initial'] = default if widget: self.field_attrs['widget'] = widget diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index 7105c38b4..e079abd17 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -4,11 +4,12 @@ from django.dispatch import receiver from core.events import * from core.signals import job_end, job_start -from extras.events import process_event_rules +from extras.events import EventContext, process_event_rules from extras.models import EventRule, Notification, Subscription from netbox.config import get_config from netbox.models.features import has_feature from netbox.signals import post_clean +from utilities.data import get_config_value_ci from utilities.exceptions import AbortRequest from .models import CustomField, TaggedItem from .utils import run_validators @@ -65,7 +66,7 @@ def run_save_validators(sender, instance, **kwargs): Run any custom validation rules for the model prior to calling save(). """ model_name = f'{sender._meta.app_label}.{sender._meta.model_name}' - validators = get_config().CUSTOM_VALIDATORS.get(model_name, []) + validators = get_config_value_ci(get_config().CUSTOM_VALIDATORS, model_name, default=[]) run_validators(instance, validators) @@ -102,14 +103,12 @@ def process_job_start_event_rules(sender, **kwargs): enabled=True, object_types=sender.object_type ) - username = sender.user.username if sender.user else None - process_event_rules( - event_rules=event_rules, - object_type=sender.object_type, + event = EventContext( event_type=JOB_STARTED, data=sender.data, - username=username + user=sender.user, ) + process_event_rules(event_rules, sender.object_type, event) @receiver(job_end) @@ -122,14 +121,12 @@ def process_job_end_event_rules(sender, **kwargs): enabled=True, object_types=sender.object_type ) - username = sender.user.username if sender.user else None - process_event_rules( - event_rules=event_rules, - object_type=sender.object_type, + event = EventContext( event_type=JOB_COMPLETED, data=sender.data, - username=username + user=sender.user, ) + process_event_rules(event_rules, sender.object_type, event) # diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 91444e2ce..727f0f803 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.models import ContentType from django.urls import reverse from django.test import tag +from unittest.mock import patch, PropertyMock from core.choices import ManagedFileRootPathChoices from core.events import * @@ -906,7 +907,7 @@ class ScriptValidationErrorTest(TestCase): user_permissions = ['extras.view_script', 'extras.run_script'] class TestScriptMixin: - bar = IntegerVar(min_value=0, max_value=30, default=30) + bar = IntegerVar(min_value=0, max_value=30) class TestScriptClass(TestScriptMixin, PythonClass): class Meta: @@ -930,8 +931,6 @@ class ScriptValidationErrorTest(TestCase): @tag('regression') def test_script_validation_error_displays_message(self): - from unittest.mock import patch - url = reverse('extras:script', kwargs={'pk': self.script.pk}) with patch('extras.views.get_workers_for_queue', return_value=['worker']): @@ -944,8 +943,6 @@ class ScriptValidationErrorTest(TestCase): @tag('regression') def test_script_validation_error_no_toast_for_fieldset_fields(self): - from unittest.mock import patch, PropertyMock - class FieldsetScript(PythonClass): class Meta: name = 'Fieldset test' @@ -967,3 +964,42 @@ class ScriptValidationErrorTest(TestCase): self.assertEqual(response.status_code, 200) messages = list(response.context['messages']) self.assertEqual(len(messages), 0) + + +class ScriptDefaultValuesTest(TestCase): + user_permissions = ['extras.view_script', 'extras.run_script'] + + class TestScriptClass(PythonClass): + class Meta: + name = 'Test script' + commit_default = False + + bool_default_true = BooleanVar(default=True) + bool_default_false = BooleanVar(default=False) + int_with_default = IntegerVar(default=0) + int_without_default = IntegerVar(required=False) + + def run(self, data, commit): + return "Complete" + + @classmethod + def setUpTestData(cls): + module = ScriptModule.objects.create(file_root=ManagedFileRootPathChoices.SCRIPTS, file_path='test_script.py') + cls.script = Script.objects.create(module=module, name='Test script', is_executable=True) + + def setUp(self): + super().setUp() + Script.python_class = property(lambda self: ScriptDefaultValuesTest.TestScriptClass) + + def test_default_values_are_used(self): + url = reverse('extras:script', kwargs={'pk': self.script.pk}) + + with patch('extras.views.get_workers_for_queue', return_value=['worker']): + with patch('extras.jobs.ScriptJob.enqueue') as mock_enqueue: + mock_enqueue.return_value.pk = 1 + self.client.post(url, {}) + call_kwargs = mock_enqueue.call_args.kwargs + self.assertEqual(call_kwargs['data']['bool_default_true'], True) + self.assertEqual(call_kwargs['data']['bool_default_false'], False) + self.assertEqual(call_kwargs['data']['int_with_default'], 0) + self.assertIsNone(call_kwargs['data']['int_without_default']) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 3c1fc395d..461ed423f 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1511,7 +1511,13 @@ class ScriptView(BaseScriptView): 'script': script, }) - form = script_class.as_form(request.POST, request.FILES) + # Populate missing variables with their default values, if defined + post_data = request.POST.copy() + for name, var in script_class._get_vars().items(): + if name not in post_data and (initial := var.field_attrs.get('initial')) is not None: + post_data[name] = initial + + form = script_class.as_form(post_data, request.FILES) # Allow execution only if RQ worker process is running if not get_workers_for_queue('default'): diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index dd2987f28..e37d17d4b 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -45,9 +45,10 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([ class VRFFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = VRF fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('import_target_id', 'export_target_id', name=_('Route Targets')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) import_target_id = DynamicModelMultipleChoiceField( queryset=RouteTarget.objects.all(), @@ -65,9 +66,10 @@ class VRFFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): class RouteTargetFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = RouteTarget fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('importing_vrf_id', 'exporting_vrf_id', name=_('VRF')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) importing_vrf_id = DynamicModelMultipleChoiceField( queryset=VRF.objects.all(), @@ -85,8 +87,9 @@ class RouteTargetFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): class RIRFilterForm(OrganizationalModelFilterSetForm): model = RIR fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('is_private', name=_('RIR')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) is_private = forms.NullBooleanField( required=False, @@ -101,9 +104,10 @@ class RIRFilterForm(OrganizationalModelFilterSetForm): class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = Aggregate fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('family', 'rir_id', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) family = forms.ChoiceField( @@ -122,9 +126,10 @@ class AggregateFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode class ASNRangeFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm): model = ASNRange fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('rir_id', 'start', 'end', name=_('Range')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) rir_id = DynamicModelMultipleChoiceField( queryset=RIR.objects.all(), @@ -145,9 +150,10 @@ class ASNRangeFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm): class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = ASN fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('rir_id', 'site_group_id', 'site_id', name=_('Assignment')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) rir_id = DynamicModelMultipleChoiceField( queryset=RIR.objects.all(), @@ -170,7 +176,8 @@ class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): class RoleFilterForm(OrganizationalModelFilterSetForm): model = Role fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) tag = TagFilterField(model) @@ -178,7 +185,7 @@ class RoleFilterForm(OrganizationalModelFilterSetForm): class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = Prefix fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet( 'within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized', name=_('Addressing') @@ -187,6 +194,7 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) mask_length__lte = forms.IntegerField( @@ -284,9 +292,10 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = IPRange fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) family = forms.ChoiceField( @@ -331,14 +340,15 @@ class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelF class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm): model = IPAddress fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet( 'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name', name=_('Attributes') ), FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')), - FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')), + FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role') @@ -409,9 +419,10 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode class FHRPGroupFilterForm(PrimaryModelFilterSetForm): model = FHRPGroup fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('name', 'protocol', 'group_id', name=_('Attributes')), FieldSet('auth_type', 'auth_key', name=_('Authentication')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) name = forms.CharField( label=_('Name'), @@ -441,11 +452,12 @@ class FHRPGroupFilterForm(PrimaryModelFilterSetForm): class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm): fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('region', 'site_group', 'site', 'location', 'rack', name=_('Location')), FieldSet('cluster_group', 'cluster', name=_('Cluster')), FieldSet('contains_vid', name=_('VLANs')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) model = VLANGroup region = DynamicModelMultipleChoiceField( @@ -495,8 +507,9 @@ class VLANGroupFilterForm(TenancyFilterForm, OrganizationalModelFilterSetForm): class VLANTranslationPolicyFilterForm(PrimaryModelFilterSetForm): model = VLANTranslationPolicy fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('name', name=_('Attributes')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) name = forms.CharField( required=False, @@ -532,11 +545,12 @@ class VLANTranslationRuleFilterForm(NetBoxModelFilterSetForm): class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): model = VLAN fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), FieldSet('group_id', 'status', 'role_id', 'vid', 'l2vpn_id', name=_('Attributes')), FieldSet('qinq_role', 'qinq_svlan_id', name=_('Q-in-Q/802.1ad')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) selector_fields = ('filter_id', 'q', 'group_id') region_id = DynamicModelMultipleChoiceField( @@ -604,8 +618,9 @@ class VLANFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): class ServiceTemplateFilterForm(PrimaryModelFilterSetForm): model = ServiceTemplate fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('protocol', 'port', name=_('Attributes')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), ) protocol = forms.ChoiceField( label=_('Protocol'), @@ -622,9 +637,10 @@ class ServiceTemplateFilterForm(PrimaryModelFilterSetForm): class ServiceFilterForm(ContactModelFilterForm, ServiceTemplateFilterForm): model = Service fieldsets = ( - FieldSet('q', 'filter_id', 'tag', 'owner_id'), + FieldSet('q', 'filter_id', 'tag'), FieldSet('protocol', 'port', name=_('Attributes')), FieldSet('device_id', 'virtual_machine_id', 'fhrpgroup_id', name=_('Assignment')), + FieldSet('owner_group_id', 'owner_id', name=_('Ownership')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) device_id = DynamicModelMultipleChoiceField( diff --git a/netbox/ipam/models/services.py b/netbox/ipam/models/services.py index c2c9ca444..74504c5dc 100644 --- a/netbox/ipam/models/services.py +++ b/netbox/ipam/models/services.py @@ -87,7 +87,9 @@ class Service(ContactsMixin, ServiceBase, PrimaryModel): help_text=_("The specific IP addresses (if any) to which this application service is bound") ) - clone_fields = ['protocol', 'ports', 'description', 'parent', 'ipaddresses', ] + clone_fields = ( + 'protocol', 'ports', 'description', 'parent_object_type', 'parent_object_id', 'ipaddresses', + ) class Meta: indexes = ( diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index 707f7f5be..a173db714 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -370,6 +370,11 @@ class AnnotatedIPAddressTable(IPAddressTable): verbose_name=_('IP Address') ) + def render_pk(self, value, record, bound_column): + if type(record) is not self._meta.model: + return '' + return bound_column.column.render(value, bound_column, record) + class Meta(IPAddressTable.Meta): pass diff --git a/netbox/ipam/tables/template_code.py b/netbox/ipam/tables/template_code.py index 14b73b28d..250873c38 100644 --- a/netbox/ipam/tables/template_code.py +++ b/netbox/ipam/tables/template_code.py @@ -6,7 +6,7 @@ PREFIX_LINK = """ {% if record.pk %} {{ record.prefix }} {% else %} - {{ record.prefix }} + {{ record.prefix }} {% endif %} """ diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py index d125da901..eaaee9a80 100644 --- a/netbox/ipam/tables/vlans.py +++ b/netbox/ipam/tables/vlans.py @@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _ from django_tables2.utils import Accessor from dcim.models import Interface +from dcim.tables.template_code import INTERFACE_LINKTERMINATION, LINKTERMINATION from ipam.models import * from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns from tenancy.tables import TenancyColumnsMixin, TenantColumn @@ -159,11 +160,26 @@ class VLANDevicesTable(VLANMembersTable): actions = columns.ActionsColumn( actions=('edit',) ) + link_peer = columns.TemplateColumn( + accessor='link_peers', + template_code=LINKTERMINATION, + orderable=False, + verbose_name=_('Link Peers'), + ) + + # Override PathEndpointTable.connection to accommodate virtual circuits + connection = columns.TemplateColumn( + accessor='_path__destinations', + template_code=INTERFACE_LINKTERMINATION, + orderable=False, + verbose_name=_('Connection'), + ) class Meta(NetBoxTable.Meta): model = Interface - fields = ('device', 'name', 'tagged', 'actions') - exclude = ('id', ) + fields = ('device', 'name', 'link_peer', 'connection', 'tagged', 'actions') + default_columns = ('device', 'name', 'connection', 'tagged', 'actions') + exclude = ('id',) class VLANVirtualMachinesTable(VLANMembersTable): diff --git a/netbox/ipam/tests/test_tables.py b/netbox/ipam/tests/test_tables.py new file mode 100644 index 000000000..2a6220f33 --- /dev/null +++ b/netbox/ipam/tests/test_tables.py @@ -0,0 +1,41 @@ +from django.test import RequestFactory, TestCase +from netaddr import IPNetwork + +from ipam.models import IPAddress, IPRange, Prefix +from ipam.tables import AnnotatedIPAddressTable +from ipam.utils import annotate_ip_space + + +class AnnotatedIPAddressTableTest(TestCase): + + @classmethod + def setUpTestData(cls): + cls.prefix = Prefix.objects.create( + prefix=IPNetwork('10.1.1.0/24'), + status='active' + ) + + cls.ip_address = IPAddress.objects.create( + address='10.1.1.1/24', + status='active' + ) + + cls.ip_range = IPRange.objects.create( + start_address=IPNetwork('10.1.1.2/24'), + end_address=IPNetwork('10.1.1.10/24'), + status='active' + ) + + def test_ipaddress_has_checkbox_iprange_does_not(self): + data = annotate_ip_space(self.prefix) + table = AnnotatedIPAddressTable(data, orderable=False) + table.columns.show('pk') + + request = RequestFactory().get('/') + html = table.as_html(request) + + ipaddress_checkbox_count = html.count(f'name="pk" value="{self.ip_address.pk}"') + self.assertEqual(ipaddress_checkbox_count, 1) + + iprange_checkbox_count = html.count(f'name="pk" value="{self.ip_range.pk}"') + self.assertEqual(iprange_checkbox_count, 0) diff --git a/netbox/ipam/utils.py b/netbox/ipam/utils.py index 790ac6503..53885367e 100644 --- a/netbox/ipam/utils.py +++ b/netbox/ipam/utils.py @@ -49,6 +49,9 @@ def add_requested_prefixes(parent, prefix_list, show_available=True, show_assign if prefix_list and show_available: # Find all unallocated space, add fake Prefix objects to child_prefixes. + # IMPORTANT: These are unsaved Prefix instances (pk=None). If this is ever changed to use + # saved Prefix instances with real pks, bulk delete will fail for mixed-type selections + # due to single-model form validation. See: https://github.com/netbox-community/netbox/issues/21176 available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list]) available_prefixes = [Prefix(prefix=p, status=None) for p in available_prefixes.iter_cidrs()] child_prefixes = child_prefixes + available_prefixes diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index 7dfd7d7eb..0e790b2df 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -1,3 +1,4 @@ +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db.backends.postgresql.psycopg_any import NumericRange from django.utils.translation import gettext as _ @@ -109,7 +110,7 @@ class ContentTypeField(RelatedField): def to_internal_value(self, data): try: app_label, model = data.split('.') - return self.queryset.get(app_label=app_label, model=model) + return ContentType.objects.get_by_natural_key(app_label=app_label, model=model) except ObjectDoesNotExist: self.fail('does_not_exist', content_type=data) except (AttributeError, TypeError, ValueError): diff --git a/netbox/netbox/api/serializers/base.py b/netbox/netbox/api/serializers/base.py index 6cd4e5738..0aa1385b0 100644 --- a/netbox/netbox/api/serializers/base.py +++ b/netbox/netbox/api/serializers/base.py @@ -1,9 +1,8 @@ from functools import cached_property -from rest_framework import serializers -from rest_framework.utils.serializer_helpers import BindingDict -from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers from utilities.api import get_related_object_by_attrs from .fields import NetBoxAPIHyperlinkedIdentityField, NetBoxURLHyperlinkedIdentityField @@ -19,16 +18,18 @@ class BaseModelSerializer(serializers.ModelSerializer): display_url = NetBoxURLHyperlinkedIdentityField() display = serializers.SerializerMethodField(read_only=True) - def __init__(self, *args, nested=False, fields=None, **kwargs): + def __init__(self, *args, nested=False, fields=None, omit=None, **kwargs): """ Extends the base __init__() method to support dynamic fields. :param nested: Set to True if this serializer is being employed within a parent serializer :param fields: An iterable of fields to include when rendering the serialized object, If nested is True but no fields are specified, Meta.brief_fields will be used. + :param omit: An iterable of fields to omit from the serialized object """ self.nested = nested - self._requested_fields = fields + self._include_fields = fields or [] + self._omit_fields = omit or [] # Disable validators for nested objects (which already exist) if self.nested: @@ -36,8 +37,8 @@ class BaseModelSerializer(serializers.ModelSerializer): # If this serializer is nested but no fields have been specified, # default to using Meta.brief_fields (if set) - if self.nested and not fields: - self._requested_fields = getattr(self.Meta, 'brief_fields', None) + if self.nested and not fields and not omit: + self._include_fields = getattr(self.Meta, 'brief_fields', None) super().__init__(*args, **kwargs) @@ -54,16 +55,19 @@ class BaseModelSerializer(serializers.ModelSerializer): @cached_property def fields(self): """ - Override the fields property to check for requested fields. If defined, - return only the applicable fields. + Override the fields property to return only specifically requested fields if needed. """ - if not self._requested_fields: - return super().fields + fields = super().fields + + # Include only requested fields + if self._include_fields: + for field_name in set(fields) - set(self._include_fields): + fields.pop(field_name, None) + + # Remove omitted fields + for field_name in set(self._omit_fields): + fields.pop(field_name, None) - fields = BindingDict(self) - for key, value in self.get_fields().items(): - if key in self._requested_fields: - fields[key] = value return fields @extend_schema_field(OpenApiTypes.STR) @@ -108,6 +112,7 @@ class ValidatedModelSerializer(BaseModelSerializer): for k, v in attrs.items(): setattr(instance, k, v) instance._m2m_values = m2m_values - instance.full_clean() + # Skip uniqueness validation of individual fields inside `full_clean()` (this is handled by the serializer) + instance.full_clean(validate_unique=False) return data diff --git a/netbox/netbox/api/viewsets/__init__.py b/netbox/netbox/api/viewsets/__init__.py index 6241be4cd..57bceb674 100644 --- a/netbox/netbox/api/viewsets/__init__.py +++ b/netbox/netbox/api/viewsets/__init__.py @@ -5,13 +5,13 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import router, transaction from django.db.models import ProtectedError, RestrictedError from django_pglocks import advisory_lock -from netbox.constants import ADVISORY_LOCK_KEYS from rest_framework import mixins as drf_mixins from rest_framework import status from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from netbox.api.serializers.features import ChangeLogMessageSerializer +from netbox.constants import ADVISORY_LOCK_KEYS from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer from utilities.exceptions import AbortRequest from utilities.query import reapply_model_ordering @@ -59,33 +59,38 @@ class BaseViewSet(GenericViewSet): serializer_class = self.get_serializer_class() # Dynamically resolve prefetches for included serializer fields and attach them to the queryset - if prefetch := get_prefetches_for_serializer(serializer_class, fields_to_include=self.requested_fields): + if prefetch := get_prefetches_for_serializer(serializer_class, **self.field_kwargs): qs = qs.prefetch_related(*prefetch) # Dynamically resolve annotations for RelatedObjectCountFields on the serializer and attach them to the queryset - if annotations := get_annotations_for_serializer(serializer_class, fields_to_include=self.requested_fields): + if annotations := get_annotations_for_serializer(serializer_class, **self.field_kwargs): qs = qs.annotate(**annotations) return qs def get_serializer(self, *args, **kwargs): - - # If specific fields have been requested, pass them to the serializer - if self.requested_fields: - kwargs['fields'] = self.requested_fields - + # Pass the fields/omit kwargs (if specified by the request) to the serializer + kwargs.update(**self.field_kwargs) return super().get_serializer(*args, **kwargs) @cached_property - def requested_fields(self): + def field_kwargs(self): + """Return a dictionary of keyword arguments to be passed when instantiating the serializer.""" # An explicit list of fields was requested if requested_fields := self.request.query_params.get('fields'): - return requested_fields.split(',') + return {'fields': requested_fields.split(',')} + + # An explicit list of fields to omit was requested + if omit_fields := self.request.query_params.get('omit'): + return {'omit': omit_fields.split(',')} + # Brief mode has been enabled for this request - elif self.brief: + if self.brief: serializer_class = self.get_serializer_class() - return getattr(serializer_class.Meta, 'brief_fields', None) - return None + if brief_fields := getattr(serializer_class.Meta, 'brief_fields', None): + return {'fields': brief_fields} + + return {} class NetBoxReadOnlyModelViewSet( @@ -165,6 +170,28 @@ class NetBoxModelViewSet( # Creates + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + bulk_create = getattr(serializer, 'many', False) + self.perform_create(serializer) + + # After creating the instance(s), re-initialize the serializer with a queryset + # to ensure related objects are prefetched. + if bulk_create: + instance_pks = [obj.pk for obj in serializer.instance] + # Order by PK to ensure that the ordering of objects in the response + # matches the ordering of those in the request. + qs = self.get_queryset().filter(pk__in=instance_pks).order_by('pk') + else: + qs = self.get_queryset().get(pk=serializer.instance.pk) + + # Re-serialize the instance(s) with prefetched data + serializer = self.get_serializer(qs, many=bulk_create) + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + def perform_create(self, serializer): model = self.queryset.model logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}') @@ -181,9 +208,20 @@ class NetBoxModelViewSet( # Updates def update(self, request, *args, **kwargs): - # Hotwire get_object() to ensure we save a pre-change snapshot - self.get_object = self.get_object_with_snapshot - return super().update(request, *args, **kwargs) + partial = kwargs.pop('partial', False) + instance = self.get_object_with_snapshot() + serializer = self.get_serializer(instance, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + # After updating the instance, re-initialize the serializer with a queryset + # to ensure related objects are prefetched. + qs = self.get_queryset().get(pk=serializer.instance.pk) + + # Re-serialize the instance(s) with prefetched data + serializer = self.get_serializer(qs) + + return Response(serializer.data) def perform_update(self, serializer): model = self.queryset.model diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index e74488164..7f753240e 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -108,13 +108,17 @@ class BulkUpdateModelMixin: obj.pop('id'): obj for obj in request.data } - data = self.perform_bulk_update(qs, update_data, partial=partial) + object_pks = self.perform_bulk_update(qs, update_data, partial=partial) - return Response(data, status=status.HTTP_200_OK) + # Prefetch related objects for all updated instances + qs = self.get_queryset().filter(pk__in=object_pks) + serializer = self.get_serializer(qs, many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) def perform_bulk_update(self, objects, update_data, partial): + updated_pks = [] with transaction.atomic(using=router.db_for_write(self.queryset.model)): - data_list = [] for obj in objects: data = update_data.get(obj.id) if hasattr(obj, 'snapshot'): @@ -122,9 +126,9 @@ class BulkUpdateModelMixin: serializer = self.get_serializer(obj, data=data, partial=partial) serializer.is_valid(raise_exception=True) self.perform_update(serializer) - data_list.append(serializer.data) + updated_pks.append(obj.pk) - return data_list + return updated_pks def bulk_partial_update(self, request, *args, **kwargs): kwargs['partial'] = True diff --git a/netbox/netbox/context.py b/netbox/netbox/context.py index 744c36df4..78cad2176 100644 --- a/netbox/netbox/context.py +++ b/netbox/netbox/context.py @@ -3,8 +3,10 @@ from contextvars import ContextVar __all__ = ( 'current_request', 'events_queue', + 'query_cache', ) current_request = ContextVar('current_request', default=None) events_queue = ContextVar('events_queue', default=dict()) +query_cache = ContextVar('query_cache', default=None) diff --git a/netbox/netbox/context_managers.py b/netbox/netbox/context_managers.py index 7b01cce94..1d2ff61a2 100644 --- a/netbox/netbox/context_managers.py +++ b/netbox/netbox/context_managers.py @@ -1,6 +1,7 @@ +from collections import defaultdict from contextlib import contextmanager -from netbox.context import current_request, events_queue +from netbox.context import current_request, events_queue, query_cache from netbox.utils import register_request_processor from extras.events import flush_events @@ -16,6 +17,7 @@ def event_tracking(request): """ current_request.set(request) events_queue.set({}) + query_cache.set(defaultdict(dict)) yield @@ -26,3 +28,4 @@ def event_tracking(request): # Clear context vars current_request.set(None) events_queue.set({}) + query_cache.set(None) diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index 7ed98209d..33f7cb4a3 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -305,18 +305,13 @@ class NetBoxModelFilterSet(ChangeLoggedModelFilterSet): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Dynamically add a Filter for each CustomField applicable to the parent model - custom_fields = CustomField.objects.filter( - object_types=ContentType.objects.get_for_model(self._meta.model) - ).exclude( - filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED - ) - custom_field_filters = {} - for custom_field in custom_fields: - filter_name = f'cf_{custom_field.name}' - filter_instance = custom_field.to_filter() - if filter_instance: + for custom_field in CustomField.objects.get_for_model(self._meta.model): + if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_DISABLED: + # Skip disabled fields + continue + if filter_instance := custom_field.to_filter(): + filter_name = f'cf_{custom_field.name}' custom_field_filters[filter_name] = filter_instance # Add relevant additional lookups diff --git a/netbox/netbox/forms/bulk_import.py b/netbox/netbox/forms/bulk_import.py index 9d04135d4..c5609a2a7 100644 --- a/netbox/netbox/forms/bulk_import.py +++ b/netbox/netbox/forms/bulk_import.py @@ -31,10 +31,11 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): ) def _get_custom_fields(self, content_type): - return CustomField.objects.filter( - object_types=content_type, - ui_editable=CustomFieldUIEditableChoices.YES - ) + # Return only custom fields that are editable in the UI + return [ + cf for cf in CustomField.objects.get_for_model(content_type.model_class()) + if cf.ui_editable == CustomFieldUIEditableChoices.YES + ] def _get_form_field(self, customfield): return customfield.to_form_field(for_csv_import=True) diff --git a/netbox/netbox/forms/filtersets.py b/netbox/netbox/forms/filtersets.py index d334ee914..8ff20264e 100644 --- a/netbox/netbox/forms/filtersets.py +++ b/netbox/netbox/forms/filtersets.py @@ -1,12 +1,10 @@ from django import forms -from django.db.models import Q from django.utils.translation import gettext_lazy as _ from extras.choices import * -from users.models import Owner -from utilities.forms.fields import DynamicModelChoiceField, QueryField +from utilities.forms.fields import QueryField from utilities.forms.mixins import FilterModifierMixin -from .mixins import CustomFieldsMixin, SavedFiltersMixin +from .mixins import CustomFieldsMixin, OwnerFilterMixin, SavedFiltersMixin __all__ = ( 'NestedGroupModelFilterSetForm', @@ -36,10 +34,13 @@ class NetBoxModelFilterSetForm(FilterModifierMixin, CustomFieldsMixin, SavedFilt selector_fields = ('filter_id', 'q') def _get_custom_fields(self, content_type): - return super()._get_custom_fields(content_type).exclude( - Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) | - Q(type=CustomFieldTypeChoices.TYPE_JSON) - ) + # Return only non-hidden custom fields for which filtering is enabled (excluding JSON fields) + return [ + cf for cf in super()._get_custom_fields(content_type) if ( + cf.filter_logic != CustomFieldFilterLogicChoices.FILTER_DISABLED and + cf.type != CustomFieldTypeChoices.TYPE_JSON + ) + ] def _get_form_field(self, customfield): return customfield.to_form_field( @@ -47,14 +48,6 @@ class NetBoxModelFilterSetForm(FilterModifierMixin, CustomFieldsMixin, SavedFilt ) -class OwnerFilterMixin(forms.Form): - owner_id = DynamicModelChoiceField( - queryset=Owner.objects.all(), - required=False, - label=_('Owner'), - ) - - class PrimaryModelFilterSetForm(OwnerFilterMixin, NetBoxModelFilterSetForm): """ FilterSet form for models which inherit from PrimaryModel. diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index 4ee11b0bb..1460c86ba 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -4,13 +4,14 @@ from django.utils.translation import gettext as _ from core.models import ObjectType from extras.choices import * from extras.models import * -from users.models import Owner +from users.models import OwnerGroup, Owner from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField __all__ = ( 'ChangelogMessageMixin', 'CustomFieldsMixin', 'OwnerMixin', + 'OwnerFilterMixin', 'SavedFiltersMixin', 'TagsMixin', ) @@ -22,7 +23,7 @@ class ChangelogMessageMixin(forms.Form): """ changelog_message = forms.CharField( required=False, - max_length=200 + max_length=200, ) def __init__(self, *args, **kwargs): @@ -42,6 +43,7 @@ class CustomFieldsMixin: Attributes: model: The model class """ + model = None def __init__(self, *args, **kwargs): @@ -63,9 +65,11 @@ class CustomFieldsMixin: return ObjectType.objects.get_for_model(self.model) def _get_custom_fields(self, content_type): - return CustomField.objects.filter(object_types=content_type).exclude( - ui_editable=CustomFieldUIEditableChoices.HIDDEN - ) + # Return only custom fields that are not hidden from the UI + return [ + cf for cf in CustomField.objects.get_for_model(content_type.model_class()) + if cf.ui_editable != CustomFieldUIEditableChoices.HIDDEN + ] def _get_form_field(self, customfield): return customfield.to_form_field() @@ -86,13 +90,20 @@ class CustomFieldsMixin: class SavedFiltersMixin(forms.Form): + """ + Form mixin for forms that support saved filters. + + Provides a field for selecting a saved filter, + with options limited to those applicable to the form's model. + """ + filter_id = DynamicModelMultipleChoiceField( queryset=SavedFilter.objects.all(), required=False, label=_('Saved Filter'), query_params={ 'usable': True, - } + }, ) def __init__(self, *args, **kwargs): @@ -107,6 +118,13 @@ class SavedFiltersMixin(forms.Form): class TagsMixin(forms.Form): + """ + Mixin for forms that support tagging. + + Provides a field for selecting tags, + with options limited to those applicable to the form's model. + """ + tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False, @@ -124,10 +142,47 @@ class TagsMixin(forms.Form): class OwnerMixin(forms.Form): """ - Add an `owner` field to forms for models which support Owner assignment. + Mixin for forms which adds ownership fields. + + Include this mixin in forms for models which + support owner and/or owner group assignment. """ + + owner_group = DynamicModelChoiceField( + label=_('Owner group'), + queryset=OwnerGroup.objects.all(), + required=False, + null_option='None', + initial_params={'members': '$owner'}, + ) owner = DynamicModelChoiceField( queryset=Owner.objects.all(), required=False, + query_params={'group_id': '$owner_group'}, + label=_('Owner'), + ) + + +class OwnerFilterMixin(forms.Form): + """ + Mixin for filterset forms which adds owner and owner group filtering. + + Include this mixin in filterset forms for models + which support owner and/or owner group assignment. + """ + + owner_group_id = DynamicModelMultipleChoiceField( + queryset=OwnerGroup.objects.all(), + required=False, + null_option='None', + label=_('Owner Group'), + ) + owner_id = DynamicModelMultipleChoiceField( + queryset=Owner.objects.all(), + required=False, + null_option='None', + query_params={ + 'group_id': '$owner_group_id' + }, label=_('Owner'), ) diff --git a/netbox/netbox/graphql/pagination.py b/netbox/netbox/graphql/pagination.py new file mode 100644 index 000000000..53c159b12 --- /dev/null +++ b/netbox/netbox/graphql/pagination.py @@ -0,0 +1,50 @@ +import strawberry +from strawberry.types.unset import UNSET +from strawberry_django.pagination import _QS, apply + +__all__ = ( + 'OffsetPaginationInfo', + 'OffsetPaginationInput', + 'apply_pagination', +) + + +@strawberry.type +class OffsetPaginationInfo: + offset: int = 0 + limit: int | None = UNSET + start: int | None = UNSET + + +@strawberry.input +class OffsetPaginationInput(OffsetPaginationInfo): + """ + Customized implementation of OffsetPaginationInput to support cursor-based pagination. + """ + pass + + +def apply_pagination( + self, + queryset: _QS, + pagination: OffsetPaginationInput | None = None, + *, + related_field_id: str | None = None, +) -> _QS: + """ + Replacement for the `apply_pagination()` method on StrawberryDjangoField to support cursor-based pagination. + """ + if pagination is not None and pagination.start not in (None, UNSET): + if pagination.offset: + raise ValueError('Cannot specify both `start` and `offset` in pagination.') + if pagination.start < 0: + raise ValueError('`start` must be greater than or equal to zero.') + + # Filter the queryset to include only records with a primary key greater than or equal to the start value, + # and force ordering by primary key to ensure consistent pagination across all records. + queryset = queryset.filter(pk__gte=pagination.start).order_by('pk') + + # Ignore `offset` when `start` is set + pagination.offset = 0 + + return apply(pagination, queryset, related_field_id=related_field_id) diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index b6eb62884..51cef465c 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -2,7 +2,7 @@ import json from collections import defaultdict from functools import cached_property -from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.core.validators import ValidationError from django.db import models @@ -121,9 +121,11 @@ class ChangeLoggingMixin(DeleteMixin, models.Model): if hasattr(self, '_prechange_snapshot'): objectchange.prechange_data = self._prechange_snapshot if action in (ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE): - objectchange.postchange_data = self.serialize_object(exclude=exclude) + self._postchange_snapshot = self.serialize_object(exclude=exclude) + objectchange.postchange_data = self._postchange_snapshot return objectchange + to_objectchange.alters_data = True class CloningMixin(models.Model): @@ -159,6 +161,13 @@ class CloningMixin(models.Model): elif field_value not in (None, ''): attrs[field_name] = field_value + # Handle GenericForeignKeys. If the CT and ID fields are being cloned, also + # include the name of the GFK attribute itself, as this is what forms expect. + for field in self._meta.private_fields: + if isinstance(field, GenericForeignKey): + if field.ct_field in attrs and field.fk_field in attrs: + attrs[field.name] = attrs[field.fk_field] + # Include tags (if applicable) if is_taggable(self): attrs['tags'] = [tag.pk for tag in self.tags.all()] @@ -317,9 +326,11 @@ class CustomFieldsMixin(models.Model): raise ValidationError(_("Missing required custom field '{name}'.").format(name=cf.name)) def save(self, *args, **kwargs): - # Populate default values if omitted - for cf in self.custom_fields.filter(default__isnull=False): - if cf.name not in self.custom_field_data: + from extras.models import CustomField + + # Populate default values for custom fields not already present in the object data + for cf in CustomField.objects.get_for_model(self): + if cf.name not in self.custom_field_data and cf.default is not None: self.custom_field_data[cf.name] = cf.default super().save(*args, **kwargs) diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 052200f47..a068f0d18 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -1,3 +1,5 @@ +from functools import cache + from django.utils.translation import gettext_lazy as _ from netbox.registry import registry @@ -409,60 +411,10 @@ ADMIN_MENU = Menu( MenuGroup( label=_('Authentication'), items=( - MenuItem( - link='users:user_list', - link_text=_('Users'), - staff_only=True, - permissions=['users.view_user'], - buttons=( - MenuItemButton( - link='users:user_add', - title='Add', - icon_class='mdi mdi-plus-thick', - permissions=['users.add_user'] - ), - MenuItemButton( - link='users:user_bulk_import', - title='Import', - icon_class='mdi mdi-upload', - permissions=['users.add_user'] - ) - ) - ), - MenuItem( - link='users:group_list', - link_text=_('Groups'), - staff_only=True, - permissions=['users.view_group'], - buttons=( - MenuItemButton( - link='users:group_add', - title='Add', - icon_class='mdi mdi-plus-thick', - permissions=['users.add_group'] - ), - MenuItemButton( - link='users:group_bulk_import', - title='Import', - icon_class='mdi mdi-upload', - permissions=['users.add_group'] - ) - ) - ), - MenuItem( - link='users:token_list', - link_text=_('API Tokens'), - staff_only=True, - permissions=['users.view_token'], - buttons=get_model_buttons('users', 'token') - ), - MenuItem( - link='users:objectpermission_list', - link_text=_('Permissions'), - staff_only=True, - permissions=['users.view_objectpermission'], - buttons=get_model_buttons('users', 'objectpermission', actions=['add']) - ), + get_model_item('users', 'user', _('Users')), + get_model_item('users', 'group', _('Groups')), + get_model_item('users', 'token', _('API Tokens')), + get_model_item('users', 'objectpermission', _('Permissions'), actions=['add']), ), ), MenuGroup( @@ -501,40 +453,49 @@ ADMIN_MENU = Menu( ), ) -MENUS = [ - ORGANIZATION_MENU, - RACKS_MENU, - DEVICES_MENU, - CONNECTIONS_MENU, - WIRELESS_MENU, - IPAM_MENU, - VPN_MENU, - VIRTUALIZATION_MENU, - CIRCUITS_MENU, - POWER_MENU, - PROVISIONING_MENU, - CUSTOMIZATION_MENU, - OPERATIONS_MENU, -] -# Add top-level plugin menus -for menu in registry['plugins']['menus']: - MENUS.append(menu) - -# Add the default "plugins" menu -if registry['plugins']['menu_items']: - - # Build the default plugins menu - groups = [ - MenuGroup(label=label, items=items) - for label, items in registry['plugins']['menu_items'].items() +@cache +def get_menus(): + """ + Dynamically build and return the list of navigation menus. + This ensures plugin menus registered during app initialization are included. + The result is cached since menus don't change without a Django restart. + """ + menus = [ + ORGANIZATION_MENU, + RACKS_MENU, + DEVICES_MENU, + CONNECTIONS_MENU, + WIRELESS_MENU, + IPAM_MENU, + VPN_MENU, + VIRTUALIZATION_MENU, + CIRCUITS_MENU, + POWER_MENU, + PROVISIONING_MENU, + CUSTOMIZATION_MENU, + OPERATIONS_MENU, ] - plugins_menu = Menu( - label=_("Plugins"), - icon_class="mdi mdi-puzzle", - groups=groups - ) - MENUS.append(plugins_menu) -# Add the admin menu last -MENUS.append(ADMIN_MENU) + # Add top-level plugin menus + for menu in registry['plugins']['menus']: + menus.append(menu) + + # Add the default "plugins" menu + if registry['plugins']['menu_items']: + # Build the default plugins menu + groups = [ + MenuGroup(label=label, items=items) + for label, items in registry['plugins']['menu_items'].items() + ] + plugins_menu = Menu( + label=_("Plugins"), + icon_class="mdi mdi-puzzle", + groups=groups + ) + menus.append(plugins_menu) + + # Add the admin menu last + menus.append(ADMIN_MENU) + + return menus diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index cb08ab4af..d73ab50d3 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -187,7 +187,6 @@ class CachedValueSearchBackend(SearchBackend): return ret def cache(self, instances, indexer=None, remove_existing=True): - object_type = None custom_fields = None # Convert a single instance to an iterable @@ -208,15 +207,18 @@ class CachedValueSearchBackend(SearchBackend): except KeyError: break - # Prefetch any associated custom fields - object_type = ObjectType.objects.get_for_model(indexer.model) - custom_fields = CustomField.objects.filter(object_types=object_type).exclude(search_weight=0) + # Prefetch any associated custom fields (excluding those with a zero search weight) + custom_fields = [ + cf for cf in CustomField.objects.get_for_model(indexer.model) + if cf.search_weight > 0 + ] # Wipe out any previously cached values for the object if remove_existing: self.remove(instance) # Generate cache data + object_type = ObjectType.objects.get_for_model(indexer.model) for field in indexer.to_cache(instance, custom_fields=custom_fields): buffer.append( CachedValue( diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index d7aa7e5c9..e0e497723 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,10 +12,13 @@ from django.core.validators import URLValidator from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ from rest_framework.utils import field_mapping +from strawberry_django import pagination +from strawberry_django.fields.field import StrawberryDjangoField from core.exceptions import IncompatiblePluginError from netbox.config import PARAMS as CONFIG_PARAMS from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW +from netbox.graphql.pagination import OffsetPaginationInput, apply_pagination from netbox.plugins import PluginConfig from netbox.registry import registry import storages.utils # type: ignore @@ -33,6 +36,12 @@ from .monkey import get_unique_validators # Override DRF's get_unique_validators() function with our own (see bug #19302) field_mapping.get_unique_validators = get_unique_validators +# Override strawberry-django's OffsetPaginationInput class to add the `start` parameter +pagination.OffsetPaginationInput = OffsetPaginationInput + +# Patch StrawberryDjangoField to use our custom `apply_pagination()` method with support for cursor-based pagination +StrawberryDjangoField.apply_pagination = apply_pagination + # # Environment setup diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index a2ba13480..a1ac0a3e4 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -242,14 +242,17 @@ class NetBoxTable(BaseTable): (name, deepcopy(column)) for name, column in registered_columns.items() ]) - # Add custom field & custom link columns - object_type = ObjectType.objects.get_for_model(self._meta.model) - custom_fields = CustomField.objects.filter( - object_types=object_type - ).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN) + # Add columns for custom fields + custom_fields = [ + cf for cf in CustomField.objects.get_for_model(self._meta.model) + if cf.ui_visible != CustomFieldUIVisibleChoices.HIDDEN + ] extra_columns.extend([ (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields ]) + + # Add columns for custom links + object_type = ObjectType.objects.get_for_model(self._meta.model) custom_links = CustomLink.objects.filter(object_types=object_type, enabled=True) extra_columns.extend([ (f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links @@ -271,9 +274,14 @@ class NetBoxTable(BaseTable): class PrimaryModelTable(NetBoxTable): + owner_group = tables.Column( + accessor='owner__group', + linkify=True, + verbose_name=_('Owner Group'), + ) owner = tables.Column( linkify=True, - verbose_name=_('Owner') + verbose_name=_('Owner'), ) comments = columns.MarkdownColumn( verbose_name=_('Comments'), @@ -281,9 +289,14 @@ class PrimaryModelTable(NetBoxTable): class OrganizationalModelTable(NetBoxTable): + owner_group = tables.Column( + accessor='owner__group', + linkify=True, + verbose_name=_('Owner Group'), + ) owner = tables.Column( linkify=True, - verbose_name=_('Owner') + verbose_name=_('Owner'), ) comments = columns.MarkdownColumn( verbose_name=_('Comments'), @@ -291,9 +304,14 @@ class OrganizationalModelTable(NetBoxTable): class NestedGroupModelTable(NetBoxTable): + owner_group = tables.Column( + accessor='owner__group', + linkify=True, + verbose_name=_('Owner Group'), + ) owner = tables.Column( linkify=True, - verbose_name=_('Owner') + verbose_name=_('Owner'), ) name = columns.MPTTColumn( verbose_name=_('Name'), diff --git a/netbox/netbox/tests/test_graphql.py b/netbox/netbox/tests/test_graphql.py index 9dfc5d5df..297fe79d3 100644 --- a/netbox/netbox/tests/test_graphql.py +++ b/netbox/netbox/tests/test_graphql.py @@ -4,10 +4,8 @@ from django.test import override_settings from django.urls import reverse from rest_framework import status -from core.models import ObjectType from dcim.choices import LocationStatusChoices from dcim.models import Site, Location -from users.models import ObjectPermission from utilities.testing import disable_warnings, APITestCase, TestCase @@ -45,17 +43,28 @@ class GraphQLTestCase(TestCase): class GraphQLAPITestCase(APITestCase): + @classmethod + def setUpTestData(cls): + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + Site(name='Site 3', slug='site-3'), + Site(name='Site 4', slug='site-4'), + Site(name='Site 5', slug='site-5'), + Site(name='Site 6', slug='site-6'), + Site(name='Site 7', slug='site-7'), + ) + Site.objects.bulk_create(sites) + @override_settings(LOGIN_REQUIRED=True) def test_graphql_filter_objects(self): """ Test the operation of filters for GraphQL API requests. """ - sites = ( - Site(name='Site 1', slug='site-1'), - Site(name='Site 2', slug='site-2'), - Site(name='Site 3', slug='site-3'), - ) - Site.objects.bulk_create(sites) + self.add_permissions('dcim.view_site', 'dcim.view_location') + url = reverse('graphql') + + sites = Site.objects.all()[:3] Location.objects.create( site=sites[0], name='Location 1', @@ -75,18 +84,6 @@ class GraphQLAPITestCase(APITestCase): status=LocationStatusChoices.STATUS_ACTIVE ), - # Add object-level permission - obj_perm = ObjectPermission( - name='Test permission', - actions=['view'] - ) - obj_perm.save() - obj_perm.users.add(self.user) - obj_perm.object_types.add(ObjectType.objects.get_for_model(Location)) - obj_perm.object_types.add(ObjectType.objects.get_for_model(Site)) - - url = reverse('graphql') - # A valid request should return the filtered list query = '{location_list(filters: {site_id: "' + str(sites[0].pk) + '"}) {id site {id}}}' response = self.client.post(url, data={'query': query}, format="json", **self.header) @@ -133,10 +130,136 @@ class GraphQLAPITestCase(APITestCase): self.assertEqual(len(data['data']['location_list']), 0) # Removing the permissions from location should result in an empty locations list - obj_perm.object_types.remove(ObjectType.objects.get_for_model(Location)) + self.remove_permissions('dcim.view_location') query = '{site(id: ' + str(sites[0].pk) + ') {id locations {id}}}' response = self.client.post(url, data={'query': query}, format="json", **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) data = json.loads(response.content) self.assertNotIn('errors', data) self.assertEqual(len(data['data']['site']['locations']), 0) + + def test_offset_pagination(self): + self.add_permissions('dcim.view_site') + url = reverse('graphql') + + # Test `limit` only + query = """ + { + site_list(pagination: {limit: 3}) { + id name + } + } + """ + response = self.client.post(url, data={'query': query}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + data = json.loads(response.content) + self.assertNotIn('errors', data) + self.assertEqual(len(data['data']['site_list']), 3) + self.assertEqual(data['data']['site_list'][0]['name'], 'Site 1') + self.assertEqual(data['data']['site_list'][1]['name'], 'Site 2') + self.assertEqual(data['data']['site_list'][2]['name'], 'Site 3') + + # Test `offset` only + query = """ + { + site_list(pagination: {offset: 3}) { + id name + } + } + """ + response = self.client.post(url, data={'query': query}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + data = json.loads(response.content) + self.assertNotIn('errors', data) + self.assertEqual(len(data['data']['site_list']), 4) + self.assertEqual(data['data']['site_list'][0]['name'], 'Site 4') + self.assertEqual(data['data']['site_list'][1]['name'], 'Site 5') + self.assertEqual(data['data']['site_list'][2]['name'], 'Site 6') + self.assertEqual(data['data']['site_list'][3]['name'], 'Site 7') + + # Test `offset` & `limit` + query = """ + { + site_list(pagination: {offset: 3, limit: 3}) { + id name + } + } + """ + response = self.client.post(url, data={'query': query}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + data = json.loads(response.content) + self.assertNotIn('errors', data) + self.assertEqual(len(data['data']['site_list']), 3) + self.assertEqual(data['data']['site_list'][0]['name'], 'Site 4') + self.assertEqual(data['data']['site_list'][1]['name'], 'Site 5') + self.assertEqual(data['data']['site_list'][2]['name'], 'Site 6') + + def test_cursor_pagination(self): + self.add_permissions('dcim.view_site') + url = reverse('graphql') + + # Page 1 + query = """ + { + site_list(pagination: {start: 0, limit: 3}) { + id name + } + } + """ + response = self.client.post(url, data={'query': query}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + data = json.loads(response.content) + self.assertNotIn('errors', data) + self.assertEqual(len(data['data']['site_list']), 3) + self.assertEqual(data['data']['site_list'][0]['name'], 'Site 1') + self.assertEqual(data['data']['site_list'][1]['name'], 'Site 2') + self.assertEqual(data['data']['site_list'][2]['name'], 'Site 3') + + # Page 2 + start_id = int(data['data']['site_list'][-1]['id']) + 1 + query = """ + { + site_list(pagination: {start: """ + str(start_id) + """, limit: 3}) { + id name + } + } + """ + response = self.client.post(url, data={'query': query}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + data = json.loads(response.content) + self.assertNotIn('errors', data) + self.assertEqual(len(data['data']['site_list']), 3) + self.assertEqual(data['data']['site_list'][0]['name'], 'Site 4') + self.assertEqual(data['data']['site_list'][1]['name'], 'Site 5') + self.assertEqual(data['data']['site_list'][2]['name'], 'Site 6') + + # Page 3 + start_id = int(data['data']['site_list'][-1]['id']) + 1 + query = """ + { + site_list(pagination: {start: """ + str(start_id) + """, limit: 3}) { + id name + } + } + """ + response = self.client.post(url, data={'query': query}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + data = json.loads(response.content) + self.assertNotIn('errors', data) + self.assertEqual(len(data['data']['site_list']), 1) + self.assertEqual(data['data']['site_list'][0]['name'], 'Site 7') + + def test_pagination_conflict(self): + url = reverse('graphql') + query = """ + { + site_list(pagination: {start: 1, offset: 1}) { + id name + } + } + """ + response = self.client.post(url, data={'query': query}, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + data = json.loads(response.content) + self.assertIn('errors', data) + self.assertEqual(data['errors'][0]['message'], 'Cannot specify both `start` and `offset` in pagination.') diff --git a/netbox/netbox/tests/test_model_features.py b/netbox/netbox/tests/test_model_features.py index 190c177ea..de548a800 100644 --- a/netbox/netbox/tests/test_model_features.py +++ b/netbox/netbox/tests/test_model_features.py @@ -1,18 +1,28 @@ +from unittest import skipIf + +from django.conf import settings from django.test import TestCase from core.models import AutoSyncRecord, DataSource +from dcim.models import Site from extras.models import CustomLink +from ipam.models import Prefix from netbox.models.features import get_model_features, has_feature, model_is_public -from netbox.tests.dummy_plugin.models import DummyModel from taggit.models import Tag class ModelFeaturesTestCase(TestCase): + """ + A test case class for verifying model features and utility functions. + """ + @skipIf('netbox.tests.dummy_plugin' not in settings.PLUGINS, 'dummy_plugin not in settings.PLUGINS') def test_model_is_public(self): """ Test that the is_public() utility function returns True for public models only. """ + from netbox.tests.dummy_plugin.models import DummyModel + # Public model self.assertFalse(hasattr(DataSource, '_netbox_private')) self.assertTrue(model_is_public(DataSource)) @@ -51,3 +61,53 @@ class ModelFeaturesTestCase(TestCase): features = get_model_features(CustomLink) self.assertIn('cloning', features) self.assertNotIn('bookmarks', features) + + def test_cloningmixin_injects_gfk_attribute(self): + """ + Tests the cloning mixin with GFK attribute injection in the `clone` method. + + This test validates that the `clone` method correctly handles + and retains the General Foreign Key (GFK) attributes on an + object when the cloning fields are explicitly defined. + """ + site = Site.objects.create(name='Test Site', slug='test-site') + prefix = Prefix.objects.create(prefix='10.0.0.0/24', scope=site) + + original_clone_fields = getattr(Prefix, 'clone_fields', None) + try: + Prefix.clone_fields = ('scope_type', 'scope_id') + attrs = prefix.clone() + + self.assertEqual(attrs['scope_type'], prefix.scope_type_id) + self.assertEqual(attrs['scope_id'], prefix.scope_id) + self.assertEqual(attrs['scope'], prefix.scope_id) + finally: + if original_clone_fields is None: + delattr(Prefix, 'clone_fields') + else: + Prefix.clone_fields = original_clone_fields + + def test_cloningmixin_does_not_inject_gfk_attribute_if_incomplete(self): + """ + Tests the cloning mixin with incomplete cloning fields does not inject the GFK attribute. + + This test validates that the `clone` method correctly handles + the case where the cloning fields are incomplete, ensuring that + the generic foreign key (GFK) attribute is not injected during + the cloning process. + """ + site = Site.objects.create(name='Test Site', slug='test-site') + prefix = Prefix.objects.create(prefix='10.0.0.0/24', scope=site) + + original_clone_fields = getattr(Prefix, 'clone_fields', None) + try: + Prefix.clone_fields = ('scope_type',) + attrs = prefix.clone() + + self.assertIn('scope_type', attrs) + self.assertNotIn('scope', attrs) + finally: + if original_clone_fields is None: + delattr(Prefix, 'clone_fields') + else: + Prefix.clone_fields = original_clone_fields diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index b8d70e112..4e6f8c343 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -5,7 +5,6 @@ from copy import deepcopy from django.contrib import messages from django.contrib.contenttypes.fields import GenericForeignKey, GenericRel -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError from django.db import IntegrityError, router, transaction from django.db.models import ManyToManyField, ProtectedError, RestrictedError @@ -484,12 +483,11 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView): else: instance = self.queryset.model() - # For newly created objects, apply any default custom field values - custom_fields = CustomField.objects.filter( - object_types=ContentType.objects.get_for_model(self.queryset.model), - ui_editable=CustomFieldUIEditableChoices.YES - ) - for cf in custom_fields: + # For newly created objects, apply any default values for custom fields + for cf in CustomField.objects.get_for_model(self.queryset.model): + if cf.ui_editable != CustomFieldUIEditableChoices.YES: + # Skip custom fields which are not editable via the UI + continue field_name = f'cf_{cf.name}' if field_name not in record: record[field_name] = cf.default diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index 4537f14c9..ef2af834d 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -1,5 +1,6 @@ import re from collections import namedtuple +import logging from django.conf import settings from django.contrib import messages @@ -28,6 +29,8 @@ __all__ = ( 'SearchView', ) +logger = logging.getLogger(f'netbox.{__name__}') + Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count')) @@ -50,7 +53,14 @@ class HomeView(ConditionalLoginRequiredMixin, View): # Check whether a new release is available. (Only for superusers.) new_release = None if request.user.is_superuser: - latest_release = cache.get('latest_release') + # cache.get() can raise an exception if the cached value can't be unpickled after dependency upgrades + try: + latest_release = cache.get('latest_release') + except Exception: + logger.debug("Failed to read 'latest_release' from cache; deleting key", exc_info=True) + cache.delete('latest_release') + latest_release = None + if latest_release: release_version, release_url = latest_release if release_version > version.parse(settings.RELEASE.version): diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index e25bb4055..b63db1911 100644 --- a/netbox/project-static/dist/netbox.js +++ b/netbox/project-static/dist/netbox.js @@ -1,11 +1,11 @@ -"use strict";(()=>{var ru=Object.create;var Ni=Object.defineProperty,ou=Object.defineProperties,su=Object.getOwnPropertyDescriptor,au=Object.getOwnPropertyDescriptors,lu=Object.getOwnPropertyNames,gs=Object.getOwnPropertySymbols,cu=Object.getPrototypeOf,vs=Object.prototype.hasOwnProperty,uu=Object.prototype.propertyIsEnumerable;var Gr=(n,e,t)=>e in n?Ni(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t,O=(n,e)=>{for(var t in e||(e={}))vs.call(e,t)&&Gr(n,t,e[t]);if(gs)for(var t of gs(e))uu.call(e,t)&&Gr(n,t,e[t]);return n},ae=(n,e)=>ou(n,au(e));var du=(n,e)=>()=>(e||n((e={exports:{}}).exports,e),e.exports),ys=(n,e)=>{for(var t in e)Ni(n,t,{get:e[t],enumerable:!0})},fu=(n,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of lu(e))!vs.call(n,r)&&r!==t&&Ni(n,r,{get:()=>e[r],enumerable:!(i=su(e,r))||i.enumerable});return n};var hu=(n,e,t)=>(t=n!=null?ru(cu(n)):{},fu(e||!n||!n.__esModule?Ni(t,"default",{value:n,enumerable:!0}):t,n));var ee=(n,e,t)=>Gr(n,typeof e!="symbol"?e+"":e,t);var at=(n,e,t)=>new Promise((i,r)=>{var o=l=>{try{a(t.next(l))}catch(c){r(c)}},s=l=>{try{a(t.throw(l))}catch(c){r(c)}},a=l=>l.done?i(l.value):Promise.resolve(l.value).then(o,s);a((t=t.apply(n,e)).next())});var Ec=du((yi,is)=>{(function(e,t){typeof yi=="object"&&typeof is=="object"?is.exports=t():typeof define=="function"&&define.amd?define([],t):typeof yi=="object"?yi.ClipboardJS=t():e.ClipboardJS=t()})(yi,function(){return(function(){var n={686:(function(i,r,o){"use strict";o.d(r,{default:function(){return Ie}});var s=o(279),a=o.n(s),l=o(370),c=o.n(l),u=o(817),d=o.n(u);function p(W){try{return document.execCommand(W)}catch(M){return!1}}var y=function(M){var D=d()(M);return p("cut"),D},m=y;function v(W){var M=document.documentElement.getAttribute("dir")==="rtl",D=document.createElement("textarea");D.style.fontSize="12pt",D.style.border="0",D.style.padding="0",D.style.margin="0",D.style.position="absolute",D.style[M?"right":"left"]="-9999px";var B=window.pageYOffset||document.documentElement.scrollTop;return D.style.top="".concat(B,"px"),D.setAttribute("readonly",""),D.value=W,D}var w=function(M,D){var B=v(M);D.container.appendChild(B);var V=d()(B);return p("copy"),B.remove(),V},T=function(M){var D=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},B="";return typeof M=="string"?B=w(M,D):M instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(M==null?void 0:M.type)?B=w(M.value,D):(B=d()(M),p("copy")),B},_=T;function S(W){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?S=function(D){return typeof D}:S=function(D){return D&&typeof Symbol=="function"&&D.constructor===Symbol&&D!==Symbol.prototype?"symbol":typeof D},S(W)}var A=function(){var M=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},D=M.action,B=D===void 0?"copy":D,V=M.container,q=M.target,U=M.text;if(B!=="copy"&&B!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&S(q)==="object"&&q.nodeType===1){if(B==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(B==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(U)return _(U,{container:V});if(q)return B==="cut"?m(q):_(q,{container:V})},K=A;function z(W){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?z=function(D){return typeof D}:z=function(D){return D&&typeof Symbol=="function"&&D.constructor===Symbol&&D!==Symbol.prototype?"symbol":typeof D},z(W)}function L(W,M){if(!(W instanceof M))throw new TypeError("Cannot call a class as a function")}function I(W,M){for(var D=0;D0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof V.action=="function"?V.action:this.defaultAction,this.target=typeof V.target=="function"?V.target:this.defaultTarget,this.text=typeof V.text=="function"?V.text:this.defaultText,this.container=z(V.container)==="object"?V.container:document.body}},{key:"listenClick",value:function(V){var q=this;this.listener=c()(V,"click",function(U){return q.onClick(U)})}},{key:"onClick",value:function(V){var q=V.delegateTarget||V.currentTarget,U=this.action(q)||"copy",Z=K({action:U,container:this.container,target:this.target(q),text:this.text(q)});this.emit(Z?"success":"error",{action:U,text:Z,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(V){return ne("action",V)}},{key:"defaultTarget",value:function(V){var q=ne("target",V);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(V){return ne("text",V)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(V){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return _(V,q)}},{key:"cut",value:function(V){return m(V)}},{key:"isSupported",value:function(){var V=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof V=="string"?[V]:V,U=!!document.queryCommandSupported;return q.forEach(function(Z){U=U&&!!document.queryCommandSupported(Z)}),U}}]),D})(a()),Ie=Ue}),828:(function(i){var r=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var o=Element.prototype;o.matches=o.matchesSelector||o.mozMatchesSelector||o.msMatchesSelector||o.oMatchesSelector||o.webkitMatchesSelector}function s(a,l){for(;a&&a.nodeType!==r;){if(typeof a.matches=="function"&&a.matches(l))return a;a=a.parentNode}}i.exports=s}),438:(function(i,r,o){var s=o(828);function a(u,d,p,y,m){var v=c.apply(this,arguments);return u.addEventListener(p,v,m),{destroy:function(){u.removeEventListener(p,v,m)}}}function l(u,d,p,y,m){return typeof u.addEventListener=="function"?a.apply(null,arguments):typeof p=="function"?a.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return a(v,d,p,y,m)}))}function c(u,d,p,y){return function(m){m.delegateTarget=s(m.target,d),m.delegateTarget&&y.call(u,m)}}i.exports=l}),879:(function(i,r){r.node=function(o){return o!==void 0&&o instanceof HTMLElement&&o.nodeType===1},r.nodeList=function(o){var s=Object.prototype.toString.call(o);return o!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in o&&(o.length===0||r.node(o[0]))},r.string=function(o){return typeof o=="string"||o instanceof String},r.fn=function(o){var s=Object.prototype.toString.call(o);return s==="[object Function]"}}),370:(function(i,r,o){var s=o(879),a=o(438);function l(p,y,m){if(!p&&!y&&!m)throw new Error("Missing required arguments");if(!s.string(y))throw new TypeError("Second argument must be a String");if(!s.fn(m))throw new TypeError("Third argument must be a Function");if(s.node(p))return c(p,y,m);if(s.nodeList(p))return u(p,y,m);if(s.string(p))return d(p,y,m);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(p,y,m){return p.addEventListener(y,m),{destroy:function(){p.removeEventListener(y,m)}}}function u(p,y,m){return Array.prototype.forEach.call(p,function(v){v.addEventListener(y,m)}),{destroy:function(){Array.prototype.forEach.call(p,function(v){v.removeEventListener(y,m)})}}}function d(p,y,m){return a(document.body,p,y,m)}i.exports=l}),817:(function(i){function r(o){var s;if(o.nodeName==="SELECT")o.focus(),s=o.value;else if(o.nodeName==="INPUT"||o.nodeName==="TEXTAREA"){var a=o.hasAttribute("readonly");a||o.setAttribute("readonly",""),o.select(),o.setSelectionRange(0,o.value.length),a||o.removeAttribute("readonly"),s=o.value}else{o.hasAttribute("contenteditable")&&o.focus();var l=window.getSelection(),c=document.createRange();c.selectNodeContents(o),l.removeAllRanges(),l.addRange(c),s=l.toString()}return s}i.exports=r}),279:(function(i){function r(){}r.prototype={on:function(o,s,a){var l=this.e||(this.e={});return(l[o]||(l[o]=[])).push({fn:s,ctx:a}),this},once:function(o,s,a){var l=this;function c(){l.off(o,c),s.apply(a,arguments)}return c._=s,this.on(o,c,a)},emit:function(o){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[o]||[]).slice(),l=0,c=a.length;for(l;lTs,afterRead:()=>_s,afterWrite:()=>Ss,applyStyles:()=>hn,arrow:()=>Hi,auto:()=>jn,basePlacements:()=>lt,beforeMain:()=>ws,beforeRead:()=>Es,beforeWrite:()=>Cs,bottom:()=>me,clippingParents:()=>Kr,computeStyles:()=>mn,createPopper:()=>Jn,createPopperBase:()=>Is,createPopperLite:()=>Ps,detectOverflow:()=>ke,end:()=>bt,eventListeners:()=>gn,flip:()=>Fi,hide:()=>$i,left:()=>he,main:()=>xs,modifierPhases:()=>Qr,offset:()=>Bi,placements:()=>qn,popper:()=>$t,popperGenerator:()=>Yt,popperOffsets:()=>En,preventOverflow:()=>Vi,read:()=>bs,reference:()=>Xr,right:()=>pe,start:()=>rt,top:()=>de,variationPlacements:()=>ki,viewport:()=>Wn,write:()=>As});var de="top",me="bottom",pe="right",he="left",jn="auto",lt=[de,me,pe,he],rt="start",bt="end",Kr="clippingParents",Wn="viewport",$t="popper",Xr="reference",ki=lt.reduce(function(n,e){return n.concat([e+"-"+rt,e+"-"+bt])},[]),qn=[].concat(lt,[jn]).reduce(function(n,e){return n.concat([e,e+"-"+rt,e+"-"+bt])},[]),Es="beforeRead",bs="read",_s="afterRead",ws="beforeMain",xs="main",Ts="afterMain",Cs="beforeWrite",As="write",Ss="afterWrite",Qr=[Es,bs,_s,ws,xs,Ts,Cs,As,Ss];function we(n){return n?(n.nodeName||"").toLowerCase():null}function ce(n){if(n==null)return window;if(n.toString()!=="[object Window]"){var e=n.ownerDocument;return e&&e.defaultView||window}return n}function Ye(n){var e=ce(n).Element;return n instanceof e||n instanceof Element}function be(n){var e=ce(n).HTMLElement;return n instanceof e||n instanceof HTMLElement}function fn(n){if(typeof ShadowRoot=="undefined")return!1;var e=ce(n).ShadowRoot;return n instanceof e||n instanceof ShadowRoot}function pu(n){var e=n.state;Object.keys(e.elements).forEach(function(t){var i=e.styles[t]||{},r=e.attributes[t]||{},o=e.elements[t];!be(o)||!we(o)||(Object.assign(o.style,i),Object.keys(r).forEach(function(s){var a=r[s];a===!1?o.removeAttribute(s):o.setAttribute(s,a===!0?"":a)}))})}function mu(n){var e=n.state,t={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,t.popper),e.styles=t,e.elements.arrow&&Object.assign(e.elements.arrow.style,t.arrow),function(){Object.keys(e.elements).forEach(function(i){var r=e.elements[i],o=e.attributes[i]||{},s=Object.keys(e.styles.hasOwnProperty(i)?e.styles[i]:t[i]),a=s.reduce(function(l,c){return l[c]="",l},{});!be(r)||!we(r)||(Object.assign(r.style,a),Object.keys(o).forEach(function(l){r.removeAttribute(l)}))})}}var hn={name:"applyStyles",enabled:!0,phase:"write",fn:pu,effect:mu,requires:["computeStyles"]};function xe(n){return n.split("-")[0]}var Ze=Math.max,Bt=Math.min,ct=Math.round;function pn(){var n=navigator.userAgentData;return n!=null&&n.brands&&Array.isArray(n.brands)?n.brands.map(function(e){return e.brand+"/"+e.version}).join(" "):navigator.userAgent}function Un(){return!/^((?!chrome|android).)*safari/i.test(pn())}function Ge(n,e,t){e===void 0&&(e=!1),t===void 0&&(t=!1);var i=n.getBoundingClientRect(),r=1,o=1;e&&be(n)&&(r=n.offsetWidth>0&&ct(i.width)/n.offsetWidth||1,o=n.offsetHeight>0&&ct(i.height)/n.offsetHeight||1);var s=Ye(n)?ce(n):window,a=s.visualViewport,l=!Un()&&t,c=(i.left+(l&&a?a.offsetLeft:0))/r,u=(i.top+(l&&a?a.offsetTop:0))/o,d=i.width/r,p=i.height/o;return{width:d,height:p,top:u,right:c+d,bottom:u+p,left:c,x:c,y:u}}function Vt(n){var e=Ge(n),t=n.offsetWidth,i=n.offsetHeight;return Math.abs(e.width-t)<=1&&(t=e.width),Math.abs(e.height-i)<=1&&(i=e.height),{x:n.offsetLeft,y:n.offsetTop,width:t,height:i}}function Yn(n,e){var t=e.getRootNode&&e.getRootNode();if(n.contains(e))return!0;if(t&&fn(t)){var i=e;do{if(i&&n.isSameNode(i))return!0;i=i.parentNode||i.host}while(i)}return!1}function Ne(n){return ce(n).getComputedStyle(n)}function Jr(n){return["table","td","th"].indexOf(we(n))>=0}function Ae(n){return((Ye(n)?n.ownerDocument:n.document)||window.document).documentElement}function ut(n){return we(n)==="html"?n:n.assignedSlot||n.parentNode||(fn(n)?n.host:null)||Ae(n)}function Ds(n){return!be(n)||Ne(n).position==="fixed"?null:n.offsetParent}function gu(n){var e=/firefox/i.test(pn()),t=/Trident/i.test(pn());if(t&&be(n)){var i=Ne(n);if(i.position==="fixed")return null}var r=ut(n);for(fn(r)&&(r=r.host);be(r)&&["html","body"].indexOf(we(r))<0;){var o=Ne(r);if(o.transform!=="none"||o.perspective!=="none"||o.contain==="paint"||["transform","perspective"].indexOf(o.willChange)!==-1||e&&o.willChange==="filter"||e&&o.filter&&o.filter!=="none")return r;r=r.parentNode}return null}function et(n){for(var e=ce(n),t=Ds(n);t&&Jr(t)&&Ne(t).position==="static";)t=Ds(t);return t&&(we(t)==="html"||we(t)==="body"&&Ne(t).position==="static")?e:t||gu(n)||e}function zt(n){return["top","bottom"].indexOf(n)>=0?"x":"y"}function jt(n,e,t){return Ze(n,Bt(e,t))}function Os(n,e,t){var i=jt(n,e,t);return i>t?t:i}function Gn(){return{top:0,right:0,bottom:0,left:0}}function Kn(n){return Object.assign({},Gn(),n)}function Xn(n,e){return e.reduce(function(t,i){return t[i]=n,t},{})}var vu=function(e,t){return e=typeof e=="function"?e(Object.assign({},t.rects,{placement:t.placement})):e,Kn(typeof e!="number"?e:Xn(e,lt))};function yu(n){var e,t=n.state,i=n.name,r=n.options,o=t.elements.arrow,s=t.modifiersData.popperOffsets,a=xe(t.placement),l=zt(a),c=[he,pe].indexOf(a)>=0,u=c?"height":"width";if(!(!o||!s)){var d=vu(r.padding,t),p=Vt(o),y=l==="y"?de:he,m=l==="y"?me:pe,v=t.rects.reference[u]+t.rects.reference[l]-s[l]-t.rects.popper[u],w=s[l]-t.rects.reference[l],T=et(o),_=T?l==="y"?T.clientHeight||0:T.clientWidth||0:0,S=v/2-w/2,A=d[y],K=_-p[u]-d[m],z=_/2-p[u]/2+S,L=jt(A,z,K),I=l;t.modifiersData[i]=(e={},e[I]=L,e.centerOffset=L-z,e)}}function Eu(n){var e=n.state,t=n.options,i=t.element,r=i===void 0?"[data-popper-arrow]":i;r!=null&&(typeof r=="string"&&(r=e.elements.popper.querySelector(r),!r)||Yn(e.elements.popper,r)&&(e.elements.arrow=r))}var Hi={name:"arrow",enabled:!0,phase:"main",fn:yu,effect:Eu,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Ke(n){return n.split("-")[1]}var bu={top:"auto",right:"auto",bottom:"auto",left:"auto"};function _u(n,e){var t=n.x,i=n.y,r=e.devicePixelRatio||1;return{x:ct(t*r)/r||0,y:ct(i*r)/r||0}}function Ls(n){var e,t=n.popper,i=n.popperRect,r=n.placement,o=n.variation,s=n.offsets,a=n.position,l=n.gpuAcceleration,c=n.adaptive,u=n.roundOffsets,d=n.isFixed,p=s.x,y=p===void 0?0:p,m=s.y,v=m===void 0?0:m,w=typeof u=="function"?u({x:y,y:v}):{x:y,y:v};y=w.x,v=w.y;var T=s.hasOwnProperty("x"),_=s.hasOwnProperty("y"),S=he,A=de,K=window;if(c){var z=et(t),L="clientHeight",I="clientWidth";if(z===ce(t)&&(z=Ae(t),Ne(z).position!=="static"&&a==="absolute"&&(L="scrollHeight",I="scrollWidth")),z=z,r===de||(r===he||r===pe)&&o===bt){A=me;var k=d&&z===K&&K.visualViewport?K.visualViewport.height:z[L];v-=k-i.height,v*=l?1:-1}if(r===he||(r===de||r===me)&&o===bt){S=pe;var Y=d&&z===K&&K.visualViewport?K.visualViewport.width:z[I];y-=Y-i.width,y*=l?1:-1}}var $=Object.assign({position:a},c&&bu),ie=u===!0?_u({x:y,y:v},ce(t)):{x:y,y:v};if(y=ie.x,v=ie.y,l){var J;return Object.assign({},$,(J={},J[A]=_?"0":"",J[S]=T?"0":"",J.transform=(K.devicePixelRatio||1)<=1?"translate("+y+"px, "+v+"px)":"translate3d("+y+"px, "+v+"px, 0)",J))}return Object.assign({},$,(e={},e[A]=_?v+"px":"",e[S]=T?y+"px":"",e.transform="",e))}function wu(n){var e=n.state,t=n.options,i=t.gpuAcceleration,r=i===void 0?!0:i,o=t.adaptive,s=o===void 0?!0:o,a=t.roundOffsets,l=a===void 0?!0:a,c={placement:xe(e.placement),variation:Ke(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:r,isFixed:e.options.strategy==="fixed"};e.modifiersData.popperOffsets!=null&&(e.styles.popper=Object.assign({},e.styles.popper,Ls(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:s,roundOffsets:l})))),e.modifiersData.arrow!=null&&(e.styles.arrow=Object.assign({},e.styles.arrow,Ls(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})}var mn={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:wu,data:{}};var Ri={passive:!0};function xu(n){var e=n.state,t=n.instance,i=n.options,r=i.scroll,o=r===void 0?!0:r,s=i.resize,a=s===void 0?!0:s,l=ce(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach(function(u){u.addEventListener("scroll",t.update,Ri)}),a&&l.addEventListener("resize",t.update,Ri),function(){o&&c.forEach(function(u){u.removeEventListener("scroll",t.update,Ri)}),a&&l.removeEventListener("resize",t.update,Ri)}}var gn={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:xu,data:{}};var Tu={left:"right",right:"left",bottom:"top",top:"bottom"};function vn(n){return n.replace(/left|right|bottom|top/g,function(e){return Tu[e]})}var Cu={start:"end",end:"start"};function Ii(n){return n.replace(/start|end/g,function(e){return Cu[e]})}function Wt(n){var e=ce(n),t=e.pageXOffset,i=e.pageYOffset;return{scrollLeft:t,scrollTop:i}}function qt(n){return Ge(Ae(n)).left+Wt(n).scrollLeft}function Zr(n,e){var t=ce(n),i=Ae(n),r=t.visualViewport,o=i.clientWidth,s=i.clientHeight,a=0,l=0;if(r){o=r.width,s=r.height;var c=Un();(c||!c&&e==="fixed")&&(a=r.offsetLeft,l=r.offsetTop)}return{width:o,height:s,x:a+qt(n),y:l}}function eo(n){var e,t=Ae(n),i=Wt(n),r=(e=n.ownerDocument)==null?void 0:e.body,o=Ze(t.scrollWidth,t.clientWidth,r?r.scrollWidth:0,r?r.clientWidth:0),s=Ze(t.scrollHeight,t.clientHeight,r?r.scrollHeight:0,r?r.clientHeight:0),a=-i.scrollLeft+qt(n),l=-i.scrollTop;return Ne(r||t).direction==="rtl"&&(a+=Ze(t.clientWidth,r?r.clientWidth:0)-o),{width:o,height:s,x:a,y:l}}function Ut(n){var e=Ne(n),t=e.overflow,i=e.overflowX,r=e.overflowY;return/auto|scroll|overlay|hidden/.test(t+r+i)}function Pi(n){return["html","body","#document"].indexOf(we(n))>=0?n.ownerDocument.body:be(n)&&Ut(n)?n:Pi(ut(n))}function _t(n,e){var t;e===void 0&&(e=[]);var i=Pi(n),r=i===((t=n.ownerDocument)==null?void 0:t.body),o=ce(i),s=r?[o].concat(o.visualViewport||[],Ut(i)?i:[]):i,a=e.concat(s);return r?a:a.concat(_t(ut(s)))}function yn(n){return Object.assign({},n,{left:n.x,top:n.y,right:n.x+n.width,bottom:n.y+n.height})}function Au(n,e){var t=Ge(n,!1,e==="fixed");return t.top=t.top+n.clientTop,t.left=t.left+n.clientLeft,t.bottom=t.top+n.clientHeight,t.right=t.left+n.clientWidth,t.width=n.clientWidth,t.height=n.clientHeight,t.x=t.left,t.y=t.top,t}function Ms(n,e,t){return e===Wn?yn(Zr(n,t)):Ye(e)?Au(e,t):yn(eo(Ae(n)))}function Su(n){var e=_t(ut(n)),t=["absolute","fixed"].indexOf(Ne(n).position)>=0,i=t&&be(n)?et(n):n;return Ye(i)?e.filter(function(r){return Ye(r)&&Yn(r,i)&&we(r)!=="body"}):[]}function to(n,e,t,i){var r=e==="clippingParents"?Su(n):[].concat(e),o=[].concat(r,[t]),s=o[0],a=o.reduce(function(l,c){var u=Ms(n,c,i);return l.top=Ze(u.top,l.top),l.right=Bt(u.right,l.right),l.bottom=Bt(u.bottom,l.bottom),l.left=Ze(u.left,l.left),l},Ms(n,s,i));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}function Qn(n){var e=n.reference,t=n.element,i=n.placement,r=i?xe(i):null,o=i?Ke(i):null,s=e.x+e.width/2-t.width/2,a=e.y+e.height/2-t.height/2,l;switch(r){case de:l={x:s,y:e.y-t.height};break;case me:l={x:s,y:e.y+e.height};break;case pe:l={x:e.x+e.width,y:a};break;case he:l={x:e.x-t.width,y:a};break;default:l={x:e.x,y:e.y}}var c=r?zt(r):null;if(c!=null){var u=c==="y"?"height":"width";switch(o){case rt:l[c]=l[c]-(e[u]/2-t[u]/2);break;case bt:l[c]=l[c]+(e[u]/2-t[u]/2);break;default:}}return l}function ke(n,e){e===void 0&&(e={});var t=e,i=t.placement,r=i===void 0?n.placement:i,o=t.strategy,s=o===void 0?n.strategy:o,a=t.boundary,l=a===void 0?Kr:a,c=t.rootBoundary,u=c===void 0?Wn:c,d=t.elementContext,p=d===void 0?$t:d,y=t.altBoundary,m=y===void 0?!1:y,v=t.padding,w=v===void 0?0:v,T=Kn(typeof w!="number"?w:Xn(w,lt)),_=p===$t?Xr:$t,S=n.rects.popper,A=n.elements[m?_:p],K=to(Ye(A)?A:A.contextElement||Ae(n.elements.popper),l,u,s),z=Ge(n.elements.reference),L=Qn({reference:z,element:S,strategy:"absolute",placement:r}),I=yn(Object.assign({},S,L)),k=p===$t?I:z,Y={top:K.top-k.top+T.top,bottom:k.bottom-K.bottom+T.bottom,left:K.left-k.left+T.left,right:k.right-K.right+T.right},$=n.modifiersData.offset;if(p===$t&&$){var ie=$[r];Object.keys(Y).forEach(function(J){var Te=[pe,me].indexOf(J)>=0?1:-1,Ce=[de,me].indexOf(J)>=0?"y":"x";Y[J]+=ie[Ce]*Te})}return Y}function no(n,e){e===void 0&&(e={});var t=e,i=t.placement,r=t.boundary,o=t.rootBoundary,s=t.padding,a=t.flipVariations,l=t.allowedAutoPlacements,c=l===void 0?qn:l,u=Ke(i),d=u?a?ki:ki.filter(function(m){return Ke(m)===u}):lt,p=d.filter(function(m){return c.indexOf(m)>=0});p.length===0&&(p=d);var y=p.reduce(function(m,v){return m[v]=ke(n,{placement:v,boundary:r,rootBoundary:o,padding:s})[xe(v)],m},{});return Object.keys(y).sort(function(m,v){return y[m]-y[v]})}function Du(n){if(xe(n)===jn)return[];var e=vn(n);return[Ii(n),e,Ii(e)]}function Ou(n){var e=n.state,t=n.options,i=n.name;if(!e.modifiersData[i]._skip){for(var r=t.mainAxis,o=r===void 0?!0:r,s=t.altAxis,a=s===void 0?!0:s,l=t.fallbackPlacements,c=t.padding,u=t.boundary,d=t.rootBoundary,p=t.altBoundary,y=t.flipVariations,m=y===void 0?!0:y,v=t.allowedAutoPlacements,w=e.options.placement,T=xe(w),_=T===w,S=l||(_||!m?[vn(w)]:Du(w)),A=[w].concat(S).reduce(function(V,q){return V.concat(xe(q)===jn?no(e,{placement:q,boundary:u,rootBoundary:d,padding:c,flipVariations:m,allowedAutoPlacements:v}):q)},[]),K=e.rects.reference,z=e.rects.popper,L=new Map,I=!0,k=A[0],Y=0;Y=0,Ce=Te?"width":"height",se=ke(e,{placement:$,boundary:u,rootBoundary:d,altBoundary:p,padding:c}),ne=Te?J?pe:he:J?me:de;K[Ce]>z[Ce]&&(ne=vn(ne));var Ue=vn(ne),Ie=[];if(o&&Ie.push(se[ie]<=0),a&&Ie.push(se[ne]<=0,se[Ue]<=0),Ie.every(function(V){return V})){k=$,I=!1;break}L.set($,Ie)}if(I)for(var W=m?3:1,M=function(q){var U=A.find(function(Z){var oe=L.get(Z);if(oe)return oe.slice(0,q).every(function(Et){return Et})});if(U)return k=U,"break"},D=W;D>0;D--){var B=M(D);if(B==="break")break}e.placement!==k&&(e.modifiersData[i]._skip=!0,e.placement=k,e.reset=!0)}}var Fi={name:"flip",enabled:!0,phase:"main",fn:Ou,requiresIfExists:["offset"],data:{_skip:!1}};function Ns(n,e,t){return t===void 0&&(t={x:0,y:0}),{top:n.top-e.height-t.y,right:n.right-e.width+t.x,bottom:n.bottom-e.height+t.y,left:n.left-e.width-t.x}}function ks(n){return[de,pe,me,he].some(function(e){return n[e]>=0})}function Lu(n){var e=n.state,t=n.name,i=e.rects.reference,r=e.rects.popper,o=e.modifiersData.preventOverflow,s=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=Ns(s,i),c=Ns(a,r,o),u=ks(l),d=ks(c);e.modifiersData[t]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:u,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":u,"data-popper-escaped":d})}var $i={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:Lu};function Mu(n,e,t){var i=xe(n),r=[he,de].indexOf(i)>=0?-1:1,o=typeof t=="function"?t(Object.assign({},e,{placement:n})):t,s=o[0],a=o[1];return s=s||0,a=(a||0)*r,[he,pe].indexOf(i)>=0?{x:a,y:s}:{x:s,y:a}}function Nu(n){var e=n.state,t=n.options,i=n.name,r=t.offset,o=r===void 0?[0,0]:r,s=qn.reduce(function(u,d){return u[d]=Mu(d,e.rects,o),u},{}),a=s[e.placement],l=a.x,c=a.y;e.modifiersData.popperOffsets!=null&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[i]=s}var Bi={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Nu};function ku(n){var e=n.state,t=n.name;e.modifiersData[t]=Qn({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})}var En={name:"popperOffsets",enabled:!0,phase:"read",fn:ku,data:{}};function io(n){return n==="x"?"y":"x"}function Hu(n){var e=n.state,t=n.options,i=n.name,r=t.mainAxis,o=r===void 0?!0:r,s=t.altAxis,a=s===void 0?!1:s,l=t.boundary,c=t.rootBoundary,u=t.altBoundary,d=t.padding,p=t.tether,y=p===void 0?!0:p,m=t.tetherOffset,v=m===void 0?0:m,w=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:u}),T=xe(e.placement),_=Ke(e.placement),S=!_,A=zt(T),K=io(A),z=e.modifiersData.popperOffsets,L=e.rects.reference,I=e.rects.popper,k=typeof v=="function"?v(Object.assign({},e.rects,{placement:e.placement})):v,Y=typeof k=="number"?{mainAxis:k,altAxis:k}:Object.assign({mainAxis:0,altAxis:0},k),$=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,ie={x:0,y:0};if(z){if(o){var J,Te=A==="y"?de:he,Ce=A==="y"?me:pe,se=A==="y"?"height":"width",ne=z[A],Ue=ne+w[Te],Ie=ne-w[Ce],W=y?-I[se]/2:0,M=_===rt?L[se]:I[se],D=_===rt?-I[se]:-L[se],B=e.elements.arrow,V=y&&B?Vt(B):{width:0,height:0},q=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:Gn(),U=q[Te],Z=q[Ce],oe=jt(0,L[se],V[se]),Et=S?L[se]/2-W-oe-U-Y.mainAxis:M-oe-U-Y.mainAxis,zr=S?-L[se]/2+W+oe+Z+Y.mainAxis:D+oe+Z+Y.mainAxis,on=e.elements.arrow&&et(e.elements.arrow),sn=on?A==="y"?on.clientTop||0:on.clientLeft||0:0,Ci=(J=$==null?void 0:$[A])!=null?J:0,jr=ne+Et-Ci-sn,Ai=ne+zr-Ci,Si=jt(y?Bt(Ue,jr):Ue,ne,y?Ze(Ie,Ai):Ie);z[A]=Si,ie[A]=Si-ne}if(a){var $n,Di=A==="x"?de:he,an=A==="x"?me:pe,ot=z[K],ln=K==="y"?"height":"width",Bn=ot+w[Di],cn=ot-w[an],un=[de,he].indexOf(T)!==-1,Ft=($n=$==null?void 0:$[K])!=null?$n:0,Oi=un?Bn:ot-L[ln]-I[ln]-Ft+Y.altAxis,Vn=un?ot+L[ln]+I[ln]-Ft-Y.altAxis:cn,Li=y&&un?Os(Oi,ot,Vn):jt(y?Oi:Bn,ot,y?Vn:cn);z[K]=Li,ie[K]=Li-ot}e.modifiersData[i]=ie}}var Vi={name:"preventOverflow",enabled:!0,phase:"main",fn:Hu,requiresIfExists:["offset"]};function ro(n){return{scrollLeft:n.scrollLeft,scrollTop:n.scrollTop}}function oo(n){return n===ce(n)||!be(n)?Wt(n):ro(n)}function Ru(n){var e=n.getBoundingClientRect(),t=ct(e.width)/n.offsetWidth||1,i=ct(e.height)/n.offsetHeight||1;return t!==1||i!==1}function so(n,e,t){t===void 0&&(t=!1);var i=be(e),r=be(e)&&Ru(e),o=Ae(e),s=Ge(n,r,t),a={scrollLeft:0,scrollTop:0},l={x:0,y:0};return(i||!i&&!t)&&((we(e)!=="body"||Ut(o))&&(a=oo(e)),be(e)?(l=Ge(e,!0),l.x+=e.clientLeft,l.y+=e.clientTop):o&&(l.x=qt(o))),{x:s.left+a.scrollLeft-l.x,y:s.top+a.scrollTop-l.y,width:s.width,height:s.height}}function Iu(n){var e=new Map,t=new Set,i=[];n.forEach(function(o){e.set(o.name,o)});function r(o){t.add(o.name);var s=[].concat(o.requires||[],o.requiresIfExists||[]);s.forEach(function(a){if(!t.has(a)){var l=e.get(a);l&&r(l)}}),i.push(o)}return n.forEach(function(o){t.has(o.name)||r(o)}),i}function ao(n){var e=Iu(n);return Qr.reduce(function(t,i){return t.concat(e.filter(function(r){return r.phase===i}))},[])}function lo(n){var e;return function(){return e||(e=new Promise(function(t){Promise.resolve().then(function(){e=void 0,t(n())})})),e}}function co(n){var e=n.reduce(function(t,i){var r=t[i.name];return t[i.name]=r?Object.assign({},r,i,{options:Object.assign({},r.options,i.options),data:Object.assign({},r.data,i.data)}):i,t},{});return Object.keys(e).map(function(t){return e[t]})}var Hs={placement:"bottom",modifiers:[],strategy:"absolute"};function Rs(){for(var n=arguments.length,e=new Array(n),t=0;t(n&&window.CSS&&window.CSS.escape&&(n=n.replace(/#([^\s"#']+)/g,(e,t)=>`#${CSS.escape(t)}`)),n),Vu=n=>n==null?`${n}`:Object.prototype.toString.call(n).match(/\s([a-z]+)/i)[1].toLowerCase(),zu=n=>{do n+=Math.floor(Math.random()*$u);while(document.getElementById(n));return n},ju=n=>{if(!n)return 0;let{transitionDuration:e,transitionDelay:t}=window.getComputedStyle(n),i=Number.parseFloat(e),r=Number.parseFloat(t);return!i&&!r?0:(e=e.split(",")[0],t=t.split(",")[0],(Number.parseFloat(e)+Number.parseFloat(t))*Bu)},ha=n=>{n.dispatchEvent(new Event(So))},dt=n=>!n||typeof n!="object"?!1:(typeof n.jquery!="undefined"&&(n=n[0]),typeof n.nodeType!="undefined"),xt=n=>dt(n)?n.jquery?n[0]:n:typeof n=="string"&&n.length>0?document.querySelector(fa(n)):null,An=n=>{if(!dt(n)||n.getClientRects().length===0)return!1;let e=getComputedStyle(n).getPropertyValue("visibility")==="visible",t=n.closest("details:not([open])");if(!t)return e;if(t!==n){let i=n.closest("summary");if(i&&i.parentNode!==t||i===null)return!1}return e},Tt=n=>!n||n.nodeType!==Node.ELEMENT_NODE||n.classList.contains("disabled")?!0:typeof n.disabled!="undefined"?n.disabled:n.hasAttribute("disabled")&&n.getAttribute("disabled")!=="false",pa=n=>{if(!document.documentElement.attachShadow)return null;if(typeof n.getRootNode=="function"){let e=n.getRootNode();return e instanceof ShadowRoot?e:null}return n instanceof ShadowRoot?n:n.parentNode?pa(n.parentNode):null},Qi=()=>{},ii=n=>{n.offsetHeight},ma=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,fo=[],Wu=n=>{document.readyState==="loading"?(fo.length||document.addEventListener("DOMContentLoaded",()=>{for(let e of fo)e()}),fo.push(n)):n()},Xe=()=>document.documentElement.dir==="rtl",Je=n=>{Wu(()=>{let e=ma();if(e){let t=n.NAME,i=e.fn[t];e.fn[t]=n.jQueryInterface,e.fn[t].Constructor=n,e.fn[t].noConflict=()=>(e.fn[t]=i,n.jQueryInterface)}})},Pe=(n,e=[],t=n)=>typeof n=="function"?n.call(...e):t,ga=(n,e,t=!0)=>{if(!t){Pe(n);return}let r=ju(e)+5,o=!1,s=({target:a})=>{a===e&&(o=!0,e.removeEventListener(So,s),Pe(n))};e.addEventListener(So,s),setTimeout(()=>{o||ha(e)},r)},Mo=(n,e,t,i)=>{let r=n.length,o=n.indexOf(e);return o===-1?!t&&i?n[r-1]:n[0]:(o+=t?1:-1,i&&(o=(o+r)%r),n[Math.max(0,Math.min(o,r-1))])},qu=/[^.]*(?=\..*)\.|.*/,Uu=/\..*/,Yu=/::\d+$/,ho={},Fs=1,va={mouseenter:"mouseover",mouseleave:"mouseout"},Gu=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function ya(n,e){return e&&`${e}::${Fs++}`||n.uidEvent||Fs++}function Ea(n){let e=ya(n);return n.uidEvent=e,ho[e]=ho[e]||{},ho[e]}function Ku(n,e){return function t(i){return No(i,{delegateTarget:n}),t.oneOff&&x.off(n,i.type,e),e.apply(n,[i])}}function Xu(n,e,t){return function i(r){let o=n.querySelectorAll(e);for(let{target:s}=r;s&&s!==this;s=s.parentNode)for(let a of o)if(a===s)return No(r,{delegateTarget:s}),i.oneOff&&x.off(n,r.type,e,t),t.apply(s,[r])}}function ba(n,e,t=null){return Object.values(n).find(i=>i.callable===e&&i.delegationSelector===t)}function _a(n,e,t){let i=typeof e=="string",r=i?t:e||t,o=wa(n);return Gu.has(o)||(o=n),[i,r,o]}function $s(n,e,t,i,r){if(typeof e!="string"||!n)return;let[o,s,a]=_a(e,t,i);e in va&&(s=(m=>function(v){if(!v.relatedTarget||v.relatedTarget!==v.delegateTarget&&!v.delegateTarget.contains(v.relatedTarget))return m.call(this,v)})(s));let l=Ea(n),c=l[a]||(l[a]={}),u=ba(c,s,o?t:null);if(u){u.oneOff=u.oneOff&&r;return}let d=ya(s,e.replace(qu,"")),p=o?Xu(n,t,s):Ku(n,s);p.delegationSelector=o?t:null,p.callable=s,p.oneOff=r,p.uidEvent=d,c[d]=p,n.addEventListener(a,p,o)}function Do(n,e,t,i,r){let o=ba(e[t],i,r);o&&(n.removeEventListener(t,o,!!r),delete e[t][o.uidEvent])}function Qu(n,e,t,i){let r=e[t]||{};for(let[o,s]of Object.entries(r))o.includes(i)&&Do(n,e,t,s.callable,s.delegationSelector)}function wa(n){return n=n.replace(Uu,""),va[n]||n}var x={on(n,e,t,i){$s(n,e,t,i,!1)},one(n,e,t,i){$s(n,e,t,i,!0)},off(n,e,t,i){if(typeof e!="string"||!n)return;let[r,o,s]=_a(e,t,i),a=s!==e,l=Ea(n),c=l[s]||{},u=e.startsWith(".");if(typeof o!="undefined"){if(!Object.keys(c).length)return;Do(n,l,s,o,r?t:null);return}if(u)for(let d of Object.keys(l))Qu(n,l,d,e.slice(1));for(let[d,p]of Object.entries(c)){let y=d.replace(Yu,"");(!a||e.includes(y))&&Do(n,l,s,p.callable,p.delegationSelector)}},trigger(n,e,t){if(typeof e!="string"||!n)return null;let i=ma(),r=wa(e),o=e!==r,s=null,a=!0,l=!0,c=!1;o&&i&&(s=i.Event(e,t),i(n).trigger(s),a=!s.isPropagationStopped(),l=!s.isImmediatePropagationStopped(),c=s.isDefaultPrevented());let u=No(new Event(e,{bubbles:a,cancelable:!0}),t);return c&&u.preventDefault(),l&&n.dispatchEvent(u),u.defaultPrevented&&s&&s.preventDefault(),u}};function No(n,e={}){for(let[t,i]of Object.entries(e))try{n[t]=i}catch(r){Object.defineProperty(n,t,{configurable:!0,get(){return i}})}return n}function Bs(n){if(n==="true")return!0;if(n==="false")return!1;if(n===Number(n).toString())return Number(n);if(n===""||n==="null")return null;if(typeof n!="string")return n;try{return JSON.parse(decodeURIComponent(n))}catch(e){return n}}function po(n){return n.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}var ft={setDataAttribute(n,e,t){n.setAttribute(`data-bs-${po(e)}`,t)},removeDataAttribute(n,e){n.removeAttribute(`data-bs-${po(e)}`)},getDataAttributes(n){if(!n)return{};let e={},t=Object.keys(n.dataset).filter(i=>i.startsWith("bs")&&!i.startsWith("bsConfig"));for(let i of t){let r=i.replace(/^bs/,"");r=r.charAt(0).toLowerCase()+r.slice(1),e[r]=Bs(n.dataset[i])}return e},getDataAttribute(n,e){return Bs(n.getAttribute(`data-bs-${po(e)}`))}},Xt=class{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(e){return e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e}_mergeConfigObj(e,t){let i=dt(t)?ft.getDataAttribute(t,"config"):{};return O(O(O(O({},this.constructor.Default),typeof i=="object"?i:{}),dt(t)?ft.getDataAttributes(t):{}),typeof e=="object"?e:{})}_typeCheckConfig(e,t=this.constructor.DefaultType){for(let[i,r]of Object.entries(t)){let o=e[i],s=dt(o)?"element":Vu(o);if(!new RegExp(r).test(s))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${i}" provided type "${s}" but expected type "${r}".`)}}},Ju="5.3.8",We=class extends Xt{constructor(e,t){super(),e=xt(e),e&&(this._element=e,this._config=this._getConfig(t),uo.set(this._element,this.constructor.DATA_KEY,this))}dispose(){uo.remove(this._element,this.constructor.DATA_KEY),x.off(this._element,this.constructor.EVENT_KEY);for(let e of Object.getOwnPropertyNames(this))this[e]=null}_queueCallback(e,t,i=!0){ga(e,t,i)}_getConfig(e){return e=this._mergeConfigObj(e,this._element),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}static getInstance(e){return uo.get(xt(e),this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,typeof t=="object"?t:null)}static get VERSION(){return Ju}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(e){return`${e}${this.EVENT_KEY}`}},mo=n=>{let e=n.getAttribute("data-bs-target");if(!e||e==="#"){let t=n.getAttribute("href");if(!t||!t.includes("#")&&!t.startsWith("."))return null;t.includes("#")&&!t.startsWith("#")&&(t=`#${t.split("#")[1]}`),e=t&&t!=="#"?t.trim():null}return e?e.split(",").map(t=>fa(t)).join(","):null},j={find(n,e=document.documentElement){return[].concat(...Element.prototype.querySelectorAll.call(e,n))},findOne(n,e=document.documentElement){return Element.prototype.querySelector.call(e,n)},children(n,e){return[].concat(...n.children).filter(t=>t.matches(e))},parents(n,e){let t=[],i=n.parentNode.closest(e);for(;i;)t.push(i),i=i.parentNode.closest(e);return t},prev(n,e){let t=n.previousElementSibling;for(;t;){if(t.matches(e))return[t];t=t.previousElementSibling}return[]},next(n,e){let t=n.nextElementSibling;for(;t;){if(t.matches(e))return[t];t=t.nextElementSibling}return[]},focusableChildren(n){let e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(t=>`${t}:not([tabindex^="-"])`).join(",");return this.find(e,n).filter(t=>!Tt(t)&&An(t))},getSelectorFromElement(n){let e=mo(n);return e&&j.findOne(e)?e:null},getElementFromSelector(n){let e=mo(n);return e?j.findOne(e):null},getMultipleElementsFromSelector(n){let e=mo(n);return e?j.find(e):[]}},or=(n,e="hide")=>{let t=`click.dismiss${n.EVENT_KEY}`,i=n.NAME;x.on(document,t,`[data-bs-dismiss="${i}"]`,function(r){if(["A","AREA"].includes(this.tagName)&&r.preventDefault(),Tt(this))return;let o=j.getElementFromSelector(this)||this.closest(`.${i}`);n.getOrCreateInstance(o)[e]()})},Zu="alert",ed="bs.alert",xa=`.${ed}`,td=`close${xa}`,nd=`closed${xa}`,id="fade",rd="show",Ji=class n extends We{static get NAME(){return Zu}close(){if(x.trigger(this._element,td).defaultPrevented)return;this._element.classList.remove(rd);let t=this._element.classList.contains(id);this._queueCallback(()=>this._destroyElement(),this._element,t)}_destroyElement(){this._element.remove(),x.trigger(this._element,nd),this.dispose()}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this);if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e](this)}})}};or(Ji,"close");Je(Ji);var od="button",sd="bs.button",ad=`.${sd}`,ld=".data-api",cd="active",Vs='[data-bs-toggle="button"]',ud=`click${ad}${ld}`,Zi=class n extends We{static get NAME(){return od}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle(cd))}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this);e==="toggle"&&t[e]()})}};x.on(document,ud,Vs,n=>{n.preventDefault();let e=n.target.closest(Vs);Zi.getOrCreateInstance(e).toggle()});Je(Zi);var dd="swipe",Sn=".bs.swipe",fd=`touchstart${Sn}`,hd=`touchmove${Sn}`,pd=`touchend${Sn}`,md=`pointerdown${Sn}`,gd=`pointerup${Sn}`,vd="touch",yd="pen",Ed="pointer-event",bd=40,_d={endCallback:null,leftCallback:null,rightCallback:null},wd={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"},er=class n extends Xt{constructor(e,t){super(),this._element=e,!(!e||!n.isSupported())&&(this._config=this._getConfig(t),this._deltaX=0,this._supportPointerEvents=!!window.PointerEvent,this._initEvents())}static get Default(){return _d}static get DefaultType(){return wd}static get NAME(){return dd}dispose(){x.off(this._element,Sn)}_start(e){if(!this._supportPointerEvents){this._deltaX=e.touches[0].clientX;return}this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX)}_end(e){this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX-this._deltaX),this._handleSwipe(),Pe(this._config.endCallback)}_move(e){this._deltaX=e.touches&&e.touches.length>1?0:e.touches[0].clientX-this._deltaX}_handleSwipe(){let e=Math.abs(this._deltaX);if(e<=bd)return;let t=e/this._deltaX;this._deltaX=0,t&&Pe(t>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(x.on(this._element,md,e=>this._start(e)),x.on(this._element,gd,e=>this._end(e)),this._element.classList.add(Ed)):(x.on(this._element,fd,e=>this._start(e)),x.on(this._element,hd,e=>this._move(e)),x.on(this._element,pd,e=>this._end(e)))}_eventIsPointerPenTouch(e){return this._supportPointerEvents&&(e.pointerType===yd||e.pointerType===vd)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}},xd="carousel",Td="bs.carousel",Dt=`.${Td}`,Ta=".data-api",Cd="ArrowLeft",Ad="ArrowRight",Sd=500,Zn="next",bn="prev",wn="left",Ki="right",Dd=`slide${Dt}`,go=`slid${Dt}`,Od=`keydown${Dt}`,Ld=`mouseenter${Dt}`,Md=`mouseleave${Dt}`,Nd=`dragstart${Dt}`,kd=`load${Dt}${Ta}`,Hd=`click${Dt}${Ta}`,Ca="carousel",ji="active",Rd="slide",Id="carousel-item-end",Pd="carousel-item-start",Fd="carousel-item-next",$d="carousel-item-prev",Aa=".active",Sa=".carousel-item",Bd=Aa+Sa,Vd=".carousel-item img",zd=".carousel-indicators",jd="[data-bs-slide], [data-bs-slide-to]",Wd='[data-bs-ride="carousel"]',qd={[Cd]:Ki,[Ad]:wn},Ud={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Yd={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"},ti=class n extends We{constructor(e,t){super(e,t),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=j.findOne(zd,this._element),this._addEventListeners(),this._config.ride===Ca&&this.cycle()}static get Default(){return Ud}static get DefaultType(){return Yd}static get NAME(){return xd}next(){this._slide(Zn)}nextWhenVisible(){!document.hidden&&An(this._element)&&this.next()}prev(){this._slide(bn)}pause(){this._isSliding&&ha(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){if(this._config.ride){if(this._isSliding){x.one(this._element,go,()=>this.cycle());return}this.cycle()}}to(e){let t=this._getItems();if(e>t.length-1||e<0)return;if(this._isSliding){x.one(this._element,go,()=>this.to(e));return}let i=this._getItemIndex(this._getActive());if(i===e)return;let r=e>i?Zn:bn;this._slide(r,t[e])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(e){return e.defaultInterval=e.interval,e}_addEventListeners(){this._config.keyboard&&x.on(this._element,Od,e=>this._keydown(e)),this._config.pause==="hover"&&(x.on(this._element,Ld,()=>this.pause()),x.on(this._element,Md,()=>this._maybeEnableCycle())),this._config.touch&&er.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(let i of j.find(Vd,this._element))x.on(i,Nd,r=>r.preventDefault());let t={leftCallback:()=>this._slide(this._directionToOrder(wn)),rightCallback:()=>this._slide(this._directionToOrder(Ki)),endCallback:()=>{this._config.pause==="hover"&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),Sd+this._config.interval))}};this._swipeHelper=new er(this._element,t)}_keydown(e){if(/input|textarea/i.test(e.target.tagName))return;let t=qd[e.key];t&&(e.preventDefault(),this._slide(this._directionToOrder(t)))}_getItemIndex(e){return this._getItems().indexOf(e)}_setActiveIndicatorElement(e){if(!this._indicatorsElement)return;let t=j.findOne(Aa,this._indicatorsElement);t.classList.remove(ji),t.removeAttribute("aria-current");let i=j.findOne(`[data-bs-slide-to="${e}"]`,this._indicatorsElement);i&&(i.classList.add(ji),i.setAttribute("aria-current","true"))}_updateInterval(){let e=this._activeElement||this._getActive();if(!e)return;let t=Number.parseInt(e.getAttribute("data-bs-interval"),10);this._config.interval=t||this._config.defaultInterval}_slide(e,t=null){if(this._isSliding)return;let i=this._getActive(),r=e===Zn,o=t||Mo(this._getItems(),i,r,this._config.wrap);if(o===i)return;let s=this._getItemIndex(o),a=y=>x.trigger(this._element,y,{relatedTarget:o,direction:this._orderToDirection(e),from:this._getItemIndex(i),to:s});if(a(Dd).defaultPrevented||!i||!o)return;let c=!!this._interval;this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(s),this._activeElement=o;let u=r?Pd:Id,d=r?Fd:$d;o.classList.add(d),ii(o),i.classList.add(u),o.classList.add(u);let p=()=>{o.classList.remove(u,d),o.classList.add(ji),i.classList.remove(ji,d,u),this._isSliding=!1,a(go)};this._queueCallback(p,i,this._isAnimated()),c&&this.cycle()}_isAnimated(){return this._element.classList.contains(Rd)}_getActive(){return j.findOne(Bd,this._element)}_getItems(){return j.find(Sa,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(e){return Xe()?e===wn?bn:Zn:e===wn?Zn:bn}_orderToDirection(e){return Xe()?e===bn?wn:Ki:e===bn?Ki:wn}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="number"){t.to(e);return}if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()}})}};x.on(document,Hd,jd,function(n){let e=j.getElementFromSelector(this);if(!e||!e.classList.contains(Ca))return;n.preventDefault();let t=ti.getOrCreateInstance(e),i=this.getAttribute("data-bs-slide-to");if(i){t.to(i),t._maybeEnableCycle();return}if(ft.getDataAttribute(this,"slide")==="next"){t.next(),t._maybeEnableCycle();return}t.prev(),t._maybeEnableCycle()});x.on(window,kd,()=>{let n=j.find(Wd);for(let e of n)ti.getOrCreateInstance(e)});Je(ti);var Gd="collapse",Kd="bs.collapse",ri=`.${Kd}`,Xd=".data-api",Qd=`show${ri}`,Jd=`shown${ri}`,Zd=`hide${ri}`,ef=`hidden${ri}`,tf=`click${ri}${Xd}`,vo="show",Tn="collapse",Wi="collapsing",nf="collapsed",rf=`:scope .${Tn} .${Tn}`,of="collapse-horizontal",sf="width",af="height",lf=".collapse.show, .collapse.collapsing",Oo='[data-bs-toggle="collapse"]',cf={parent:null,toggle:!0},uf={parent:"(null|element)",toggle:"boolean"},Ct=class n extends We{constructor(e,t){super(e,t),this._isTransitioning=!1,this._triggerArray=[];let i=j.find(Oo);for(let r of i){let o=j.getSelectorFromElement(r),s=j.find(o).filter(a=>a===this._element);o!==null&&s.length&&this._triggerArray.push(r)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return cf}static get DefaultType(){return uf}static get NAME(){return Gd}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let e=[];if(this._config.parent&&(e=this._getFirstLevelChildren(lf).filter(a=>a!==this._element).map(a=>n.getOrCreateInstance(a,{toggle:!1}))),e.length&&e[0]._isTransitioning||x.trigger(this._element,Qd).defaultPrevented)return;for(let a of e)a.hide();let i=this._getDimension();this._element.classList.remove(Tn),this._element.classList.add(Wi),this._element.style[i]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;let r=()=>{this._isTransitioning=!1,this._element.classList.remove(Wi),this._element.classList.add(Tn,vo),this._element.style[i]="",x.trigger(this._element,Jd)},s=`scroll${i[0].toUpperCase()+i.slice(1)}`;this._queueCallback(r,this._element,!0),this._element.style[i]=`${this._element[s]}px`}hide(){if(this._isTransitioning||!this._isShown()||x.trigger(this._element,Zd).defaultPrevented)return;let t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,ii(this._element),this._element.classList.add(Wi),this._element.classList.remove(Tn,vo);for(let r of this._triggerArray){let o=j.getElementFromSelector(r);o&&!this._isShown(o)&&this._addAriaAndCollapsedClass([r],!1)}this._isTransitioning=!0;let i=()=>{this._isTransitioning=!1,this._element.classList.remove(Wi),this._element.classList.add(Tn),x.trigger(this._element,ef)};this._element.style[t]="",this._queueCallback(i,this._element,!0)}_isShown(e=this._element){return e.classList.contains(vo)}_configAfterMerge(e){return e.toggle=!!e.toggle,e.parent=xt(e.parent),e}_getDimension(){return this._element.classList.contains(of)?sf:af}_initializeChildren(){if(!this._config.parent)return;let e=this._getFirstLevelChildren(Oo);for(let t of e){let i=j.getElementFromSelector(t);i&&this._addAriaAndCollapsedClass([t],this._isShown(i))}}_getFirstLevelChildren(e){let t=j.find(rf,this._config.parent);return j.find(e,this._config.parent).filter(i=>!t.includes(i))}_addAriaAndCollapsedClass(e,t){if(e.length)for(let i of e)i.classList.toggle(nf,!t),i.setAttribute("aria-expanded",t)}static jQueryInterface(e){let t={};return typeof e=="string"&&/show|hide/.test(e)&&(t.toggle=!1),this.each(function(){let i=n.getOrCreateInstance(this,t);if(typeof e=="string"){if(typeof i[e]=="undefined")throw new TypeError(`No method named "${e}"`);i[e]()}})}};x.on(document,tf,Oo,function(n){(n.target.tagName==="A"||n.delegateTarget&&n.delegateTarget.tagName==="A")&&n.preventDefault();for(let e of j.getMultipleElementsFromSelector(this))Ct.getOrCreateInstance(e,{toggle:!1}).toggle()});Je(Ct);var zs="dropdown",df="bs.dropdown",Jt=`.${df}`,ko=".data-api",ff="Escape",js="Tab",hf="ArrowUp",Ws="ArrowDown",pf=2,mf=`hide${Jt}`,gf=`hidden${Jt}`,vf=`show${Jt}`,yf=`shown${Jt}`,Da=`click${Jt}${ko}`,Oa=`keydown${Jt}${ko}`,Ef=`keyup${Jt}${ko}`,xn="show",bf="dropup",_f="dropend",wf="dropstart",xf="dropup-center",Tf="dropdown-center",Gt='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',Cf=`${Gt}.${xn}`,Xi=".dropdown-menu",Af=".navbar",Sf=".navbar-nav",Df=".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",Of=Xe()?"top-end":"top-start",Lf=Xe()?"top-start":"top-end",Mf=Xe()?"bottom-end":"bottom-start",Nf=Xe()?"bottom-start":"bottom-end",kf=Xe()?"left-start":"right-start",Hf=Xe()?"right-start":"left-start",Rf="top",If="bottom",Pf={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},Ff={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"},At=class n extends We{constructor(e,t){super(e,t),this._popper=null,this._parent=this._element.parentNode,this._menu=j.next(this._element,Xi)[0]||j.prev(this._element,Xi)[0]||j.findOne(Xi,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return Pf}static get DefaultType(){return Ff}static get NAME(){return zs}toggle(){return this._isShown()?this.hide():this.show()}show(){if(Tt(this._element)||this._isShown())return;let e={relatedTarget:this._element};if(!x.trigger(this._element,vf,e).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(Sf))for(let i of[].concat(...document.body.children))x.on(i,"mouseover",Qi);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(xn),this._element.classList.add(xn),x.trigger(this._element,yf,e)}}hide(){if(Tt(this._element)||!this._isShown())return;let e={relatedTarget:this._element};this._completeHide(e)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(e){if(!x.trigger(this._element,mf,e).defaultPrevented){if("ontouchstart"in document.documentElement)for(let i of[].concat(...document.body.children))x.off(i,"mouseover",Qi);this._popper&&this._popper.destroy(),this._menu.classList.remove(xn),this._element.classList.remove(xn),this._element.setAttribute("aria-expanded","false"),ft.removeDataAttribute(this._menu,"popper"),x.trigger(this._element,gf,e)}}_getConfig(e){if(e=super._getConfig(e),typeof e.reference=="object"&&!dt(e.reference)&&typeof e.reference.getBoundingClientRect!="function")throw new TypeError(`${zs.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return e}_createPopper(){if(typeof zi=="undefined")throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)");let e=this._element;this._config.reference==="parent"?e=this._parent:dt(this._config.reference)?e=xt(this._config.reference):typeof this._config.reference=="object"&&(e=this._config.reference);let t=this._getPopperConfig();this._popper=Jn(e,this._menu,t)}_isShown(){return this._menu.classList.contains(xn)}_getPlacement(){let e=this._parent;if(e.classList.contains(_f))return kf;if(e.classList.contains(wf))return Hf;if(e.classList.contains(xf))return Rf;if(e.classList.contains(Tf))return If;let t=getComputedStyle(this._menu).getPropertyValue("--bs-position").trim()==="end";return e.classList.contains(bf)?t?Lf:Of:t?Nf:Mf}_detectNavbar(){return this._element.closest(Af)!==null}_getOffset(){let{offset:e}=this._config;return typeof e=="string"?e.split(",").map(t=>Number.parseInt(t,10)):typeof e=="function"?t=>e(t,this._element):e}_getPopperConfig(){let e={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||this._config.display==="static")&&(ft.setDataAttribute(this._menu,"popper","static"),e.modifiers=[{name:"applyStyles",enabled:!1}]),O(O({},e),Pe(this._config.popperConfig,[void 0,e]))}_selectMenuItem({key:e,target:t}){let i=j.find(Df,this._menu).filter(r=>An(r));i.length&&Mo(i,t,e===Ws,!i.includes(t)).focus()}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()}})}static clearMenus(e){if(e.button===pf||e.type==="keyup"&&e.key!==js)return;let t=j.find(Cf);for(let i of t){let r=n.getInstance(i);if(!r||r._config.autoClose===!1)continue;let o=e.composedPath(),s=o.includes(r._menu);if(o.includes(r._element)||r._config.autoClose==="inside"&&!s||r._config.autoClose==="outside"&&s||r._menu.contains(e.target)&&(e.type==="keyup"&&e.key===js||/input|select|option|textarea|form/i.test(e.target.tagName)))continue;let a={relatedTarget:r._element};e.type==="click"&&(a.clickEvent=e),r._completeHide(a)}}static dataApiKeydownHandler(e){let t=/input|textarea/i.test(e.target.tagName),i=e.key===ff,r=[hf,Ws].includes(e.key);if(!r&&!i||t&&!i)return;e.preventDefault();let o=this.matches(Gt)?this:j.prev(this,Gt)[0]||j.next(this,Gt)[0]||j.findOne(Gt,e.delegateTarget.parentNode),s=n.getOrCreateInstance(o);if(r){e.stopPropagation(),s.show(),s._selectMenuItem(e);return}s._isShown()&&(e.stopPropagation(),s.hide(),o.focus())}};x.on(document,Oa,Gt,At.dataApiKeydownHandler);x.on(document,Oa,Xi,At.dataApiKeydownHandler);x.on(document,Da,At.clearMenus);x.on(document,Ef,At.clearMenus);x.on(document,Da,Gt,function(n){n.preventDefault(),At.getOrCreateInstance(this).toggle()});Je(At);var La="backdrop",$f="fade",qs="show",Us=`mousedown.bs.${La}`,Bf={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Vf={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"},tr=class extends Xt{constructor(e){super(),this._config=this._getConfig(e),this._isAppended=!1,this._element=null}static get Default(){return Bf}static get DefaultType(){return Vf}static get NAME(){return La}show(e){if(!this._config.isVisible){Pe(e);return}this._append();let t=this._getElement();this._config.isAnimated&&ii(t),t.classList.add(qs),this._emulateAnimation(()=>{Pe(e)})}hide(e){if(!this._config.isVisible){Pe(e);return}this._getElement().classList.remove(qs),this._emulateAnimation(()=>{this.dispose(),Pe(e)})}dispose(){this._isAppended&&(x.off(this._element,Us),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){let e=document.createElement("div");e.className=this._config.className,this._config.isAnimated&&e.classList.add($f),this._element=e}return this._element}_configAfterMerge(e){return e.rootElement=xt(e.rootElement),e}_append(){if(this._isAppended)return;let e=this._getElement();this._config.rootElement.append(e),x.on(e,Us,()=>{Pe(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(e){ga(e,this._getElement(),this._config.isAnimated)}},zf="focustrap",jf="bs.focustrap",nr=`.${jf}`,Wf=`focusin${nr}`,qf=`keydown.tab${nr}`,Uf="Tab",Yf="forward",Ys="backward",Gf={autofocus:!0,trapElement:null},Kf={autofocus:"boolean",trapElement:"element"},ir=class extends Xt{constructor(e){super(),this._config=this._getConfig(e),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Gf}static get DefaultType(){return Kf}static get NAME(){return zf}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),x.off(document,nr),x.on(document,Wf,e=>this._handleFocusin(e)),x.on(document,qf,e=>this._handleKeydown(e)),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,x.off(document,nr))}_handleFocusin(e){let{trapElement:t}=this._config;if(e.target===document||e.target===t||t.contains(e.target))return;let i=j.focusableChildren(t);i.length===0?t.focus():this._lastTabNavDirection===Ys?i[i.length-1].focus():i[0].focus()}_handleKeydown(e){e.key===Uf&&(this._lastTabNavDirection=e.shiftKey?Ys:Yf)}},Gs=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",Ks=".sticky-top",qi="padding-right",Xs="margin-right",ni=class{constructor(){this._element=document.body}getWidth(){let e=document.documentElement.clientWidth;return Math.abs(window.innerWidth-e)}hide(){let e=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,qi,t=>t+e),this._setElementAttributes(Gs,qi,t=>t+e),this._setElementAttributes(Ks,Xs,t=>t-e)}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,qi),this._resetElementAttributes(Gs,qi),this._resetElementAttributes(Ks,Xs)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(e,t,i){let r=this.getWidth(),o=s=>{if(s!==this._element&&window.innerWidth>s.clientWidth+r)return;this._saveInitialAttribute(s,t);let a=window.getComputedStyle(s).getPropertyValue(t);s.style.setProperty(t,`${i(Number.parseFloat(a))}px`)};this._applyManipulationCallback(e,o)}_saveInitialAttribute(e,t){let i=e.style.getPropertyValue(t);i&&ft.setDataAttribute(e,t,i)}_resetElementAttributes(e,t){let i=r=>{let o=ft.getDataAttribute(r,t);if(o===null){r.style.removeProperty(t);return}ft.removeDataAttribute(r,t),r.style.setProperty(t,o)};this._applyManipulationCallback(e,i)}_applyManipulationCallback(e,t){if(dt(e)){t(e);return}for(let i of j.find(e,this._element))t(i)}},Xf="modal",Qf="bs.modal",Qe=`.${Qf}`,Jf=".data-api",Zf="Escape",eh=`hide${Qe}`,th=`hidePrevented${Qe}`,Ma=`hidden${Qe}`,Na=`show${Qe}`,nh=`shown${Qe}`,ih=`resize${Qe}`,rh=`click.dismiss${Qe}`,oh=`mousedown.dismiss${Qe}`,sh=`keydown.dismiss${Qe}`,ah=`click${Qe}${Jf}`,Qs="modal-open",lh="fade",Js="show",yo="modal-static",ch=".modal.show",uh=".modal-dialog",dh=".modal-body",fh='[data-bs-toggle="modal"]',hh={backdrop:!0,focus:!0,keyboard:!0},ph={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"},tt=class n extends We{constructor(e,t){super(e,t),this._dialog=j.findOne(uh,this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new ni,this._addEventListeners()}static get Default(){return hh}static get DefaultType(){return ph}static get NAME(){return Xf}toggle(e){return this._isShown?this.hide():this.show(e)}show(e){this._isShown||this._isTransitioning||x.trigger(this._element,Na,{relatedTarget:e}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Qs),this._adjustDialog(),this._backdrop.show(()=>this._showElement(e)))}hide(){!this._isShown||this._isTransitioning||x.trigger(this._element,eh).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Js),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated()))}dispose(){x.off(window,Qe),x.off(this._dialog,Qe),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new tr({isVisible:!!this._config.backdrop,isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new ir({trapElement:this._element})}_showElement(e){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;let t=j.findOne(dh,this._dialog);t&&(t.scrollTop=0),ii(this._element),this._element.classList.add(Js);let i=()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,x.trigger(this._element,nh,{relatedTarget:e})};this._queueCallback(i,this._dialog,this._isAnimated())}_addEventListeners(){x.on(this._element,sh,e=>{if(e.key===Zf){if(this._config.keyboard){this.hide();return}this._triggerBackdropTransition()}}),x.on(window,ih,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),x.on(this._element,oh,e=>{x.one(this._element,rh,t=>{if(!(this._element!==e.target||this._element!==t.target)){if(this._config.backdrop==="static"){this._triggerBackdropTransition();return}this._config.backdrop&&this.hide()}})})}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove(Qs),this._resetAdjustments(),this._scrollBar.reset(),x.trigger(this._element,Ma)})}_isAnimated(){return this._element.classList.contains(lh)}_triggerBackdropTransition(){if(x.trigger(this._element,th).defaultPrevented)return;let t=this._element.scrollHeight>document.documentElement.clientHeight,i=this._element.style.overflowY;i==="hidden"||this._element.classList.contains(yo)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(yo),this._queueCallback(()=>{this._element.classList.remove(yo),this._queueCallback(()=>{this._element.style.overflowY=i},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){let e=this._element.scrollHeight>document.documentElement.clientHeight,t=this._scrollBar.getWidth(),i=t>0;if(i&&!e){let r=Xe()?"paddingLeft":"paddingRight";this._element.style[r]=`${t}px`}if(!i&&e){let r=Xe()?"paddingRight":"paddingLeft";this._element.style[r]=`${t}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(e,t){return this.each(function(){let i=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof i[e]=="undefined")throw new TypeError(`No method named "${e}"`);i[e](t)}})}};x.on(document,ah,fh,function(n){let e=j.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&n.preventDefault(),x.one(e,Na,r=>{r.defaultPrevented||x.one(e,Ma,()=>{An(this)&&this.focus()})});let t=j.findOne(ch);t&&tt.getInstance(t).hide(),tt.getOrCreateInstance(e).toggle(this)});or(tt);Je(tt);var mh="offcanvas",gh="bs.offcanvas",mt=`.${gh}`,ka=".data-api",vh=`load${mt}${ka}`,yh="Escape",Zs="show",ea="showing",ta="hiding",Eh="offcanvas-backdrop",Ha=".offcanvas.show",bh=`show${mt}`,_h=`shown${mt}`,wh=`hide${mt}`,na=`hidePrevented${mt}`,Ra=`hidden${mt}`,xh=`resize${mt}`,Th=`click${mt}${ka}`,Ch=`keydown.dismiss${mt}`,Ah='[data-bs-toggle="offcanvas"]',Sh={backdrop:!0,keyboard:!0,scroll:!1},Dh={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"},St=class n extends We{constructor(e,t){super(e,t),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Sh}static get DefaultType(){return Dh}static get NAME(){return mh}toggle(e){return this._isShown?this.hide():this.show(e)}show(e){if(this._isShown||x.trigger(this._element,bh,{relatedTarget:e}).defaultPrevented)return;this._isShown=!0,this._backdrop.show(),this._config.scroll||new ni().hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(ea);let i=()=>{(!this._config.scroll||this._config.backdrop)&&this._focustrap.activate(),this._element.classList.add(Zs),this._element.classList.remove(ea),x.trigger(this._element,_h,{relatedTarget:e})};this._queueCallback(i,this._element,!0)}hide(){if(!this._isShown||x.trigger(this._element,wh).defaultPrevented)return;this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(ta),this._backdrop.hide();let t=()=>{this._element.classList.remove(Zs,ta),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||new ni().reset(),x.trigger(this._element,Ra)};this._queueCallback(t,this._element,!0)}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){let e=()=>{if(this._config.backdrop==="static"){x.trigger(this._element,na);return}this.hide()},t=!!this._config.backdrop;return new tr({className:Eh,isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?e:null})}_initializeFocusTrap(){return new ir({trapElement:this._element})}_addEventListeners(){x.on(this._element,Ch,e=>{if(e.key===yh){if(this._config.keyboard){this.hide();return}x.trigger(this._element,na)}})}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e](this)}})}};x.on(document,Th,Ah,function(n){let e=j.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&n.preventDefault(),Tt(this))return;x.one(e,Ra,()=>{An(this)&&this.focus()});let t=j.findOne(Ha);t&&t!==e&&St.getInstance(t).hide(),St.getOrCreateInstance(e).toggle(this)});x.on(window,vh,()=>{for(let n of j.find(Ha))St.getOrCreateInstance(n).show()});x.on(window,xh,()=>{for(let n of j.find("[aria-modal][class*=show][class*=offcanvas-]"))getComputedStyle(n).position!=="fixed"&&St.getOrCreateInstance(n).hide()});or(St);Je(St);var Oh=/^aria-[\w-]*$/i,Ia={"*":["class","dir","id","lang","role",Oh],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Lh=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Mh=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Nh=(n,e)=>{let t=n.nodeName.toLowerCase();return e.includes(t)?Lh.has(t)?!!Mh.test(n.nodeValue):!0:e.filter(i=>i instanceof RegExp).some(i=>i.test(t))};function kh(n,e,t){if(!n.length)return n;if(t&&typeof t=="function")return t(n);let r=new window.DOMParser().parseFromString(n,"text/html"),o=[].concat(...r.body.querySelectorAll("*"));for(let s of o){let a=s.nodeName.toLowerCase();if(!Object.keys(e).includes(a)){s.remove();continue}let l=[].concat(...s.attributes),c=[].concat(e["*"]||[],e[a]||[]);for(let u of l)Nh(u,c)||s.removeAttribute(u.nodeName)}return r.body.innerHTML}var Hh="TemplateFactory",Rh={allowList:Ia,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Ih={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Ph={entry:"(string|element|function|null)",selector:"(string|element)"},Lo=class extends Xt{constructor(e){super(),this._config=this._getConfig(e)}static get Default(){return Rh}static get DefaultType(){return Ih}static get NAME(){return Hh}getContent(){return Object.values(this._config.content).map(e=>this._resolvePossibleFunction(e)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(e){return this._checkContent(e),this._config.content=O(O({},this._config.content),e),this}toHtml(){let e=document.createElement("div");e.innerHTML=this._maybeSanitize(this._config.template);for(let[r,o]of Object.entries(this._config.content))this._setContent(e,o,r);let t=e.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&t.classList.add(...i.split(" ")),t}_typeCheckConfig(e){super._typeCheckConfig(e),this._checkContent(e.content)}_checkContent(e){for(let[t,i]of Object.entries(e))super._typeCheckConfig({selector:t,entry:i},Ph)}_setContent(e,t,i){let r=j.findOne(i,e);if(r){if(t=this._resolvePossibleFunction(t),!t){r.remove();return}if(dt(t)){this._putElementInTemplate(xt(t),r);return}if(this._config.html){r.innerHTML=this._maybeSanitize(t);return}r.textContent=t}}_maybeSanitize(e){return this._config.sanitize?kh(e,this._config.allowList,this._config.sanitizeFn):e}_resolvePossibleFunction(e){return Pe(e,[void 0,this])}_putElementInTemplate(e,t){if(this._config.html){t.innerHTML="",t.append(e);return}t.textContent=e.textContent}},Fh="tooltip",$h=new Set(["sanitize","allowList","sanitizeFn"]),Eo="fade",Bh="modal",Ui="show",Vh=".tooltip-inner",ia=`.${Bh}`,ra="hide.bs.modal",ei="hover",bo="focus",_o="click",zh="manual",jh="hide",Wh="hidden",qh="show",Uh="shown",Yh="inserted",Gh="click",Kh="focusin",Xh="focusout",Qh="mouseenter",Jh="mouseleave",Zh={AUTO:"auto",TOP:"top",RIGHT:Xe()?"left":"right",BOTTOM:"bottom",LEFT:Xe()?"right":"left"},ep={allowList:Ia,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},tp={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"},ht=class n extends We{constructor(e,t){if(typeof zi=="undefined")throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(e,t),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return ep}static get DefaultType(){return tp}static get NAME(){return Fh}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){if(this._isEnabled){if(this._isShown()){this._leave();return}this._enter()}}dispose(){clearTimeout(this._timeout),x.off(this._element.closest(ia),ra,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if(this._element.style.display==="none")throw new Error("Please use show on visible elements");if(!(this._isWithContent()&&this._isEnabled))return;let e=x.trigger(this._element,this.constructor.eventName(qh)),i=(pa(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(e.defaultPrevented||!i)return;this._disposePopper();let r=this._getTipElement();this._element.setAttribute("aria-describedby",r.getAttribute("id"));let{container:o}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(o.append(r),x.trigger(this._element,this.constructor.eventName(Yh))),this._popper=this._createPopper(r),r.classList.add(Ui),"ontouchstart"in document.documentElement)for(let a of[].concat(...document.body.children))x.on(a,"mouseover",Qi);let s=()=>{x.trigger(this._element,this.constructor.eventName(Uh)),this._isHovered===!1&&this._leave(),this._isHovered=!1};this._queueCallback(s,this.tip,this._isAnimated())}hide(){if(!this._isShown()||x.trigger(this._element,this.constructor.eventName(jh)).defaultPrevented)return;if(this._getTipElement().classList.remove(Ui),"ontouchstart"in document.documentElement)for(let r of[].concat(...document.body.children))x.off(r,"mouseover",Qi);this._activeTrigger[_o]=!1,this._activeTrigger[bo]=!1,this._activeTrigger[ei]=!1,this._isHovered=null;let i=()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),x.trigger(this._element,this.constructor.eventName(Wh)))};this._queueCallback(i,this.tip,this._isAnimated())}update(){this._popper&&this._popper.update()}_isWithContent(){return!!this._getTitle()}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(e){let t=this._getTemplateFactory(e).toHtml();if(!t)return null;t.classList.remove(Eo,Ui),t.classList.add(`bs-${this.constructor.NAME}-auto`);let i=zu(this.constructor.NAME).toString();return t.setAttribute("id",i),this._isAnimated()&&t.classList.add(Eo),t}setContent(e){this._newContent=e,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(e){return this._templateFactory?this._templateFactory.changeContent(e):this._templateFactory=new Lo(ae(O({},this._config),{content:e,extraClass:this._resolvePossibleFunction(this._config.customClass)})),this._templateFactory}_getContentForTemplate(){return{[Vh]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(e){return this.constructor.getOrCreateInstance(e.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Eo)}_isShown(){return this.tip&&this.tip.classList.contains(Ui)}_createPopper(e){let t=Pe(this._config.placement,[this,e,this._element]),i=Zh[t.toUpperCase()];return Jn(this._element,e,this._getPopperConfig(i))}_getOffset(){let{offset:e}=this._config;return typeof e=="string"?e.split(",").map(t=>Number.parseInt(t,10)):typeof e=="function"?t=>e(t,this._element):e}_resolvePossibleFunction(e){return Pe(e,[this._element,this._element])}_getPopperConfig(e){let t={placement:e,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:i=>{this._getTipElement().setAttribute("data-popper-placement",i.state.placement)}}]};return O(O({},t),Pe(this._config.popperConfig,[void 0,t]))}_setListeners(){let e=this._config.trigger.split(" ");for(let t of e)if(t==="click")x.on(this._element,this.constructor.eventName(Gh),this._config.selector,i=>{let r=this._initializeOnDelegatedTarget(i);r._activeTrigger[_o]=!(r._isShown()&&r._activeTrigger[_o]),r.toggle()});else if(t!==zh){let i=t===ei?this.constructor.eventName(Qh):this.constructor.eventName(Kh),r=t===ei?this.constructor.eventName(Jh):this.constructor.eventName(Xh);x.on(this._element,i,this._config.selector,o=>{let s=this._initializeOnDelegatedTarget(o);s._activeTrigger[o.type==="focusin"?bo:ei]=!0,s._enter()}),x.on(this._element,r,this._config.selector,o=>{let s=this._initializeOnDelegatedTarget(o);s._activeTrigger[o.type==="focusout"?bo:ei]=s._element.contains(o.relatedTarget),s._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},x.on(this._element.closest(ia),ra,this._hideModalHandler)}_fixTitle(){let e=this._element.getAttribute("title");e&&(!this._element.getAttribute("aria-label")&&!this._element.textContent.trim()&&this._element.setAttribute("aria-label",e),this._element.setAttribute("data-bs-original-title",e),this._element.removeAttribute("title"))}_enter(){if(this._isShown()||this._isHovered){this._isHovered=!0;return}this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show)}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(e,t){clearTimeout(this._timeout),this._timeout=setTimeout(e,t)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(e){let t=ft.getDataAttributes(this._element);for(let i of Object.keys(t))$h.has(i)&&delete t[i];return e=O(O({},t),typeof e=="object"&&e?e:{}),e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e.container=e.container===!1?document.body:xt(e.container),typeof e.delay=="number"&&(e.delay={show:e.delay,hide:e.delay}),typeof e.title=="number"&&(e.title=e.title.toString()),typeof e.content=="number"&&(e.content=e.content.toString()),e}_getDelegateConfig(){let e={};for(let[t,i]of Object.entries(this._config))this.constructor.Default[t]!==i&&(e[t]=i);return e.selector=!1,e.trigger="manual",e}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()}})}};Je(ht);var np="popover",ip=".popover-header",rp=".popover-body",op=ae(O({},ht.Default),{content:"",offset:[0,8],placement:"right",template:'',trigger:"click"}),sp=ae(O({},ht.DefaultType),{content:"(null|string|element|function)"}),Cn=class n extends ht{static get Default(){return op}static get DefaultType(){return sp}static get NAME(){return np}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ip]:this._getTitle(),[rp]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()}})}};Je(Cn);var ap="scrollspy",lp="bs.scrollspy",Ho=`.${lp}`,cp=".data-api",up=`activate${Ho}`,oa=`click${Ho}`,dp=`load${Ho}${cp}`,fp="dropdown-item",_n="active",hp='[data-bs-spy="scroll"]',wo="[href]",pp=".nav, .list-group",sa=".nav-link",mp=".nav-item",gp=".list-group-item",vp=`${sa}, ${mp} > ${sa}, ${gp}`,yp=".dropdown",Ep=".dropdown-toggle",bp={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},_p={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"},rr=class n extends We{constructor(e,t){super(e,t),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement=getComputedStyle(this._element).overflowY==="visible"?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return bp}static get DefaultType(){return _p}static get NAME(){return ap}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(let e of this._observableSections.values())this._observer.observe(e)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(e){return e.target=xt(e.target)||document.body,e.rootMargin=e.offset?`${e.offset}px 0px -30%`:e.rootMargin,typeof e.threshold=="string"&&(e.threshold=e.threshold.split(",").map(t=>Number.parseFloat(t))),e}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(x.off(this._config.target,oa),x.on(this._config.target,oa,wo,e=>{let t=this._observableSections.get(e.target.hash);if(t){e.preventDefault();let i=this._rootElement||window,r=t.offsetTop-this._element.offsetTop;if(i.scrollTo){i.scrollTo({top:r,behavior:"smooth"});return}i.scrollTop=r}}))}_getNewObserver(){let e={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(t=>this._observerCallback(t),e)}_observerCallback(e){let t=s=>this._targetLinks.get(`#${s.target.id}`),i=s=>{this._previousScrollData.visibleEntryTop=s.target.offsetTop,this._process(t(s))},r=(this._rootElement||document.documentElement).scrollTop,o=r>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=r;for(let s of e){if(!s.isIntersecting){this._activeTarget=null,this._clearActiveClass(t(s));continue}let a=s.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(o&&a){if(i(s),!r)return;continue}!o&&!a&&i(s)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;let e=j.find(wo,this._config.target);for(let t of e){if(!t.hash||Tt(t))continue;let i=j.findOne(decodeURI(t.hash),this._element);An(i)&&(this._targetLinks.set(decodeURI(t.hash),t),this._observableSections.set(t.hash,i))}}_process(e){this._activeTarget!==e&&(this._clearActiveClass(this._config.target),this._activeTarget=e,e.classList.add(_n),this._activateParents(e),x.trigger(this._element,up,{relatedTarget:e}))}_activateParents(e){if(e.classList.contains(fp)){j.findOne(Ep,e.closest(yp)).classList.add(_n);return}for(let t of j.parents(e,pp))for(let i of j.prev(t,vp))i.classList.add(_n)}_clearActiveClass(e){e.classList.remove(_n);let t=j.find(`${wo}.${_n}`,e);for(let i of t)i.classList.remove(_n)}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()}})}};x.on(window,dp,()=>{for(let n of j.find(hp))rr.getOrCreateInstance(n)});Je(rr);var wp="tab",xp="bs.tab",Zt=`.${xp}`,Tp=`hide${Zt}`,Cp=`hidden${Zt}`,Ap=`show${Zt}`,Sp=`shown${Zt}`,Dp=`click${Zt}`,Op=`keydown${Zt}`,Lp=`load${Zt}`,Mp="ArrowLeft",aa="ArrowRight",Np="ArrowUp",la="ArrowDown",xo="Home",ca="End",Kt="active",ua="fade",To="show",kp="dropdown",Pa=".dropdown-toggle",Hp=".dropdown-menu",Co=`:not(${Pa})`,Rp='.list-group, .nav, [role="tablist"]',Ip=".nav-item, .list-group-item",Pp=`.nav-link${Co}, .list-group-item${Co}, [role="tab"]${Co}`,Fa='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Ao=`${Pp}, ${Fa}`,Fp=`.${Kt}[data-bs-toggle="tab"], .${Kt}[data-bs-toggle="pill"], .${Kt}[data-bs-toggle="list"]`,Qt=class n extends We{constructor(e){super(e),this._parent=this._element.closest(Rp),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),x.on(this._element,Op,t=>this._keydown(t)))}static get NAME(){return wp}show(){let e=this._element;if(this._elemIsActive(e))return;let t=this._getActiveElem(),i=t?x.trigger(t,Tp,{relatedTarget:e}):null;x.trigger(e,Ap,{relatedTarget:t}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(t,e),this._activate(e,t))}_activate(e,t){if(!e)return;e.classList.add(Kt),this._activate(j.getElementFromSelector(e));let i=()=>{if(e.getAttribute("role")!=="tab"){e.classList.add(To);return}e.removeAttribute("tabindex"),e.setAttribute("aria-selected",!0),this._toggleDropDown(e,!0),x.trigger(e,Sp,{relatedTarget:t})};this._queueCallback(i,e,e.classList.contains(ua))}_deactivate(e,t){if(!e)return;e.classList.remove(Kt),e.blur(),this._deactivate(j.getElementFromSelector(e));let i=()=>{if(e.getAttribute("role")!=="tab"){e.classList.remove(To);return}e.setAttribute("aria-selected",!1),e.setAttribute("tabindex","-1"),this._toggleDropDown(e,!1),x.trigger(e,Cp,{relatedTarget:t})};this._queueCallback(i,e,e.classList.contains(ua))}_keydown(e){if(![Mp,aa,Np,la,xo,ca].includes(e.key))return;e.stopPropagation(),e.preventDefault();let t=this._getChildren().filter(r=>!Tt(r)),i;if([xo,ca].includes(e.key))i=t[e.key===xo?0:t.length-1];else{let r=[aa,la].includes(e.key);i=Mo(t,e.target,r,!0)}i&&(i.focus({preventScroll:!0}),n.getOrCreateInstance(i).show())}_getChildren(){return j.find(Ao,this._parent)}_getActiveElem(){return this._getChildren().find(e=>this._elemIsActive(e))||null}_setInitialAttributes(e,t){this._setAttributeIfNotExists(e,"role","tablist");for(let i of t)this._setInitialAttributesOnChild(i)}_setInitialAttributesOnChild(e){e=this._getInnerElement(e);let t=this._elemIsActive(e),i=this._getOuterElement(e);e.setAttribute("aria-selected",t),i!==e&&this._setAttributeIfNotExists(i,"role","presentation"),t||e.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(e,"role","tab"),this._setInitialAttributesOnTargetPanel(e)}_setInitialAttributesOnTargetPanel(e){let t=j.getElementFromSelector(e);t&&(this._setAttributeIfNotExists(t,"role","tabpanel"),e.id&&this._setAttributeIfNotExists(t,"aria-labelledby",`${e.id}`))}_toggleDropDown(e,t){let i=this._getOuterElement(e);if(!i.classList.contains(kp))return;let r=(o,s)=>{let a=j.findOne(o,i);a&&a.classList.toggle(s,t)};r(Pa,Kt),r(Hp,To),i.setAttribute("aria-expanded",t)}_setAttributeIfNotExists(e,t,i){e.hasAttribute(t)||e.setAttribute(t,i)}_elemIsActive(e){return e.classList.contains(Kt)}_getInnerElement(e){return e.matches(Ao)?e:j.findOne(Ao,e)}_getOuterElement(e){return e.closest(Ip)||e}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this);if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()}})}};x.on(document,Dp,Fa,function(n){["A","AREA"].includes(this.tagName)&&n.preventDefault(),!Tt(this)&&Qt.getOrCreateInstance(this).show()});x.on(window,Lp,()=>{for(let n of j.find(Fp))Qt.getOrCreateInstance(n)});Je(Qt);var $p="toast",Bp="bs.toast",Ot=`.${Bp}`,Vp=`mouseover${Ot}`,zp=`mouseout${Ot}`,jp=`focusin${Ot}`,Wp=`focusout${Ot}`,qp=`hide${Ot}`,Up=`hidden${Ot}`,Yp=`show${Ot}`,Gp=`shown${Ot}`,Kp="fade",da="hide",Yi="show",Gi="showing",Xp={animation:"boolean",autohide:"boolean",delay:"number"},Qp={animation:!0,autohide:!0,delay:5e3},pt=class n extends We{constructor(e,t){super(e,t),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Qp}static get DefaultType(){return Xp}static get NAME(){return $p}show(){if(x.trigger(this._element,Yp).defaultPrevented)return;this._clearTimeout(),this._config.animation&&this._element.classList.add(Kp);let t=()=>{this._element.classList.remove(Gi),x.trigger(this._element,Gp),this._maybeScheduleHide()};this._element.classList.remove(da),ii(this._element),this._element.classList.add(Yi,Gi),this._queueCallback(t,this._element,this._config.animation)}hide(){if(!this.isShown()||x.trigger(this._element,qp).defaultPrevented)return;let t=()=>{this._element.classList.add(da),this._element.classList.remove(Gi,Yi),x.trigger(this._element,Up)};this._element.classList.add(Gi),this._queueCallback(t,this._element,this._config.animation)}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Yi),super.dispose()}isShown(){return this._element.classList.contains(Yi)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(e,t){switch(e.type){case"mouseover":case"mouseout":{this._hasMouseInteraction=t;break}case"focusin":case"focusout":{this._hasKeyboardInteraction=t;break}}if(t){this._clearTimeout();return}let i=e.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){x.on(this._element,Vp,e=>this._onInteraction(e,!0)),x.on(this._element,zp,e=>this._onInteraction(e,!1)),x.on(this._element,jp,e=>this._onInteraction(e,!0)),x.on(this._element,Wp,e=>this._onInteraction(e,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(e){return this.each(function(){let t=n.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e](this)}})}};or(pt);Je(pt);var Jp=(function(){"use strict";let htmx={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(n,e){return getInputValues(n,e||"post").values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:!0,historyCacheSize:10,refreshOnHistoryMiss:!1,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:!0,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:!0,allowScriptTags:!0,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:!1,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:!1,getCacheBusterParam:!1,globalViewTransitions:!1,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:!0,ignoreTitle:!1,scrollIntoViewOnBoost:!0,triggerSpecsCache:null,disableInheritance:!1,responseHandling:[{code:"204",swap:!1},{code:"[23]..",swap:!0},{code:"[45]..",swap:!1,error:!0}],allowNestedOobSwaps:!0,historyRestoreAsHxRequest:!0,reportValidityOfForms:!1},parseInterval:null,location,_:null,version:"2.0.8"};htmx.onLoad=onLoadHelper,htmx.process=processNode,htmx.on=addEventListenerImpl,htmx.off=removeEventListenerImpl,htmx.trigger=triggerEvent,htmx.ajax=ajaxHelper,htmx.find=find,htmx.findAll=findAll,htmx.closest=closest,htmx.remove=removeElement,htmx.addClass=addClassToElement,htmx.removeClass=removeClassFromElement,htmx.toggleClass=toggleClassOnElement,htmx.takeClass=takeClassForElement,htmx.swap=swap,htmx.defineExtension=defineExtension,htmx.removeExtension=removeExtension,htmx.logAll=logAll,htmx.logNone=logNone,htmx.parseInterval=parseInterval,htmx._=internalEval;let internalAPI={addTriggerHandler,bodyContains,canAccessLocalStorage,findThisElement,filterValues,swap,hasAttribute,getAttributeValue,getClosestAttributeValue,getClosestMatch,getExpressionVars,getHeaders,getInputValues,getInternalData,getSwapSpecification,getTriggerSpecs,getTarget,makeFragment,mergeObjects,makeSettleInfo,oobSwap,querySelectorExt,settleImmediately,shouldCancel,triggerEvent,triggerErrorEvent,withExtensions},VERBS=["get","post","put","delete","patch"],VERB_SELECTOR=VERBS.map(function(n){return"[hx-"+n+"], [data-hx-"+n+"]"}).join(", ");function parseInterval(n){if(n==null)return;let e=NaN;return n.slice(-2)=="ms"?e=parseFloat(n.slice(0,-2)):n.slice(-1)=="s"?e=parseFloat(n.slice(0,-1))*1e3:n.slice(-1)=="m"?e=parseFloat(n.slice(0,-1))*1e3*60:e=parseFloat(n),isNaN(e)?void 0:e}function getRawAttribute(n,e){return n instanceof Element&&n.getAttribute(e)}function hasAttribute(n,e){return!!n.hasAttribute&&(n.hasAttribute(e)||n.hasAttribute("data-"+e))}function getAttributeValue(n,e){return getRawAttribute(n,e)||getRawAttribute(n,"data-"+e)}function parentElt(n){let e=n.parentElement;return!e&&n.parentNode instanceof ShadowRoot?n.parentNode:e}function getDocument(){return document}function getRootNode(n,e){return n.getRootNode?n.getRootNode({composed:e}):getDocument()}function getClosestMatch(n,e){for(;n&&!e(n);)n=parentElt(n);return n||null}function getAttributeValueWithDisinheritance(n,e,t){let i=getAttributeValue(e,t),r=getAttributeValue(e,"hx-disinherit");var o=getAttributeValue(e,"hx-inherit");if(n!==e){if(htmx.config.disableInheritance)return o&&(o==="*"||o.split(" ").indexOf(t)>=0)?i:null;if(r&&(r==="*"||r.split(" ").indexOf(t)>=0))return"unset"}return i}function getClosestAttributeValue(n,e){let t=null;if(getClosestMatch(n,function(i){return!!(t=getAttributeValueWithDisinheritance(n,asElement(i),e))}),t!=="unset")return t}function matches(n,e){return n instanceof Element&&n.matches(e)}function getStartTag(n){let t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i.exec(n);return t?t[1].toLowerCase():""}function parseHTML(n){return"parseHTMLUnsafe"in Document?Document.parseHTMLUnsafe(n):new DOMParser().parseFromString(n,"text/html")}function takeChildrenFor(n,e){for(;e.childNodes.length>0;)n.append(e.childNodes[0])}function duplicateScript(n){let e=getDocument().createElement("script");return forEach(n.attributes,function(t){e.setAttribute(t.name,t.value)}),e.textContent=n.textContent,e.async=!1,htmx.config.inlineScriptNonce&&(e.nonce=htmx.config.inlineScriptNonce),e}function isJavaScriptScriptNode(n){return n.matches("script")&&(n.type==="text/javascript"||n.type==="module"||n.type==="")}function normalizeScriptTags(n){Array.from(n.querySelectorAll("script")).forEach(e=>{if(isJavaScriptScriptNode(e)){let t=duplicateScript(e),i=e.parentNode;try{i.insertBefore(t,e)}catch(r){logError(r)}finally{e.remove()}}})}function makeFragment(n){let e=n.replace(/]*)?>[\s\S]*?<\/head>/i,""),t=getStartTag(e),i;if(t==="html"){i=new DocumentFragment;let o=parseHTML(n);takeChildrenFor(i,o.body),i.title=o.title}else if(t==="body"){i=new DocumentFragment;let o=parseHTML(e);takeChildrenFor(i,o.body),i.title=o.title}else{let o=parseHTML('");i=o.querySelector("template").content,i.title=o.title;var r=i.querySelector("title");r&&r.parentNode===i&&(r.remove(),i.title=r.innerText)}return i&&(htmx.config.allowScriptTags?normalizeScriptTags(i):i.querySelectorAll("script").forEach(o=>o.remove())),i}function maybeCall(n){n&&n()}function isType(n,e){return Object.prototype.toString.call(n)==="[object "+e+"]"}function isFunction(n){return typeof n=="function"}function isRawObject(n){return isType(n,"Object")}function getInternalData(n){let e="htmx-internal-data",t=n[e];return t||(t=n[e]={}),t}function toArray(n){let e=[];if(n)for(let t=0;t=0}function bodyContains(n){return n.getRootNode({composed:!0})===document}function splitOnWhitespace(n){return n.trim().split(/\s+/)}function mergeObjects(n,e){for(let t in e)e.hasOwnProperty(t)&&(n[t]=e[t]);return n}function parseJSON(n){try{return JSON.parse(n)}catch(e){return logError(e),null}}function canAccessLocalStorage(){let n="htmx:sessionStorageTest";try{return sessionStorage.setItem(n,n),sessionStorage.removeItem(n),!0}catch(e){return!1}}function normalizePath(n){let e=new URL(n,"http://x");return e&&(n=e.pathname+e.search),n!="/"&&(n=n.replace(/\/+$/,"")),n}function internalEval(str){return maybeEval(getDocument().body,function(){return eval(str)})}function onLoadHelper(n){return htmx.on("htmx:load",function(t){n(t.detail.elt)})}function logAll(){htmx.logger=function(n,e,t){console&&console.log(e,n,t)}}function logNone(){htmx.logger=null}function find(n,e){return typeof n!="string"?n.querySelector(e):find(getDocument(),n)}function findAll(n,e){return typeof n!="string"?n.querySelectorAll(e):findAll(getDocument(),n)}function getWindow(){return window}function removeElement(n,e){n=resolveTarget(n),e?getWindow().setTimeout(function(){removeElement(n),n=null},e):parentElt(n).removeChild(n)}function asElement(n){return n instanceof Element?n:null}function asHtmlElement(n){return n instanceof HTMLElement?n:null}function asString(n){return typeof n=="string"?n:null}function asParentNode(n){return n instanceof Element||n instanceof Document||n instanceof DocumentFragment?n:null}function addClassToElement(n,e,t){n=asElement(resolveTarget(n)),n&&(t?getWindow().setTimeout(function(){addClassToElement(n,e),n=null},t):n.classList&&n.classList.add(e))}function removeClassFromElement(n,e,t){let i=asElement(resolveTarget(n));i&&(t?getWindow().setTimeout(function(){removeClassFromElement(i,e),i=null},t):i.classList&&(i.classList.remove(e),i.classList.length===0&&i.removeAttribute("class")))}function toggleClassOnElement(n,e){n=resolveTarget(n),n.classList.toggle(e)}function takeClassForElement(n,e){n=resolveTarget(n),forEach(n.parentElement.children,function(t){removeClassFromElement(t,e)}),addClassToElement(asElement(n),e)}function closest(n,e){return n=asElement(resolveTarget(n)),n?n.closest(e):null}function startsWith(n,e){return n.substring(0,e.length)===e}function endsWith(n,e){return n.substring(n.length-e.length)===e}function normalizeSelector(n){let e=n.trim();return startsWith(e,"<")&&endsWith(e,"/>")?e.substring(1,e.length-2):e}function querySelectorAllExt(n,e,t){if(e.indexOf("global ")===0)return querySelectorAllExt(n,e.slice(7),!0);n=resolveTarget(n);let i=[];{let s=0,a=0;for(let l=0;l"&&s--}a0;){let s=normalizeSelector(i.shift()),a;s.indexOf("closest ")===0?a=closest(asElement(n),normalizeSelector(s.slice(8))):s.indexOf("find ")===0?a=find(asParentNode(n),normalizeSelector(s.slice(5))):s==="next"||s==="nextElementSibling"?a=asElement(n).nextElementSibling:s.indexOf("next ")===0?a=scanForwardQuery(n,normalizeSelector(s.slice(5)),!!t):s==="previous"||s==="previousElementSibling"?a=asElement(n).previousElementSibling:s.indexOf("previous ")===0?a=scanBackwardsQuery(n,normalizeSelector(s.slice(9)),!!t):s==="document"?a=document:s==="window"?a=window:s==="body"?a=document.body:s==="root"?a=getRootNode(n,!!t):s==="host"?a=n.getRootNode().host:o.push(s),a&&r.push(a)}if(o.length>0){let s=o.join(","),a=asParentNode(getRootNode(n,!!t));r.push(...toArray(a.querySelectorAll(s)))}return r}var scanForwardQuery=function(n,e,t){let i=asParentNode(getRootNode(n,t)).querySelectorAll(e);for(let r=0;r=0;r--){let o=i[r];if(o.compareDocumentPosition(n)===Node.DOCUMENT_POSITION_FOLLOWING)return o}};function querySelectorExt(n,e){return typeof n!="string"?querySelectorAllExt(n,e)[0]:querySelectorAllExt(getDocument().body,n)[0]}function resolveTarget(n,e){return typeof n=="string"?find(asParentNode(e)||document,n):n}function processEventArgs(n,e,t,i){return isFunction(e)?{target:getDocument().body,event:asString(n),listener:e,options:t}:{target:resolveTarget(n),event:asString(e),listener:t,options:i}}function addEventListenerImpl(n,e,t,i){return ready(function(){let o=processEventArgs(n,e,t,i);o.target.addEventListener(o.event,o.listener,o.options)}),isFunction(e)?e:t}function removeEventListenerImpl(n,e,t){return ready(function(){let i=processEventArgs(n,e,t);i.target.removeEventListener(i.event,i.listener)}),isFunction(e)?e:t}let DUMMY_ELT=getDocument().createElement("output");function findAttributeTargets(n,e){let t=getClosestAttributeValue(n,e);if(t){if(t==="this")return[findThisElement(n,e)];{let i=querySelectorAllExt(n,t);if(/(^|,)(\s*)inherit(\s*)($|,)/.test(t)){let o=asElement(getClosestMatch(n,function(s){return s!==n&&hasAttribute(asElement(s),e)}));o&&i.push(...findAttributeTargets(o,e))}return i.length===0?(logError('The selector "'+t+'" on '+e+" returned no matches!"),[DUMMY_ELT]):i}}}function findThisElement(n,e){return asElement(getClosestMatch(n,function(t){return getAttributeValue(asElement(t),e)!=null}))}function getTarget(n){let e=getClosestAttributeValue(n,"hx-target");return e?e==="this"?findThisElement(n,"hx-target"):querySelectorExt(n,e):getInternalData(n).boosted?getDocument().body:n}function shouldSettleAttribute(n){return htmx.config.attributesToSettle.includes(n)}function cloneAttributes(n,e){forEach(Array.from(n.attributes),function(t){!e.hasAttribute(t.name)&&shouldSettleAttribute(t.name)&&n.removeAttribute(t.name)}),forEach(e.attributes,function(t){shouldSettleAttribute(t.name)&&n.setAttribute(t.name,t.value)})}function isInlineSwap(n,e){let t=getExtensions(e);for(let i=0;i0?(o=n.substring(0,n.indexOf(":")),r=n.substring(n.indexOf(":")+1)):o=n),e.removeAttribute("hx-swap-oob"),e.removeAttribute("data-hx-swap-oob");let s=querySelectorAllExt(i,r,!1);return s.length?(forEach(s,function(a){let l,c=e.cloneNode(!0);l=getDocument().createDocumentFragment(),l.appendChild(c),isInlineSwap(o,a)||(l=asParentNode(c));let u={shouldSwap:!0,target:a,fragment:l};triggerEvent(a,"htmx:oobBeforeSwap",u)&&(a=u.target,u.shouldSwap&&(handlePreservedElements(l),swapWithStyle(o,a,a,l,t),restorePreservedElements()),forEach(t.elts,function(d){triggerEvent(d,"htmx:oobAfterSwap",u)}))}),e.parentNode.removeChild(e)):(e.parentNode.removeChild(e),triggerErrorEvent(getDocument().body,"htmx:oobErrorNoTarget",{content:e})),n}function restorePreservedElements(){let n=find("#--htmx-preserve-pantry--");if(n){for(let e of[...n.children]){let t=find("#"+e.id);t.parentNode.moveBefore(e,t),t.remove()}n.remove()}}function handlePreservedElements(n){forEach(findAll(n,"[hx-preserve], [data-hx-preserve]"),function(e){let t=getAttributeValue(e,"id"),i=getDocument().getElementById(t);if(i!=null)if(e.moveBefore){let r=find("#--htmx-preserve-pantry--");r==null&&(getDocument().body.insertAdjacentHTML("afterend","
"),r=find("#--htmx-preserve-pantry--")),r.moveBefore(i,null)}else e.parentNode.replaceChild(i,e)})}function handleAttributes(n,e,t){forEach(e.querySelectorAll("[id]"),function(i){let r=getRawAttribute(i,"id");if(r&&r.length>0){let o=r.replace("'","\\'"),s=i.tagName.replace(":","\\:"),a=asParentNode(n),l=a&&a.querySelector(s+"[id='"+o+"']");if(l&&l!==a){let c=i.cloneNode();cloneAttributes(i,l),t.tasks.push(function(){cloneAttributes(i,c)})}}})}function makeAjaxLoadTask(n){return function(){removeClassFromElement(n,htmx.config.addedClass),processNode(asElement(n)),processFocus(asParentNode(n)),triggerEvent(n,"htmx:load")}}function processFocus(n){let e="[autofocus]",t=asHtmlElement(matches(n,e)?n:n.querySelector(e));t!=null&&t.focus()}function insertNodesBefore(n,e,t,i){for(handleAttributes(n,t,i);t.childNodes.length>0;){let r=t.firstChild;addClassToElement(asElement(r),htmx.config.addedClass),n.insertBefore(r,e),r.nodeType!==Node.TEXT_NODE&&r.nodeType!==Node.COMMENT_NODE&&i.tasks.push(makeAjaxLoadTask(r))}}function stringHash(n,e){let t=0;for(;t0}function swap(n,e,t,i){i||(i={});let r=null,o=null,s=function(){maybeCall(i.beforeSwapCallback),n=resolveTarget(n);let c=i.contextElement?getRootNode(i.contextElement,!1):getDocument(),u=document.activeElement,d={};d={elt:u,start:u?u.selectionStart:null,end:u?u.selectionEnd:null};let p=makeSettleInfo(n);if(t.swapStyle==="textContent")n.textContent=e;else{let m=makeFragment(e);if(p.title=i.title||m.title,i.historyRequest&&(m=m.querySelector("[hx-history-elt],[data-hx-history-elt]")||m),i.selectOOB){let v=i.selectOOB.split(",");for(let w=0;w0?getWindow().setTimeout(y,t.settleDelay):y()},a=htmx.config.globalViewTransitions;t.hasOwnProperty("transition")&&(a=t.transition);let l=i.contextElement||getDocument();if(a&&triggerEvent(l,"htmx:beforeTransition",i.eventInfo)&&typeof Promise!="undefined"&&document.startViewTransition){let c=new Promise(function(d,p){r=d,o=p}),u=s;s=function(){document.startViewTransition(function(){return u(),c})}}try{t!=null&&t.swapDelay&&t.swapDelay>0?getWindow().setTimeout(s,t.swapDelay):s()}catch(c){throw triggerErrorEvent(l,"htmx:swapError",i.eventInfo),maybeCall(o),c}}function handleTriggerHeader(n,e,t){let i=n.getResponseHeader(e);if(i.indexOf("{")===0){let r=parseJSON(i);for(let o in r)if(r.hasOwnProperty(o)){let s=r[o];isRawObject(s)?t=s.target!==void 0?s.target:t:s={value:s},triggerEvent(t,o,s)}}else{let r=i.split(",");for(let o=0;o0;){let s=e[0];if(s==="]"){if(i--,i===0){o===null&&(r=r+"true"),e.shift(),r+=")})";try{let a=maybeEval(n,function(){return Function(r)()},function(){return!0});return a.source=r,a}catch(a){return triggerErrorEvent(getDocument().body,"htmx:syntax:error",{error:a,source:r}),null}}}else s==="["&&i++;isPossibleRelativeReference(s,o,t)?r+="(("+t+"."+s+") ? ("+t+"."+s+") : (window."+s+"))":r=r+s,o=e.shift()}}}function consumeUntil(n,e){let t="";for(;n.length>0&&!e.test(n[0]);)t+=n.shift();return t}function consumeCSSSelector(n){let e;return n.length>0&&COMBINED_SELECTOR_START.test(n[0])?(n.shift(),e=consumeUntil(n,COMBINED_SELECTOR_END).trim(),n.shift()):e=consumeUntil(n,WHITESPACE_OR_COMMA),e}let INPUT_SELECTOR="input, textarea, select";function parseAndCacheTrigger(n,e,t){let i=[],r=tokenizeString(e);do{consumeUntil(r,NOT_WHITESPACE);let a=r.length,l=consumeUntil(r,/[,\[\s]/);if(l!=="")if(l==="every"){let c={trigger:"every"};consumeUntil(r,NOT_WHITESPACE),c.pollInterval=parseInterval(consumeUntil(r,/[,\[\s]/)),consumeUntil(r,NOT_WHITESPACE);var o=maybeGenerateConditional(n,r,"event");o&&(c.eventFilter=o),i.push(c)}else{let c={trigger:l};var o=maybeGenerateConditional(n,r,"event");for(o&&(c.eventFilter=o),consumeUntil(r,NOT_WHITESPACE);r.length>0&&r[0]!==",";){let d=r.shift();if(d==="changed")c.changed=!0;else if(d==="once")c.once=!0;else if(d==="consume")c.consume=!0;else if(d==="delay"&&r[0]===":")r.shift(),c.delay=parseInterval(consumeUntil(r,WHITESPACE_OR_COMMA));else if(d==="from"&&r[0]===":"){if(r.shift(),COMBINED_SELECTOR_START.test(r[0]))var s=consumeCSSSelector(r);else{var s=consumeUntil(r,WHITESPACE_OR_COMMA);if(s==="closest"||s==="find"||s==="next"||s==="previous"){r.shift();let y=consumeCSSSelector(r);y.length>0&&(s+=" "+y)}}c.from=s}else d==="target"&&r[0]===":"?(r.shift(),c.target=consumeCSSSelector(r)):d==="throttle"&&r[0]===":"?(r.shift(),c.throttle=parseInterval(consumeUntil(r,WHITESPACE_OR_COMMA))):d==="queue"&&r[0]===":"?(r.shift(),c.queue=consumeUntil(r,WHITESPACE_OR_COMMA)):d==="root"&&r[0]===":"?(r.shift(),c[d]=consumeCSSSelector(r)):d==="threshold"&&r[0]===":"?(r.shift(),c[d]=consumeUntil(r,WHITESPACE_OR_COMMA)):triggerErrorEvent(n,"htmx:syntax:error",{token:r.shift()});consumeUntil(r,NOT_WHITESPACE)}i.push(c)}r.length===a&&triggerErrorEvent(n,"htmx:syntax:error",{token:r.shift()}),consumeUntil(r,NOT_WHITESPACE)}while(r[0]===","&&r.shift());return t&&(t[e]=i),i}function getTriggerSpecs(n){let e=getAttributeValue(n,"hx-trigger"),t=[];if(e){let i=htmx.config.triggerSpecsCache;t=i&&i[e]||parseAndCacheTrigger(n,e,i)}return t.length>0?t:matches(n,"form")?[{trigger:"submit"}]:matches(n,'input[type="button"], input[type="submit"]')?[{trigger:"click"}]:matches(n,INPUT_SELECTOR)?[{trigger:"change"}]:[{trigger:"click"}]}function cancelPolling(n){getInternalData(n).cancelled=!0}function processPolling(n,e,t){let i=getInternalData(n);i.timeout=getWindow().setTimeout(function(){bodyContains(n)&&i.cancelled!==!0&&(maybeFilterEvent(t,n,makeEvent("hx:poll:trigger",{triggerSpec:t,target:n}))||e(n),processPolling(n,e,t))},t.pollInterval)}function isLocalLink(n){return location.hostname===n.hostname&&getRawAttribute(n,"href")&&getRawAttribute(n,"href").indexOf("#")!==0}function eltIsDisabled(n){return closest(n,htmx.config.disableSelector)}function boostElement(n,e,t){if(n instanceof HTMLAnchorElement&&isLocalLink(n)&&(n.target===""||n.target==="_self")||n.tagName==="FORM"&&String(getRawAttribute(n,"method")).toLowerCase()!=="dialog"){e.boosted=!0;let i,r;if(n.tagName==="A")i="get",r=getRawAttribute(n,"href");else{let o=getRawAttribute(n,"method");i=o?o.toLowerCase():"get",r=getRawAttribute(n,"action"),(r==null||r==="")&&(r=location.href),i==="get"&&r.includes("?")&&(r=r.replace(/\?[^#]+/,""))}t.forEach(function(o){addEventListener(n,function(s,a){let l=asElement(s);if(eltIsDisabled(l)){cleanUpElement(l);return}issueAjaxRequest(i,r,l,a)},e,o,!0)})}}function shouldCancel(n,e){if(n.type==="submit"&&e.tagName==="FORM")return!0;if(n.type==="click"){let t=e.closest('input[type="submit"], button');if(t&&t.form&&t.type==="submit")return!0;let i=e.closest("a"),r=/^#.+/;if(i&&i.href&&!r.test(i.getAttribute("href")))return!0}return!1}function ignoreBoostedAnchorCtrlClick(n,e){return getInternalData(n).boosted&&n instanceof HTMLAnchorElement&&e.type==="click"&&(e.ctrlKey||e.metaKey)}function maybeFilterEvent(n,e,t){let i=n.eventFilter;if(i)try{return i.call(e,t)!==!0}catch(r){let o=i.source;return triggerErrorEvent(getDocument().body,"htmx:eventFilter:error",{error:r,source:o}),!0}return!1}function addEventListener(n,e,t,i,r){let o=getInternalData(n),s;i.from?s=querySelectorAllExt(n,i.from):s=[n],i.changed&&("lastValue"in o||(o.lastValue=new WeakMap),s.forEach(function(a){o.lastValue.has(i)||o.lastValue.set(i,new WeakMap),o.lastValue.get(i).set(a,a.value)})),forEach(s,function(a){let l=function(c){if(!bodyContains(n)){a.removeEventListener(i.trigger,l);return}if(ignoreBoostedAnchorCtrlClick(n,c)||((r||shouldCancel(c,a))&&c.preventDefault(),maybeFilterEvent(i,n,c)))return;let u=getInternalData(c);if(u.triggerSpec=i,u.handledFor==null&&(u.handledFor=[]),u.handledFor.indexOf(n)<0){if(u.handledFor.push(n),i.consume&&c.stopPropagation(),i.target&&c.target&&!matches(asElement(c.target),i.target))return;if(i.once){if(o.triggeredOnce)return;o.triggeredOnce=!0}if(i.changed){let d=c.target,p=d.value,y=o.lastValue.get(i);if(y.has(d)&&y.get(d)===p)return;y.set(d,p)}if(o.delayed&&clearTimeout(o.delayed),o.throttle)return;i.throttle>0?o.throttle||(triggerEvent(n,"htmx:trigger"),e(n,c),o.throttle=getWindow().setTimeout(function(){o.throttle=null},i.throttle)):i.delay>0?o.delayed=getWindow().setTimeout(function(){triggerEvent(n,"htmx:trigger"),e(n,c)},i.delay):(triggerEvent(n,"htmx:trigger"),e(n,c))}};t.listenerInfos==null&&(t.listenerInfos=[]),t.listenerInfos.push({trigger:i.trigger,listener:l,on:a}),a.addEventListener(i.trigger,l)})}let windowIsScrolling=!1,scrollHandler=null;function initScrollHandler(){scrollHandler||(scrollHandler=function(){windowIsScrolling=!0},window.addEventListener("scroll",scrollHandler),window.addEventListener("resize",scrollHandler),setInterval(function(){windowIsScrolling&&(windowIsScrolling=!1,forEach(getDocument().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(n){maybeReveal(n)}))},200))}function maybeReveal(n){!hasAttribute(n,"data-hx-revealed")&&isScrolledIntoView(n)&&(n.setAttribute("data-hx-revealed","true"),getInternalData(n).initHash?triggerEvent(n,"revealed"):n.addEventListener("htmx:afterProcessNode",function(){triggerEvent(n,"revealed")},{once:!0}))}function loadImmediately(n,e,t,i){let r=function(){t.loaded||(t.loaded=!0,triggerEvent(n,"htmx:trigger"),e(n))};i>0?getWindow().setTimeout(r,i):r()}function processVerbs(n,e,t){let i=!1;return forEach(VERBS,function(r){if(hasAttribute(n,"hx-"+r)){let o=getAttributeValue(n,"hx-"+r);i=!0,e.path=o,e.verb=r,t.forEach(function(s){addTriggerHandler(n,s,e,function(a,l){let c=asElement(a);if(eltIsDisabled(c)){cleanUpElement(c);return}issueAjaxRequest(r,o,c,l)})})}}),i}function addTriggerHandler(n,e,t,i){if(e.trigger==="revealed")initScrollHandler(),addEventListener(n,i,t,e),maybeReveal(asElement(n));else if(e.trigger==="intersect"){let r={};e.root&&(r.root=querySelectorExt(n,e.root)),e.threshold&&(r.threshold=parseFloat(e.threshold)),new IntersectionObserver(function(s){for(let a=0;a0?(t.polling=!0,processPolling(asElement(n),i,e)):addEventListener(n,i,t,e)}function shouldProcessHxOn(n){let e=asElement(n);if(!e)return!1;let t=e.attributes;for(let i=0;i", "+o).join(""))}else return[]}function maybeSetLastButtonClicked(n){let e=getTargetButton(n.target),t=getRelatedFormData(n);t&&(t.lastButtonClicked=e)}function maybeUnsetLastButtonClicked(n){let e=getRelatedFormData(n);e&&(e.lastButtonClicked=null)}function getTargetButton(n){return closest(asElement(n),"button, input[type='submit']")}function getRelatedForm(n){return n.form||closest(n,"form")}function getRelatedFormData(n){let e=getTargetButton(n.target);if(!e)return;let t=getRelatedForm(e);if(t)return getInternalData(t)}function initButtonTracking(n){n.addEventListener("click",maybeSetLastButtonClicked),n.addEventListener("focusin",maybeSetLastButtonClicked),n.addEventListener("focusout",maybeUnsetLastButtonClicked)}function addHxOnEventHandler(n,e,t){let i=getInternalData(n);Array.isArray(i.onHandlers)||(i.onHandlers=[]);let r,o=function(s){maybeEval(n,function(){eltIsDisabled(n)||(r||(r=new Function("event",t)),r.call(n,s))})};n.addEventListener(e,o),i.onHandlers.push({event:e,listener:o})}function processHxOnWildcard(n){deInitOnHandlers(n);for(let e=0;ehtmx.config.historyCacheSize;)o.shift();for(;o.length>0;)try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(o));break}catch(a){triggerErrorEvent(getDocument().body,"htmx:historyCacheError",{cause:a,cache:o}),o.shift()}}function getCachedHistory(n){if(!canAccessLocalStorage())return null;n=normalizePath(n);let e=parseJSON(sessionStorage.getItem("htmx-history-cache"))||[];for(let t=0;t=200&&this.status<400?(i.response=this.response,triggerEvent(getDocument().body,"htmx:historyCacheMissLoad",i),swap(i.historyElt,i.response,t,{contextElement:i.historyElt,historyRequest:!0}),setCurrentPathForHistory(i.path),triggerEvent(getDocument().body,"htmx:historyRestore",{path:n,cacheMiss:!0,serverResponse:i.response})):triggerErrorEvent(getDocument().body,"htmx:historyCacheMissLoadError",i)},triggerEvent(getDocument().body,"htmx:historyCacheMiss",i)&&e.send()}function restoreHistory(n){saveCurrentPageToHistory(),n=n||location.pathname+location.search;let e=getCachedHistory(n);if(e){let t={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:e.scroll},i={path:n,item:e,historyElt:getHistoryElement(),swapSpec:t};triggerEvent(getDocument().body,"htmx:historyCacheHit",i)&&(swap(i.historyElt,e.content,t,{contextElement:i.historyElt,title:e.title}),setCurrentPathForHistory(i.path),triggerEvent(getDocument().body,"htmx:historyRestore",i))}else htmx.config.refreshOnHistoryMiss?htmx.location.reload(!0):loadHistoryFromServer(n)}function addRequestIndicatorClasses(n){let e=findAttributeTargets(n,"hx-indicator");return e==null&&(e=[n]),forEach(e,function(t){let i=getInternalData(t);i.requestCount=(i.requestCount||0)+1,t.classList.add.call(t.classList,htmx.config.requestClass)}),e}function disableElements(n){let e=findAttributeTargets(n,"hx-disabled-elt");return e==null&&(e=[]),forEach(e,function(t){let i=getInternalData(t);i.requestCount=(i.requestCount||0)+1,t.setAttribute("disabled",""),t.setAttribute("data-disabled-by-htmx","")}),e}function removeRequestIndicators(n,e){forEach(n.concat(e),function(t){let i=getInternalData(t);i.requestCount=(i.requestCount||1)-1}),forEach(n,function(t){getInternalData(t).requestCount===0&&t.classList.remove.call(t.classList,htmx.config.requestClass)}),forEach(e,function(t){getInternalData(t).requestCount===0&&(t.removeAttribute("disabled"),t.removeAttribute("data-disabled-by-htmx"))})}function haveSeenNode(n,e){for(let t=0;te.indexOf(r)<0):i=i.filter(r=>r!==e),t.delete(n),forEach(i,r=>t.append(n,r))}}function getValueFromInput(n){return n instanceof HTMLSelectElement&&n.multiple?toArray(n.querySelectorAll("option:checked")).map(function(e){return e.value}):n instanceof HTMLInputElement&&n.files?toArray(n.files):n.value}function processInputValue(n,e,t,i,r){if(!(i==null||haveSeenNode(n,i))){if(n.push(i),shouldInclude(i)){let o=getRawAttribute(i,"name");addValueToFormData(o,getValueFromInput(i),e),r&&validateElement(i,t)}i instanceof HTMLFormElement&&(forEach(i.elements,function(o){n.indexOf(o)>=0?removeValueFromFormData(o.name,getValueFromInput(o),e):n.push(o),r&&validateElement(o,t)}),new FormData(i).forEach(function(o,s){o instanceof File&&o.name===""||addValueToFormData(s,o,e)}))}}function validateElement(n,e){let t=n;t.willValidate&&(triggerEvent(t,"htmx:validation:validate"),t.checkValidity()||(triggerEvent(t,"htmx:validation:failed",{message:t.validationMessage,validity:t.validity})&&!e.length&&htmx.config.reportValidityOfForms&&t.reportValidity(),e.push({elt:t,message:t.validationMessage,validity:t.validity})))}function overrideFormData(n,e){for(let t of e.keys())n.delete(t);return e.forEach(function(t,i){n.append(i,t)}),n}function getInputValues(n,e){let t=[],i=new FormData,r=new FormData,o=[],s=getInternalData(n);s.lastButtonClicked&&!bodyContains(s.lastButtonClicked)&&(s.lastButtonClicked=null);let a=n instanceof HTMLFormElement&&n.noValidate!==!0||getAttributeValue(n,"hx-validate")==="true";if(s.lastButtonClicked&&(a=a&&s.lastButtonClicked.formNoValidate!==!0),e!=="get"&&processInputValue(t,r,o,getRelatedForm(n),a),processInputValue(t,i,o,n,a),s.lastButtonClicked||n.tagName==="BUTTON"||n.tagName==="INPUT"&&getRawAttribute(n,"type")==="submit"){let c=s.lastButtonClicked||n,u=getRawAttribute(c,"name");addValueToFormData(u,c.value,r)}let l=findAttributeTargets(n,"hx-include");return forEach(l,function(c){processInputValue(t,i,o,asElement(c),a),matches(c,"form")||forEach(asParentNode(c).querySelectorAll(INPUT_SELECTOR),function(u){processInputValue(t,i,o,u,a)})}),overrideFormData(i,r),{errors:o,formData:i,values:formDataProxy(i)}}function appendParam(n,e,t){n!==""&&(n+="&"),String(t)==="[object Object]"&&(t=JSON.stringify(t));let i=encodeURIComponent(t);return n+=encodeURIComponent(e)+"="+i,n}function urlEncode(n){n=formDataFromObject(n);let e="";return n.forEach(function(t,i){e=appendParam(e,i,t)}),e}function getHeaders(n,e,t){let i={"HX-Request":"true","HX-Trigger":getRawAttribute(n,"id"),"HX-Trigger-Name":getRawAttribute(n,"name"),"HX-Target":getAttributeValue(e,"id"),"HX-Current-URL":location.href};return getValuesForElement(n,"hx-headers",!1,i),t!==void 0&&(i["HX-Prompt"]=t),getInternalData(n).boosted&&(i["HX-Boosted"]="true"),i}function filterValues(n,e){let t=getClosestAttributeValue(e,"hx-params");if(t){if(t==="none")return new FormData;if(t==="*")return n;if(t.indexOf("not ")===0)return forEach(t.slice(4).split(","),function(i){i=i.trim(),n.delete(i)}),n;{let i=new FormData;return forEach(t.split(","),function(r){r=r.trim(),n.has(r)&&n.getAll(r).forEach(function(o){i.append(r,o)})}),i}}else return n}function isAnchorLink(n){return!!getRawAttribute(n,"href")&&getRawAttribute(n,"href").indexOf("#")>=0}function getSwapSpecification(n,e){let t=e||getClosestAttributeValue(n,"hx-swap"),i={swapStyle:getInternalData(n).boosted?"innerHTML":htmx.config.defaultSwapStyle,swapDelay:htmx.config.defaultSwapDelay,settleDelay:htmx.config.defaultSettleDelay};if(htmx.config.scrollIntoViewOnBoost&&getInternalData(n).boosted&&!isAnchorLink(n)&&(i.show="top"),t){let s=splitOnWhitespace(t);if(s.length>0)for(let a=0;a0?r.join(":"):null;i.scroll=u,i.scrollTarget=o}else if(l.indexOf("show:")===0){var r=l.slice(5).split(":");let d=r.pop();var o=r.length>0?r.join(":"):null;i.show=d,i.showTarget=o}else if(l.indexOf("focus-scroll:")===0){let c=l.slice(13);i.focusScroll=c=="true"}else a==0?i.swapStyle=l:logError("Unknown modifier in hx-swap: "+l)}}return i}function usesFormData(n){return getClosestAttributeValue(n,"hx-encoding")==="multipart/form-data"||matches(n,"form")&&getRawAttribute(n,"enctype")==="multipart/form-data"}function encodeParamsForBody(n,e,t){let i=null;return withExtensions(e,function(r){i==null&&(i=r.encodeParameters(n,t,e))}),i!=null?i:usesFormData(e)?overrideFormData(new FormData,formDataFromObject(t)):urlEncode(t)}function makeSettleInfo(n){return{tasks:[],elts:[n]}}function updateScrollState(n,e){let t=n[0],i=n[n.length-1];if(e.scroll){var r=null;e.scrollTarget&&(r=asElement(querySelectorExt(t,e.scrollTarget))),e.scroll==="top"&&(t||r)&&(r=r||t,r.scrollTop=0),e.scroll==="bottom"&&(i||r)&&(r=r||i,r.scrollTop=r.scrollHeight),typeof e.scroll=="number"&&getWindow().setTimeout(function(){window.scrollTo(0,e.scroll)},0)}if(e.show){var r=null;if(e.showTarget){let s=e.showTarget;e.showTarget==="window"&&(s="body"),r=asElement(querySelectorExt(t,s))}e.show==="top"&&(t||r)&&(r=r||t,r.scrollIntoView({block:"start",behavior:htmx.config.scrollBehavior})),e.show==="bottom"&&(i||r)&&(r=r||i,r.scrollIntoView({block:"end",behavior:htmx.config.scrollBehavior}))}}function getValuesForElement(n,e,t,i,r){if(i==null&&(i={}),n==null)return i;let o=getAttributeValue(n,e);if(o){let s=o.trim(),a=t;if(s==="unset")return null;s.indexOf("javascript:")===0?(s=s.slice(11),a=!0):s.indexOf("js:")===0&&(s=s.slice(3),a=!0),s.indexOf("{")!==0&&(s="{"+s+"}");let l;a?l=maybeEval(n,function(){return r?Function("event","return ("+s+")").call(n,r):Function("return ("+s+")").call(n)},{}):l=parseJSON(s);for(let c in l)l.hasOwnProperty(c)&&i[c]==null&&(i[c]=l[c])}return getValuesForElement(asElement(parentElt(n)),e,t,i,r)}function maybeEval(n,e,t){return htmx.config.allowEval?e():(triggerErrorEvent(n,"htmx:evalDisallowedError"),t)}function getHXVarsForElement(n,e,t){return getValuesForElement(n,"hx-vars",!0,t,e)}function getHXValsForElement(n,e,t){return getValuesForElement(n,"hx-vals",!1,t,e)}function getExpressionVars(n,e){return mergeObjects(getHXVarsForElement(n,e),getHXValsForElement(n,e))}function safelySetHeaderValue(n,e,t){if(t!==null)try{n.setRequestHeader(e,t)}catch(i){n.setRequestHeader(e,encodeURIComponent(t)),n.setRequestHeader(e+"-URI-AutoEncoded","true")}}function getPathFromResponse(n){if(n.responseURL)try{let e=new URL(n.responseURL);return e.pathname+e.search}catch(e){triggerErrorEvent(getDocument().body,"htmx:badResponseUrl",{url:n.responseURL})}}function hasHeader(n,e){return e.test(n.getAllResponseHeaders())}function ajaxHelper(n,e,t){if(n=n.toLowerCase(),t){if(t instanceof Element||typeof t=="string")return issueAjaxRequest(n,e,null,null,{targetOverride:resolveTarget(t)||DUMMY_ELT,returnPromise:!0});{let i=resolveTarget(t.target);return(t.target&&!i||t.source&&!i&&!resolveTarget(t.source))&&(i=DUMMY_ELT),issueAjaxRequest(n,e,resolveTarget(t.source),t.event,{handler:t.handler,headers:t.headers,values:t.values,targetOverride:i,swapOverride:t.swap,select:t.select,returnPromise:!0,push:t.push,replace:t.replace,selectOOB:t.selectOOB})}}else return issueAjaxRequest(n,e,null,null,{returnPromise:!0})}function hierarchyForElt(n){let e=[];for(;n;)e.push(n),n=n.parentElement;return e}function verifyPath(n,e,t){let i=new URL(e,location.protocol!=="about:"?location.href:window.origin),o=(location.protocol!=="about:"?location.origin:window.origin)===i.origin;return htmx.config.selfRequestsOnly&&!o?!1:triggerEvent(n,"htmx:validateUrl",mergeObjects({url:i,sameHost:o},t))}function formDataFromObject(n){if(n instanceof FormData)return n;let e=new FormData;for(let t in n)n.hasOwnProperty(t)&&(n[t]&&typeof n[t].forEach=="function"?n[t].forEach(function(i){e.append(t,i)}):typeof n[t]=="object"&&!(n[t]instanceof Blob)?e.append(t,JSON.stringify(n[t])):e.append(t,n[t]));return e}function formDataArrayProxy(n,e,t){return new Proxy(t,{get:function(i,r){return typeof r=="number"?i[r]:r==="length"?i.length:r==="push"?function(o){i.push(o),n.append(e,o)}:typeof i[r]=="function"?function(){i[r].apply(i,arguments),n.delete(e),i.forEach(function(o){n.append(e,o)})}:i[r]&&i[r].length===1?i[r][0]:i[r]},set:function(i,r,o){return i[r]=o,n.delete(e),i.forEach(function(s){n.append(e,s)}),!0}})}function formDataProxy(n){return new Proxy(n,{get:function(e,t){if(typeof t=="symbol"){let r=Reflect.get(e,t);return typeof r=="function"?function(){return r.apply(n,arguments)}:r}if(t==="toJSON")return()=>Object.fromEntries(n);if(t in e&&typeof e[t]=="function")return function(){return n[t].apply(n,arguments)};let i=n.getAll(t);if(i.length!==0)return i.length===1?i[0]:formDataArrayProxy(e,t,i)},set:function(e,t,i){return typeof t!="string"?!1:(e.delete(t),i&&typeof i.forEach=="function"?i.forEach(function(r){e.append(t,r)}):typeof i=="object"&&!(i instanceof Blob)?e.append(t,JSON.stringify(i)):e.append(t,i),!0)},deleteProperty:function(e,t){return typeof t=="string"&&e.delete(t),!0},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function issueAjaxRequest(n,e,t,i,r,o){let s=null,a=null;if(r=r!=null?r:{},r.returnPromise&&typeof Promise!="undefined")var l=new Promise(function(U,Z){s=U,a=Z});t==null&&(t=getDocument().body);let c=r.handler||handleAjaxResponse,u=r.select||null;if(!bodyContains(t))return maybeCall(s),l;let d=r.targetOverride||asElement(getTarget(t));if(d==null||d==DUMMY_ELT)return triggerErrorEvent(t,"htmx:targetError",{target:getClosestAttributeValue(t,"hx-target")}),maybeCall(a),l;let p=getInternalData(t),y=p.lastButtonClicked;if(y){let U=getRawAttribute(y,"formaction");U!=null&&(e=U);let Z=getRawAttribute(y,"formmethod");if(Z!=null)if(VERBS.includes(Z.toLowerCase()))n=Z;else return maybeCall(s),l}let m=getClosestAttributeValue(t,"hx-confirm");if(o===void 0&&triggerEvent(t,"htmx:confirm",{target:d,elt:t,path:e,verb:n,triggeringEvent:i,etc:r,issueRequest:function(oe){return issueAjaxRequest(n,e,t,i,r,!!oe)},question:m})===!1)return maybeCall(s),l;let v=t,w=getClosestAttributeValue(t,"hx-sync"),T=null,_=!1;if(w){let U=w.split(":"),Z=U[0].trim();if(Z==="this"?v=findThisElement(t,"hx-sync"):v=asElement(querySelectorExt(t,Z)),w=(U[1]||"drop").trim(),p=getInternalData(v),w==="drop"&&p.xhr&&p.abortable!==!0)return maybeCall(s),l;if(w==="abort"){if(p.xhr)return maybeCall(s),l;_=!0}else w==="replace"?triggerEvent(v,"htmx:abort"):w.indexOf("queue")===0&&(T=(w.split(" ")[1]||"last").trim())}if(p.xhr)if(p.abortable)triggerEvent(v,"htmx:abort");else{if(T==null){if(i){let U=getInternalData(i);U&&U.triggerSpec&&U.triggerSpec.queue&&(T=U.triggerSpec.queue)}T==null&&(T="last")}return p.queuedRequests==null&&(p.queuedRequests=[]),T==="first"&&p.queuedRequests.length===0?p.queuedRequests.push(function(){issueAjaxRequest(n,e,t,i,r)}):T==="all"?p.queuedRequests.push(function(){issueAjaxRequest(n,e,t,i,r)}):T==="last"&&(p.queuedRequests=[],p.queuedRequests.push(function(){issueAjaxRequest(n,e,t,i,r)})),maybeCall(s),l}let S=new XMLHttpRequest;p.xhr=S,p.abortable=_;let A=function(){p.xhr=null,p.abortable=!1,p.queuedRequests!=null&&p.queuedRequests.length>0&&p.queuedRequests.shift()()},K=getClosestAttributeValue(t,"hx-prompt");if(K){var z=prompt(K);if(z===null||!triggerEvent(t,"htmx:prompt",{prompt:z,target:d}))return maybeCall(s),A(),l}if(m&&!o&&!confirm(m))return maybeCall(s),A(),l;let L=getHeaders(t,d,z);n!=="get"&&!usesFormData(t)&&(L["Content-Type"]="application/x-www-form-urlencoded"),r.headers&&(L=mergeObjects(L,r.headers));let I=getInputValues(t,n),k=I.errors,Y=I.formData;r.values&&overrideFormData(Y,formDataFromObject(r.values));let $=formDataFromObject(getExpressionVars(t,i)),ie=overrideFormData(Y,$),J=filterValues(ie,t);htmx.config.getCacheBusterParam&&n==="get"&&J.set("org.htmx.cache-buster",getRawAttribute(d,"id")||"true"),(e==null||e==="")&&(e=location.href);let Te=getValuesForElement(t,"hx-request"),Ce=getInternalData(t).boosted,se=htmx.config.methodsThatUseUrlParams.indexOf(n)>=0,ne={boosted:Ce,useUrlParams:se,formData:J,parameters:formDataProxy(J),unfilteredFormData:ie,unfilteredParameters:formDataProxy(ie),headers:L,elt:t,target:d,verb:n,errors:k,withCredentials:r.credentials||Te.credentials||htmx.config.withCredentials,timeout:r.timeout||Te.timeout||htmx.config.timeout,path:e,triggeringEvent:i};if(!triggerEvent(t,"htmx:configRequest",ne))return maybeCall(s),A(),l;if(e=ne.path,n=ne.verb,L=ne.headers,J=formDataFromObject(ne.parameters),k=ne.errors,se=ne.useUrlParams,k&&k.length>0)return triggerEvent(t,"htmx:validation:halted",ne),maybeCall(s),A(),l;let Ue=e.split("#"),Ie=Ue[0],W=Ue[1],M=e;if(se&&(M=Ie,!J.keys().next().done&&(M.indexOf("?")<0?M+="?":M+="&",M+=urlEncode(J),W&&(M+="#"+W))),!verifyPath(t,M,ne))return triggerErrorEvent(t,"htmx:invalidPath",ne),maybeCall(a),A(),l;if(S.open(n.toUpperCase(),M,!0),S.overrideMimeType("text/html"),S.withCredentials=ne.withCredentials,S.timeout=ne.timeout,!Te.noHeaders){for(let U in L)if(L.hasOwnProperty(U)){let Z=L[U];safelySetHeaderValue(S,U,Z)}}let D={xhr:S,target:d,requestConfig:ne,etc:r,boosted:Ce,select:u,pathInfo:{requestPath:e,finalRequestPath:M,responsePath:null,anchor:W}};if(S.onload=function(){try{let U=hierarchyForElt(t);if(D.pathInfo.responsePath=getPathFromResponse(S),c(t,D),D.keepIndicators!==!0&&removeRequestIndicators(B,V),triggerEvent(t,"htmx:afterRequest",D),triggerEvent(t,"htmx:afterOnLoad",D),!bodyContains(t)){let Z=null;for(;U.length>0&&Z==null;){let oe=U.shift();bodyContains(oe)&&(Z=oe)}Z&&(triggerEvent(Z,"htmx:afterRequest",D),triggerEvent(Z,"htmx:afterOnLoad",D))}maybeCall(s)}catch(U){throw triggerErrorEvent(t,"htmx:onLoadError",mergeObjects({error:U},D)),U}finally{A()}},S.onerror=function(){removeRequestIndicators(B,V),triggerErrorEvent(t,"htmx:afterRequest",D),triggerErrorEvent(t,"htmx:sendError",D),maybeCall(a),A()},S.onabort=function(){removeRequestIndicators(B,V),triggerErrorEvent(t,"htmx:afterRequest",D),triggerErrorEvent(t,"htmx:sendAbort",D),maybeCall(a),A()},S.ontimeout=function(){removeRequestIndicators(B,V),triggerErrorEvent(t,"htmx:afterRequest",D),triggerErrorEvent(t,"htmx:timeout",D),maybeCall(a),A()},!triggerEvent(t,"htmx:beforeRequest",D))return maybeCall(s),A(),l;var B=addRequestIndicatorClasses(t),V=disableElements(t);forEach(["loadstart","loadend","progress","abort"],function(U){forEach([S,S.upload],function(Z){Z.addEventListener(U,function(oe){triggerEvent(t,"htmx:xhr:"+U,{lengthComputable:oe.lengthComputable,loaded:oe.loaded,total:oe.total})})})}),triggerEvent(t,"htmx:beforeSend",D);let q=se?null:encodeParamsForBody(S,t,J);return S.send(q),l}function determineHistoryUpdates(n,e){let t=e.xhr,i=null,r=null;if(hasHeader(t,/HX-Push:/i)?(i=t.getResponseHeader("HX-Push"),r="push"):hasHeader(t,/HX-Push-Url:/i)?(i=t.getResponseHeader("HX-Push-Url"),r="push"):hasHeader(t,/HX-Replace-Url:/i)&&(i=t.getResponseHeader("HX-Replace-Url"),r="replace"),i)return i==="false"?{}:{type:r,path:i};let o=e.pathInfo.finalRequestPath,s=e.pathInfo.responsePath,a=e.etc.push||getClosestAttributeValue(n,"hx-push-url"),l=e.etc.replace||getClosestAttributeValue(n,"hx-replace-url"),c=getInternalData(n).boosted,u=null,d=null;return a?(u="push",d=a):l?(u="replace",d=l):c&&(u="push",d=s||o),d?d==="false"?{}:(d==="true"&&(d=s||o),e.pathInfo.anchor&&d.indexOf("#")===-1&&(d=d+"#"+e.pathInfo.anchor),{type:u,path:d}):{}}function codeMatches(n,e){var t=new RegExp(n.code);return t.test(e.toString(10))}function resolveResponseHandling(n){for(var e=0;e.${e}{opacity:0;visibility: hidden} .${t} .${e}, .${t}.${e}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}`)}}function getMetaConfig(){let n=getDocument().querySelector('meta[name="htmx-config"]');return n?parseJSON(n.content):null}function mergeMetaConfig(){let n=getMetaConfig();n&&(htmx.config=mergeObjects(htmx.config,n))}return ready(function(){mergeMetaConfig(),insertIndicatorStyles();let n=getDocument().body;processNode(n);let e=getDocument().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");n.addEventListener("htmx:abort",function(i){let r=i.detail.elt||i.target,o=getInternalData(r);o&&o.xhr&&o.xhr.abort()});let t=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(i){i.state&&i.state.htmx?(restoreHistory(),forEach(e,function(r){triggerEvent(r,"htmx:restored",{document:getDocument(),triggerEvent})})):t&&t(i)},getWindow().setTimeout(function(){triggerEvent(n,"htmx:load",{}),n=null},0)}),htmx})(),O_=Jp;function Ro(n,e){n.split(/\s+/).forEach(t=>{e(t)})}var oi=class{constructor(){this._events={}}on(e,t){Ro(e,i=>{let r=this._events[i]||[];r.push(t),this._events[i]=r})}off(e,t){var i=arguments.length;if(i===0){this._events={};return}Ro(e,r=>{if(i===1){delete this._events[r];return}let o=this._events[r];o!==void 0&&(o.splice(o.indexOf(t),1),this._events[r]=o)})}trigger(e,...t){var i=this;Ro(e,r=>{let o=i._events[r];o!==void 0&&o.forEach(s=>{s.apply(i,t)})})}};function Io(n){return n.plugins={},class extends n{constructor(){super(...arguments),this.plugins={names:[],settings:{},requested:{},loaded:{}}}static define(e,t){n.plugins[e]={name:e,fn:t}}initializePlugins(e){var t,i;let r=this,o=[];if(Array.isArray(e))e.forEach(s=>{typeof s=="string"?o.push(s):(r.plugins.settings[s.name]=s.options,o.push(s.name))});else if(e)for(t in e)e.hasOwnProperty(t)&&(r.plugins.settings[t]=e[t],o.push(t));for(;i=o.shift();)r.require(i)}loadPlugin(e){var t=this,i=t.plugins,r=n.plugins[e];if(!n.plugins.hasOwnProperty(e))throw new Error('Unable to find "'+e+'" plugin');i.requested[e]=!0,i.loaded[e]=r.fn.apply(t,[t.plugins.settings[e]||{}]),i.names.push(e)}require(e){var t=this,i=t.plugins;if(!t.plugins.loaded.hasOwnProperty(e)){if(i.requested[e])throw new Error('Plugin has circular dependency ("'+e+'")');t.loadPlugin(e)}return i.loaded[e]}}}var si=n=>(n=n.filter(Boolean),n.length<2?n[0]||"":em(n)==1?"["+n.join("")+"]":"(?:"+n.join("|")+")"),Po=n=>{if(!Zp(n))return n.join("");let e="",t=0,i=()=>{t>1&&(e+="{"+t+"}")};return n.forEach((r,o)=>{if(r===n[o-1]){t++;return}i(),e+=r,t=1}),i(),e},Fo=n=>{let e=Array.from(n);return si(e)},Zp=n=>new Set(n).size!==n.length,Lt=n=>(n+"").replace(/([\$\(\)\*\+\.\?\[\]\^\{\|\}\\])/gu,"\\$1"),em=n=>n.reduce((e,t)=>Math.max(e,tm(t)),0),tm=n=>Array.from(n).length;var $o=n=>{if(n.length===1)return[[n]];let e=[],t=n.substring(1);return $o(t).forEach(function(r){let o=r.slice(0);o[0]=n.charAt(0)+o[0],e.push(o),o=r.slice(0),o.unshift(n.charAt(0)),e.push(o)}),e};var nm=[[0,65535]],im="[\u0300-\u036F\xB7\u02BE\u02BC]",sr,za,rm=3,Bo={},$a={"/":"\u2044\u2215",0:"\u07C0",a:"\u2C65\u0250\u0251",aa:"\uA733",ae:"\xE6\u01FD\u01E3",ao:"\uA735",au:"\uA737",av:"\uA739\uA73B",ay:"\uA73D",b:"\u0180\u0253\u0183",c:"\uA73F\u0188\u023C\u2184",d:"\u0111\u0257\u0256\u1D05\u018C\uABB7\u0501\u0266",e:"\u025B\u01DD\u1D07\u0247",f:"\uA77C\u0192",g:"\u01E5\u0260\uA7A1\u1D79\uA77F\u0262",h:"\u0127\u2C68\u2C76\u0265",i:"\u0268\u0131",j:"\u0249\u0237",k:"\u0199\u2C6A\uA741\uA743\uA745\uA7A3",l:"\u0142\u019A\u026B\u2C61\uA749\uA747\uA781\u026D",m:"\u0271\u026F\u03FB",n:"\uA7A5\u019E\u0272\uA791\u1D0E\u043B\u0509",o:"\xF8\u01FF\u0254\u0275\uA74B\uA74D\u1D11",oe:"\u0153",oi:"\u01A3",oo:"\uA74F",ou:"\u0223",p:"\u01A5\u1D7D\uA751\uA753\uA755\u03C1",q:"\uA757\uA759\u024B",r:"\u024D\u027D\uA75B\uA7A7\uA783",s:"\xDF\u023F\uA7A9\uA785\u0282",t:"\u0167\u01AD\u0288\u2C66\uA787",th:"\xFE",tz:"\uA729",u:"\u0289",v:"\u028B\uA75F\u028C",vy:"\uA761",w:"\u2C73",y:"\u01B4\u024F\u1EFF",z:"\u01B6\u0225\u0240\u2C6C\uA763",hv:"\u0195"};for(let n in $a){let e=$a[n]||"";for(let t=0;t{sr===void 0&&(sr=um(n||nm))},Ba=(n,e="NFKD")=>n.normalize(e),ai=n=>Array.from(n).reduce((e,t)=>e+am(t),""),am=n=>(n=Ba(n).toLowerCase().replace(om,e=>Bo[e]||""),Ba(n,"NFC"));function*lm(n){for(let[e,t]of n)for(let i=e;i<=t;i++){let r=String.fromCharCode(i),o=ai(r);o!=r.toLowerCase()&&(o.length>rm||o.length!=0&&(yield{folded:o,composed:r,code_point:i}))}}var cm=n=>{let e={},t=(i,r)=>{let o=e[i]||new Set,s=new RegExp("^"+Fo(o)+"$","iu");r.match(s)||(o.add(Lt(r)),e[i]=o)};for(let i of lm(n))t(i.folded,i.folded),t(i.folded,i.composed);return e},um=n=>{let e=cm(n),t={},i=[];for(let o in e){let s=e[o];s&&(t[o]=Fo(s)),o.length>1&&i.push(Lt(o))}i.sort((o,s)=>s.length-o.length);let r=si(i);return za=new RegExp("^"+r,"u"),t},dm=(n,e=1)=>{let t=0;return n=n.map(i=>(sr[i]&&(t+=i.length),sr[i]||i)),t>=e?Po(n):""},fm=(n,e=1)=>(e=Math.max(e,n.length-1),si($o(n).map(t=>dm(t,e)))),Va=(n,e=!0)=>{let t=n.length>1?1:0;return si(n.map(i=>{let r=[],o=e?i.length():i.length()-1;for(let s=0;s{for(let t of e){if(t.start!=n.start||t.end!=n.end||t.substrs.join("")!==n.substrs.join(""))continue;let i=n.parts,r=s=>{for(let a of i){if(a.start===s.start&&a.substr===s.substr)return!1;if(!(s.length==1||a.length==1)&&(s.starta.start||a.starts.start))return!0}return!1};if(!(t.parts.filter(r).length>0))return!0}return!1},ar=class n{constructor(){ee(this,"parts");ee(this,"substrs");ee(this,"start");ee(this,"end");this.parts=[],this.substrs=[],this.start=0,this.end=0}add(e){e&&(this.parts.push(e),this.substrs.push(e.substr),this.start=Math.min(e.start,this.start),this.end=Math.max(e.end,this.end))}last(){return this.parts[this.parts.length-1]}length(){return this.parts.length}clone(e,t){let i=new n,r=JSON.parse(JSON.stringify(this.parts)),o=r.pop();for(let l of r)i.add(l);let s=t.substr.substring(0,e-o.start),a=s.length;return i.add({start:o.start,end:o.start+a,length:a,substr:s}),i}},ja=n=>{sm(),n=ai(n);let e="",t=[new ar];for(let i=0;i0){l=l.sort((u,d)=>u.length()-d.length());for(let u of l)hm(u,t)||t.push(u);continue}if(i>0&&c.size==1&&!c.has("3")){e+=Va(t,!1);let u=new ar,d=t[0];d&&u.add(d.last()),t=[u]}}return e+=Va(t,!0),e};var Wa=(n,e)=>{if(n)return n[e]},qa=(n,e)=>{if(n){for(var t,i=e.split(".");(t=i.shift())&&(n=n[t]););return n}},lr=(n,e,t)=>{var i,r;return!n||(n=n+"",e.regex==null)||(r=n.search(e.regex),r===-1)?0:(i=e.string.length/n.length,r===0&&(i+=.5),i*t)},cr=(n,e)=>{var t=n[e];if(typeof t=="function")return t;t&&!Array.isArray(t)&&(n[e]=[t])},li=(n,e)=>{if(Array.isArray(n))n.forEach(e);else for(var t in n)n.hasOwnProperty(t)&&e(n[t],t)},Ua=(n,e)=>typeof n=="number"&&typeof e=="number"?n>e?1:ne?1:e>n?-1:0);var ur=class{constructor(e,t){ee(this,"items");ee(this,"settings");this.items=e,this.settings=t||{diacritics:!0}}tokenize(e,t,i){if(!e||!e.length)return[];let r=[],o=e.split(/\s+/);var s;return i&&(s=new RegExp("^("+Object.keys(i).map(Lt).join("|")+"):(.*)$")),o.forEach(a=>{let l,c=null,u=null;s&&(l=a.match(s))&&(c=l[1],a=l[2]),a.length>0&&(this.settings.diacritics?u=ja(a)||null:u=Lt(a),u&&t&&(u="\\b"+u)),r.push({string:a,regex:u?new RegExp(u,"iu"):null,field:c})}),r}getScoreFunction(e,t){var i=this.prepareSearch(e,t);return this._getScoreFunction(i)}_getScoreFunction(e){let t=e.tokens,i=t.length;if(!i)return function(){return 0};let r=e.options.fields,o=e.weights,s=r.length,a=e.getAttrFn;if(!s)return function(){return 1};let l=(function(){return s===1?function(c,u){let d=r[0].field;return lr(a(u,d),c,o[d]||1)}:function(c,u){var d=0;if(c.field){let p=a(u,c.field);!c.regex&&p?d+=1/s:d+=lr(p,c,1)}else li(o,(p,y)=>{d+=lr(a(u,y),c,p)});return d/s}})();return i===1?function(c){return l(t[0],c)}:e.options.conjunction==="and"?function(c){var u,d=0;for(let p of t){if(u=l(p,c),u<=0)return 0;d+=u}return d/i}:function(c){var u=0;return li(t,d=>{u+=l(d,c)}),u/i}}getSortFunction(e,t){var i=this.prepareSearch(e,t);return this._getSortFunction(i)}_getSortFunction(e){var t,i=[];let r=this,o=e.options,s=!e.query&&o.sort_empty?o.sort_empty:o.sort;if(typeof s=="function")return s.bind(this);let a=function(c,u){return c==="$score"?u.score:e.getAttrFn(r.items[u.id],c)};if(s)for(let c of s)(e.query||c.field!=="$score")&&i.push(c);if(e.query){t=!0;for(let c of i)if(c.field==="$score"){t=!1;break}t&&i.unshift({field:"$score",direction:"desc"})}else i=i.filter(c=>c.field!=="$score");return i.length?function(c,u){var d,p;for(let y of i)if(p=y.field,d=(y.direction==="desc"?-1:1)*Ua(a(p,c),a(p,u)),d)return d;return 0}:null}prepareSearch(e,t){let i={};var r=Object.assign({},t);if(cr(r,"sort"),cr(r,"sort_empty"),r.fields){cr(r,"fields");let o=[];r.fields.forEach(s=>{typeof s=="string"&&(s={field:s,weight:1}),o.push(s),i[s.field]="weight"in s?s.weight:1}),r.fields=o}return{options:r,query:e.toLowerCase().trim(),tokens:this.tokenize(e,r.respect_word_boundaries,i),total:0,items:[],weights:i,getAttrFn:r.nesting?qa:Wa}}search(e,t){var i=this,r,o;o=this.prepareSearch(e,t),t=o.options,e=o.query;let s=t.score||i._getScoreFunction(o);e.length?li(i.items,(l,c)=>{r=s(l),(t.filter===!1||r>0)&&o.items.push({score:r,id:c})}):li(i.items,(l,c)=>{o.items.push({score:1,id:c})});let a=i._getSortFunction(o);return a&&o.items.sort(a),o.total=o.items.length,typeof t.limit=="number"&&(o.items=o.items.slice(0,t.limit)),o}};var qe=n=>typeof n=="undefined"||n===null?null:ci(n),ci=n=>typeof n=="boolean"?n?"1":"0":n+"",dr=n=>(n+"").replace(/&/g,"&").replace(//g,">").replace(/"/g,"""),Ya=(n,e)=>e>0?window.setTimeout(n,e):(n.call(null),null),Ga=(n,e)=>{var t;return function(i,r){var o=this;t&&(o.loading=Math.max(o.loading-1,0),clearTimeout(t)),t=setTimeout(function(){t=null,o.loadedSearches[i]=!0,n.call(o,i,r)},e)}},Vo=(n,e,t)=>{var i,r=n.trigger,o={};n.trigger=function(){var s=arguments[0];if(e.indexOf(s)!==-1)o[s]=arguments;else return r.apply(n,arguments)},t.apply(n,[]),n.trigger=r;for(i of e)i in o&&r.apply(n,o[i])},Ka=n=>({start:n.selectionStart||0,length:(n.selectionEnd||0)-(n.selectionStart||0)}),fe=(n,e=!1)=>{n&&(n.preventDefault(),e&&n.stopPropagation())},De=(n,e,t,i)=>{n.addEventListener(e,t,i)},Mt=(n,e)=>{if(!e||!e[n])return!1;var t=(e.altKey?1:0)+(e.ctrlKey?1:0)+(e.shiftKey?1:0)+(e.metaKey?1:0);return t===1},fr=(n,e)=>{let t=n.getAttribute("id");return t||(n.setAttribute("id",e),e)},zo=n=>n.replace(/[\\"']/g,"\\$&"),Nt=(n,e)=>{e&&n.append(e)},ge=(n,e)=>{if(Array.isArray(n))n.forEach(e);else for(var t in n)n.hasOwnProperty(t)&&e(n[t],t)};var nt=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(jo(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},jo=n=>typeof n=="string"&&n.indexOf("<")>-1,Xa=n=>n.replace(/['"\\]/g,"\\$&"),hr=(n,e)=>{var t=document.createEvent("HTMLEvents");t.initEvent(e,!0,!1),n.dispatchEvent(t)},ui=(n,e)=>{Object.assign(n.style,e)},Fe=(n,...e)=>{var t=Qa(e);n=Ja(n),n.map(i=>{t.map(r=>{i.classList.add(r)})})},gt=(n,...e)=>{var t=Qa(e);n=Ja(n),n.map(i=>{t.map(r=>{i.classList.remove(r)})})},Qa=n=>{var e=[];return ge(n,t=>{typeof t=="string"&&(t=t.trim().split(/[\t\n\f\r\s]/)),Array.isArray(t)&&(e=e.concat(t))}),e.filter(Boolean)},Ja=n=>(Array.isArray(n)||(n=[n]),n),pr=(n,e,t)=>{if(!(t&&!t.contains(n)))for(;n&&n.matches;){if(n.matches(e))return n;n=n.parentNode}},Wo=(n,e=0)=>e>0?n[n.length-1]:n[0],Za=n=>Object.keys(n).length===0,qo=(n,e)=>{if(!n)return-1;e=e||n.nodeName;for(var t=0;n=n.previousElementSibling;)n.matches(e)&&t++;return t},le=(n,e)=>{ge(e,(t,i)=>{t==null?n.removeAttribute(i):n.setAttribute(i,""+t)})},di=(n,e)=>{n.parentNode&&n.parentNode.replaceChild(e,n)};var el=(n,e)=>{if(e===null)return;if(typeof e=="string"){if(!e.length)return;e=new RegExp(e,"i")}let t=o=>{var s=o.data.match(e);if(s&&o.data.length>0){var a=document.createElement("span");a.className="highlight";var l=o.splitText(s.index);l.splitText(s[0].length);var c=l.cloneNode(!0);return a.appendChild(c),di(l,a),1}return 0},i=o=>{o.nodeType===1&&o.childNodes&&!/(script|style)/i.test(o.tagName)&&(o.className!=="highlight"||o.tagName!=="SPAN")&&Array.from(o.childNodes).forEach(s=>{r(s)})},r=o=>o.nodeType===3?t(o):(i(o),0);r(n)},tl=n=>{var e=n.querySelectorAll("span.highlight");Array.prototype.forEach.call(e,function(t){var i=t.parentNode;i.replaceChild(t.firstChild,t),i.normalize()})};var pm=typeof navigator=="undefined"?!1:/Mac/.test(navigator.userAgent),fi=pm?"metaKey":"ctrlKey";var Uo={options:[],optgroups:[],plugins:[],delimiter:",",splitOn:null,persist:!0,diacritics:!0,create:null,createOnBlur:!1,createFilter:null,highlight:!0,openOnFocus:!0,shouldOpen:null,maxOptions:50,maxItems:null,hideSelected:null,duplicates:!1,addPrecedence:!1,selectOnTab:!1,preload:null,allowEmptyOption:!1,refreshThrottle:300,loadThrottle:300,loadingClass:"loading",dataAttr:null,optgroupField:"optgroup",valueField:"value",labelField:"text",disabledField:"disabled",optgroupLabelField:"label",optgroupValueField:"value",lockOptgroupOrder:!1,sortField:"$order",searchField:["text"],searchConjunction:"and",mode:null,wrapperClass:"ts-wrapper",controlClass:"ts-control",dropdownClass:"ts-dropdown",dropdownContentClass:"ts-dropdown-content",itemClass:"item",optionClass:"option",dropdownParent:null,controlInput:'',copyClassesToDropdown:!1,placeholder:null,hidePlaceholder:null,shouldLoad:function(n){return n.length>0},render:{}};function mr(n,e){var t=Object.assign({},Uo,e),i=t.dataAttr,r=t.labelField,o=t.valueField,s=t.disabledField,a=t.optgroupField,l=t.optgroupLabelField,c=t.optgroupValueField,u=n.tagName.toLowerCase(),d=n.getAttribute("placeholder")||n.getAttribute("data-placeholder");if(!d&&!t.allowEmptyOption){let v=n.querySelector('option[value=""]');v&&(d=v.textContent)}var p={placeholder:d,options:[],optgroups:[],items:[],maxItems:null},y=()=>{var v,w=p.options,T={},_=1;let S=0;var A=L=>{var I=Object.assign({},L.dataset),k=i&&I[i];return typeof k=="string"&&k.length&&(I=Object.assign(I,JSON.parse(k))),I},K=(L,I)=>{var k=qe(L.value);if(k!=null&&!(!k&&!t.allowEmptyOption)){if(T.hasOwnProperty(k)){if(I){var Y=T[k][a];Y?Array.isArray(Y)?Y.push(I):T[k][a]=[Y,I]:T[k][a]=I}}else{var $=A(L);$[r]=$[r]||L.textContent,$[o]=$[o]||k,$[s]=$[s]||L.disabled,$[a]=$[a]||I,$.$option=L,$.$order=$.$order||++S,T[k]=$,w.push($)}L.selected&&p.items.push(k)}},z=L=>{var I,k;k=A(L),k[l]=k[l]||L.getAttribute("label")||"",k[c]=k[c]||_++,k[s]=k[s]||L.disabled,k.$order=k.$order||++S,p.optgroups.push(k),I=k[c],ge(L.children,Y=>{K(Y,I)})};p.maxItems=n.hasAttribute("multiple")?null:1,ge(n.children,L=>{v=L.tagName.toLowerCase(),v==="optgroup"?z(L):v==="option"&&K(L)})},m=()=>{let v=n.getAttribute(i);if(v)p.options=JSON.parse(v),ge(p.options,T=>{p.items.push(T[o])});else{var w=n.value.trim()||"";if(!t.allowEmptyOption&&!w.length)return;let T=w.split(t.delimiter);ge(T,_=>{let S={};S[r]=_,S[o]=_,p.options.push(S)}),p.items=T}};return u==="select"?y():m(),Object.assign({},Uo,p,e)}var rl=0,_e=class extends Io(oi){constructor(e,t){super(),this.order=0,this.isOpen=!1,this.isDisabled=!1,this.isReadOnly=!1,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.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,rl++;var i,r=nt(e);if(r.tomselect)throw new Error("Tom Select already initialized on this element");r.tomselect=this;var o=window.getComputedStyle&&window.getComputedStyle(r,null);i=o.getPropertyValue("direction");let s=mr(r,t);this.settings=s,this.input=r,this.tabIndex=r.tabIndex||0,this.is_select_tag=r.tagName.toLowerCase()==="select",this.rtl=/rtl/i.test(i),this.inputId=fr(r,"tomselect-"+rl),this.isRequired=r.required,this.sifter=new ur(this.options,{diacritics:s.diacritics}),s.mode=s.mode||(s.maxItems===1?"single":"multi"),typeof s.hideSelected!="boolean"&&(s.hideSelected=s.mode==="multi"),typeof s.hidePlaceholder!="boolean"&&(s.hidePlaceholder=s.mode!=="multi");var a=s.createFilter;typeof a!="function"&&(typeof a=="string"&&(a=new RegExp(a)),a instanceof RegExp?s.createFilter=w=>a.test(w):s.createFilter=w=>this.settings.duplicates||!this.options[w]),this.initializePlugins(s.plugins),this.setupCallbacks(),this.setupTemplates();let l=nt("
"),c=nt("
"),u=this._render("dropdown"),d=nt('
'),p=this.input.getAttribute("class")||"",y=s.mode;var m;if(Fe(l,s.wrapperClass,p,y),Fe(c,s.controlClass),Nt(l,c),Fe(u,s.dropdownClass,y),s.copyClassesToDropdown&&Fe(u,p),Fe(d,s.dropdownContentClass),Nt(u,d),nt(s.dropdownParent||l).appendChild(u),jo(s.controlInput)){m=nt(s.controlInput);var v=["autocorrect","autocapitalize","autocomplete","spellcheck"];ge(v,w=>{r.getAttribute(w)&&le(m,{[w]:r.getAttribute(w)})}),m.tabIndex=-1,c.appendChild(m),this.focus_node=m}else s.controlInput?(m=nt(s.controlInput),this.focus_node=m):(m=nt(""),this.focus_node=c);this.wrapper=l,this.dropdown=u,this.dropdown_content=d,this.control=c,this.control_input=m,this.setup()}setup(){let e=this,t=e.settings,i=e.control_input,r=e.dropdown,o=e.dropdown_content,s=e.wrapper,a=e.control,l=e.input,c=e.focus_node,u={passive:!0},d=e.inputId+"-ts-dropdown";le(o,{id:d}),le(c,{role:"combobox","aria-haspopup":"listbox","aria-expanded":"false","aria-controls":d});let p=fr(c,e.inputId+"-ts-control"),y="label[for='"+Xa(e.inputId)+"']",m=document.querySelector(y),v=e.focus.bind(e);if(m){De(m,"click",v),le(m,{for:p});let _=fr(m,e.inputId+"-ts-label");le(c,{"aria-labelledby":_}),le(o,{"aria-labelledby":_})}if(s.style.width=l.style.width,e.plugins.names.length){let _="plugin-"+e.plugins.names.join(" plugin-");Fe([s,r],_)}(t.maxItems===null||t.maxItems>1)&&e.is_select_tag&&le(l,{multiple:"multiple"}),t.placeholder&&le(i,{placeholder:t.placeholder}),!t.splitOn&&t.delimiter&&(t.splitOn=new RegExp("\\s*"+Lt(t.delimiter)+"+\\s*")),t.load&&t.loadThrottle&&(t.load=Ga(t.load,t.loadThrottle)),De(r,"mousemove",()=>{e.ignoreHover=!1}),De(r,"mouseenter",_=>{var S=pr(_.target,"[data-selectable]",r);S&&e.onOptionHover(_,S)},{capture:!0}),De(r,"click",_=>{let S=pr(_.target,"[data-selectable]");S&&(e.onOptionSelect(_,S),fe(_,!0))}),De(a,"click",_=>{var S=pr(_.target,"[data-ts-item]",a);if(S&&e.onItemSelect(_,S)){fe(_,!0);return}i.value==""&&(e.onClick(),fe(_,!0))}),De(c,"keydown",_=>e.onKeyDown(_)),De(i,"keypress",_=>e.onKeyPress(_)),De(i,"input",_=>e.onInput(_)),De(c,"blur",_=>e.onBlur(_)),De(c,"focus",_=>e.onFocus(_)),De(i,"paste",_=>e.onPaste(_));let w=_=>{let S=_.composedPath()[0];if(!s.contains(S)&&!r.contains(S)){e.isFocused&&e.blur(),e.inputState();return}S==i&&e.isOpen?_.stopPropagation():fe(_,!0)},T=()=>{e.isOpen&&e.positionDropdown()};De(document,"mousedown",w),De(window,"scroll",T,u),De(window,"resize",T,u),this._destroy=()=>{document.removeEventListener("mousedown",w),window.removeEventListener("scroll",T),window.removeEventListener("resize",T),m&&m.removeEventListener("click",v)},this.revertSettings={innerHTML:l.innerHTML,tabIndex:l.tabIndex},l.tabIndex=-1,l.insertAdjacentElement("afterend",e.wrapper),e.sync(!1),t.items=[],delete t.optgroups,delete t.options,De(l,"invalid",()=>{e.isValid&&(e.isValid=!1,e.isInvalid=!0,e.refreshState())}),e.updateOriginalInput(),e.refreshItems(),e.close(!1),e.inputState(),e.isSetup=!0,l.disabled?e.disable():l.readOnly?e.setReadOnly(!0):e.enable(),e.on("change",this.onChange),Fe(l,"tomselected","ts-hidden-accessible"),e.trigger("initialize"),t.preload===!0&&e.preload()}setupOptions(e=[],t=[]){this.addOptions(e),ge(t,i=>{this.registerOptionGroup(i)})}setupTemplates(){var e=this,t=e.settings.labelField,i=e.settings.optgroupLabelField,r={optgroup:o=>{let s=document.createElement("div");return s.className="optgroup",s.appendChild(o.options),s},optgroup_header:(o,s)=>'
'+s(o[i])+"
",option:(o,s)=>"
"+s(o[t])+"
",item:(o,s)=>"
"+s(o[t])+"
",option_create:(o,s)=>'
Add '+s(o.input)+"
",no_results:()=>'
No results found
',loading:()=>'
',not_loading:()=>{},dropdown:()=>"
"};e.settings.render=Object.assign({},r,e.settings.render)}setupCallbacks(){var e,t,i={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(e in i)t=this.settings[i[e]],t&&this.on(e,t)}sync(e=!0){let t=this,i=e?mr(t.input,{delimiter:t.settings.delimiter}):t.settings;t.setupOptions(i.options,i.optgroups),t.setValue(i.items||[],!0),t.lastQuery=null}onClick(){var e=this;if(e.activeItems.length>0){e.clearActiveItems(),e.focus();return}e.isFocused&&e.isOpen?e.blur():e.focus()}onMouseDown(){}onChange(){hr(this.input,"input"),hr(this.input,"change")}onPaste(e){var t=this;if(t.isInputHidden||t.isLocked){fe(e);return}t.settings.splitOn&&setTimeout(()=>{var i=t.inputValue();if(i.match(t.settings.splitOn)){var r=i.trim().split(t.settings.splitOn);ge(r,o=>{qe(o)&&(this.options[o]?t.addItem(o):t.createItem(o))})}},0)}onKeyPress(e){var t=this;if(t.isLocked){fe(e);return}var i=String.fromCharCode(e.keyCode||e.which);if(t.settings.create&&t.settings.mode==="multi"&&i===t.settings.delimiter){t.createItem(),fe(e);return}}onKeyDown(e){var t=this;if(t.ignoreHover=!0,t.isLocked){e.keyCode!==9&&fe(e);return}switch(e.keyCode){case 65:if(Mt(fi,e)&&t.control_input.value==""){fe(e),t.selectAll();return}break;case 27:t.isOpen&&(fe(e,!0),t.close()),t.clearActiveItems();return;case 40:if(!t.isOpen&&t.hasOptions)t.open();else if(t.activeOption){let i=t.getAdjacent(t.activeOption,1);i&&t.setActiveOption(i)}fe(e);return;case 38:if(t.activeOption){let i=t.getAdjacent(t.activeOption,-1);i&&t.setActiveOption(i)}fe(e);return;case 13:t.canSelect(t.activeOption)?(t.onOptionSelect(e,t.activeOption),fe(e)):t.settings.create&&t.createItem()?fe(e):document.activeElement==t.control_input&&t.isOpen&&fe(e);return;case 37:t.advanceSelection(-1,e);return;case 39:t.advanceSelection(1,e);return;case 9:t.settings.selectOnTab&&(t.canSelect(t.activeOption)&&(t.onOptionSelect(e,t.activeOption),fe(e)),t.settings.create&&t.createItem()&&fe(e));return;case 8:case 46:t.deleteSelection(e);return}t.isInputHidden&&!Mt(fi,e)&&fe(e)}onInput(e){if(this.isLocked)return;let t=this.inputValue();if(this.lastValue!==t){if(this.lastValue=t,t==""){this._onInput();return}this.refreshTimeout&&window.clearTimeout(this.refreshTimeout),this.refreshTimeout=Ya(()=>{this.refreshTimeout=null,this._onInput()},this.settings.refreshThrottle)}}_onInput(){let e=this.lastValue;this.settings.shouldLoad.call(this,e)&&this.load(e),this.refreshOptions(),this.trigger("type",e)}onOptionHover(e,t){this.ignoreHover||this.setActiveOption(t,!1)}onFocus(e){var t=this,i=t.isFocused;if(t.isDisabled||t.isReadOnly){t.blur(),fe(e);return}t.ignoreFocus||(t.isFocused=!0,t.settings.preload==="focus"&&t.preload(),i||t.trigger("focus"),t.activeItems.length||(t.inputState(),t.refreshOptions(!!t.settings.openOnFocus)),t.refreshState())}onBlur(e){if(document.hasFocus()!==!1){var t=this;if(t.isFocused){t.isFocused=!1,t.ignoreFocus=!1;var i=()=>{t.close(),t.setActiveItem(),t.setCaret(t.items.length),t.trigger("blur")};t.settings.create&&t.settings.createOnBlur?t.createItem(null,i):i()}}}onOptionSelect(e,t){var i,r=this;t.parentElement&&t.parentElement.matches("[data-disabled]")||(t.classList.contains("create")?r.createItem(null,()=>{r.settings.closeAfterSelect&&r.close()}):(i=t.dataset.value,typeof i!="undefined"&&(r.lastQuery=null,r.addItem(i),r.settings.closeAfterSelect&&r.close(),!r.settings.hideSelected&&e.type&&/click/.test(e.type)&&r.setActiveOption(t))))}canSelect(e){return!!(this.isOpen&&e&&this.dropdown_content.contains(e))}onItemSelect(e,t){var i=this;return!i.isLocked&&i.settings.mode==="multi"?(fe(e),i.setActiveItem(t,e),!0):!1}canLoad(e){return!(!this.settings.load||this.loadedSearches.hasOwnProperty(e))}load(e){let t=this;if(!t.canLoad(e))return;Fe(t.wrapper,t.settings.loadingClass),t.loading++;let i=t.loadCallback.bind(t);t.settings.load.call(t,e,i)}loadCallback(e,t){let i=this;i.loading=Math.max(i.loading-1,0),i.lastQuery=null,i.clearActiveOption(),i.setupOptions(e,t),i.refreshOptions(i.isFocused&&!i.isInputHidden),i.loading||gt(i.wrapper,i.settings.loadingClass),i.trigger("load",e,t)}preload(){var e=this.wrapper.classList;e.contains("preloaded")||(e.add("preloaded"),this.load(""))}setTextboxValue(e=""){var t=this.control_input,i=t.value!==e;i&&(t.value=e,hr(t,"update"),this.lastValue=e)}getValue(){return this.is_select_tag&&this.input.hasAttribute("multiple")?this.items:this.items.join(this.settings.delimiter)}setValue(e,t){var i=t?[]:["change"];Vo(this,i,()=>{this.clear(t),this.addItems(e,t)})}setMaxItems(e){e===0&&(e=null),this.settings.maxItems=e,this.refreshState()}setActiveItem(e,t){var i=this,r,o,s,a,l,c;if(i.settings.mode!=="single"){if(!e){i.clearActiveItems(),i.isFocused&&i.inputState();return}if(r=t&&t.type.toLowerCase(),r==="click"&&Mt("shiftKey",t)&&i.activeItems.length){for(c=i.getLastActive(),s=Array.prototype.indexOf.call(i.control.children,c),a=Array.prototype.indexOf.call(i.control.children,e),s>a&&(l=s,s=a,a=l),o=s;o<=a;o++)e=i.control.children[o],i.activeItems.indexOf(e)===-1&&i.setActiveItemClass(e);fe(t)}else r==="click"&&Mt(fi,t)||r==="keydown"&&Mt("shiftKey",t)?e.classList.contains("active")?i.removeActiveItem(e):i.setActiveItemClass(e):(i.clearActiveItems(),i.setActiveItemClass(e));i.inputState(),i.isFocused||i.focus()}}setActiveItemClass(e){let t=this,i=t.control.querySelector(".last-active");i&>(i,"last-active"),Fe(e,"active last-active"),t.trigger("item_select",e),t.activeItems.indexOf(e)==-1&&t.activeItems.push(e)}removeActiveItem(e){var t=this.activeItems.indexOf(e);this.activeItems.splice(t,1),gt(e,"active")}clearActiveItems(){gt(this.activeItems,"active"),this.activeItems=[]}setActiveOption(e,t=!0){e!==this.activeOption&&(this.clearActiveOption(),e&&(this.activeOption=e,le(this.focus_node,{"aria-activedescendant":e.getAttribute("id")}),le(e,{"aria-selected":"true"}),Fe(e,"active"),t&&this.scrollToOption(e)))}scrollToOption(e,t){if(!e)return;let i=this.dropdown_content,r=i.clientHeight,o=i.scrollTop||0,s=e.offsetHeight,a=e.getBoundingClientRect().top-i.getBoundingClientRect().top+o;a+s>r+o?this.scroll(a-r+s,t):a{e.setActiveItemClass(i)}))}inputState(){var e=this;e.control.contains(e.control_input)&&(le(e.control_input,{placeholder:e.settings.placeholder}),e.activeItems.length>0||!e.isFocused&&e.settings.hidePlaceholder&&e.items.length>0?(e.setTextboxValue(),e.isInputHidden=!0):(e.settings.hidePlaceholder&&e.items.length>0&&le(e.control_input,{placeholder:""}),e.isInputHidden=!1),e.wrapper.classList.toggle("input-hidden",e.isInputHidden))}inputValue(){return this.control_input.value.trim()}focus(){var e=this;e.isDisabled||e.isReadOnly||(e.ignoreFocus=!0,e.control_input.offsetWidth?e.control_input.focus():e.focus_node.focus(),setTimeout(()=>{e.ignoreFocus=!1,e.onFocus()},0))}blur(){this.focus_node.blur(),this.onBlur()}getScoreFunction(e){return this.sifter.getScoreFunction(e,this.getSearchOptions())}getSearchOptions(){var e=this.settings,t=e.sortField;return typeof e.sortField=="string"&&(t=[{field:e.sortField}]),{fields:e.searchField,conjunction:e.searchConjunction,sort:t,nesting:e.nesting}}search(e){var t,i,r=this,o=this.getSearchOptions();if(r.settings.score&&(i=r.settings.score.call(r,e),typeof i!="function"))throw new Error('Tom Select "score" setting must be a function that returns a function');return e!==r.lastQuery?(r.lastQuery=e,t=r.sifter.search(e,Object.assign(o,{score:i})),r.currentResults=t):t=Object.assign({},r.currentResults),r.settings.hideSelected&&(t.items=t.items.filter(s=>{let a=qe(s.id);return!(a&&r.items.indexOf(a)!==-1)})),t}refreshOptions(e=!0){var t,i,r,o,s,a,l,c,u,d;let p={},y=[];var m=this,v=m.inputValue();let w=v===m.lastQuery||v==""&&m.lastQuery==null;var T=m.search(v),_=null,S=m.settings.shouldOpen||!1,A=m.dropdown_content;w&&(_=m.activeOption,_&&(u=_.closest("[data-group]"))),o=T.items.length,typeof m.settings.maxOptions=="number"&&(o=Math.min(o,m.settings.maxOptions)),o>0&&(S=!0);let K=(L,I)=>{let k=p[L];if(k!==void 0){let $=y[k];if($!==void 0)return[k,$.fragment]}let Y=document.createDocumentFragment();return k=y.length,y.push({fragment:Y,order:I,optgroup:L}),[k,Y]};for(t=0;t0&&($=$.cloneNode(!0),le($,{id:k.$id+"-clone-"+i,"aria-selected":null}),$.classList.add("ts-cloned"),gt($,"active"),m.activeOption&&m.activeOption.dataset.value==I&&u&&u.dataset.group===s.toString()&&(_=$)),Ce.appendChild($),s!=""&&(p[s]=Te)}}m.settings.lockOptgroupOrder&&y.sort((L,I)=>L.order-I.order),l=document.createDocumentFragment(),ge(y,L=>{let I=L.fragment,k=L.optgroup;if(!I||!I.children.length)return;let Y=m.optgroups[k];if(Y!==void 0){let $=document.createDocumentFragment(),ie=m.render("optgroup_header",Y);Nt($,ie),Nt($,I);let J=m.render("optgroup",{group:Y,options:$});Nt(l,J)}else Nt(l,I)}),A.innerHTML="",Nt(A,l),m.settings.highlight&&(tl(A),T.query.length&&T.tokens.length&&ge(T.tokens,L=>{el(A,L.regex)}));var z=L=>{let I=m.render(L,{input:v});return I&&(S=!0,A.insertBefore(I,A.firstChild)),I};if(m.loading?z("loading"):m.settings.shouldLoad.call(m,v)?T.items.length===0&&z("no_results"):z("not_loading"),c=m.canCreate(v),c&&(d=z("option_create")),m.hasOptions=T.items.length>0||c,S){if(T.items.length>0){if(!_&&m.settings.mode==="single"&&m.items[0]!=null&&(_=m.getOption(m.items[0])),!A.contains(_)){let L=0;d&&!m.settings.addPrecedence&&(L=1),_=m.selectable()[L]}}else d&&(_=d);e&&!m.isOpen&&(m.open(),m.scrollToOption(_,"auto")),m.setActiveOption(_)}else m.clearActiveOption(),e&&m.isOpen&&m.close(!1)}selectable(){return this.dropdown_content.querySelectorAll("[data-selectable]")}addOption(e,t=!1){let i=this;if(Array.isArray(e))return i.addOptions(e,t),!1;let r=qe(e[i.settings.valueField]);return r===null||i.options.hasOwnProperty(r)?!1:(e.$order=e.$order||++i.order,e.$id=i.inputId+"-opt-"+e.$order,i.options[r]=e,i.lastQuery=null,t&&(i.userOptions[r]=t,i.trigger("option_add",r,e)),r)}addOptions(e,t=!1){ge(e,i=>{this.addOption(i,t)})}registerOption(e){return this.addOption(e)}registerOptionGroup(e){var t=qe(e[this.settings.optgroupValueField]);return t===null?!1:(e.$order=e.$order||++this.order,this.optgroups[t]=e,t)}addOptionGroup(e,t){var i;t[this.settings.optgroupValueField]=e,(i=this.registerOptionGroup(t))&&this.trigger("optgroup_add",i,t)}removeOptionGroup(e){this.optgroups.hasOwnProperty(e)&&(delete this.optgroups[e],this.clearCache(),this.trigger("optgroup_remove",e))}clearOptionGroups(){this.optgroups={},this.clearCache(),this.trigger("optgroup_clear")}updateOption(e,t){let i=this;var r,o;let s=qe(e),a=qe(t[i.settings.valueField]);if(s===null)return;let l=i.options[s];if(l==null)return;if(typeof a!="string")throw new Error("Value must be set in option data");let c=i.getOption(s),u=i.getItem(s);if(t.$order=t.$order||l.$order,delete i.options[s],i.uncacheValue(a),i.options[a]=t,c){if(i.dropdown_content.contains(c)){let d=i._render("option",t);di(c,d),i.activeOption===c&&i.setActiveOption(d)}c.remove()}u&&(o=i.items.indexOf(s),o!==-1&&i.items.splice(o,1,a),r=i._render("item",t),u.classList.contains("active")&&Fe(r,"active"),di(u,r)),i.lastQuery=null}removeOption(e,t){let i=this;e=ci(e),i.uncacheValue(e),delete i.userOptions[e],delete i.options[e],i.lastQuery=null,i.trigger("option_remove",e),i.removeItem(e,t)}clearOptions(e){let t=(e||this.clearFilter).bind(this);this.loadedSearches={},this.userOptions={},this.clearCache();let i={};ge(this.options,(r,o)=>{t(r,o)&&(i[o]=r)}),this.options=this.sifter.items=i,this.lastQuery=null,this.trigger("option_clear")}clearFilter(e,t){return this.items.indexOf(t)>=0}getOption(e,t=!1){let i=qe(e);if(i===null)return null;let r=this.options[i];if(r!=null){if(r.$div)return r.$div;if(t)return this._render("option",r)}return null}getAdjacent(e,t,i="option"){var r=this,o;if(!e)return null;i=="item"?o=r.controlChildren():o=r.dropdown_content.querySelectorAll("[data-selectable]");for(let s=0;s0?o[s+1]:o[s-1];return null}getItem(e){if(typeof e=="object")return e;var t=qe(e);return t!==null?this.control.querySelector(`[data-value="${zo(t)}"]`):null}addItems(e,t){var i=this,r=Array.isArray(e)?e:[e];r=r.filter(s=>i.items.indexOf(s)===-1);let o=r[r.length-1];r.forEach(s=>{i.isPending=s!==o,i.addItem(s,t)})}addItem(e,t){var i=t?[]:["change","dropdown_close"];Vo(this,i,()=>{var r,o;let s=this,a=s.settings.mode,l=qe(e);if(!(l&&s.items.indexOf(l)!==-1&&(a==="single"&&s.close(),a==="single"||!s.settings.duplicates))&&!(l===null||!s.options.hasOwnProperty(l))&&(a==="single"&&s.clear(t),!(a==="multi"&&s.isFull()))){if(r=s._render("item",s.options[l]),s.control.contains(r)&&(r=r.cloneNode(!0)),o=s.isFull(),s.items.splice(s.caretPos,0,l),s.insertAtCaret(r),s.isSetup){if(!s.isPending&&s.settings.hideSelected){let c=s.getOption(l),u=s.getAdjacent(c,1);u&&s.setActiveOption(u)}!s.isPending&&!s.settings.closeAfterSelect&&s.refreshOptions(s.isFocused&&a!=="single"),s.settings.closeAfterSelect!=!1&&s.isFull()?s.close():s.isPending||s.positionDropdown(),s.trigger("item_add",l,r),s.isPending||s.updateOriginalInput({silent:t})}(!s.isPending||!o&&s.isFull())&&(s.inputState(),s.refreshState())}})}removeItem(e=null,t){let i=this;if(e=i.getItem(e),!e)return;var r,o;let s=e.dataset.value;r=qo(e),e.remove(),e.classList.contains("active")&&(o=i.activeItems.indexOf(e),i.activeItems.splice(o,1),gt(e,"active")),i.items.splice(r,1),i.lastQuery=null,!i.settings.persist&&i.userOptions.hasOwnProperty(s)&&i.removeOption(s,t),r{}){arguments.length===3&&(t=arguments[2]),typeof t!="function"&&(t=()=>{});var i=this,r=i.caretPos,o;if(e=e||i.inputValue(),!i.canCreate(e))return t(),!1;i.lock();var s=!1,a=l=>{if(i.unlock(),!l||typeof l!="object")return t();var c=qe(l[i.settings.valueField]);if(typeof c!="string")return t();i.setTextboxValue(),i.addOption(l,!0),i.setCaret(r),i.addItem(c),t(l),s=!0};return typeof i.settings.create=="function"?o=i.settings.create.call(this,e,a):o={[i.settings.labelField]:e,[i.settings.valueField]:e},s||a(o),!0}refreshItems(){var e=this;e.lastQuery=null,e.isSetup&&e.addItems(e.items),e.updateOriginalInput(),e.refreshState()}refreshState(){let e=this;e.refreshValidityState();let t=e.isFull(),i=e.isLocked;e.wrapper.classList.toggle("rtl",e.rtl);let r=e.wrapper.classList;r.toggle("focus",e.isFocused),r.toggle("disabled",e.isDisabled),r.toggle("readonly",e.isReadOnly),r.toggle("required",e.isRequired),r.toggle("invalid",!e.isValid),r.toggle("locked",i),r.toggle("full",t),r.toggle("input-active",e.isFocused&&!e.isInputHidden),r.toggle("dropdown-active",e.isOpen),r.toggle("has-options",Za(e.options)),r.toggle("has-items",e.items.length>0)}refreshValidityState(){var e=this;e.input.validity&&(e.isValid=e.input.validity.valid,e.isInvalid=!e.isValid)}isFull(){return this.settings.maxItems!==null&&this.items.length>=this.settings.maxItems}updateOriginalInput(e={}){let t=this;var i,r;let o=t.input.querySelector('option[value=""]');if(t.is_select_tag){let l=function(c,u,d){return c||(c=nt('")),c!=o&&t.input.append(c),s.push(c),(c!=o||a>0)&&(c.selected=!0),c},s=[],a=t.input.querySelectorAll("option:checked").length;t.input.querySelectorAll("option:checked").forEach(c=>{c.selected=!1}),t.items.length==0&&t.settings.mode=="single"?l(o,"",""):t.items.forEach(c=>{if(i=t.options[c],r=i[t.settings.labelField]||"",s.includes(i.$option)){let u=t.input.querySelector(`option[value="${zo(c)}"]:not(:checked)`);l(u,c,r)}else i.$option=l(i.$option,c,r)})}else t.input.value=t.getValue();t.isSetup&&(e.silent||t.trigger("change",t.getValue()))}open(){var e=this;e.isLocked||e.isOpen||e.settings.mode==="multi"&&e.isFull()||(e.isOpen=!0,le(e.focus_node,{"aria-expanded":"true"}),e.refreshState(),ui(e.dropdown,{visibility:"hidden",display:"block"}),e.positionDropdown(),ui(e.dropdown,{visibility:"visible",display:"block"}),e.focus(),e.trigger("dropdown_open",e.dropdown))}close(e=!0){var t=this,i=t.isOpen;e&&(t.setTextboxValue(),t.settings.mode==="single"&&t.items.length&&t.inputState()),t.isOpen=!1,le(t.focus_node,{"aria-expanded":"false"}),ui(t.dropdown,{display:"none"}),t.settings.hideSelected&&t.clearActiveOption(),t.refreshState(),i&&t.trigger("dropdown_close",t.dropdown)}positionDropdown(){if(this.settings.dropdownParent==="body"){var e=this.control,t=e.getBoundingClientRect(),i=e.offsetHeight+t.top+window.scrollY,r=t.left+window.scrollX;ui(this.dropdown,{width:t.width+"px",top:i+"px",left:r+"px"})}}clear(e){var t=this;if(t.items.length){var i=t.controlChildren();ge(i,r=>{t.removeItem(r,!0)}),t.inputState(),e||t.updateOriginalInput(),t.trigger("clear")}}insertAtCaret(e){let t=this,i=t.caretPos,r=t.control;r.insertBefore(e,r.children[i]||null),t.setCaret(i+1)}deleteSelection(e){var t,i,r,o,s=this;t=e&&e.keyCode===8?-1:1,i=Ka(s.control_input);let a=[];if(s.activeItems.length)o=Wo(s.activeItems,t),r=qo(o),t>0&&r++,ge(s.activeItems,l=>a.push(l));else if((s.isFocused||s.settings.mode==="single")&&s.items.length){let l=s.controlChildren(),c;t<0&&i.start===0&&i.length===0?c=l[s.caretPos-1]:t>0&&i.start===s.inputValue().length&&(c=l[s.caretPos]),c!==void 0&&a.push(c)}if(!s.shouldDelete(a,e))return!1;for(fe(e,!0),typeof r!="undefined"&&s.setCaret(r);a.length;)s.removeItem(a.pop());return s.inputState(),s.positionDropdown(),s.refreshOptions(!1),!0}shouldDelete(e,t){let i=e.map(r=>r.dataset.value);return!(!i.length||typeof this.settings.onDelete=="function"&&this.settings.onDelete(i,t)===!1)}advanceSelection(e,t){var i,r,o=this;o.rtl&&(e*=-1),!o.inputValue().length&&(Mt(fi,t)||Mt("shiftKey",t)?(i=o.getLastActive(e),i?i.classList.contains("active")?r=o.getAdjacent(i,e,"item"):r=i:e>0?r=o.control_input.nextElementSibling:r=o.control_input.previousElementSibling,r&&(r.classList.contains("active")&&o.removeActiveItem(i),o.setActiveItemClass(r))):o.moveCaret(e))}moveCaret(e){}getLastActive(e){let t=this.control.querySelector(".last-active");if(t)return t;var i=this.control.querySelectorAll(".active");if(i)return Wo(i,e)}setCaret(e){this.caretPos=this.items.length}controlChildren(){return Array.from(this.control.querySelectorAll("[data-ts-item]"))}lock(){this.setLocked(!0)}unlock(){this.setLocked(!1)}setLocked(e=this.isReadOnly||this.isDisabled){this.isLocked=e,this.refreshState()}disable(){this.setDisabled(!0),this.close()}enable(){this.setDisabled(!1)}setDisabled(e){this.focus_node.tabIndex=e?-1:this.tabIndex,this.isDisabled=e,this.input.disabled=e,this.control_input.disabled=e,this.setLocked()}setReadOnly(e){this.isReadOnly=e,this.input.readOnly=e,this.control_input.readOnly=e,this.setLocked()}destroy(){var e=this,t=e.revertSettings;e.trigger("destroy"),e.off(),e.wrapper.remove(),e.dropdown.remove(),e.input.innerHTML=t.innerHTML,e.input.tabIndex=t.tabIndex,gt(e.input,"tomselected","ts-hidden-accessible"),e._destroy(),delete e.input.tomselect}render(e,t){var i,r;let o=this;if(typeof this.settings.render[e]!="function"||(r=o.settings.render[e].call(this,t,dr),!r))return null;if(r=nt(r),e==="option"||e==="option_create"?t[o.settings.disabledField]?le(r,{"aria-disabled":"true"}):le(r,{"data-selectable":""}):e==="optgroup"&&(i=t.group[o.settings.optgroupValueField],le(r,{"data-group":i}),t.group[o.settings.disabledField]&&le(r,{"data-disabled":""})),e==="option"||e==="item"){let s=ci(t[o.settings.valueField]);le(r,{"data-value":s}),e==="item"?(Fe(r,o.settings.itemClass),le(r,{"data-ts-item":""})):(Fe(r,o.settings.optionClass),le(r,{role:"option",id:t.$id}),t.$div=r,o.options[s]=t)}return r}_render(e,t){let i=this.render(e,t);if(i==null)throw"HTMLElement expected";return i}clearCache(){ge(this.options,e=>{e.$div&&(e.$div.remove(),delete e.$div)})}uncacheValue(e){let t=this.getOption(e);t&&t.remove()}canCreate(e){return this.settings.create&&e.length>0&&this.settings.createFilter.call(this,e)}hook(e,t,i){var r=this,o=r[t];r[t]=function(){var s,a;return e==="after"&&(s=o.apply(r,arguments)),a=i.apply(r,arguments),e==="instead"?a:(e==="before"&&(s=o.apply(r,arguments)),s)}}};var Tm=(n,e,t,i)=>{n.addEventListener(e,t,i)};function ol(){Tm(this.input,"change",()=>{this.sync()})}var Cm=n=>typeof n=="undefined"||n===null?null:Am(n),Am=n=>typeof n=="boolean"?n?"1":"0":n+"",sl=(n,e=!1)=>{n&&(n.preventDefault(),e&&n.stopPropagation())},Sm=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(Dm(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},Dm=n=>typeof n=="string"&&n.indexOf("<")>-1;function al(n){var e=this,t=e.onOptionSelect;e.settings.hideSelected=!1;let i=Object.assign({className:"tomselect-checkbox",checkedClassNames:void 0,uncheckedClassNames:void 0},n);var r=function(a,l){l?(a.checked=!0,i.uncheckedClassNames&&a.classList.remove(...i.uncheckedClassNames),i.checkedClassNames&&a.classList.add(...i.checkedClassNames)):(a.checked=!1,i.checkedClassNames&&a.classList.remove(...i.checkedClassNames),i.uncheckedClassNames&&a.classList.add(...i.uncheckedClassNames))},o=function(a){setTimeout(()=>{var l=a.querySelector("input."+i.className);l instanceof HTMLInputElement&&r(l,a.classList.contains("selected"))},1)};e.hook("after","setupTemplates",()=>{var s=e.settings.render.option;e.settings.render.option=(a,l)=>{var c=Sm(s.call(e,a,l)),u=document.createElement("input");i.className&&u.classList.add(i.className),u.addEventListener("click",function(p){sl(p)}),u.type="checkbox";let d=Cm(a[e.settings.valueField]);return r(u,!!(d&&e.items.indexOf(d)>-1)),c.prepend(u),c}}),e.on("item_remove",s=>{var a=e.getOption(s);a&&(a.classList.remove("selected"),o(a))}),e.on("item_add",s=>{var a=e.getOption(s);a&&o(a)}),e.hook("instead","onOptionSelect",(s,a)=>{if(a.classList.contains("selected")){a.classList.remove("selected"),e.removeItem(a.dataset.value),e.refreshOptions(),sl(s,!0);return}t.call(e,s,a),o(a)})}var Om=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(Lm(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},Lm=n=>typeof n=="string"&&n.indexOf("<")>-1;function ll(n){let e=this,t=Object.assign({className:"clear-button",title:"Clear All",html:i=>`
`},n);e.on("initialize",()=>{var i=Om(t.html(t));i.addEventListener("click",r=>{e.isLocked||(e.clear(),e.settings.mode==="single"&&e.settings.allowEmptyOption&&e.addItem(""),r.preventDefault(),r.stopPropagation())}),e.control.appendChild(i)})}var Mm=(n,e=!1)=>{n&&(n.preventDefault(),e&&n.stopPropagation())},Dn=(n,e,t,i)=>{n.addEventListener(e,t,i)},Nm=(n,e)=>{if(Array.isArray(n))n.forEach(e);else for(var t in n)n.hasOwnProperty(t)&&e(n[t],t)},km=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(Hm(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},Hm=n=>typeof n=="string"&&n.indexOf("<")>-1,Rm=(n,e)=>{Nm(e,(t,i)=>{t==null?n.removeAttribute(i):n.setAttribute(i,""+t)})},Im=(n,e)=>{var t;(t=n.parentNode)==null||t.insertBefore(e,n.nextSibling)},Pm=(n,e)=>{var t;(t=n.parentNode)==null||t.insertBefore(e,n)},Fm=(n,e)=>{do{var t;if(e=(t=e)==null?void 0:t.previousElementSibling,n==e)return!0}while(e&&e.previousElementSibling);return!1};function cl(){var n=this;if(n.settings.mode!=="multi")return;var e=n.lock,t=n.unlock;let i=!0,r;n.hook("after","setupTemplates",()=>{var o=n.settings.render.item;n.settings.render.item=(s,a)=>{let l=km(o.call(n,s,a));Rm(l,{draggable:"true"});let c=v=>{i||Mm(v),v.stopPropagation()},u=v=>{r=l,setTimeout(()=>{l.classList.add("ts-dragging")},0)},d=v=>{v.preventDefault(),l.classList.add("ts-drag-over"),y(l,r)},p=()=>{l.classList.remove("ts-drag-over")},y=(v,w)=>{w!==void 0&&(Fm(w,l)?Im(v,w):Pm(v,w))},m=()=>{var v;document.querySelectorAll(".ts-drag-over").forEach(T=>T.classList.remove("ts-drag-over")),(v=r)==null||v.classList.remove("ts-dragging"),r=void 0;var w=[];n.control.querySelectorAll("[data-value]").forEach(T=>{if(T.dataset.value){let _=T.dataset.value;_&&w.push(_)}}),n.setValue(w)};return Dn(l,"mousedown",c),Dn(l,"dragstart",u),Dn(l,"dragenter",d),Dn(l,"dragover",d),Dn(l,"dragleave",p),Dn(l,"dragend",m),l}}),n.hook("instead","lock",()=>(i=!1,e.call(n))),n.hook("instead","unlock",()=>(i=!0,t.call(n)))}var $m=(n,e=!1)=>{n&&(n.preventDefault(),e&&n.stopPropagation())},Bm=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(Vm(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},Vm=n=>typeof n=="string"&&n.indexOf("<")>-1;function ul(n){let e=this,t=Object.assign({title:"Untitled",headerClass:"dropdown-header",titleRowClass:"dropdown-header-title",labelClass:"dropdown-header-label",closeClass:"dropdown-header-close",html:i=>'
'+i.title+'×
'},n);e.on("initialize",()=>{var i=Bm(t.html(t)),r=i.querySelector("."+t.closeClass);r&&r.addEventListener("click",o=>{$m(o,!0),e.close()}),e.dropdown.insertBefore(i,e.dropdown.firstChild)})}var zm=(n,e)=>{if(Array.isArray(n))n.forEach(e);else for(var t in n)n.hasOwnProperty(t)&&e(n[t],t)},jm=(n,...e)=>{var t=Wm(e);n=qm(n),n.map(i=>{t.map(r=>{i.classList.remove(r)})})},Wm=n=>{var e=[];return zm(n,t=>{typeof t=="string"&&(t=t.trim().split(/[\t\n\f\r\s]/)),Array.isArray(t)&&(e=e.concat(t))}),e.filter(Boolean)},qm=n=>(Array.isArray(n)||(n=[n]),n),Um=(n,e)=>{if(!n)return-1;e=e||n.nodeName;for(var t=0;n=n.previousElementSibling;)n.matches(e)&&t++;return t};function dl(){var n=this;n.hook("instead","setCaret",e=>{n.settings.mode==="single"||!n.control.contains(n.control_input)?e=n.items.length:(e=Math.max(0,Math.min(n.items.length,e)),e!=n.caretPos&&!n.isPending&&n.controlChildren().forEach((t,i)=>{i{if(!n.isFocused)return;let t=n.getLastActive(e);if(t){let i=Um(t);n.setCaret(e>0?i+1:i),n.setActiveItem(),jm(t,"last-active")}else n.setCaret(n.caretPos+e)})}var Ym=(n,e=!1)=>{n&&(n.preventDefault(),e&&n.stopPropagation())},Gm=(n,e,t,i)=>{n.addEventListener(e,t,i)},Km=(n,e)=>{if(Array.isArray(n))n.forEach(e);else for(var t in n)n.hasOwnProperty(t)&&e(n[t],t)},fl=n=>{if(n.jquery)return n[0];if(n instanceof HTMLElement)return n;if(Xm(n)){var e=document.createElement("template");return e.innerHTML=n.trim(),e.content.firstChild}return document.querySelector(n)},Xm=n=>typeof n=="string"&&n.indexOf("<")>-1,Qm=(n,...e)=>{var t=Jm(e);n=Zm(n),n.map(i=>{t.map(r=>{i.classList.add(r)})})},Jm=n=>{var e=[];return Km(n,t=>{typeof t=="string"&&(t=t.trim().split(/[\t\n\f\r\s]/)),Array.isArray(t)&&(e=e.concat(t))}),e.filter(Boolean)},Zm=n=>(Array.isArray(n)||(n=[n]),n);function hl(){let n=this;n.settings.shouldOpen=!0,n.hook("before","setup",()=>{n.focus_node=n.control,Qm(n.control_input,"dropdown-input");let e=fl('