diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/01-feature_request.yaml
similarity index 83%
rename from .github/ISSUE_TEMPLATE/feature_request.yaml
rename to .github/ISSUE_TEMPLATE/01-feature_request.yaml
index 2c6384b27a2..f0138a22ab0 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/01-feature_request.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v4.0.6
+ placeholder: v4.1.4
validations:
required: true
- type: dropdown
@@ -24,6 +24,21 @@ body:
- Data model extension
- New functionality
- Change to existing functionality
+ - Other
+ validations:
+ required: true
+ - type: dropdown
+ attributes:
+ label: Triage priority
+ description: >
+ Issue triage may be prioritized in some cases. Select whichever of the following
+ conditions applies, if any.
+ options:
+ - I volunteer to perform this work (if approved)
+ - I'm a NetBox Labs customer
+ - This is a very minor change
+ - N/A
+ default: 3
validations:
required: true
- type: textarea
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/02-bug_report.yaml
similarity index 84%
rename from .github/ISSUE_TEMPLATE/bug_report.yaml
rename to .github/ISSUE_TEMPLATE/02-bug_report.yaml
index 3af0fa7405a..a8a2cc45b87 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/02-bug_report.yaml
@@ -22,11 +22,25 @@ body:
- Self-hosted
validations:
required: true
+ - type: dropdown
+ attributes:
+ label: Triage priority
+ description: >
+ Issue triage may be prioritized in some cases. Select whichever of the following
+ conditions applies, if any.
+ options:
+ - I volunteer to perform this work (if approved)
+ - I'm a NetBox Labs customer
+ - This is preventing me from using NetBox
+ - N/A
+ default: 3
+ validations:
+ required: true
- type: input
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
- placeholder: v4.0.6
+ placeholder: v4.1.4
validations:
required: true
- type: dropdown
diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/03-documentation_change.yaml
similarity index 100%
rename from .github/ISSUE_TEMPLATE/documentation_change.yaml
rename to .github/ISSUE_TEMPLATE/03-documentation_change.yaml
diff --git a/.github/ISSUE_TEMPLATE/translation.yaml b/.github/ISSUE_TEMPLATE/04-translation.yaml
similarity index 100%
rename from .github/ISSUE_TEMPLATE/translation.yaml
rename to .github/ISSUE_TEMPLATE/04-translation.yaml
diff --git a/.github/ISSUE_TEMPLATE/housekeeping.yaml b/.github/ISSUE_TEMPLATE/05-housekeeping.yaml
similarity index 100%
rename from .github/ISSUE_TEMPLATE/housekeeping.yaml
rename to .github/ISSUE_TEMPLATE/05-housekeeping.yaml
diff --git a/.github/ISSUE_TEMPLATE/deprecation.yaml b/.github/ISSUE_TEMPLATE/06-deprecation.yaml
similarity index 100%
rename from .github/ISSUE_TEMPLATE/deprecation.yaml
rename to .github/ISSUE_TEMPLATE/06-deprecation.yaml
diff --git a/.github/workflows/auto-assign-issue.yml b/.github/workflows/auto-assign-issue.yml
deleted file mode 100644
index 309f79800ec..00000000000
--- a/.github/workflows/auto-assign-issue.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-# auto-assign-issue (https://github.com/marketplace/actions/auto-assign-issue)
-name: Issue assignment
-
-on:
- issues:
- types: [opened]
-
-permissions:
- issues: write
-
-jobs:
- auto-assign:
- runs-on: ubuntu-latest
- steps:
- - uses: pozil/auto-assign-issue@v2
- if: "contains(github.event.issue.labels.*.name, 'status: needs triage')"
- with:
- # Weighted assignments
- assignees: arthanson:3, jeffgdotorg:3, jeremystretch:3, DanSheps
- numOfAssignee: 1
- abortIfPreviousAssignees: true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b4be037427f..d77da90e3f5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,7 +23,7 @@ jobs:
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
- node-version: ['18.x']
+ node-version: ['20.x']
services:
redis:
image: redis
@@ -73,7 +73,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- pip install pycodestyle coverage tblib
+ pip install ruff coverage tblib
- name: Build documentation
run: mkdocs build
@@ -85,7 +85,7 @@ jobs:
run: python netbox/manage.py makemigrations --check
- name: Check PEP8 compliance
- run: pycodestyle --ignore=W504,E501 --exclude=node_modules netbox/
+ run: ruff check netbox/
- name: Check UI ESLint, TypeScript, and Prettier Compliance
run: yarn --cwd netbox/project-static validate
diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml
index b02ffdacd4d..1e0e193df17 100644
--- a/.github/workflows/close-stale-issues.yml
+++ b/.github/workflows/close-stale-issues.yml
@@ -18,7 +18,7 @@ jobs:
- uses: actions/stale@v9
with:
# General parameters
- operations-per-run: 100
+ operations-per-run: 200
remove-stale-when-updated: false
# Issue parameters
@@ -43,8 +43,9 @@ jobs:
# Pull request parameters
close-pr-message: >
This PR has been automatically closed due to lack of activity.
- days-before-pr-stale: 15
+ days-before-pr-stale: 30
days-before-pr-close: 15
+ exempt-pr-labels: 'status: blocked'
stale-pr-label: 'pending closure'
stale-pr-message: >
This PR has been automatically marked as stale because it has not had
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000000..09f935b61f7
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,44 @@
+repos:
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.6.9
+ hooks:
+ - id: ruff
+ name: "Ruff linter"
+ args: [ netbox/ ]
+- repo: local
+ hooks:
+ - id: django-check
+ name: "Django system check"
+ description: "Run Django's internal check for common problems"
+ entry: python netbox/manage.py check
+ language: system
+ pass_filenames: false
+ types: [python]
+ - id: django-makemigrations
+ name: "Django migrations check"
+ description: "Check for any missing Django migrations"
+ entry: python netbox/manage.py makemigrations --check
+ language: system
+ pass_filenames: false
+ types: [python]
+ - id: mkdocs-build
+ name: "Build documentation"
+ description: "Build the documentation with mkdocs"
+ files: 'docs/'
+ entry: mkdocs build
+ language: system
+ pass_filenames: false
+ - id: yarn-validate
+ name: "Yarn validate"
+ description: "Check UI ESLint, TypeScript, and Prettier compliance"
+ files: 'netbox/project-static/'
+ entry: yarn --cwd netbox/project-static validate
+ language: system
+ pass_filenames: false
+ - id: verify-bundles
+ name: "Verify static asset bundles"
+ description: "Ensure that any modified static assets have been compiled"
+ files: 'netbox/project-static/'
+ entry: scripts/verify-bundles.sh
+ language: system
+ pass_filenames: false
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f94893021ea..a760b83711e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -40,7 +40,7 @@ NetBox users are welcome to participate in either role, on stage or in the crowd
* First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) of NetBox. If you're running an older version, it's likely that the bug has already been fixed.
-* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
+* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the bottom left corner of the issue and add a thumbs up ( :thumbsup: ). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
* If you can't find any existing issues (open or closed) that seem to match yours, you're welcome to [submit a new bug report](https://github.com/netbox-community/netbox/issues/new?label=type%3A+bug&template=bug_report.yaml). Be sure to complete the entire report template, including detailed steps that someone triaging your issue can follow to confirm the reported behavior. (If we're not able to replicate the bug based on the information provided, we'll ask for additional detail.)
@@ -56,7 +56,9 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
## :bulb: Feature Requests
-* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the feature you have in mind has already been proposed. If you happen to find an open feature request that matches your idea, click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This ensures that the issue has a better chance of receiving attention. Also feel free to add a comment with any additional justification for the feature.
+* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the feature you have in mind has already been proposed. If you happen to find an open feature request that matches your idea, click "add a reaction" in the top right corner of the issue and add a thumbs up ( :thumbsup: ). This ensures that the issue has a better chance of receiving attention. Also feel free to add a comment with any additional justification for the feature.
+
+* Please don't submit duplicate issues! Sometimes we reject feature requests, for various reasons. Even if you disagree with those reasons, please **do not** submit a duplicate feature request. It is very disrepectful of the maintainers' time, and you may be barred from opening future issues.
* If you have a rough idea that's not quite ready for formal submission yet, start a [GitHub discussion](https://github.com/netbox-community/netbox/discussions) instead. This is a great way to test the viability and narrow down the scope of a new feature prior to submitting a formal proposal, and can serve to generate interest in your idea from other community members.
diff --git a/README.md b/README.md
index da07f226de8..0c793b8a4c4 100644
--- a/README.md
+++ b/README.md
@@ -5,9 +5,13 @@
-
+
-
+
+ NetBox Community |
+ NetBox Cloud |
+ NetBox Enterprise
+
NetBox exists to empower network engineers. Since its release in 2016, it has become the go-to solution for modeling and documenting network infrastructure for thousands of organizations worldwide. As a successor to legacy IPAM and DCIM applications, NetBox provides a cohesive, extensive, and accessible data model for all things networked. By providing a single robust user interface and programmable APIs for everything from cable maps to device configurations, NetBox serves as the central source of truth for the modern network.
@@ -81,11 +85,6 @@ NetBox automatically logs the creation, modification, and deletion of all manage
* The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction.
* Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox!
-
-
- Looking for a managed solution? Check out NetBox Cloud or NetBox Enterprise !
-
-
## Get Involved
* Follow [@NetBoxOfficial](https://twitter.com/NetBoxOfficial) on Twitter!
diff --git a/SECURITY.md b/SECURITY.md
index 4ca6ef33a40..97881a9019e 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -16,7 +16,7 @@ Administrators are encouraged to adhere to industry best practices concerning th
## Reporting a Suspected Vulnerability
-If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so via email. Please note that any reported vulnerabilities **MUST** meet all the following conditions:
+If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so by emailing `security@netboxlabs.com`. Please ensure that your report meets all the following conditions:
* Affects the most recent stable release of NetBox, or a current beta release
* Affects a NetBox instance installed and configured per the official documentation
@@ -24,7 +24,7 @@ If you believe you've uncovered a security vulnerability and wish to report it c
Please note that we **DO NOT** accept reports generated by automated tooling which merely suggest that a file or file(s) _may_ be vulnerable under certain conditions, as these are most often innocuous.
-If you believe that you've found a vulnerability which meets all of these conditions, please [submit a draft security advisory](https://github.com/netbox-community/netbox/security/advisories/new) on GitHub. For any security concerns regarding NetBox deployed via Docker, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
+For any security concerns regarding the community-maintained Docker image for NetBox, please see the [netbox-docker](https://github.com/netbox-community/netbox-docker) project.
### Bug Bounties
diff --git a/base_requirements.txt b/base_requirements.txt
index 841dc0a2436..76955a6e1dc 100644
--- a/base_requirements.txt
+++ b/base_requirements.txt
@@ -10,7 +10,7 @@ django-cors-headers
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
# Pinned for DNS looukp bug; see https://github.com/netbox-community/netbox/issues/16454
# and https://github.com/jazzband/django-debug-toolbar/issues/1927
-django-debug-toolbar==4.3.0
+django-debug-toolbar
# Library for writing reusable URL query filters
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
@@ -84,10 +84,6 @@ Jinja2
# https://python-markdown.github.io/changelog/
Markdown
-# File inclusion plugin for Python-Markdown
-# https://github.com/cmacmackin/markdown-include
-markdown-include
-
# MkDocs Material theme (for documentation build)
# https://squidfunk.github.io/mkdocs-material/changelog/
mkdocs-material
diff --git a/contrib/apache.conf b/contrib/apache.conf
index 73fd45c26b8..fdd0543f7d3 100644
--- a/contrib/apache.conf
+++ b/contrib/apache.conf
@@ -20,7 +20,7 @@
Alias /static /opt/netbox/netbox/static
- Options Indexes FollowSymLinks MultiViews
+ Options FollowSymLinks MultiViews
AllowOverride None
Require all granted
diff --git a/contrib/generated_schema.json b/contrib/generated_schema.json
index deda2b82126..56ddee50e96 100644
--- a/contrib/generated_schema.json
+++ b/contrib/generated_schema.json
@@ -12,6 +12,9 @@
"left-to-right",
"right-to-left",
"side-to-rear",
+ "rear-to-side",
+ "bottom-to-top",
+ "top-to-bottom",
"passive",
"mixed"
]
@@ -149,6 +152,7 @@
"nema-l15-60p",
"nema-l21-20p",
"nema-l21-30p",
+ "nema-l22-20p",
"nema-l22-30p",
"cs6361c",
"cs6365c",
@@ -262,6 +266,7 @@
"nema-l15-60r",
"nema-l21-20r",
"nema-l21-30r",
+ "nema-l22-20r",
"nema-l22-30r",
"CS6360C",
"CS6364C",
@@ -288,6 +293,7 @@
"molex-micro-fit-2x2",
"molex-micro-fit-2x4",
"dc-terminal",
+ "eaton-c39",
"hdot-cx",
"saf-d-grid",
"neutrik-powercon-20a",
@@ -328,6 +334,7 @@
"5gbase-t",
"10gbase-t",
"10gbase-cx4",
+ "100base-x-sfp",
"1000base-x-gbic",
"1000base-x-sfp",
"10gbase-x-sfpp",
@@ -377,7 +384,9 @@
"ieee802.11ad",
"ieee802.11ax",
"ieee802.11ay",
+ "ieee802.11be",
"ieee802.15.1",
+ "ieee802.15.4",
"other-wireless",
"gsm",
"cdma",
@@ -517,6 +526,14 @@
"urm-p4",
"urm-p8",
"splice",
+ "usb-a",
+ "usb-b",
+ "usb-c",
+ "usb-mini-a",
+ "usb-mini-b",
+ "usb-micro-a",
+ "usb-micro-b",
+ "usb-micro-ab",
"other"
]
}
@@ -574,6 +591,14 @@
"urm-p4",
"urm-p8",
"splice",
+ "usb-a",
+ "usb-b",
+ "usb-c",
+ "usb-mini-a",
+ "usb-mini-b",
+ "usb-micro-a",
+ "usb-micro-b",
+ "usb-micro-ab",
"other"
]
}
diff --git a/docs/_theme/partials/copyright.html b/docs/_theme/partials/copyright.html
new file mode 100644
index 00000000000..cfbfd54445b
--- /dev/null
+++ b/docs/_theme/partials/copyright.html
@@ -0,0 +1,18 @@
+
+ {% if config.copyright %}
+
+ {{ config.copyright }}
+
+ {% endif %}
+ {% if not config.extra.generator == false %}
+ Made with
+
+ Material for MkDocs
+
+ {% endif %}
+
+{% if not config.extra.build_public %}
+
+ ℹ️ Documentation is being served locally
+
+{% endif %}
diff --git a/docs/administration/authentication/microsoft-azure-ad.md b/docs/administration/authentication/microsoft-entra-id.md
similarity index 94%
rename from docs/administration/authentication/microsoft-azure-ad.md
rename to docs/administration/authentication/microsoft-entra-id.md
index 17b13081896..3451c656f1a 100644
--- a/docs/administration/authentication/microsoft-azure-ad.md
+++ b/docs/administration/authentication/microsoft-entra-id.md
@@ -1,8 +1,8 @@
-# Microsoft Azure AD
+# Microsoft Entra ID
-This guide explains how to configure single sign-on (SSO) support for NetBox using [Microsoft Azure Active Directory (AD)](https://azure.microsoft.com/en-us/services/active-directory/) as an authentication backend.
+This guide explains how to configure single sign-on (SSO) support for NetBox using [Microsoft Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id) as an authentication backend.
-## Azure AD Configuration
+## Entra ID Configuration
### 1. Create a test user (optional)
@@ -16,7 +16,7 @@ Under the Azure Active Directory dashboard, navigate to **Add > App registration
Enter a name for the registration (e.g. "NetBox") and ensure that the "single tenant" option is selected.
-Under "Redirect URI", select "Web" for the platform and enter the path to your NetBox installation, ending with `/oauth/complete/azuread-oauth2/`. Note that this URI **must** begin with `https://` unless you are referencing localhost (for development purposes).
+Under "Redirect URI", select "Web" for the platform and enter the path to your NetBox installation, ending with `/oauth/complete/entraid-oauth2/`. Note that this URI **must** begin with `https://` unless you are referencing localhost (for development purposes).

diff --git a/docs/administration/authentication/overview.md b/docs/administration/authentication/overview.md
index a6c3a315976..e582f009e71 100644
--- a/docs/administration/authentication/overview.md
+++ b/docs/administration/authentication/overview.md
@@ -40,3 +40,22 @@ REMOTE_AUTH_BACKEND = 'social_core.backends.google.GoogleOAuth2'
NetBox supports single sign-on authentication via the [python-social-auth](https://github.com/python-social-auth) library. To enable SSO, specify the path to the desired authentication backend within the `social_core` Python package. Please see the complete list of [supported authentication backends](https://github.com/python-social-auth/social-core/tree/master/social_core/backends) for the available options.
Most remote authentication backends require some additional configuration through settings prefixed with `SOCIAL_AUTH_`. These will be automatically imported from NetBox's `configuration.py` file. Additionally, the [authentication pipeline](https://python-social-auth.readthedocs.io/en/latest/pipeline.html) can be customized via the `SOCIAL_AUTH_PIPELINE` parameter. (NetBox's default pipeline is defined in `netbox/settings.py` for your reference.)
+
+#### Configuring the SSO module's appearance
+
+The way a remote authentication backend is displayed to the user on the login
+page may be adjusted via the `SOCIAL_AUTH_BACKEND_ATTRS` parameter, defaulting
+to an empty dictionary. This dictionary maps a `social_core` module's name (ie.
+`REMOTE_AUTH_BACKEND.name`) to a couple of parameters, `(display_name, icon)`.
+
+The `display_name` is the name displayed to the user on the login page. The
+icon may either be the URL of an icon; refer to a [Material Design
+Icons](https://github.com/google/material-design-icons) icon's name; or be
+`None` for no icon.
+
+For instance, the OIDC backend may be customized with
+```python
+SOCIAL_AUTH_BACKEND_ATTRS = {
+ 'oidc': ("My awesome SSO", "login"),
+}
+```
diff --git a/docs/configuration/development.md b/docs/configuration/development.md
index 1579f2cdb0d..6e1a4d9c419 100644
--- a/docs/configuration/development.md
+++ b/docs/configuration/development.md
@@ -5,7 +5,7 @@
Default: False
This setting enables debugging. Debugging should be enabled only during development or troubleshooting. Note that only
-clients which access NetBox from a recognized [internal IP address](#internal_ips) will see debugging tools in the user
+clients which access NetBox from a recognized [internal IP address](./system.md#internal_ips) will see debugging tools in the user
interface.
!!! warning
diff --git a/docs/configuration/error-reporting.md b/docs/configuration/error-reporting.md
index 8c3526dec86..56f18784545 100644
--- a/docs/configuration/error-reporting.md
+++ b/docs/configuration/error-reporting.md
@@ -31,6 +31,17 @@ The sampling rate for errors. Must be a value between 0 (disabled) and 1.0 (repo
---
+## SENTRY_SEND_DEFAULT_PII
+
+Default: False
+
+Maps to the Sentry SDK's [`send_default_pii`](https://docs.sentry.io/platforms/python/configuration/options/#send-default-pii) parameter. If enabled, certain personally identifiable information (PII) is added.
+
+!!! warning "Sensitive data"
+ If you enable this option, be aware that sensitive data such as cookies and authentication tokens will be logged.
+
+---
+
## SENTRY_TAGS
An optional dictionary of tag names and values to apply to Sentry error reports.For example:
diff --git a/docs/configuration/graphql-api.md b/docs/configuration/graphql-api.md
new file mode 100644
index 00000000000..a792da54486
--- /dev/null
+++ b/docs/configuration/graphql-api.md
@@ -0,0 +1,17 @@
+# GraphQL API Parameters
+
+## GRAPHQL_ENABLED
+
+!!! tip "Dynamic Configuration Parameter"
+
+Default: True
+
+Setting this to False will disable the GraphQL API.
+
+---
+
+## GRAPHQL_MAX_ALIASES
+
+Default: 10
+
+The maximum number of queries that a GraphQL API request may contain.
diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index 6a2ecdc7fc4..dab7f61353c 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -25,7 +25,7 @@ Some configuration parameters are primarily controlled via NetBox's admin interf
* [`CUSTOM_VALIDATORS`](./data-validation.md#custom_validators)
* [`DEFAULT_USER_PREFERENCES`](./default-values.md#default_user_preferences)
* [`ENFORCE_GLOBAL_UNIQUE`](./miscellaneous.md#enforce_global_unique)
-* [`GRAPHQL_ENABLED`](./miscellaneous.md#graphql_enabled)
+* [`GRAPHQL_ENABLED`](./graphql-api.md#graphql_enabled)
* [`JOB_RETENTION`](./miscellaneous.md#job_retention)
* [`MAINTENANCE_MODE`](./miscellaneous.md#maintenance_mode)
* [`MAPS_URL`](./miscellaneous.md#maps_url)
diff --git a/docs/configuration/miscellaneous.md b/docs/configuration/miscellaneous.md
index 1f0a2781bca..576eb8739d3 100644
--- a/docs/configuration/miscellaneous.md
+++ b/docs/configuration/miscellaneous.md
@@ -96,14 +96,6 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
---
-## DJANGO_ADMIN_ENABLED
-
-Default: False
-
-Setting this to True installs the `django.contrib.admin` app and enables the [Django admin UI](https://docs.djangoproject.com/en/5.0/ref/contrib/admin/). This may be necessary to support older plugins which do not integrate with the native NetBox interface.
-
----
-
## ENFORCE_GLOBAL_UNIQUE
!!! tip "Dynamic Configuration Parameter"
@@ -122,16 +114,6 @@ The maximum amount (in bytes) of uploaded data that will be held in memory befor
---
-## GRAPHQL_ENABLED
-
-!!! tip "Dynamic Configuration Parameter"
-
-Default: True
-
-Setting this to False will disable the GraphQL API.
-
----
-
## JOB_RETENTION
!!! tip "Dynamic Configuration Parameter"
diff --git a/docs/configuration/security.md b/docs/configuration/security.md
index 15702f6490f..b97f3143205 100644
--- a/docs/configuration/security.md
+++ b/docs/configuration/security.md
@@ -20,19 +20,29 @@ A list of permitted URL schemes referenced when rendering links within NetBox. N
## AUTH_PASSWORD_VALIDATORS
-This parameter acts as a pass-through for configuring Django's built-in password validators for local user accounts. If configured, these will be applied whenever a user's password is updated to ensure that it meets minimum criteria such as length or complexity. An example is provided below. For more detail on the available options, please see [the Django documentation](https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation).
+This parameter acts as a pass-through for configuring Django's built-in password validators for local user accounts. These rules are applied whenever a user's password is created or updated to ensure that it meets minimum criteria such as length or complexity. The default configuration is shown below.
```python
AUTH_PASSWORD_VALIDATORS = [
{
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
- 'OPTIONS': {
- 'min_length': 10,
- }
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ "OPTIONS": {
+ "min_length": 12,
+ },
+ },
+ {
+ "NAME": "utilities.password_validation.AlphanumericPasswordValidator",
},
]
```
+The default configuration enforces the follow criteria:
+
+* A password must be at least 12 characters in length.
+* A password must have at least one uppercase letter, one lowercase letter, and one numeric digit.
+
+Although it is not recommended, the default validation rules can be disabled by setting `AUTH_PASSWORD_VALIDATORS = []` in the configuration file. For more detail on customizing password validation, please see [the Django documentation](https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation).
+
---
## CORS_ORIGIN_ALLOW_ALL
diff --git a/docs/configuration/system.md b/docs/configuration/system.md
index a1e0ebb17f9..25c724bc922 100644
--- a/docs/configuration/system.md
+++ b/docs/configuration/system.md
@@ -83,7 +83,20 @@ Default: `('127.0.0.1', '::1')`
A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For
example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
-addresses (and [`DEBUG`](#debug) is true).
+addresses (and [`DEBUG`](./development.md#debug) is true).
+
+---
+
+## ISOLATED_DEPLOYMENT
+
+!!! info "This feature was introduced in NetBox v4.1."
+
+Default: False
+
+Set this configuration parameter to True for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
+
+!!! note
+ If Internet access is available via a proxy, set [`HTTP_PROXIES`](#http_proxies) instead.
---
@@ -106,7 +119,7 @@ JINJA2_FILTERS = {
## LOGGING
-By default, all messages of INFO severity or higher will be logged to the console. Additionally, if [`DEBUG`](#debug) is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in [`ADMINS`](#admins).
+By default, all messages of INFO severity or higher will be logged to the console. Additionally, if [`DEBUG`](./development.md#debug) is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in [`ADMINS`](./miscellaneous.md#admins).
The Django framework on which NetBox runs allows for the customization of logging format and destination. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/stable/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a local file:
@@ -143,7 +156,7 @@ LOGGING = {
## MEDIA_ROOT
-Default: $INSTALL_ROOT/netbox/media/
+Default: `$INSTALL_ROOT/netbox/media/`
The file path to the location where media files (such as image attachments) are stored. By default, this is the `netbox/media/` directory within the base NetBox installation path.
@@ -177,7 +190,7 @@ The dotted path to the desired search backend class. `CachedValueSearchBackend`
Default: None (local storage)
-The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) package, which provides backends for several popular file storage services. If not configured, local filesystem storage will be used.
+The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) and [`django-storage-swift`](https://github.com/dennisv/django-storage-swift) packages, which provide backends for several popular file storage services. If not configured, local filesystem storage will be used.
The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting.
@@ -187,7 +200,7 @@ The configuration parameters for the specified storage backend are defined under
Default: Empty
-A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the [`django-storages` documentation](https://django-storages.readthedocs.io/en/stable/) for more detail.
+A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the documentation for your selected backend ([`django-storages`](https://django-storages.readthedocs.io/en/stable/) or [`django-storage-swift`](https://github.com/dennisv/django-storage-swift)) for more detail.
If `STORAGE_BACKEND` is not defined, this setting will be ignored.
diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md
index 1f9a4a8bfd9..4658cc7e6f2 100644
--- a/docs/customization/custom-fields.md
+++ b/docs/customization/custom-fields.md
@@ -74,6 +74,8 @@ If a default value is specified for a selection field, it must exactly match one
An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point.
+By default, an object choice field will make all objects of that type available for selection in the drop-down. The list choices can be filtered to show only objects with certain values by providing a `query_params` dict in the Related Object Filter field, as a JSON value. More information about `query_params` can be found [here](./custom-scripts.md#objectvar).
+
## Custom Fields in Templates
Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`).
diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md
index e6f6bb85f04..3fa6491d231 100644
--- a/docs/customization/custom-scripts.md
+++ b/docs/customization/custom-scripts.md
@@ -17,6 +17,9 @@ They can also be used as a mechanism for validating the integrity of data within
Custom scripts are Python code which exists outside the NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
+!!! danger "Only install trusted scripts"
+ Custom scripts have unrestricted access to change anything in the databse and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
+
## Writing Custom Scripts
All custom scripts must inherit from the `extras.scripts.Script` base class. This class provides the functionality necessary to generate forms and log activity.
diff --git a/docs/customization/custom-validation.md b/docs/customization/custom-validation.md
index 909846e2047..4a2aab99802 100644
--- a/docs/customization/custom-validation.md
+++ b/docs/customization/custom-validation.md
@@ -86,8 +86,6 @@ CUSTOM_VALIDATORS = {
#### Referencing Related Object Attributes
-!!! info "This feature was introduced in NetBox v4.0."
-
The attributes of a related object can be referenced by specifying a dotted path. For example, to reference the name of a region to which a site is assigned, use `region.name`:
```python
@@ -104,8 +102,6 @@ CUSTOM_VALIDATORS = {
#### Validating Request Parameters
-!!! info "This feature was introduced in NetBox v4.0."
-
In addition to validating object attributes, custom validators can also match against parameters of the current request (where available). For example, the following rule will permit only the user named "admin" to modify an object:
```json
diff --git a/docs/development/adding-models.md b/docs/development/adding-models.md
index 823789641ae..f3aa9cfcca6 100644
--- a/docs/development/adding-models.md
+++ b/docs/development/adding-models.md
@@ -71,7 +71,6 @@ Add the relevant navigation menu items in `netbox/netbox/navigation/menu.py`.
Create the following for each model:
* Detailed (full) model serializer in `api/serializers.py`
-* Nested serializer in `api/nested_serializers.py`
* API view in `api/views.py`
* Endpoint route in `api/urls.py`
diff --git a/docs/development/extending-models.md b/docs/development/extending-models.md
index bf54313378a..16d1c345144 100644
--- a/docs/development/extending-models.md
+++ b/docs/development/extending-models.md
@@ -50,7 +50,7 @@ If you're adding a relational field (e.g. `ForeignKey`) and intend to include th
## 5. Update API serializer
-Extend the model's API serializer in `.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal representation of the model.
+Extend the model's API serializer in `.api.serializers` to include the new field.
## 6. Add fields to forms
diff --git a/docs/development/getting-started.md b/docs/development/getting-started.md
index 4dbdb63b28e..6e425d5a3f5 100644
--- a/docs/development/getting-started.md
+++ b/docs/development/getting-started.md
@@ -62,22 +62,7 @@ $issue-$description
The description should be just two or three words to imply the focus of the work being performed. For example, bug #1234 to fix a TypeError exception when creating a device might be named `1234-device-typerror`. This ensures that branches are always follow some logical ordering (e.g. when running `git branch -a`) and helps other developers quickly identify the purpose of each.
-### 3. Enable Pre-Commit Hooks
-
-NetBox ships with a [git pre-commit hook](https://githooks.com/) script that automatically checks for style compliance and missing database migrations prior to committing changes. This helps avoid erroneous commits that result in CI test failures. You are encouraged to enable it by creating a link to `scripts/git-hooks/pre-commit`:
-
-```no-highlight
-cd .git/hooks/
-ln -s ../../scripts/git-hooks/pre-commit
-```
-For the pre-commit hooks to work, you will also need to install the pycodestyle package:
-
-```no-highlight
-python -m pip install pycodestyle
-```
-...and set up the yarn packages as shown in the [Web UI Development Guide](web-ui.md)
-
-### 4. Create a Python Virtual Environment
+### 3. Create a Python Virtual Environment
A [virtual environment](https://docs.python.org/3/tutorial/venv.html) (or "venv" for short) is like a container for a set of Python packages. These allow you to build environments suited to specific projects without interfering with system packages or other projects. When installed per the documentation, NetBox uses a virtual environment in production.
@@ -101,7 +86,7 @@ source ~/.venv/netbox/bin/activate
Notice that the console prompt changes to indicate the active environment. This updates the necessary system environment variables to ensure that any Python scripts are run within the virtual environment.
-### 5. Install Required Packages
+### 4. Install Required Packages
With the virtual environment activated, install the project's required Python packages using the `pip` module. Required packages are defined in `requirements.txt`. Each line in this file specifies the name and specific version of a required package.
@@ -109,6 +94,26 @@ With the virtual environment activated, install the project's required Python pa
python -m pip install -r requirements.txt
```
+### 5. Install Pre-Commit
+
+NetBox uses [`pre-commit`](https://pre-commit.com/) to automatically validate code when commiting new changes. This includes the following operations:
+
+* Run the `ruff` Python linter
+* Run Django's internal system check
+* Check for missing database migrations
+* Validate any changes to the documentation with `mkdocs`
+* Validate Typescript & Sass styling with `yarn`
+* Ensure that any modified static front end assets have been recompiled
+
+Enable `pre-commit` with the following commands _prior_ to commiting any changes:
+
+```no-highlight
+python -m pip install ruff pre-commit
+pre-commit install
+```
+
+You may also need to set up the yarn packages as shown in the [Web UI Development Guide](web-ui.md).
+
### 6. Configure NetBox
Within the `netbox/netbox/` directory, copy `configuration_example.py` to `configuration.py` and update the following parameters:
diff --git a/docs/development/models.md b/docs/development/models.md
index 19b7be6dee7..1b91db515ad 100644
--- a/docs/development/models.md
+++ b/docs/development/models.md
@@ -18,7 +18,7 @@ Depending on its classification, each NetBox model may support various features
| [Custom links](../customization/custom-links.md) | `CustomLinksMixin` | `custom_links` | These models support the assignment of custom links |
| [Custom validation](../customization/custom-validation.md) | `CustomValidationMixin` | - | Supports the enforcement of custom validation rules |
| [Export templates](../customization/export-templates.md) | `ExportTemplatesMixin` | `export_templates` | Users can create custom export templates for these models |
-| [Job results](../features/background-jobs.md) | `JobsMixin` | `jobs` | Users can create custom export templates for these models |
+| [Job results](../features/background-jobs.md) | `JobsMixin` | `jobs` | Background jobs can be scheduled for these models |
| [Journaling](../features/journaling.md) | `JournalingMixin` | `journaling` | These models support persistent historical commentary |
| [Synchronized data](../integrations/synchronized-data.md) | `SyncedDataMixin` | `synced_data` | Certain model data can be automatically synchronized from a remote data source |
| [Tagging](../models/extras/tag.md) | `TagsMixin` | `tags` | The models can be tagged with user-defined tags |
@@ -34,7 +34,9 @@ These are considered the "core" application models which are used to model netwo
* [circuits.Provider](../models/circuits/provider.md)
* [circuits.ProviderAccount](../models/circuits/provideraccount.md)
* [circuits.ProviderNetwork](../models/circuits/providernetwork.md)
+* [core.DataFile](../models/core/datafile.md)
* [core.DataSource](../models/core/datasource.md)
+* [core.Job](../models/core/job.md)
* [dcim.Cable](../models/dcim/cable.md)
* [dcim.Device](../models/dcim/device.md)
* [dcim.DeviceType](../models/dcim/devicetype.md)
@@ -44,12 +46,14 @@ These are considered the "core" application models which are used to model netwo
* [dcim.PowerPanel](../models/dcim/powerpanel.md)
* [dcim.Rack](../models/dcim/rack.md)
* [dcim.RackReservation](../models/dcim/rackreservation.md)
+* [dcim.RackType](../models/dcim/racktype.md)
* [dcim.Site](../models/dcim/site.md)
* [dcim.VirtualChassis](../models/dcim/virtualchassis.md)
* [dcim.VirtualDeviceContext](../models/dcim/virtualdevicecontext.md)
* [ipam.Aggregate](../models/ipam/aggregate.md)
* [ipam.ASN](../models/ipam/asn.md)
* [ipam.FHRPGroup](../models/ipam/fhrpgroup.md)
+* [ipam.FHRPGroupAssignment](../models/ipam/fhrpgroupassignment.md)
* [ipam.IPAddress](../models/ipam/ipaddress.md)
* [ipam.IPRange](../models/ipam/iprange.md)
* [ipam.Prefix](../models/ipam/prefix.md)
@@ -76,6 +80,7 @@ These are considered the "core" application models which are used to model netwo
Organization models are used to organize and classify primary models.
+* [circuits.CircuitGroup](../models/circuits/circuitgroup.md)
* [circuits.CircuitType](../models/circuits/circuittype.md)
* [dcim.DeviceRole](../models/dcim/devicerole.md)
* [dcim.Manufacturer](../models/dcim/manufacturer.md)
@@ -88,6 +93,7 @@ Organization models are used to organize and classify primary models.
* [tenancy.ContactRole](../models/tenancy/contactrole.md)
* [virtualization.ClusterGroup](../models/virtualization/clustergroup.md)
* [virtualization.ClusterType](../models/virtualization/clustertype.md)
+* [vpn.TunnelGroup](../models/vpn/tunnelgroup.md)
### Nested Group Models
@@ -131,3 +137,10 @@ These function as templates to effect the replication of device and virtual mach
* [dcim.PowerOutletTemplate](../models/dcim/poweroutlettemplate.md)
* [dcim.PowerPortTemplate](../models/dcim/powerporttemplate.md)
* [dcim.RearPortTemplate](../models/dcim/rearporttemplate.md)
+
+### Connection Models
+
+Connection models are used to model the connections, or connection endpoints between models.
+
+* [circuits.CircuitTermination](../models/circuits/circuittermination.md)
+* [vpn.TunnelTermination](../models/vpn/tunneltermination.md)
diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md
index 91162f08ad7..7c8c96f3926 100644
--- a/docs/development/release-checklist.md
+++ b/docs/development/release-checklist.md
@@ -2,9 +2,9 @@
This documentation describes the process of packaging and publishing a new NetBox release. There are three types of release:
-* Major release (e.g. v2.11 to v3.0)
-* Minor release (e.g. v3.2 to v3.3)
-* Patch release (e.g. v3.3.0 to v3.3.1)
+* Major release (e.g. v3.7.8 to v4.0.0)
+* Minor release (e.g. v4.0.10 to v4.1.0)
+* Patch release (e.g. v4.1.0 to v4.1.1)
While major releases generally introduce some very substantial change to the application, they are typically treated the same as minor version increments for the purpose of release packaging.
@@ -19,7 +19,7 @@ Sometimes it becomes necessary to constrain dependencies to a particular version
djangorestframework==3.8.1
```
-These version constraints are added to `base_requirements.txt` to ensure that newer packages are not installed when updating the pinned dependencies in `requirements.txt` (see the [Update Requirements](#update-requirements) section below). Before each new minor version of NetBox is released, all such constraints on dependent packages should be addressed if feasible. This guards against the collection of stale constraints over time.
+These version constraints are added to `base_requirements.txt` to ensure that newer packages are not installed when updating the pinned dependencies in `requirements.txt` (see the [Update Requirements](#update-python-dependencies) section below). Before each new minor version of NetBox is released, all such constraints on dependent packages should be addressed if feasible. This guards against the collection of stale constraints over time.
### Close the Release Milestone
@@ -39,6 +39,10 @@ mkdocs serve
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation, and ensure that it is kept up-to-date for each release. Make any necessary changes to the documentation before proceeding with the release.
+### Test Upgrade Paths
+
+Upgrading from a previous version typically involves database migrations, which must work without errors. Supported upgrade paths include from one minor version to another within the same major version (i.e. 4.0 to 4.1), as well as from the latest patch version of the previous minor version (i.e. 3.7 to 4.0 or to 4.1). Prior to release, test all these supported paths by loading demo data from the source version and performing a `./manage.py migrate`.
+
### Merge the Release Branch
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
@@ -90,7 +94,7 @@ Updated language translations should be pulled from [Transifex](https://app.tran
### Update Version and Changelog
-* Update the `VERSION` constant in `settings.py` to the new release version.
+* Update the version and published date in `release.yaml` with the current version & date. Add a designation (e.g.g `beta1`) if applicable.
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
* Replace the "FUTURE" placeholder in the release notes with the current date.
@@ -113,20 +117,10 @@ Create a [new release](https://github.com/netbox-community/netbox/releases/new)
* **Tag:** Current version (e.g. `v3.3.1`)
* **Target:** `master`
* **Title:** Version and date (e.g. `v3.3.1 - 2022-08-25`)
-* **Description:** Copy from the pull request body
+* **Description:** Copy from the pull request body, then promote the `###` headers to `##` ones
Once created, the release will become available for users to install.
-### Update the Development Version
-
-On the `develop` branch, update `VERSION` in `settings.py` to point to the next release. For example, if you just released v3.3.1, set:
-
-```
-VERSION = 'v3.3.2-dev'
-```
-
-Commit this change with the comment "PRVB" (for _post-release version bump_) and push the commit upstream.
-
### Update the Public Documentation
After a release has been published, the public NetBox documentation needs to be updated. This is accomplished by running two actions on the [netboxlabs-docs](https://github.com/netboxlabs/netboxlabs-docs) repository.
@@ -135,4 +129,6 @@ First, run the `build-site` action, by navigating to Actions > build-site > Run
Once the documentation files have been compiled, they must be published by running the `deploy-kinsta` action. Select the desired deployment environment (staging or production) and specify `latest` as the deploy tag.
+Clear the CDN cache from the [Kinsta](https://my.kinsta.com/) portal. Navigate to _Sites_ / _NetBox Labs_ / _Live_, select _Cache_ in the left-nav, click the _Clear Cache_ button, and confirm the clear operation.
+
Finally, verify that the documentation at has been updated.
diff --git a/docs/development/style-guide.md b/docs/development/style-guide.md
index 283ad698cfc..0d4caf395a6 100644
--- a/docs/development/style-guide.md
+++ b/docs/development/style-guide.md
@@ -1,6 +1,6 @@
# Style Guide
-NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations.
+NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [ruff](https://docs.astral.sh/ruff/) is used for linting (with certain [exceptions](#linter-exceptions)).
## Code
@@ -20,32 +20,32 @@ NetBox generally follows the [Django style guide](https://docs.djangoproject.com
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
-### PEP 8 Exceptions
+### Linting
-NetBox ignores certain PEP8 assertions. These are listed below.
+The [ruff](https://docs.astral.sh/ruff/) linter is used to enforce code style. A [pre-commit hook](./getting-started.md#3-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `ruff` manually, run:
-#### Wildcard Imports
+```
+ruff check netbox/
+```
+
+#### Linter Exceptions
+
+The following rules are ignored when linting.
+
+##### [E501](https://docs.astral.sh/ruff/rules/line-too-long/): Line too long
+
+NetBox does not enforce a hard restriction on line length, although a maximum length of 120 characters is strongly encouraged for Python code where possible. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
+
+##### [F403](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star/): Undefined local with import star
Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions:
* The library being import contains only constant declarations (e.g. `constants.py`)
* The library being imported explicitly defines `__all__`
-#### Maximum Line Length (E501)
+##### [F405](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star-usage/): Undefined local with import star usage
-NetBox does not restrict lines to a maximum length of 79 characters. We use a maximum line length of 120 characters, however this is not enforced by CI. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
-
-#### Line Breaks Following Binary Operators (W504)
-
-Line breaks are permitted following binary operators.
-
-### Enforcing Code Style
-
-The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#2-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
-
-```
-pycodestyle --ignore=W504,E501 netbox/
-```
+The justification for ignoring this rule is the same as F403 above.
### Introducing New Dependencies
diff --git a/docs/development/translations.md b/docs/development/translations.md
index b23e89d71fe..eca9ce71f9c 100644
--- a/docs/development/translations.md
+++ b/docs/development/translations.md
@@ -20,6 +20,8 @@ Then, commit the change and push to the `develop` branch on GitHub. Any new stri
Typically, translated strings need to be updated only as part of the NetBox [release process](./release-checklist.md).
+Check the Transifex dashboard for languages that are not marked _ready for use_, being sure to click _Show all languages_ if it appears at the bottom of the list. Use machine translation to round out any not-ready languages. It's not necessary to review the machine translation immediately as the translation teams will handle that aspect; the goal at this stage is to get translations included in the Transifex pull request.
+
To update translated strings, start by initiating a sync from Transifex. From the Transifex dashboard, navigate to Settings > Integrations > GitHub > Manage, and click the **Manual Sync** button at top right.

diff --git a/docs/features/authentication-permissions.md b/docs/features/authentication-permissions.md
index 14e13d5cdef..cf3d11ef386 100644
--- a/docs/features/authentication-permissions.md
+++ b/docs/features/authentication-permissions.md
@@ -41,7 +41,7 @@ NetBox integrates with the open source [python-social-auth](https://github.com/p
* Google
* Hashicorp Vault
* Keycloak
-* Microsoft Azure AD
+* Microsoft Entra ID
* Microsoft Graph
* Okta
* OIDC
diff --git a/docs/features/event-rules.md b/docs/features/event-rules.md
index 158dc111a71..14b54f00087 100644
--- a/docs/features/event-rules.md
+++ b/docs/features/event-rules.md
@@ -1,9 +1,10 @@
# Event Rules
-NetBox includes the ability to execute certain functions in response to internal object changes. These include:
+NetBox includes the ability to automatically perform certain functions in response to internal events. These include:
-* [Scripts](../customization/custom-scripts.md) execution
-* [Webhooks](../integrations/webhooks.md) execution
+* Executing a [custom script](../customization/custom-scripts.md)
+* Sending a [webhook](../integrations/webhooks.md)
+* Generating [user notifications](../features/notifications.md)
For example, suppose you want to automatically configure a monitoring system to start monitoring a device when its operational status is changed to active, and remove it from monitoring for any other status. You can create a webhook in NetBox for the device model and craft its content and destination URL to effect the desired change on the receiving system. You can then associate an event rule with this webhook and the webhook will be sent automatically by NetBox whenever the configured constraints are met.
diff --git a/docs/features/facilities.md b/docs/features/facilities.md
index 84c7c57333b..4c8dfe265bc 100644
--- a/docs/features/facilities.md
+++ b/docs/features/facilities.md
@@ -56,6 +56,10 @@ A site typically represents a building within a region and/or site group. Each s
A location can be any logical subdivision within a building, such as a floor or room. Like regions and site groups, locations can be nested into a self-recursive hierarchy for maximum flexibility. And like sites, each location has an operational status assigned to it.
+## Rack Types
+
+A rack type represents a unique specification of a rack which exists in the real world. Each rack type can be setup with weight, height, and unit ordering. New racks of this type can then be created in NetBox, and any associated specifications will be automatically replicated from the device type.
+
## Racks
Finally, NetBox models each equipment rack as a discrete object within a site and location. These are physical objects into which devices are installed. Each rack can be assigned an operational status, type, facility ID, and other attributes related to inventory tracking. Each rack also must define a height (in rack units) and width, and may optionally specify its physical dimensions.
diff --git a/docs/features/notifications.md b/docs/features/notifications.md
new file mode 100644
index 00000000000..a28a17947ae
--- /dev/null
+++ b/docs/features/notifications.md
@@ -0,0 +1,10 @@
+# Notifications
+
+!!! info "This feature was introduced in NetBox v4.1."
+
+NetBox includes a system for generating user notifications, which can be marked as read or deleted by individual users. There are two built-in mechanisms for generating a notification:
+
+* A user can subscribe to an object. When that object is modified, a notification is created to inform the user of the change.
+* An [event rule](./event-rules.md) can be defined to automatically generate a notification for one or more users in response to specific system events.
+
+Additionally, NetBox plugins can generate notifications for their own purposes.
diff --git a/docs/features/synchronized-data.md b/docs/features/synchronized-data.md
index 8c95c87791f..23c79feed88 100644
--- a/docs/features/synchronized-data.md
+++ b/docs/features/synchronized-data.md
@@ -13,6 +13,9 @@ To enable remote data synchronization, the NetBox administrator first designates
!!! info
Data backends which connect to external sources typically require the installation of one or more supporting Python libraries. The Git backend requires the [`dulwich`](https://www.dulwich.io/) package, and the S3 backend requires the [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) package. These must be installed within NetBox's environment to enable these backends.
+!!! info
+ If you are configuring Git and have `HTTP_PROXIES` configured to use the SOCKS protocol, you will also need to install the [`python_socks`](https://pypi.org/project/python-socks/) Python library.
+
Each type of remote source has its own configuration parameters. For instance, a git source will ask the user to specify a branch and authentication credentials. Once the source has been created, a synchronization job is run to automatically replicate remote files in the local database.
The following NetBox models can be associated with replicated data files:
diff --git a/docs/integrations/graphql-api.md b/docs/integrations/graphql-api.md
index 3ccb4d4a19f..425c3adda9d 100644
--- a/docs/integrations/graphql-api.md
+++ b/docs/integrations/graphql-api.md
@@ -112,4 +112,4 @@ Authorization: Token $TOKEN
## Disabling the GraphQL API
-If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/miscellaneous.md#graphql_enabled) configuration parameter to False and restarting NetBox.
+If not needed, the GraphQL API can be disabled by setting the [`GRAPHQL_ENABLED`](../configuration/graphql-api.md#graphql_enabled) configuration parameter to False and restarting NetBox.
diff --git a/docs/integrations/rest-api.md b/docs/integrations/rest-api.md
index 1ba00958fdc..215b561a74c 100644
--- a/docs/integrations/rest-api.md
+++ b/docs/integrations/rest-api.md
@@ -101,7 +101,7 @@ See the [filtering documentation](../reference/filtering.md) for more details on
## Serialization
-The REST API employs two types of serializers to represent model data: base serializers and nested serializers. The base serializer is used to present the complete view of a model. This includes all database table fields which comprise the model, and may include additional metadata. A base serializer includes relationships to parent objects, but **does not** include child objects. For example, the `VLANSerializer` includes a nested representation its parent VLANGroup (if any), but does not include any assigned Prefixes.
+The REST API generally represents objects in one of two ways: complete or brief. The base serializer is used to present the complete view of an object. This includes all database table fields which comprise the model, and may include additional metadata. A base serializer includes relationships to parent objects, but **does not** include child objects. For example, the `VLANSerializer` includes a nested representation its parent VLANGroup (if any), but does not include any assigned Prefixes. Serializers employ a minimal "brief" representation of related objects, which includes only the attributes prudent for identifying the object.
```json
{
@@ -139,7 +139,7 @@ The REST API employs two types of serializers to represent model data: base seri
### Related Objects
-Related objects (e.g. `ForeignKey` fields) are represented using nested serializers. A nested serializer provides a minimal representation of an object, including only its direct URL and enough information to display the object to a user. When performing write API actions (`POST`, `PUT`, and `PATCH`), related objects may be specified by either numeric ID (primary key), or by a set of attributes sufficiently unique to return the desired object.
+Related objects (e.g. `ForeignKey` fields) are included using nested brief representations. This is a minimal representation of an object, including only its direct URL and enough information to display the object to a user. When performing write API actions (`POST`, `PUT`, and `PATCH`), related objects may be specified by either numeric ID (primary key), or by a set of attributes sufficiently unique to return the desired object.
For example, when creating a new device, its rack can be specified by NetBox ID (PK):
@@ -151,7 +151,7 @@ For example, when creating a new device, its rack can be specified by NetBox ID
}
```
-Or by a set of nested attributes which uniquely identify the rack:
+Or by a set of attributes which uniquely identify the rack:
```json
{
diff --git a/docs/media/misc/netbox_cloud.png b/docs/media/misc/netbox_cloud.png
deleted file mode 100644
index f9deca67446..00000000000
Binary files a/docs/media/misc/netbox_cloud.png and /dev/null differ
diff --git a/docs/media/misc/netbox_logo.png b/docs/media/misc/netbox_logo.png
deleted file mode 100644
index c6e0a58e625..00000000000
Binary files a/docs/media/misc/netbox_logo.png and /dev/null differ
diff --git a/docs/media/screenshots/cable-trace.png b/docs/media/screenshots/cable-trace.png
index 63cba056f90..d2b5a6c2d26 100644
Binary files a/docs/media/screenshots/cable-trace.png and b/docs/media/screenshots/cable-trace.png differ
diff --git a/docs/media/screenshots/home-dark.png b/docs/media/screenshots/home-dark.png
index 95a6468fef8..c19685cca59 100644
Binary files a/docs/media/screenshots/home-dark.png and b/docs/media/screenshots/home-dark.png differ
diff --git a/docs/media/screenshots/home-light.png b/docs/media/screenshots/home-light.png
index ed249cbddbb..dff33b36f5f 100644
Binary files a/docs/media/screenshots/home-light.png and b/docs/media/screenshots/home-light.png differ
diff --git a/docs/media/screenshots/prefixes-list.png b/docs/media/screenshots/prefixes-list.png
index a5e5a4b8be6..d114a5d50e3 100644
Binary files a/docs/media/screenshots/prefixes-list.png and b/docs/media/screenshots/prefixes-list.png differ
diff --git a/docs/media/screenshots/rack.png b/docs/media/screenshots/rack.png
index a5954d644ee..812ce87481b 100644
Binary files a/docs/media/screenshots/rack.png and b/docs/media/screenshots/rack.png differ
diff --git a/docs/models/circuits/circuit.md b/docs/models/circuits/circuit.md
index 19fd8c88262..c75e2032212 100644
--- a/docs/models/circuits/circuit.md
+++ b/docs/models/circuits/circuit.md
@@ -36,6 +36,12 @@ The operational status of the circuit. By default, the following statuses are av
!!! tip "Custom circuit statuses"
Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
+### Distance
+
+!!! info "This field was introduced in NetBox v4.2."
+
+The distance between the circuit's two endpoints, including a unit designation (e.g. 100 meters or 25 feet).
+
### Description
A brief description of the circuit.
diff --git a/docs/models/circuits/circuitgroup.md b/docs/models/circuits/circuitgroup.md
new file mode 100644
index 00000000000..faa9dbc1466
--- /dev/null
+++ b/docs/models/circuits/circuitgroup.md
@@ -0,0 +1,15 @@
+# Circuit Groups
+
+!!! info "This feature was introduced in NetBox v4.1."
+
+[Circuits](./circuit.md) can be arranged into administrative groups for organization. The assignment of a circuit to a group is optional.
+
+## Fields
+
+### Name
+
+A unique human-friendly name.
+
+### Slug
+
+A unique URL-friendly identifier. (This value can be used for filtering.)
diff --git a/docs/models/circuits/circuitgroupassignment.md b/docs/models/circuits/circuitgroupassignment.md
new file mode 100644
index 00000000000..2aaa375af01
--- /dev/null
+++ b/docs/models/circuits/circuitgroupassignment.md
@@ -0,0 +1,25 @@
+# Circuit Group Assignments
+
+Circuits can be assigned to [circuit groups](./circuitgroup.md) for correlation purposes. For instance, three circuits, each belonging to a different provider, may each be assigned to the same circuit group. Each assignment may optionally include a priority designation.
+
+## Fields
+
+### Group
+
+The [circuit group](./circuitgroup.md) being assigned.
+
+### Circuit
+
+The [circuit](./circuit.md) that is being assigned to the group.
+
+### Priority
+
+The circuit's operation priority relative to its peers within the group. The assignment of a priority is optional. Choices include:
+
+* Primary
+* Secondary
+* Tertiary
+* Inactive
+
+!!! tip
+ Additional priority choices may be defined by setting `CircuitGroupAssignment.priority` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
diff --git a/docs/models/dcim/inventoryitem.md b/docs/models/dcim/inventoryitem.md
index b9029f75cf7..a6dfa32db3a 100644
--- a/docs/models/dcim/inventoryitem.md
+++ b/docs/models/dcim/inventoryitem.md
@@ -44,3 +44,7 @@ The serial number assigned by the manufacturer.
### Asset Tag
A unique, locally-administered label used to identify hardware resources.
+
+### Status
+
+The inventory item's operational status.
diff --git a/docs/models/dcim/modulebay.md b/docs/models/dcim/modulebay.md
index c7790951141..1bff799c2f2 100644
--- a/docs/models/dcim/modulebay.md
+++ b/docs/models/dcim/modulebay.md
@@ -14,6 +14,12 @@ Module bays represent a space or slot within a device in which a field-replaceab
The device to which this module bay belongs.
+### Module
+
+!!! info "This feature was introduced in NetBox v4.1."
+
+The module to which this bay belongs (optional).
+
### Name
The module bay's name. Must be unique to the parent device.
diff --git a/docs/models/dcim/moduletype.md b/docs/models/dcim/moduletype.md
index 3122d2e00cd..225873d6164 100644
--- a/docs/models/dcim/moduletype.md
+++ b/docs/models/dcim/moduletype.md
@@ -39,3 +39,9 @@ An alternative part number to uniquely identify the module type.
### Weight
The numeric weight of the module, including a unit designation (e.g. 3 kilograms or 1 pound).
+
+### Airflow
+
+!!! info "The `airflow` field was introduced in NetBox v4.1."
+
+The direction in which air circulates through the device chassis for cooling.
diff --git a/docs/models/dcim/poweroutlet.md b/docs/models/dcim/poweroutlet.md
index 5c8bd6ff060..fe939005610 100644
--- a/docs/models/dcim/poweroutlet.md
+++ b/docs/models/dcim/poweroutlet.md
@@ -29,6 +29,10 @@ An alternative physical label identifying the power outlet.
The type of power outlet.
+### Color
+
+The power outlet's color (optional).
+
### Power Port
When modeling a device which redistributes power from an upstream supply, such as a power distribution unit (PDU), each power outlet should be mapped to the respective [power port](./powerport.md) on the device which supplies power. For example, a 24-outlet PDU may two power ports, each distributing power to 12 of its outlets.
diff --git a/docs/models/dcim/rack.md b/docs/models/dcim/rack.md
index 3989e2b0495..d610f63686a 100644
--- a/docs/models/dcim/rack.md
+++ b/docs/models/dcim/rack.md
@@ -20,6 +20,10 @@ The [location](./location.md) within a site where the rack has been installed (o
The rack's name or identifier. Must be unique to the rack's location, if assigned.
+### Rack Type
+
+The [physical type](./racktype.md) of this rack. The rack type defines physical attributes such as height and weight.
+
### Status
Operational status.
@@ -43,44 +47,5 @@ The unique physical serial number assigned to this rack.
A unique, locally-administered label used to identify hardware resources.
-### Type
-
-A rack can be designated as one of the following types:
-
-* 2-post frame
-* 4-post frame
-* 4-post cabinet
-* Wall-mounted frame
-* Wall-mounted cabinet
-
-### Width
-
-The canonical distance between the two vertical rails on a face. (This is typically 19 inches, however other standard widths exist.)
-
-### Height
-
-The height of the rack, measured in units.
-
-### Starting Unit
-
-The number of the numerically lowest unit in the rack. This value defaults to one, but may be higher in certain situations. For example, you may want to model only a select range of units within a shared physical rack (e.g. U13 through U24).
-
-### Outer Dimensions
-
-The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
-
-### Mounting Depth
-
-The maximum depth of a mounted device that the rack can accommodate, in millimeters. For four-post frames or cabinets, this is the horizontal distance between the front and rear vertical rails. (Note that this measurement does _not_ include space between the rails and the cabinet doors.)
-
-### Weight
-
-The numeric weight of the rack, including a unit designation (e.g. 10 kilograms or 20 pounds).
-
-### Maximum Weight
-
-The maximum total weight capacity for all installed devices, inclusive of the rack itself.
-
-### Descending Units
-
-If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use ascending numbering, with unit 1 assigned to the bottommost position.)
+!!! note
+ Some additional fields pertaining to physical attributes such as height and weight can also be defined on each rack, but should generally be defined instead on the [rack type](./racktype.md).
diff --git a/docs/models/dcim/racktype.md b/docs/models/dcim/racktype.md
new file mode 100644
index 00000000000..eeb90bd29da
--- /dev/null
+++ b/docs/models/dcim/racktype.md
@@ -0,0 +1,61 @@
+# Rack Types
+
+!!! info "This feature was introduced in NetBox v4.1."
+
+A rack type defines the physical characteristics of a particular model of [rack](./rack.md).
+
+## Fields
+
+### Manufacturer
+
+The [manufacturer](./manufacturer.md) which produces this type of rack.
+
+### Model
+
+The model number assigned to this rack type by its manufacturer. Must be unique to the manufacturer.
+
+### Slug
+
+A unique URL-friendly representation of the model identifier. (This value can be used for filtering.)
+
+### Form Factor
+
+A rack can be designated as one of the following form factors:
+
+* 2-post frame
+* 4-post frame
+* 4-post cabinet
+* Wall-mounted frame
+* Wall-mounted cabinet
+
+### Width
+
+The canonical distance between the two vertical rails on a face. (This is typically 19 inches, however other standard widths exist.)
+
+### Height
+
+The height of the rack, measured in units.
+
+### Starting Unit
+
+The number of the numerically lowest unit in the rack. This value defaults to one, but may be higher in certain situations. For example, you may want to model only a select range of units within a shared physical rack (e.g. U13 through U24).
+
+### Outer Dimensions
+
+The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
+
+### Mounting Depth
+
+The maximum depth of a mounted device that the rack can accommodate, in millimeters. For four-post frames or cabinets, this is the horizontal distance between the front and rear vertical rails. (Note that this measurement does _not_ include space between the rails and the cabinet doors.)
+
+### Weight
+
+The numeric weight of the rack, including a unit designation (e.g. 10 kilograms or 20 pounds).
+
+### Maximum Weight
+
+The maximum total weight capacity for all installed devices, inclusive of the rack itself.
+
+### Descending Units
+
+If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use ascending numbering, with unit 1 assigned to the bottommost position.)
diff --git a/docs/models/extras/branch.md b/docs/models/extras/branch.md
index fd7922b7e36..4599fed8599 100644
--- a/docs/models/extras/branch.md
+++ b/docs/models/extras/branch.md
@@ -1,5 +1,8 @@
# Branches
+!!! danger "Deprecated Feature"
+ This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
+
A branch is a collection of related [staged changes](./stagedchange.md) that have been prepared for merging into the active database. A branch can be merged by executing its `commit()` method. Deleting a branch will delete all its related changes.
## Fields
diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md
index 2353bc2b975..9aab66a36ca 100644
--- a/docs/models/extras/customfield.md
+++ b/docs/models/extras/customfield.md
@@ -42,13 +42,26 @@ The type of data this field holds. This must be one of the following:
For object and multiple-object fields only. Designates the type of NetBox object being referenced.
+### Related Object Filter
+
+!!! info "This field was introduced in NetBox v4.1."
+
+For object and multi-object custom fields, a filter may be defined to limit the available objects when populating a field value. This filter maps object attributes to values. For example, `{"status": "active"}` will include only objects with a status of "active."
+
+!!! warning
+ This setting is employed for convenience only, and should not be relied upon to enforce data integrity.
+
### Weight
A numeric weight used to override alphabetic ordering of fields by name. Custom fields with a lower weight will be listed before those with a higher weight. (Note that weight applies within the context of a custom field group, if defined.)
### Required
-If checked, this custom field must be populated with a valid value for the object to pass validation.
+If enabled, this custom field must be populated with a valid value for the object to pass validation.
+
+### Unique
+
+If enabled, each object must have a unique value set for this custom field (per object type).
### Description
@@ -107,7 +120,3 @@ For numeric custom fields only. The maximum valid value (optional).
### Validation Regex
For string-based custom fields only. A regular expression used to validate the field's value (optional).
-
-### Uniqueness Validation
-
-If enabled, each object must have a unique value set for this custom field (per object type).
diff --git a/docs/models/extras/eventrule.md b/docs/models/extras/eventrule.md
index c105a2630cf..b48e17a1eef 100644
--- a/docs/models/extras/eventrule.md
+++ b/docs/models/extras/eventrule.md
@@ -18,17 +18,22 @@ The type(s) of object in NetBox that will trigger the rule.
If not selected, the event rule will not be processed.
-### Events
+### Events Types
-The events which will trigger the rule. At least one event type must be selected.
+The event types which will trigger the rule. At least one event type must be selected.
-| Name | Description |
-|------------|--------------------------------------|
-| Creations | A new object has been created |
-| Updates | An existing object has been modified |
-| Deletions | An object has been deleted |
-| Job starts | A job for an object starts |
-| Job ends | A job for an object terminates |
+| Name | Description |
+|----------------|---------------------------------------------|
+| Object created | A new object has been created |
+| Object updated | An existing object has been modified |
+| Object deleted | An object has been deleted |
+| Job started | A background job is initiated |
+| Job completed | A background job completes successfully |
+| Job failed | A background job fails |
+| Job errored | A background job is aborted due to an error |
+
+!!! tip "Custom Event Types"
+ The above list includes only built-in event types. NetBox plugins can also register their own custom event types.
### Conditions
diff --git a/docs/models/extras/notification.md b/docs/models/extras/notification.md
new file mode 100644
index 00000000000..e72a35bec53
--- /dev/null
+++ b/docs/models/extras/notification.md
@@ -0,0 +1,17 @@
+# Notification
+
+A notification alerts a user that a specific action has taken place in NetBox, such as an object being modified or a background job completing. A notification may be generated via a user's [subscription](./subscription.md) to a particular object, or by an event rule targeting a [notification group](./notificationgroup.md) of which the user is a member.
+
+## Fields
+
+### User
+
+The recipient of the notification.
+
+### Object
+
+The object to which the notification relates.
+
+### Event Type
+
+The type of event indicated by the notification.
diff --git a/docs/models/extras/notificationgroup.md b/docs/models/extras/notificationgroup.md
new file mode 100644
index 00000000000..6463d137ad3
--- /dev/null
+++ b/docs/models/extras/notificationgroup.md
@@ -0,0 +1,17 @@
+# Notification Group
+
+A set of NetBox users and/or groups of users identified as recipients for certain [notifications](./notification.md).
+
+## Fields
+
+### Name
+
+The name of the notification group.
+
+### Users
+
+One or more users directly designated as members of the notification group.
+
+### Groups
+
+All users of any selected groups are considered as members of the notification group.
diff --git a/docs/models/extras/stagedchange.md b/docs/models/extras/stagedchange.md
index feda2fee6fa..0693a32d31d 100644
--- a/docs/models/extras/stagedchange.md
+++ b/docs/models/extras/stagedchange.md
@@ -1,5 +1,8 @@
# Staged Changes
+!!! danger "Deprecated Feature"
+ This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
+
A staged change represents the creation of a new object or the modification or deletion of an existing object to be performed at some future point. Each change must be assigned to a [branch](./branch.md).
Changes can be applied individually via the `apply()` method, however it is recommended to apply changes in bulk using the parent branch's `commit()` method.
diff --git a/docs/models/extras/subscription.md b/docs/models/extras/subscription.md
new file mode 100644
index 00000000000..3fc4a1f11be
--- /dev/null
+++ b/docs/models/extras/subscription.md
@@ -0,0 +1,15 @@
+# Subscription
+
+A record indicating that a user is to be notified of any changes to a particular NetBox object. A notification maps exactly one user to exactly one object.
+
+When an object to which a user is subscribed changes, a [notification](./notification.md) is generated for the user.
+
+## Fields
+
+### User
+
+The subscribed user.
+
+### Object
+
+The object to which the user is subscribed.
diff --git a/docs/models/ipam/asn.md b/docs/models/ipam/asn.md
index 8de3cfd930c..17fa5ebe3e2 100644
--- a/docs/models/ipam/asn.md
+++ b/docs/models/ipam/asn.md
@@ -1,6 +1,6 @@
# ASNs
-An Autonomous System Number (ASN) is a numeric identifier used in the BGP protocol to identify which [autonomous system](https://en.wikipedia.org/wiki/Autonomous_system_%28Internet%29) a particular prefix is originating and transiting through. NetBox support both 32- and 64- ASNs.
+An Autonomous System Number (ASN) is a numeric identifier used in the Border Gateway Protocol (BGP) to identify which [autonomous system](https://en.wikipedia.org/wiki/Autonomous_system_%28Internet%29) a particular prefix is originating from or transiting through. NetBox supports both 16- and 32-bit ASNs.
ASNs must be globally unique within NetBox, and may be allocated from within a [defined range](./asnrange.md). Each ASN may be assigned to multiple [sites](../dcim/site.md).
@@ -8,7 +8,7 @@ ASNs must be globally unique within NetBox, and may be allocated from within a [
### AS Number
-The 32- or 64-bit AS number.
+The 16- or 32-bit AS number.
### RIR
diff --git a/docs/models/ipam/vlangroup.md b/docs/models/ipam/vlangroup.md
index a2920fb70a4..20989452f90 100644
--- a/docs/models/ipam/vlangroup.md
+++ b/docs/models/ipam/vlangroup.md
@@ -14,9 +14,11 @@ A unique human-friendly name.
A unique URL-friendly identifier. (This value can be used for filtering.)
-### Minimum & Maximum VLAN IDs
+### VLAN ID Ranges
-A minimum and maximum child VLAN ID must be set for each group. (These default to 1 and 4094 respectively.) VLANs created within a group must have a VID that falls between these values (inclusive).
+!!! info "This field replaced the legacy `min_vid` and `max_vid` fields in NetBox v4.1."
+
+The set of VLAN IDs which are encompassed by the group. By default, this will be the entire range of valid IEEE 802.1Q VLAN IDs (1 to 4094, inclusive). VLANs created within a group must have a VID that falls within one of these ranges. Ranges may not overlap.
### Scope
diff --git a/docs/models/virtualization/virtualdisk.md b/docs/models/virtualization/virtualdisk.md
index 9d256bb66c1..3f170c05a76 100644
--- a/docs/models/virtualization/virtualdisk.md
+++ b/docs/models/virtualization/virtualdisk.md
@@ -10,4 +10,4 @@ A human-friendly name that is unique to the assigned virtual machine.
### Size
-The allocated disk size, in gigabytes.
+The allocated disk size, in megabytes.
diff --git a/docs/models/virtualization/virtualmachine.md b/docs/models/virtualization/virtualmachine.md
index 7a801ca656f..7ea31111ce5 100644
--- a/docs/models/virtualization/virtualmachine.md
+++ b/docs/models/virtualization/virtualmachine.md
@@ -50,9 +50,13 @@ The amount of running memory provisioned, in megabytes.
### Disk
-The amount of disk storage provisioned, in gigabytes.
+The amount of disk storage provisioned, in megabytes.
+
+!!! warning
+ This field may be directly modified only on virtual machines which do not define discrete [virtual disks](./virtualdisk.md). Otherwise, it will report the sum of all attached disks.
### Serial Number
-Optional serial number assigned to this VM.
+!!! info "This field was introduced in NetBox v4.1."
+Optional serial number assigned to this virtual machine. Unlike devices, uniqueness is not enforced for virtual machine serial numbers.
diff --git a/docs/models/vpn/l2vpn.md b/docs/models/vpn/l2vpn.md
index 79b7435bfae..1167c1c1780 100644
--- a/docs/models/vpn/l2vpn.md
+++ b/docs/models/vpn/l2vpn.md
@@ -28,6 +28,7 @@ The technology employed in forming and operating the L2VPN. Choices include:
* VXLAN-EVPN
* MPLS-EVPN
* PBB-EVPN
+* EVPN-VPWS
!!! note
Designating the type as VPWS, EPL, EP-LAN, EP-TREE will limit the L2VPN instance to two terminations.
diff --git a/docs/models/wireless/wirelesslink.md b/docs/models/wireless/wirelesslink.md
index e670b69ec4f..7553902b069 100644
--- a/docs/models/wireless/wirelesslink.md
+++ b/docs/models/wireless/wirelesslink.md
@@ -20,6 +20,12 @@ The operational status of the link. Options include:
The service set identifier (SSID) for the wireless link (optional).
+### Distance
+
+!!! info "This field was introduced in NetBox v4.1."
+
+The distance between the link's two endpoints, including a unit designation (e.g. 100 meters or 25 feet).
+
### Authentication Type
The type of wireless authentication in use. Options include:
@@ -40,7 +46,3 @@ The security cipher used to apply wireless authentication. Options include:
### Pre-Shared Key
The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
-
-### Distance
-
-The numeric distance of the link, including a unit designation (e.g. 100 meters or 25 feet).
diff --git a/docs/netbox_logo.png b/docs/netbox_logo.png
deleted file mode 100644
index c6e0a58e625..00000000000
Binary files a/docs/netbox_logo.png and /dev/null differ
diff --git a/docs/netbox_logo.svg b/docs/netbox_logo.svg
index 5321be100ee..93362cbe6ff 100644
--- a/docs/netbox_logo.svg
+++ b/docs/netbox_logo.svg
@@ -1,21 +1,24 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/plugins/development/background-jobs.md b/docs/plugins/development/background-jobs.md
new file mode 100644
index 00000000000..873390a5886
--- /dev/null
+++ b/docs/plugins/development/background-jobs.md
@@ -0,0 +1,99 @@
+# Background Jobs
+
+!!! info "This feature was introduced in NetBox v4.1."
+
+NetBox plugins can defer certain operations by enqueuing [background jobs](../../features/background-jobs.md), which are executed asynchronously by background workers. This is helpful for decoupling long-running processes from the user-facing request-response cycle.
+
+For example, your plugin might need to fetch data from a remote system. Depending on the amount of data and the responsiveness of the remote server, this could take a few minutes. Deferring this task to a queued job ensures that it can be completed in the background, without interrupting the user. The data it fetches can be made available once the job has completed.
+
+## Job Runners
+
+A background job implements a basic [Job](../../models/core/job.md) executor for all kinds of tasks. It has logic implemented to handle the management of the associated job object, rescheduling of periodic jobs in the given interval and error handling. Adding custom jobs is done by subclassing NetBox's `JobRunner` class.
+
+::: netbox.jobs.JobRunner
+
+#### Example
+
+```python title="jobs.py"
+from netbox.jobs import JobRunner
+
+
+class MyTestJob(JobRunner):
+ class Meta:
+ name = "My Test Job"
+
+ def run(self, *args, **kwargs):
+ obj = self.job.object
+ # your logic goes here
+```
+
+You can schedule the background job from within your code (e.g. from a model's `save()` method or a view) by calling `MyTestJob.enqueue()`. This method passes through all arguments to `Job.enqueue()`. However, no `name` argument must be passed, as the background job name will be used instead.
+
+### Attributes
+
+`JobRunner` attributes are defined under a class named `Meta` within the job. These are optional, but encouraged.
+
+#### `name`
+
+This is the human-friendly names of your background job. If omitted, the class name will be used.
+
+### Scheduled Jobs
+
+As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`.
+
+!!! tip
+ It is not forbidden to `enqueue()` additional jobs while an interval schedule is active. An example use of this would be to schedule a periodic daily synchronization, but also trigger additional synchronizations on demand when the user presses a button.
+
+#### Example
+
+```python title="jobs.py"
+from netbox.jobs import JobRunner
+
+
+class MyHousekeepingJob(JobRunner):
+ class Meta:
+ name = "Housekeeping"
+
+ def run(self, *args, **kwargs):
+ # your logic goes here
+```
+
+```python title="__init__.py"
+from netbox.plugins import PluginConfig
+
+class MyPluginConfig(PluginConfig):
+ def ready(self):
+ from .jobs import MyHousekeepingJob
+ MyHousekeepingJob.setup(interval=60)
+```
+
+## Task queues
+
+Three task queues of differing priority are defined by default:
+
+* High
+* Default
+* Low
+
+Any tasks in the "high" queue are completed before the default queue is checked, and any tasks in the default queue are completed before those in the "low" queue.
+
+Plugins can also add custom queues for their own needs by setting the `queues` attribute under the PluginConfig class. An example is included below:
+
+```python
+class MyPluginConfig(PluginConfig):
+ name = 'myplugin'
+ ...
+ queues = [
+ 'foo',
+ 'bar',
+ ]
+```
+
+The `PluginConfig` above creates two custom queues with the following names `my_plugin.foo` and `my_plugin.bar`. (The plugin's name is prepended to each queue to avoid conflicts between plugins.)
+
+!!! warning "Configuring the RQ worker process"
+ By default, NetBox's RQ worker process only services the high, default, and low queues. Plugins which introduce custom queues should advise users to either reconfigure the default worker, or run a dedicated worker specifying the necessary queues. For example:
+
+ ```
+ python manage.py rqworker my_plugin.foo my_plugin.bar
+ ```
diff --git a/docs/plugins/development/background-tasks.md b/docs/plugins/development/background-tasks.md
deleted file mode 100644
index 5ed05752af3..00000000000
--- a/docs/plugins/development/background-tasks.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# Background Tasks
-
-NetBox supports the queuing of tasks that need to be performed in the background, decoupled from the request-response cycle, using the [Python RQ](https://python-rq.org/) library. Three task queues of differing priority are defined by default:
-
-* High
-* Default
-* Low
-
-Any tasks in the "high" queue are completed before the default queue is checked, and any tasks in the default queue are completed before those in the "low" queue.
-
-Plugins can also add custom queues for their own needs by setting the `queues` attribute under the PluginConfig class. An example is included below:
-
-```python
-class MyPluginConfig(PluginConfig):
- name = 'myplugin'
- ...
- queues = [
- 'foo',
- 'bar',
- ]
-```
-
-The PluginConfig above creates two custom queues with the following names `my_plugin.foo` and `my_plugin.bar`. (The plugin's name is prepended to each queue to avoid conflicts between plugins.)
-
-!!! warning "Configuring the RQ worker process"
- By default, NetBox's RQ worker process only services the high, default, and low queues. Plugins which introduce custom queues should advise users to either reconfigure the default worker, or run a dedicated worker specifying the necessary queues. For example:
-
- ```
- python manage.py rqworker my_plugin.foo my_plugin.bar
- ```
diff --git a/docs/plugins/development/event-types.md b/docs/plugins/development/event-types.md
new file mode 100644
index 00000000000..4bcdeea31a1
--- /dev/null
+++ b/docs/plugins/development/event-types.md
@@ -0,0 +1,18 @@
+# Event Types
+
+!!! info "This feature was introduced in NetBox v4.1."
+
+Plugins can register their own custom event types for use with NetBox [event rules](../../models/extras/eventrule.md). This is accomplished by calling the `register()` method on an instance of the `EventType` class. This can be done anywhere within the plugin. An example is provided below.
+
+```python
+from django.utils.translation import gettext_lazy as _
+from netbox.events import EventType, EVENT_TYPE_KIND_SUCCESS
+
+EventType(
+ name='ticket_opened',
+ text=_('Ticket opened'),
+ kind=EVENT_TYPE_KIND_SUCCESS
+).register()
+```
+
+::: netbox.events.EventType
diff --git a/docs/plugins/development/index.md b/docs/plugins/development/index.md
index c042be6ec7e..f3f9a3e4fff 100644
--- a/docs/plugins/development/index.md
+++ b/docs/plugins/development/index.md
@@ -47,6 +47,7 @@ project-name/
- __init__.py
- filtersets.py
- graphql.py
+ - jobs.py
- models.py
- middleware.py
- navigation.py
diff --git a/docs/plugins/development/models.md b/docs/plugins/development/models.md
index 902ee9c82df..03cedda1699 100644
--- a/docs/plugins/development/models.md
+++ b/docs/plugins/development/models.md
@@ -130,6 +130,8 @@ For more information about database migrations, see the [Django documentation](h
::: netbox.models.features.ExportTemplatesMixin
+::: netbox.models.features.JobsMixin
+
::: netbox.models.features.JournalingMixin
::: netbox.models.features.TagsMixin
diff --git a/docs/plugins/development/rest-api.md b/docs/plugins/development/rest-api.md
index 62dd2c882b5..8cb5b371362 100644
--- a/docs/plugins/development/rest-api.md
+++ b/docs/plugins/development/rest-api.md
@@ -25,6 +25,8 @@ project-name/
Serializers are responsible for converting Python objects to JSON data suitable for conveying to consumers, and vice versa. NetBox provides the `NetBoxModelSerializer` class for use by plugins to handle the assignment of tags and custom field data. (These features can also be included ad hoc via the `CustomFieldModelSerializer` and `TaggableModelSerializer` classes.)
+The default nested representation of an object is defined by the `brief_fields` attributes under the serializer's `Meta` class. (Older versions of NetBox required the definition of a separate nested serializer.)
+
#### Example
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.
@@ -41,31 +43,7 @@ class MyModelSerializer(NetBoxModelSerializer):
class Meta:
model = MyModel
fields = ('id', 'foo', 'bar', 'baz')
-```
-
-### Nested Serializers
-
-There are two cases where it is generally desirable to show only a minimal representation of an object:
-
-1. When displaying an object related to the one being viewed (for example, the region to which a site is assigned)
-2. Listing many objects using "brief" mode
-
-To accommodate these, it is recommended to create nested serializers accompanying the "full" serializer for each model. NetBox provides the `WritableNestedSerializer` class for just this purpose. This class accepts a primary key value on write, but displays an object representation for read requests. It also includes a read-only `display` attribute which conveys the string representation of the object.
-
-#### Example
-
-```python
-# api/serializers.py
-from rest_framework import serializers
-from netbox.api.serializers import WritableNestedSerializer
-from my_plugin.models import MyModel
-
-class NestedMyModelSerializer(WritableNestedSerializer):
- foo = SiteSerializer(nested=True, allow_null=True)
-
- class Meta:
- model = MyModel
- fields = ('id', 'display', 'foo')
+ brief_fields = ('id', 'url', 'display', 'bar')
```
## Viewsets
@@ -80,11 +58,11 @@ To create a viewset for a plugin model, subclass `NetBoxModelViewSet` in `api/vi
```python
# api/views.py
-from netbox.api.viewsets import ModelViewSet
+from netbox.api.viewsets import NetBoxModelViewSet
from my_plugin.models import MyModel
from .serializers import MyModelSerializer
-class MyModelViewSet(ModelViewSet):
+class MyModelViewSet(NetBoxModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
```
diff --git a/docs/plugins/development/staged-changes.md b/docs/plugins/development/staged-changes.md
index 64a1a43e023..a8fd1d232e0 100644
--- a/docs/plugins/development/staged-changes.md
+++ b/docs/plugins/development/staged-changes.md
@@ -1,7 +1,7 @@
# Staged Changes
-!!! danger "Experimental Feature"
- This feature is still under active development and considered experimental in nature. Its use in production is strongly discouraged at this time.
+!!! danger "Deprecated Feature"
+ This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
NetBox provides a programmatic API to stage the creation, modification, and deletion of objects without actually committing those changes to the active database. This can be useful for performing a "dry run" of bulk operations, or preparing a set of changes for administrative approval, for example.
diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md
index 9781cfa55a4..1f5f164fdee 100644
--- a/docs/plugins/development/views.md
+++ b/docs/plugins/development/views.md
@@ -196,13 +196,14 @@ Plugins can inject custom content into certain areas of core NetBox views. This
| Method | View | Description |
|---------------------|-------------|-----------------------------------------------------|
| `navbar()` | All | Inject content inside the top navigation bar |
+| `list_buttons()` | List view | Add buttons to the top of the page |
+| `buttons()` | Object view | Add buttons to the top of the page |
+| `alerts()` | Object view | Inject content at the top of the page |
| `left_page()` | Object view | Inject content on the left side of the page |
| `right_page()` | Object view | Inject content on the right side of the page |
| `full_width_page()` | Object view | Inject content across the entire bottom of the page |
-| `buttons()` | Object view | Add buttons to the top of the page |
-| `list_buttons()` | List view | Add buttons to the top of the page |
-!!! info "The `navbar()` method was introduced in NetBox v4.1."
+!!! info "The `navbar()` and `alerts()` methods were introduced in NetBox v4.1."
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
diff --git a/docs/plugins/removal.md b/docs/plugins/removal.md
index f5e81bdc083..37228a939b6 100644
--- a/docs/plugins/removal.md
+++ b/docs/plugins/removal.md
@@ -70,3 +70,19 @@ DROP TABLE
netbox=> DROP TABLE pluginname_bar;
DROP TABLE
```
+
+### Remove the Django Migration Records
+
+After removing the tables created by a plugin, the migrations that created the tables need to be removed from Django's migration history as well. This is necessary to make it possible to reinstall the plugin at a later time. If the migration history were left in place, Django would skip all migrations that were executed in the course of a previous installation, which would cause the plugin to fail after reinstallation.
+
+```no-highlight
+netbox=> SELECT * FROM django_migrations WHERE app='pluginname';
+ id | app | name | applied
+-----+------------+------------------------+-------------------------------
+ 492 | pluginname | 0001_initial | 2023-12-21 11:59:59.325995+00
+ 493 | pluginname | 0002_add_foo | 2023-12-21 11:59:59.330026+00
+netbox=> DELETE FROM django_migrations WHERE app='pluginname';
+```
+
+!!! warning
+ Exercise extreme caution when altering Django system tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.
diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md
index 4537a8800f9..864029eba9c 100644
--- a/docs/release-notes/version-4.0.md
+++ b/docs/release-notes/version-4.0.md
@@ -1,6 +1,133 @@
# NetBox v4.0
-## v4.0.7 (FUTURE)
+## v4.0.11 (2024-09-03)
+
+### Bug Fixes
+
+* [#17310](https://github.com/netbox-community/netbox/issues/17310) - Enforce restricted queryset for related objects in GraphQL API requests
+* [#17321](https://github.com/netbox-community/netbox/issues/17321) - Ensure the job is attributed to the specified user when using the `runscript` management command
+* [#17323](https://github.com/netbox-community/netbox/issues/17323) - Associate job with script object when executed using the `runscript` management command
+* [#17337](https://github.com/netbox-community/netbox/issues/17337) - Fix ordering of virtual device contexts by device name
+* [#17341](https://github.com/netbox-community/netbox/issues/17341) - Avoid `NoReverseMatch` exceptions with specific dashboard widget configurations
+
+## v4.0.10 (2024-08-29)
+
+### Enhancements
+
+* [#16857](https://github.com/netbox-community/netbox/issues/16857) - Scroll long rendered Markdown content within tables
+* [#16905](https://github.com/netbox-community/netbox/issues/16905) - Enable filtering of device components by device status
+* [#16949](https://github.com/netbox-community/netbox/issues/16949) - Add device count column to sites table
+* [#17072](https://github.com/netbox-community/netbox/issues/17072) - Linkify email addresses & phone numbers in contact assignments list
+* [#17177](https://github.com/netbox-community/netbox/issues/17177) - Add facility field to locations filter form
+
+### Bug Fixes
+
+* [#16292](https://github.com/netbox-community/netbox/issues/16292) - Ensure consistent evaluation of queryset for both individual and list GraphQL API queries
+* [#16385](https://github.com/netbox-community/netbox/issues/16385) - Restore support for white, gray, and black background colors
+* [#16640](https://github.com/netbox-community/netbox/issues/16640) - Fix potential corruption of JSON values in custom fields that are not UI-editable
+* [#16670](https://github.com/netbox-community/netbox/issues/16670) - Fix conflicts within OpenAPI schema definition regarding nested serializers
+* [#16733](https://github.com/netbox-community/netbox/issues/16733) - Fix bulk edit/delete of objects when using "select all" widget
+* [#16756](https://github.com/netbox-community/netbox/issues/16756) - Fix dynamic pagination of custom script results table
+* [#16825](https://github.com/netbox-community/netbox/issues/16825) - Avoid `NoReverseMatch` exception when displaying count of related object type with no list view
+* [#16946](https://github.com/netbox-community/netbox/issues/16946) - GraphQL API requests with an invalid filter should return an empty set
+* [#16959](https://github.com/netbox-community/netbox/issues/16959) - Fix function of "reset" button on objects filter form
+* [#16973](https://github.com/netbox-community/netbox/issues/16973) - Fix support for evaluating user token (`$user`) against custom field values in permission constraints
+* [#17007](https://github.com/netbox-community/netbox/issues/17007) - Center SSO authentication icon when backend is unnamed
+* [#17070](https://github.com/netbox-community/netbox/issues/17070) - Image height & width values should not be required when creating an image attachment via the REST API
+* [#17108](https://github.com/netbox-community/netbox/issues/17108) - Ensure template date & time filters always return localtime-aware values
+* [#17117](https://github.com/netbox-community/netbox/issues/17117) - Work around Safari rendering bug
+* [#17186](https://github.com/netbox-community/netbox/issues/17186) - Fix display of custom links with default style under dark mode
+* [#17219](https://github.com/netbox-community/netbox/issues/17219) - Fix system config view exception when custom validator classes are employed
+* [#17230](https://github.com/netbox-community/netbox/issues/17230) - Ensure consistent rendering for all dashboard widget colors
+* [#17256](https://github.com/netbox-community/netbox/issues/17256) - Fix VLAN group scope selection for non-English languages
+* [#17278](https://github.com/netbox-community/netbox/issues/17278) - Ensure hierarchy is recalculated when bulk editing recursively nested object types (e.g. tenant groups)
+* [#17279](https://github.com/netbox-community/netbox/issues/17279) - Do not regenerate key when updating a token via REST API
+* [#17286](https://github.com/netbox-community/netbox/issues/17286) - Fix exception when adding member device to virtual chassis via web UI
+
+---
+
+## v4.0.9 (2024-08-14)
+
+### Enhancements
+
+* [#16692](https://github.com/netbox-community/netbox/issues/16692) - Enable modifying VLAN assignment while bulk editing prefixes
+* [#17006](https://github.com/netbox-community/netbox/issues/17006) - Add IEEE 802.11be interface type
+
+### Bug Fixes
+
+* [#13459](https://github.com/netbox-community/netbox/issues/13459) - Correct OpenAPI schema type for `TreeNodeMultipleChoiceFilter`
+* [#16073](https://github.com/netbox-community/netbox/issues/16073) - Respect default values for custom fields during bulk import of objects
+* [#16176](https://github.com/netbox-community/netbox/issues/16176) - Restore ability to select multiple terminating devices when connecting a cable
+* [#16871](https://github.com/netbox-community/netbox/issues/16871) - Sanitize device ID query parameter when bulk editing components to prevent exception
+* [#17038](https://github.com/netbox-community/netbox/issues/17038) - Fix AttributeError exception when attempting to export system status data
+* [#17064](https://github.com/netbox-community/netbox/issues/17064) - Fix misaligned text within rendered Markdown code blocks
+* [#17124](https://github.com/netbox-community/netbox/issues/17124) - `BaseTable` should follow reverse one-to-one relationships when prefetching related objects
+* [#17131](https://github.com/netbox-community/netbox/issues/17131) - Fix exception when creating object-type custom field without selecting related object type
+* [#17144](https://github.com/netbox-community/netbox/issues/17144) - Avoid showing duplicated pop-up messages
+
+---
+
+## v4.0.8 (2024-07-26)
+
+### Enhancements
+
+* [#14640](https://github.com/netbox-community/netbox/issues/14640) - Add Dutch language support
+* [#14792](https://github.com/netbox-community/netbox/issues/14792) - Add Polish language support
+* [#15375](https://github.com/netbox-community/netbox/issues/15375) - Enable customization of SSO backend name & icon
+* [#15660](https://github.com/netbox-community/netbox/issues/15660) - Add Czech language support
+* [#15696](https://github.com/netbox-community/netbox/issues/15696) - Add Danish language support
+* [#16793](https://github.com/netbox-community/netbox/issues/16793) - Add Italian language support
+* [#16933](https://github.com/netbox-community/netbox/issues/16933) - Enable toggling true/false marks on BooleanColumn
+* [#16943](https://github.com/netbox-community/netbox/issues/16943) - Expand navigation breadcrumbs on job view to include the parent object
+
+### Bug Fixes
+
+* [#16357](https://github.com/netbox-community/netbox/issues/16357) - Replicate assigned type & tenant for cable when clicking "create an add another"
+* [#16402](https://github.com/netbox-community/netbox/issues/16402) - Remove inoperative links from report result view
+* [#16536](https://github.com/netbox-community/netbox/issues/16536) - Revert `role` & `role_id` filters for device components to `device_role` & `device_role_id` to avoid conflict with inventory item `role` field
+* [#16624](https://github.com/netbox-community/netbox/issues/16624) - Correct OpenAPI schema definitions for several fields
+* [#16760](https://github.com/netbox-community/netbox/issues/16760) - Fix data source syncing using git via a local path
+* [#16819](https://github.com/netbox-community/netbox/issues/16819) - Highlight parent device in rack when viewing child device
+* [#16838](https://github.com/netbox-community/netbox/issues/16838) - ActionsColumn should render extra buttons even when no stock actions are enabled
+* [#16867](https://github.com/netbox-community/netbox/issues/16867) - Fix exception when a dashboard list widget references a model which has been removed
+* [#16963](https://github.com/netbox-community/netbox/issues/16963) - Fix filtering of "accounts" link under providers list
+* [#16964](https://github.com/netbox-community/netbox/issues/16964) - Ensure configured password validators are enforced
+
+---
+
+## v4.0.7 (2024-07-09)
+
+### Enhancements
+
+* [#14554](https://github.com/netbox-community/netbox/issues/14554) - Add support for [django-storage-swift](https://github.com/dennisv/django-storage-swift) storage backend
+* [#16424](https://github.com/netbox-community/netbox/issues/16424) - Enable filtering of devices by cluster and cluster group
+* [#16716](https://github.com/netbox-community/netbox/issues/16716) - Display NAT address (if any) for OOB IP address under device view
+* [#16725](https://github.com/netbox-community/netbox/issues/16725) - Always position the admin section last in the navigation menu
+* [#16791](https://github.com/netbox-community/netbox/issues/16791) - Add 200 & 400 Gbps selections for circuit termination port speed
+* [#16802](https://github.com/netbox-community/netbox/issues/16802) - Introduce `SENTRY_SEND_DEFAULT_PII` configuration parameter and disable PII export by default
+* [#16817](https://github.com/netbox-community/netbox/issues/16817) - Add 200 & 400 Gbps selections for circuit commit rate
+
+### Bug Fixes
+
+* [#16523](https://github.com/netbox-community/netbox/issues/16523) - Restore highlighting of current device in virtual chassis members panel
+* [#16654](https://github.com/netbox-community/netbox/issues/16654) - Fix parent item assignment for inventory item bulk import
+* [#16657](https://github.com/netbox-community/netbox/issues/16657) - Fix translation of object types in global search
+* [#16679](https://github.com/netbox-community/netbox/issues/16679) - Avoid overwriting custom JSON fields during bulk edit
+* [#16689](https://github.com/netbox-community/netbox/issues/16689) - System configuration view should reflect static parameters when no config revisions exist
+* [#16714](https://github.com/netbox-community/netbox/issues/16714) - Fix cloning of device types with 0U height
+* [#16721](https://github.com/netbox-community/netbox/issues/16721) - Fix errant API request after deselecting a rack in device edit form
+* [#16723](https://github.com/netbox-community/netbox/issues/16723) - Fix escaping of path to virtual environment in `upgrade.sh`
+* [#16735](https://github.com/netbox-community/netbox/issues/16735) - Object list "results" tab should show a count of zero when empty
+* [#16747](https://github.com/netbox-community/netbox/issues/16747) - Avoid clearing entire search cache when manually reindexing specific apps/models
+* [#16758](https://github.com/netbox-community/netbox/issues/16758) - Ensure manually selected lagnuage persists across browser sessions
+* [#16779](https://github.com/netbox-community/netbox/issues/16779) - Fix saved filter selection for child object lists
+* [#16780](https://github.com/netbox-community/netbox/issues/16780) - IKE proposal created via REST API should not require authentication_algorithm
+* [#16796](https://github.com/netbox-community/netbox/issues/16796) - Allow assignment of VM with no site to a cluster with a site
+* [#16806](https://github.com/netbox-community/netbox/issues/16806) - Fix redirect URL when creating contact assignments with "add another" button
+* [#16807](https://github.com/netbox-community/netbox/issues/16807) - Fix layout of VLAN edit form when custom fields are present
+* [#16808](https://github.com/netbox-community/netbox/issues/16808) - Fix event rule triggering in scenario where objects are updated immediately prior to deletion
+* [#16813](https://github.com/netbox-community/netbox/issues/16813) - Fix AttributeError exception when filtering bookmarks in dashboard widget by object type
+* [#16843](https://github.com/netbox-community/netbox/issues/16843) - Permit creation of IKE policies via REST API without specifying an IKE mode
---
diff --git a/docs/release-notes/version-4.1.md b/docs/release-notes/version-4.1.md
index c6c260ee01c..d48bb899ff4 100644
--- a/docs/release-notes/version-4.1.md
+++ b/docs/release-notes/version-4.1.md
@@ -1,29 +1,227 @@
# NetBox v4.1
-## v4.1.0 (FUTURE)
+## v4.1.4 (2024-10-15)
+
+### Enhancements
+
+* [#11671](https://github.com/netbox-community/netbox/issues/11671) - Display device's rack position in cable traces
+* [#15829](https://github.com/netbox-community/netbox/issues/15829) - Rename Microsoft Azure AD SSO backend to Microsoft Entra ID
+* [#16009](https://github.com/netbox-community/netbox/issues/16009) - Float form & bulk operation buttons within UI
+* [#17079](https://github.com/netbox-community/netbox/issues/17079) - Introduce additional choices for device airflow direction
+* [#17216](https://github.com/netbox-community/netbox/issues/17216) - Add EVPN-VPWS L2VPN type
+* [#17655](https://github.com/netbox-community/netbox/issues/17655) - Limit the display of tagged VLANs within interface tables
+* [#17669](https://github.com/netbox-community/netbox/issues/17669) - Enable filtering VLANs by assigned device or VM interface
+
+### Bug Fixes
+
+* [#16024](https://github.com/netbox-community/netbox/issues/16024) - Fix AND/OR filtering in GraphQL API for selection fields
+* [#17400](https://github.com/netbox-community/netbox/issues/17400) - Fix cable tracing across split paths
+* [#17562](https://github.com/netbox-community/netbox/issues/17562) - Fix GraphQL API query support for custom field choices
+* [#17566](https://github.com/netbox-community/netbox/issues/17566) - Fix AttributeError exception resulting from background jobs with no associated object type
+* [#17614](https://github.com/netbox-community/netbox/issues/17614) - Disallow removal of a master device from its virtual chassis
+* [#17636](https://github.com/netbox-community/netbox/issues/17636) - Fix filtering of related objects when adding a power port, rear port, or inventory item template to a device type
+* [#17644](https://github.com/netbox-community/netbox/issues/17644) - Correct sizing of logo & SSO icons on login page
+* [#17648](https://github.com/netbox-community/netbox/issues/17648) - Fix AttributeError exception when attempting to delete a background job under certain conditions
+* [#17663](https://github.com/netbox-community/netbox/issues/17663) - Fix extended lookups for choice field filters
+* [#17671](https://github.com/netbox-community/netbox/issues/17671) - Fix the display of rack types in global search results
+* [#17713](https://github.com/netbox-community/netbox/issues/17713) - Fix UnboundLocalError exception when attempting to sync data source in parallel
+
+---
+
+## v4.1.3 (2024-10-02)
+
+### Enhancements
+
+* [#17639](https://github.com/netbox-community/netbox/issues/17639) - Add SOCKS support to proxy settings for Git remote data sources
+
+### Bug Fixes
+
+* [#17558](https://github.com/netbox-community/netbox/issues/17558) - Raise validation error when attempting to remove a custom field choice in use
+
+---
+
+## v4.1.2 (2024-09-26)
+
+### Enhancements
+
+* [#14201](https://github.com/netbox-community/netbox/issues/14201) - Enable global search for AS numbers using "AS" prefix
+* [#15408](https://github.com/netbox-community/netbox/issues/15408) - Enable bulk import of primary IPv4 & IPv6 addresses for virtual device contexts (VDCs)
+* [#16781](https://github.com/netbox-community/netbox/issues/16781) - Add 100Base-X SFP interface type
+* [#17255](https://github.com/netbox-community/netbox/issues/17255) - Include return URL when creating new IP address from prefix IPs list
+* [#17471](https://github.com/netbox-community/netbox/issues/17471) - Add Eaton C39 power outlet type
+* [#17482](https://github.com/netbox-community/netbox/issues/17482) - Do not preload Branch & StagedChange models in `nbshell`
+* [#17550](https://github.com/netbox-community/netbox/issues/17550) - Add IEEE 802.15.4 wireless interface type
+
+### Bug Fixes
+
+* [#16837](https://github.com/netbox-community/netbox/issues/16837) - Fix filtering of cables with no type assigned
+* [#17083](https://github.com/netbox-community/netbox/issues/17083) - Trim clickable area of form field labels
+* [#17126](https://github.com/netbox-community/netbox/issues/17126) - Show total device weight in both imperial & metric units
+* [#17360](https://github.com/netbox-community/netbox/issues/17360) - Fix AttributeError under child object views when experimental HTMX navigation is enabled
+* [#17406](https://github.com/netbox-community/netbox/issues/17406) - Fix the cleanup of stale custom field data after removing a plugin
+* [#17419](https://github.com/netbox-community/netbox/issues/17419) - Rebuild MPTT for module bays on upgrade to v4.1
+* [#17492](https://github.com/netbox-community/netbox/issues/17492) - Fix URL resolution in `NetBoxModelSerializer` for plugin models
+* [#17497](https://github.com/netbox-community/netbox/issues/17497) - Fix uncaught FieldError exception when referencing an invalid field on a related object during bulk import
+* [#17498](https://github.com/netbox-community/netbox/issues/17498) - Fix MultipleObjectsReturned exception when importing a device type without uniquely specifying a manufacturer
+* [#17501](https://github.com/netbox-community/netbox/issues/17501) - Fix reporting of last run time & status for custom scripts under UI
+* [#17511](https://github.com/netbox-community/netbox/issues/17511) - Restore consistent font support for non-Latin characters
+* [#17517](https://github.com/netbox-community/netbox/issues/17517) - Fix cable termination selection after switching termination type
+* [#17521](https://github.com/netbox-community/netbox/issues/17521) - Correct text color in notification pop-ups under dark mode
+* [#17522](https://github.com/netbox-community/netbox/issues/17522) - Fix language translation of form field labels under user preferences
+* [#17537](https://github.com/netbox-community/netbox/issues/17537) - Fix global search support for ASN range names
+* [#17555](https://github.com/netbox-community/netbox/issues/17555) - Fix toggling disconnected interfaces under device view
+* [#17601](https://github.com/netbox-community/netbox/issues/17601) - Record change to terminating object when disconnecting a cable
+* [#17605](https://github.com/netbox-community/netbox/issues/17605) - Fix calculation of aggregate VM disk space under cluster view
+* [#17611](https://github.com/netbox-community/netbox/issues/17611) - Correct custom field minimum value validation error message
+
+---
+
+## v4.1.1 (2024-09-12)
+
+### Enhancements
+
+* [#16926](https://github.com/netbox-community/netbox/issues/16926) - Add USB front & rear port types
+* [#17347](https://github.com/netbox-community/netbox/issues/17347) - Add NEMA L22-20 power port & outlet types
+
+### Bug Fixes
+
+* [#17066](https://github.com/netbox-community/netbox/issues/17066) - Fix OpenAPI schema definition for custom scripts REST API endpoint
+* [#17332](https://github.com/netbox-community/netbox/issues/17332) - Restore pagination for object list dashboard widgets
+* [#17333](https://github.com/netbox-community/netbox/issues/17333) - Avoid prefetching all jobs when retrieving custom scripts via the REST API
+* [#17353](https://github.com/netbox-community/netbox/issues/17353) - Fix styling of map buttons under site and device views
+* [#17354](https://github.com/netbox-community/netbox/issues/17354) - Prevent object & multi-object custom fields from breaking bulk import forms
+* [#17362](https://github.com/netbox-community/netbox/issues/17362) - Remove duplicate prefixes & IP addresses returned by the `present_in_vrf` query filter
+* [#17364](https://github.com/netbox-community/netbox/issues/17364) - Fix rendering of Markdown tables inside object list dashboard widgets
+* [#17387](https://github.com/netbox-community/netbox/issues/17387) - Fix display of the changelog tab for users with sufficient permission
+* [#17410](https://github.com/netbox-community/netbox/issues/17410) - Enable debug toolbar middleware for `strawberry-django` only when `DEBUG` is true
+* [#17414](https://github.com/netbox-community/netbox/issues/17414) - Fix support for declaring individual VLAN IDs within a VLAN group
+* [#17431](https://github.com/netbox-community/netbox/issues/17431) - Fix database migration error when upgrading to v4.1 from v3.7 or earlier
+* [#17437](https://github.com/netbox-community/netbox/issues/17437) - Fix exception when specifying a bridge relationship on an interface template
+* [#17444](https://github.com/netbox-community/netbox/issues/17444) - Custom script fails to execute when triggered by an event rule
+* [#17457](https://github.com/netbox-community/netbox/issues/17457) - GraphQL `service_list` filter should not require a port number
+
+---
+
+## v4.1.0 (2024-09-03)
### Breaking Changes
* Several filters deprecated in v4.0 have been removed (see [#15410](https://github.com/netbox-community/netbox/issues/15410)).
-* The unit size for virtual disk size has been changed from 1 gigabyte to 1 megabyte. Existing values have been updated accordingly.
+* The unit size for `VirtualMachine.disk` and `VirtualDisk.size` has been changed from 1 gigabyte to 1 megabyte. Existing values will be adjusted automatically during the upgrade process.
+* The `min_vid` and `max_vid` fields on the VLAN group model have been replaced with `vid_ranges`, an array of starting and ending VLAN ID pairs.
+* The five individual event type fields on the EventRule model have been replaced by a single `event_types` array field, which lists applicable event types by name.
+* All UI views & API endpoints associated with change records have been moved from `/extras` to `/core`.
+* The `validate()` method on CustomValidator subclasses now **must** accept the request argument (deprecated in v4.0 by [#14279](https://github.com/netbox-community/netbox/issues/14279/)).
### New Features
+#### Circuit Groups ([#7025](https://github.com/netbox-community/netbox/issues/7025))
+
+Circuits can now be assigned to groups for administrative purposes. Each circuit may be assigned to multiple groups, and each assignment may optionally indicate a priority (primary, secondary, or tertiary).
+
+#### VLAN Group ID Ranges ([#9627](https://github.com/netbox-community/netbox/issues/9627))
+
+The VLAN group model has been enhanced to support multiple VLAN ID (VID) ranges, whereas previously it could track only a single beginning and ending VID pair. VID ranges are stored as an array of beginning and ending (inclusive) integer pairs, e.g. `1-100,1000-1999`.
+
+#### Nested Device Modules ([#10500](https://github.com/netbox-community/netbox/issues/10500))
+
+Module bays can now be added to modules to effect a hierarchical arrangement of submodules within a device. A module installed within a device's module bay may itself have module bays into which child modules may be installed.
+
+#### Rack Types ([#12826](https://github.com/netbox-community/netbox/issues/12826))
+
+A new rack type model has been introduced, which functions similarly to device types. Users can now define a common make and model of equipment rack, the attributes of which are automatically populated when creating a new rack of that type. Backward compatibility for racks with individually defined characteristics is fully retained.
+
+#### Plugins Catalog Integration ([#14731](https://github.com/netbox-community/netbox/issues/14731))
+
+The NetBox UI now integrates directly with the canonical [plugins catalog](https://netboxlabs.com/netbox-plugins/) hosted by [NetBox Labs](https://netboxlabs.com). Users can now explore available plugins and check for newer releases natively within the NetBox user interface.
+
+#### User Notifications ([#15621](https://github.com/netbox-community/netbox/issues/15621))
+
+NetBox now includes a user notification system. Users can subscribe to individual objects and be alerted to changes within the web interface. Additionally, event rules can be created to trigger notifications for specific users and/or groups. Plugins can also employ this notification system for their own purposes.
+
### Enhancements
* [#7537](https://github.com/netbox-community/netbox/issues/7537) - Add a serial number field for virtual machines
+* [#8198](https://github.com/netbox-community/netbox/issues/8198) - Enable uniqueness enforcement for custom field values
+* [#8984](https://github.com/netbox-community/netbox/issues/8984) - Enable filtering of custom script output by log level
+* [#11969](https://github.com/netbox-community/netbox/issues/11969) - Support for tracking airflow on racks and module types
+* [#14656](https://github.com/netbox-community/netbox/issues/14656) - Dynamically render the custom field edit form depending on the selected field type
+* [#15106](https://github.com/netbox-community/netbox/issues/15106) - Add `distance` and `distance_unit` fields for wireless links
+* [#15156](https://github.com/netbox-community/netbox/issues/15156) - Add `display_url` field to all REST API serializers, which links to the corresponding UI view for an object
+* [#16574](https://github.com/netbox-community/netbox/issues/16574) - Add `last_synced` time to REST API serializer for data sources
+* [#16580](https://github.com/netbox-community/netbox/issues/16580) - Enable plugin views to enforce `LOGIN_REQUIRED` selectively (remove `AUTH_EXEMPT_PATHS`)
+* [#16782](https://github.com/netbox-community/netbox/issues/16782) - Enable filtering of selection choices for object and multi-object custom fields
+* [#16907](https://github.com/netbox-community/netbox/issues/16907) - Update user interface styling
+* [#17051](https://github.com/netbox-community/netbox/issues/17051) - Introduce `ISOLATED_DEPLOYMENT` config parameter for denoting Internet isolation
+* [#17221](https://github.com/netbox-community/netbox/issues/17221) - `ObjectEditView` now supports HTMX-based object editing
+* [#17288](https://github.com/netbox-community/netbox/issues/17288) - Introduce a configurable limit on the number of aliases within a GraphQL API request
+* [#17289](https://github.com/netbox-community/netbox/issues/17289) - Enforce a standard policy for local passwords by default
+* [#17318](https://github.com/netbox-community/netbox/issues/17318) - Include the assigned provider in nested API representation of circuits
+
+### Bug Fixes (From Beta1)
+
+* [#17086](https://github.com/netbox-community/netbox/issues/17086) - Fix exception when viewing a job with no related object
+* [#17097](https://github.com/netbox-community/netbox/issues/17097) - Record static object representation when calling `NotificationGroup.notify()`
+* [#17098](https://github.com/netbox-community/netbox/issues/17098) - Prevent automatic deletion of related notifications when deleting an object
+* [#17159](https://github.com/netbox-community/netbox/issues/17159) - Correct file paths in plugin installation instructions
+* [#17163](https://github.com/netbox-community/netbox/issues/17163) - Fix filtering of related services under IP address view
+* [#17169](https://github.com/netbox-community/netbox/issues/17169) - Avoid duplicating catalog listings for installed plugins
+* [#17301](https://github.com/netbox-community/netbox/issues/17301) - Correct styling of the edit & delete buttons for custom script modules
+* [#17302](https://github.com/netbox-community/netbox/issues/17302) - Fix log level filtering support for custom script messages
+* [#17306](https://github.com/netbox-community/netbox/issues/17306) - Correct rounding of reported VLAN group utilization
+
+### Plugins
+
+* [#15692](https://github.com/netbox-community/netbox/issues/15692) - Introduce improved plugin support for background jobs
* [#16359](https://github.com/netbox-community/netbox/issues/16359) - Enable plugins to embed content in the top navigation bar
+* [#16726](https://github.com/netbox-community/netbox/issues/16726) - Extend `PluginTemplateExtension` to enable registering multiple models
+* [#16776](https://github.com/netbox-community/netbox/issues/16776) - Add an `alerts()` method to `PluginTemplateExtension` for embedding important information on object views
+* [#16886](https://github.com/netbox-community/netbox/issues/16886) - Introduce a mechanism for plugins to register custom event types (for use with user notifications)
### Other Changes
-* [#14692](https://github.com/netbox-community/netbox/issues/14692) - Change atomic unit for virtual disks from 1GB to 1MB
-* [#15410](https://github.com/netbox-community/netbox/issues/15410) - Removed various deprecated filters
+* [#14692](https://github.com/netbox-community/netbox/issues/14692) - Change the atomic unit for virtual disks from 1GB to 1MB
+* [#14861](https://github.com/netbox-community/netbox/issues/14861) - The URL path for UI views concerning virtual disks has been standardized to `/virtualization/virtual-disks/`
+* [#15410](https://github.com/netbox-community/netbox/issues/15410) - Remove various deprecated query filters
* [#15908](https://github.com/netbox-community/netbox/issues/15908) - Indicate product edition in release data
* [#16388](https://github.com/netbox-community/netbox/issues/16388) - Move all change logging resources from `extras` to `core`
+* [#16884](https://github.com/netbox-community/netbox/issues/16884) - Remove the ID column from the default table configuration for changelog records
+* [#16988](https://github.com/netbox-community/netbox/issues/16988) - Relocate rack items in navigation menu
+* [#17143](https://github.com/netbox-community/netbox/issues/17143) - The use of legacy "nested" serializer classes has been deprecated
### REST API Changes
-* The `/api/extras/object-changes/` endpoint has moved to `/api/core/object-changes/`
+* The `/api/extras/object-changes/` endpoint has moved to `/api/core/object-changes/`.
+* Most object representations now include a read-only `display_url` field, which links to the object's corresponding UI view.
+* Added the following endpoints:
+ * `/api/circuits/circuit-groups/`
+ * `/api/circuits/circuit-group-assignments/`
+ * `/api/dcim/rack-types/`
+ * `/api/extras/notification-groups/`
+ * `/api/extras/notifications/`
+ * `/api/extras/subscriptions/`
+* circuits.Circuit
+ * Added the `assignments` field, which lists all group assignments
+* core.DataSource
+ * Added the read-only `last_synced` field
+* dcim.ModuleBay
+ * Added the optional `module` foreign key field
+* dcim.ModuleBayTemplate
+ * Added the optional `module_type` foreign key field
+* dcim.ModuleType
+ * Added the optional `airflow` choice field
+* dcim.Rack
+ * Added the optional `rack_type` foreign key field
+ * Added the optional `airflow` choice field
+* extras.CustomField
+ * Added the `related_object_filter` JSON field for object and multi-object custom fields
+ * Added the `validation_unique` boolean field
+* extras.EventRule
+ * Removed the `type_create`, `type_update`, `type_delete`, `type_job_start`, and `type_job_end` boolean fields
+ * Added the `event_types` array field
+* ipam.VLANGroup
+ * Removed the `min_vid` and `max_vid` fields
+ * Added the `vid_ranges` field, an array of starting & ending VLAN IDs
* virtualization.VirtualMachine
* Added the optional `serial` field
* wireless.WirelessLink
diff --git a/mkdocs.yml b/mkdocs.yml
index f90ef4dbe22..94a4edcb32d 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -86,6 +86,7 @@ nav:
- Change Logging: 'features/change-logging.md'
- Journaling: 'features/journaling.md'
- Event Rules: 'features/event-rules.md'
+ - Notifications: 'features/notifications.md'
- Background Jobs: 'features/background-jobs.md'
- Auth & Permissions: 'features/authentication-permissions.md'
- API & Integration: 'features/api-integration.md'
@@ -108,6 +109,7 @@ nav:
- Required Parameters: 'configuration/required-parameters.md'
- System: 'configuration/system.md'
- Security: 'configuration/security.md'
+ - GraphQL API: 'configuration/graphql-api.md'
- Remote Authentication: 'configuration/remote-authentication.md'
- Data & Validation: 'configuration/data-validation.md'
- Default Values: 'configuration/default-values.md'
@@ -142,10 +144,11 @@ nav:
- Forms: 'plugins/development/forms.md'
- Filters & Filter Sets: 'plugins/development/filtersets.md'
- Search: 'plugins/development/search.md'
+ - Event Types: 'plugins/development/event-types.md'
- Data Backends: 'plugins/development/data-backends.md'
- REST API: 'plugins/development/rest-api.md'
- GraphQL API: 'plugins/development/graphql-api.md'
- - Background Tasks: 'plugins/development/background-tasks.md'
+ - Background Jobs: 'plugins/development/background-jobs.md'
- Dashboard Widgets: 'plugins/development/dashboard-widgets.md'
- Staged Changes: 'plugins/development/staged-changes.md'
- Exceptions: 'plugins/development/exceptions.md'
@@ -153,7 +156,7 @@ nav:
- Administration:
- Authentication:
- Overview: 'administration/authentication/overview.md'
- - Microsoft Azure AD: 'administration/authentication/microsoft-azure-ad.md'
+ - Microsoft Entra ID: 'administration/authentication/microsoft-entra-id.md'
- Okta: 'administration/authentication/okta.md'
- Permissions: 'administration/permissions.md'
- Error Reporting: 'administration/error-reporting.md'
@@ -163,6 +166,8 @@ nav:
- Data Model:
- Circuits:
- Circuit: 'models/circuits/circuit.md'
+ - CircuitGroup: 'models/circuits/circuitgroup.md'
+ - CircuitGroupAssignment: 'models/circuits/circuitgroupassignment.md'
- Circuit Termination: 'models/circuits/circuittermination.md'
- Circuit Type: 'models/circuits/circuittype.md'
- Provider: 'models/circuits/provider.md'
@@ -206,6 +211,7 @@ nav:
- Rack: 'models/dcim/rack.md'
- RackReservation: 'models/dcim/rackreservation.md'
- RackRole: 'models/dcim/rackrole.md'
+ - RackType: 'models/dcim/racktype.md'
- RearPort: 'models/dcim/rearport.md'
- RearPortTemplate: 'models/dcim/rearporttemplate.md'
- Region: 'models/dcim/region.md'
@@ -225,8 +231,11 @@ nav:
- ExportTemplate: 'models/extras/exporttemplate.md'
- ImageAttachment: 'models/extras/imageattachment.md'
- JournalEntry: 'models/extras/journalentry.md'
+ - Notification: 'models/extras/notification.md'
+ - NotificationGroup: 'models/extras/notificationgroup.md'
- SavedFilter: 'models/extras/savedfilter.md'
- StagedChange: 'models/extras/stagedchange.md'
+ - Subscription: 'models/extras/subscription.md'
- Tag: 'models/extras/tag.md'
- Webhook: 'models/extras/webhook.md'
- IPAM:
diff --git a/netbox/account/urls.py b/netbox/account/urls.py
index 1276dce40d1..d74677599ee 100644
--- a/netbox/account/urls.py
+++ b/netbox/account/urls.py
@@ -9,6 +9,8 @@ urlpatterns = [
# Account views
path('profile/', views.ProfileView.as_view(), name='profile'),
path('bookmarks/', views.BookmarkListView.as_view(), name='bookmarks'),
+ path('notifications/', views.NotificationListView.as_view(), name='notifications'),
+ path('subscriptions/', views.SubscriptionListView.as_view(), name='subscriptions'),
path('preferences/', views.UserConfigView.as_view(), name='preferences'),
path('password/', views.ChangePasswordView.as_view(), name='change_password'),
path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'),
diff --git a/netbox/account/views.py b/netbox/account/views.py
index 5220c6fe84b..05f40df3f00 100644
--- a/netbox/account/views.py
+++ b/netbox/account/views.py
@@ -22,7 +22,7 @@ from account.models import UserToken
from core.models import ObjectChange
from core.tables import ObjectChangeTable
from extras.models import Bookmark
-from extras.tables import BookmarkTable
+from extras.tables import BookmarkTable, NotificationTable, SubscriptionTable
from netbox.authentication import get_auth_backend_display, get_saml_idps
from netbox.config import get_config
from netbox.views import generic
@@ -46,10 +46,20 @@ class LoginView(View):
return super().dispatch(*args, **kwargs)
def gen_auth_data(self, name, url, params):
- display_name, icon_name = get_auth_backend_display(name)
+ display_name, icon_source = get_auth_backend_display(name)
+
+ icon_name = None
+ icon_img = None
+ if icon_source:
+ if '://' in icon_source:
+ icon_img = icon_source
+ else:
+ icon_name = icon_source
+
return {
'display_name': display_name,
'icon_name': icon_name,
+ 'icon_img': icon_img,
'url': f'{url}?{urlencode(params)}',
}
@@ -101,7 +111,7 @@ class LoginView(View):
# Authenticate user
auth_login(request, form.get_user())
logger.info(f"User {request.user} successfully authenticated")
- messages.success(request, f"Logged in as {request.user}.")
+ messages.success(request, _("Logged in as {user}.").format(user=request.user))
# Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.)
@@ -113,7 +123,7 @@ class LoginView(View):
# Set the user's preferred language (if any)
if language := request.user.config.get('locale.language'):
- response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
+ response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
return response
@@ -151,7 +161,7 @@ class LogoutView(View):
username = request.user
auth_logout(request)
logger.info(f"User {username} has logged out")
- messages.info(request, "You have logged out.")
+ messages.info(request, _("You have logged out."))
# Delete session key & language cookies (if set) upon logout
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
@@ -208,7 +218,7 @@ class UserConfigView(LoginRequiredMixin, View):
# Set/clear language cookie
if language := form.cleaned_data['locale.language']:
- response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
+ response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
else:
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
@@ -226,7 +236,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
def get(self, request):
# LDAP users cannot change their password here
if getattr(request.user, 'ldap_username', None):
- messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
+ messages.warning(request, _("LDAP-authenticated user credentials cannot be changed within NetBox."))
return redirect('account:profile')
form = PasswordChangeForm(user=request.user)
@@ -241,7 +251,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
if form.is_valid():
form.save()
update_session_auth_hash(request, form.user)
- messages.success(request, "Your password has been changed successfully.")
+ messages.success(request, _("Your password has been changed successfully."))
return redirect('account:profile')
return render(request, self.template_name, {
@@ -267,6 +277,36 @@ class BookmarkListView(LoginRequiredMixin, generic.ObjectListView):
}
+#
+# Notifications & subscriptions
+#
+
+class NotificationListView(LoginRequiredMixin, generic.ObjectListView):
+ table = NotificationTable
+ template_name = 'account/notifications.html'
+
+ def get_queryset(self, request):
+ return request.user.notifications.all()
+
+ def get_extra_context(self, request):
+ return {
+ 'active_tab': 'notifications',
+ }
+
+
+class SubscriptionListView(LoginRequiredMixin, generic.ObjectListView):
+ table = SubscriptionTable
+ template_name = 'account/subscriptions.html'
+
+ def get_queryset(self, request):
+ return request.user.subscriptions.all()
+
+ def get_extra_context(self, request):
+ return {
+ 'active_tab': 'subscriptions',
+ }
+
+
#
# User views for token management
#
diff --git a/netbox/circuits/api/nested_serializers.py b/netbox/circuits/api/nested_serializers.py
index 4201b37612d..487749872aa 100644
--- a/netbox/circuits/api/nested_serializers.py
+++ b/netbox/circuits/api/nested_serializers.py
@@ -1,9 +1,11 @@
+import warnings
+
from drf_spectacular.utils import extend_schema_serializer
-from rest_framework import serializers
from circuits.models import *
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
+from .serializers_.nested import NestedProviderAccountSerializer
__all__ = [
'NestedCircuitSerializer',
@@ -14,6 +16,12 @@ __all__ = [
'NestedProviderAccountSerializer',
]
+# TODO: Remove in v4.2
+warnings.warn(
+ "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+ DeprecationWarning
+)
+
#
# Provider networks
@@ -41,17 +49,6 @@ class NestedProviderSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
-#
-# Provider Accounts
-#
-
-class NestedProviderAccountSerializer(WritableNestedSerializer):
-
- class Meta:
- model = ProviderAccount
- fields = ['id', 'url', 'display_url', 'display', 'name', 'account']
-
-
#
# Circuits
#
diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py
index 5e048218c5f..9a2af4a6749 100644
--- a/netbox/circuits/api/serializers.py
+++ b/netbox/circuits/api/serializers.py
@@ -1,3 +1,2 @@
from .serializers_.providers import *
from .serializers_.circuits import *
-from .nested_serializers import *
diff --git a/netbox/circuits/api/serializers_/circuits.py b/netbox/circuits/api/serializers_/circuits.py
index 7010bb2c6bb..111fa6f8726 100644
--- a/netbox/circuits/api/serializers_/circuits.py
+++ b/netbox/circuits/api/serializers_/circuits.py
@@ -1,17 +1,18 @@
-from rest_framework import serializers
-
-from circuits.choices import CircuitStatusChoices
-from circuits.models import Circuit, CircuitTermination, CircuitType
+from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices
+from circuits.models import Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType
from dcim.api.serializers_.cables import CabledObjectSerializer
from dcim.api.serializers_.sites import SiteSerializer
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
+from netbox.choices import DistanceUnitChoices
from tenancy.api.serializers_.tenants import TenantSerializer
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
__all__ = (
'CircuitSerializer',
+ 'CircuitGroupAssignmentSerializer',
+ 'CircuitGroupSerializer',
'CircuitTerminationSerializer',
'CircuitTypeSerializer',
)
@@ -43,6 +44,34 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
]
+class CircuitGroupSerializer(NetBoxModelSerializer):
+ tenant = TenantSerializer(nested=True, required=False, allow_null=True)
+ circuit_count = RelatedObjectCountField('assignments')
+
+ class Meta:
+ model = CircuitGroup
+ fields = [
+ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant',
+ 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count'
+ ]
+ brief_fields = ('id', 'url', 'display', 'name')
+
+
+class CircuitGroupAssignmentSerializer_(NetBoxModelSerializer):
+ """
+ Base serializer for group assignments under CircuitSerializer.
+ """
+ group = CircuitGroupSerializer(nested=True)
+ priority = ChoiceField(choices=CircuitPriorityChoices, allow_blank=True, required=False)
+
+ class Meta:
+ model = CircuitGroupAssignment
+ fields = [
+ 'id', 'url', 'display_url', 'display', 'group', 'priority', 'tags', 'created', 'last_updated',
+ ]
+ brief_fields = ('id', 'url', 'display', 'group', 'priority')
+
+
class CircuitSerializer(NetBoxModelSerializer):
provider = ProviderSerializer(nested=True)
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
@@ -51,15 +80,17 @@ class CircuitSerializer(NetBoxModelSerializer):
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
+ assignments = CircuitGroupAssignmentSerializer_(nested=True, many=True, required=False)
+ distance_unit = ChoiceField(choices=DistanceUnitChoices, allow_blank=True, required=False, allow_null=True)
class Meta:
model = Circuit
fields = [
'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z',
- 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+ 'distance', 'distance_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments',
]
- brief_fields = ('id', 'url', 'display', 'cid', 'description')
+ brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
@@ -75,3 +106,14 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
+
+
+class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
+ circuit = CircuitSerializer(nested=True)
+
+ class Meta:
+ model = CircuitGroupAssignment
+ fields = [
+ 'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
+ ]
+ brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
diff --git a/netbox/circuits/api/serializers_/nested.py b/netbox/circuits/api/serializers_/nested.py
new file mode 100644
index 00000000000..7f03de94237
--- /dev/null
+++ b/netbox/circuits/api/serializers_/nested.py
@@ -0,0 +1,13 @@
+from circuits.models import ProviderAccount
+from netbox.api.serializers import WritableNestedSerializer
+
+__all__ = (
+ 'NestedProviderAccountSerializer',
+)
+
+
+class NestedProviderAccountSerializer(WritableNestedSerializer):
+
+ class Meta:
+ model = ProviderAccount
+ fields = ['id', 'url', 'display_url', 'display', 'name', 'account']
diff --git a/netbox/circuits/api/serializers_/providers.py b/netbox/circuits/api/serializers_/providers.py
index b0e27754b93..4e37871079b 100644
--- a/netbox/circuits/api/serializers_/providers.py
+++ b/netbox/circuits/api/serializers_/providers.py
@@ -5,7 +5,7 @@ from ipam.api.serializers_.asns import ASNSerializer
from ipam.models import ASN
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
-from ..nested_serializers import *
+from .nested import NestedProviderAccountSerializer
__all__ = (
'ProviderAccountSerializer',
diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py
index fcb7a1a512c..00af3dec684 100644
--- a/netbox/circuits/api/urls.py
+++ b/netbox/circuits/api/urls.py
@@ -14,6 +14,8 @@ router.register('provider-networks', views.ProviderNetworkViewSet)
router.register('circuit-types', views.CircuitTypeViewSet)
router.register('circuits', views.CircuitViewSet)
router.register('circuit-terminations', views.CircuitTerminationViewSet)
+router.register('circuit-groups', views.CircuitGroupViewSet)
+router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet)
app_name = 'circuits-api'
urlpatterns = router.urls
diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py
index fffb59a5740..8cce013d74f 100644
--- a/netbox/circuits/api/views.py
+++ b/netbox/circuits/api/views.py
@@ -55,6 +55,26 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
filterset_class = filtersets.CircuitTerminationFilterSet
+#
+# Circuit Groups
+#
+
+class CircuitGroupViewSet(NetBoxModelViewSet):
+ queryset = CircuitGroup.objects.all()
+ serializer_class = serializers.CircuitGroupSerializer
+ filterset_class = filtersets.CircuitGroupFilterSet
+
+
+#
+# Circuit Group Assignments
+#
+
+class CircuitGroupAssignmentViewSet(NetBoxModelViewSet):
+ queryset = CircuitGroupAssignment.objects.all()
+ serializer_class = serializers.CircuitGroupAssignmentSerializer
+ filterset_class = filtersets.CircuitGroupAssignmentFilterSet
+
+
#
# Provider accounts
#
diff --git a/netbox/circuits/apps.py b/netbox/circuits/apps.py
index df6804303b7..4d5f177e24e 100644
--- a/netbox/circuits/apps.py
+++ b/netbox/circuits/apps.py
@@ -7,7 +7,7 @@ class CircuitsConfig(AppConfig):
def ready(self):
from netbox.models.features import register_models
- from . import signals, search
+ from . import signals, search # noqa: F401
# Register models
register_models(*self.get_models())
diff --git a/netbox/circuits/choices.py b/netbox/circuits/choices.py
index 5d0065edc7a..8c25c7459de 100644
--- a/netbox/circuits/choices.py
+++ b/netbox/circuits/choices.py
@@ -38,6 +38,8 @@ class CircuitCommitRateChoices(ChoiceSet):
(25000000, '25 Gbps'),
(40000000, '40 Gbps'),
(100000000, '100 Gbps'),
+ (200000000, '200 Gbps'),
+ (400000000, '400 Gbps'),
(1544, 'T1 (1.544 Mbps)'),
(2048, 'E1 (2.048 Mbps)'),
]
@@ -69,6 +71,24 @@ class CircuitTerminationPortSpeedChoices(ChoiceSet):
(25000000, '25 Gbps'),
(40000000, '40 Gbps'),
(100000000, '100 Gbps'),
+ (200000000, '200 Gbps'),
+ (400000000, '400 Gbps'),
(1544, 'T1 (1.544 Mbps)'),
(2048, 'E1 (2.048 Mbps)'),
]
+
+
+class CircuitPriorityChoices(ChoiceSet):
+ key = 'CircuitGroupAssignment.priority'
+
+ PRIORITY_PRIMARY = 'primary'
+ PRIORITY_SECONDARY = 'secondary'
+ PRIORITY_TERTIARY = 'tertiary'
+ PRIORITY_INACTIVE = 'inactive'
+
+ CHOICES = [
+ (PRIORITY_PRIMARY, _('Primary')),
+ (PRIORITY_SECONDARY, _('Secondary')),
+ (PRIORITY_TERTIARY, _('Tertiary')),
+ (PRIORITY_INACTIVE, _('Inactive')),
+ ]
diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py
index e526738743d..ebd1fe28d2f 100644
--- a/netbox/circuits/filtersets.py
+++ b/netbox/circuits/filtersets.py
@@ -13,6 +13,8 @@ from .models import *
__all__ = (
'CircuitFilterSet',
+ 'CircuitGroupAssignmentFilterSet',
+ 'CircuitGroupFilterSet',
'CircuitTerminationFilterSet',
'CircuitTypeFilterSet',
'ProviderNetworkFilterSet',
@@ -237,7 +239,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
class Meta:
model = Circuit
- fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate')
+ fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit')
def search(self, queryset, name, value):
if not value.strip():
@@ -303,3 +305,60 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
Q(pp_info__icontains=value) |
Q(description__icontains=value)
).distinct()
+
+
+class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
+
+ class Meta:
+ model = CircuitGroup
+ fields = ('id', 'name', 'slug', 'description')
+
+
+class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
+ q = django_filters.CharFilter(
+ method='search',
+ label=_('Search'),
+ )
+ provider_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='circuit__provider',
+ queryset=Provider.objects.all(),
+ label=_('Provider (ID)'),
+ )
+ provider = django_filters.ModelMultipleChoiceFilter(
+ field_name='circuit__provider__slug',
+ queryset=Provider.objects.all(),
+ to_field_name='slug',
+ label=_('Provider (slug)'),
+ )
+ circuit_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=Circuit.objects.all(),
+ label=_('Circuit (ID)'),
+ )
+ circuit = django_filters.ModelMultipleChoiceFilter(
+ field_name='circuit__cid',
+ queryset=Circuit.objects.all(),
+ to_field_name='cid',
+ label=_('Circuit (CID)'),
+ )
+ group_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=CircuitGroup.objects.all(),
+ label=_('Circuit group (ID)'),
+ )
+ group = django_filters.ModelMultipleChoiceFilter(
+ field_name='group__slug',
+ queryset=CircuitGroup.objects.all(),
+ to_field_name='slug',
+ label=_('Circuit group (slug)'),
+ )
+
+ class Meta:
+ model = CircuitGroupAssignment
+ fields = ('id', 'priority')
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(circuit__cid__icontains=value) |
+ Q(group__name__icontains=value)
+ )
diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py
index ea15c30100c..5cb7b5d306a 100644
--- a/netbox/circuits/forms/bulk_edit.py
+++ b/netbox/circuits/forms/bulk_edit.py
@@ -1,10 +1,11 @@
from django import forms
from django.utils.translation import gettext_lazy as _
-from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
+from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices
from circuits.models import *
from dcim.models import Site
from ipam.models import ASN
+from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice
@@ -14,6 +15,8 @@ from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, Numbe
__all__ = (
'CircuitBulkEditForm',
+ 'CircuitGroupAssignmentBulkEditForm',
+ 'CircuitGroupBulkEditForm',
'CircuitTerminationBulkEditForm',
'CircuitTypeBulkEditForm',
'ProviderBulkEditForm',
@@ -158,6 +161,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
options=CircuitCommitRateChoices
)
)
+ distance = forms.DecimalField(
+ label=_('Distance'),
+ min_value=0,
+ required=False
+ )
+ distance_unit = forms.ChoiceField(
+ label=_('Distance unit'),
+ choices=add_blank_choice(DistanceUnitChoices),
+ required=False,
+ initial=''
+ )
description = forms.CharField(
label=_('Description'),
max_length=100,
@@ -169,6 +183,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
fieldsets = (
FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
+ FieldSet('distance', 'distance_unit', name=_('Attributes')),
FieldSet('tenant', name=_('Tenancy')),
)
nullable_fields = (
@@ -219,3 +234,40 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
FieldSet('port_speed', 'upstream_speed', name=_('Termination Details')),
)
nullable_fields = ('description')
+
+
+class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
+ )
+ tenant = DynamicModelChoiceField(
+ label=_('Tenant'),
+ queryset=Tenant.objects.all(),
+ required=False
+ )
+
+ model = CircuitGroup
+ nullable_fields = (
+ 'description', 'tenant',
+ )
+
+
+class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
+ circuit = DynamicModelChoiceField(
+ label=_('Circuit'),
+ queryset=Circuit.objects.all(),
+ required=False
+ )
+ priority = forms.ChoiceField(
+ label=_('Priority'),
+ choices=add_blank_choice(CircuitPriorityChoices),
+ required=False
+ )
+
+ model = CircuitGroupAssignment
+ fieldsets = (
+ FieldSet('circuit', 'priority'),
+ )
+ nullable_fields = ('priority',)
diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py
index 1ceb44b6070..d5cdc00a7ea 100644
--- a/netbox/circuits/forms/bulk_import.py
+++ b/netbox/circuits/forms/bulk_import.py
@@ -1,16 +1,18 @@
from django import forms
-from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from circuits.models import *
from dcim.models import Site
+from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
__all__ = (
'CircuitImportForm',
+ 'CircuitGroupAssignmentImportForm',
+ 'CircuitGroupImportForm',
'CircuitTerminationImportForm',
'CircuitTerminationImportRelatedForm',
'CircuitTypeImportForm',
@@ -66,9 +68,6 @@ class CircuitTypeImportForm(NetBoxModelImportForm):
class Meta:
model = CircuitType
fields = ('name', 'slug', 'color', 'description', 'tags')
- help_texts = {
- 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'),
- }
class CircuitImportForm(NetBoxModelImportForm):
@@ -96,6 +95,12 @@ class CircuitImportForm(NetBoxModelImportForm):
choices=CircuitStatusChoices,
help_text=_('Operational status')
)
+ distance_unit = CSVChoiceField(
+ label=_('Distance unit'),
+ choices=DistanceUnitChoices,
+ required=False,
+ help_text=_('Distance unit')
+ )
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
@@ -108,7 +113,7 @@ class CircuitImportForm(NetBoxModelImportForm):
model = Circuit
fields = [
'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
- 'commit_rate', 'description', 'comments', 'tags'
+ 'commit_rate', 'distance', 'distance_unit', 'description', 'comments', 'tags'
]
@@ -153,3 +158,24 @@ class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTermination
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'pp_info', 'description', 'tags'
]
+
+
+class CircuitGroupImportForm(NetBoxModelImportForm):
+ tenant = CSVModelChoiceField(
+ label=_('Tenant'),
+ queryset=Tenant.objects.all(),
+ required=False,
+ to_field_name='name',
+ help_text=_('Assigned tenant')
+ )
+
+ class Meta:
+ model = CircuitGroup
+ fields = ('name', 'slug', 'description', 'tenant', 'tags')
+
+
+class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
+
+ class Meta:
+ model = CircuitGroupAssignment
+ fields = ('circuit', 'group', 'priority')
diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py
index 6f6473c3d9e..2e9b358e891 100644
--- a/netbox/circuits/forms/filtersets.py
+++ b/netbox/circuits/forms/filtersets.py
@@ -1,18 +1,22 @@
from django import forms
from django.utils.translation import gettext as _
-from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices, CircuitTerminationSideChoices
+from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices
from circuits.models import *
from dcim.models import Region, Site, SiteGroup
from ipam.models import ASN
+from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
+from utilities.forms import add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = (
'CircuitFilterForm',
+ 'CircuitGroupAssignmentFilterForm',
+ 'CircuitGroupFilterForm',
'CircuitTerminationFilterForm',
'CircuitTypeFilterForm',
'ProviderFilterForm',
@@ -112,7 +116,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
fieldsets = (
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', name=_('Attributes')),
+ FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit', name=_('Attributes')),
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
@@ -186,6 +190,15 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
options=CircuitCommitRateChoices
)
)
+ distance = forms.DecimalField(
+ label=_('Distance'),
+ required=False,
+ )
+ distance_unit = forms.ChoiceField(
+ label=_('Distance unit'),
+ choices=add_blank_choice(DistanceUnitChoices),
+ required=False
+ )
tag = TagFilterField(model)
@@ -230,3 +243,41 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
label=_('Provider')
)
tag = TagFilterField(model)
+
+
+class CircuitGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+ model = CircuitGroup
+ fieldsets = (
+ FieldSet('q', 'filter_id', 'tag'),
+ FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
+ )
+ tag = TagFilterField(model)
+
+
+class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
+ model = CircuitGroupAssignment
+ fieldsets = (
+ FieldSet('q', 'filter_id', 'tag'),
+ FieldSet('provider_id', 'circuit_id', 'group_id', 'priority', name=_('Assignment')),
+ )
+ provider_id = DynamicModelMultipleChoiceField(
+ queryset=Provider.objects.all(),
+ required=False,
+ label=_('Provider')
+ )
+ circuit_id = DynamicModelMultipleChoiceField(
+ queryset=Circuit.objects.all(),
+ required=False,
+ label=_('Circuit')
+ )
+ group_id = DynamicModelMultipleChoiceField(
+ queryset=CircuitGroup.objects.all(),
+ required=False,
+ label=_('Group')
+ )
+ priority = forms.MultipleChoiceField(
+ label=_('Priority'),
+ choices=CircuitPriorityChoices,
+ required=False
+ )
+ tag = TagFilterField(model)
diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py
index ee5e47ce713..e00034a1098 100644
--- a/netbox/circuits/forms/model_forms.py
+++ b/netbox/circuits/forms/model_forms.py
@@ -7,11 +7,13 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
-from utilities.forms.rendering import FieldSet, TabbedGroups
+from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = (
'CircuitForm',
+ 'CircuitGroupAssignmentForm',
+ 'CircuitGroupForm',
'CircuitTerminationForm',
'CircuitTypeForm',
'ProviderForm',
@@ -106,7 +108,17 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
fieldsets = (
- FieldSet('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
+ FieldSet(
+ 'provider',
+ 'provider_account',
+ 'cid',
+ 'type',
+ 'status',
+ InlineFields('distance', 'distance_unit', label=_('Distance')),
+ 'description',
+ 'tags',
+ name=_('Circuit')
+ ),
FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
@@ -115,7 +127,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
model = Circuit
fields = [
'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
- 'description', 'tenant_group', 'tenant', 'comments', 'tags',
+ 'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'comments', 'tags',
]
widgets = {
'install_date': DatePicker(),
@@ -171,3 +183,36 @@ class CircuitTerminationForm(NetBoxModelForm):
options=CircuitTerminationPortSpeedChoices
),
}
+
+
+class CircuitGroupForm(TenancyForm, NetBoxModelForm):
+ slug = SlugField()
+
+ fieldsets = (
+ FieldSet('name', 'slug', 'description', 'tags', name=_('Circuit Group')),
+ FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
+ )
+
+ class Meta:
+ model = CircuitGroup
+ fields = [
+ 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
+ ]
+
+
+class CircuitGroupAssignmentForm(NetBoxModelForm):
+ group = DynamicModelChoiceField(
+ label=_('Group'),
+ queryset=CircuitGroup.objects.all(),
+ )
+ circuit = DynamicModelChoiceField(
+ label=_('Circuit'),
+ queryset=Circuit.objects.all(),
+ selector=True
+ )
+
+ class Meta:
+ model = CircuitGroupAssignment
+ fields = [
+ 'group', 'circuit', 'priority', 'tags',
+ ]
diff --git a/netbox/circuits/graphql/filters.py b/netbox/circuits/graphql/filters.py
index 10887ce3f0c..b8398b2b9b5 100644
--- a/netbox/circuits/graphql/filters.py
+++ b/netbox/circuits/graphql/filters.py
@@ -1,12 +1,13 @@
-import strawberry
import strawberry_django
-from circuits import filtersets, models
+from circuits import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'CircuitTerminationFilter',
'CircuitFilter',
+ 'CircuitGroupAssignmentFilter',
+ 'CircuitGroupFilter',
'CircuitTypeFilter',
'ProviderFilter',
'ProviderAccountFilter',
@@ -32,6 +33,18 @@ class CircuitTypeFilter(BaseFilterMixin):
pass
+@strawberry_django.filter(models.CircuitGroup, lookups=True)
+@autotype_decorator(filtersets.CircuitGroupFilterSet)
+class CircuitGroupFilter(BaseFilterMixin):
+ pass
+
+
+@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True)
+@autotype_decorator(filtersets.CircuitGroupAssignmentFilterSet)
+class CircuitGroupAssignmentFilter(BaseFilterMixin):
+ pass
+
+
@strawberry_django.filter(models.Provider, lookups=True)
@autotype_decorator(filtersets.ProviderFilterSet)
class ProviderFilter(BaseFilterMixin):
diff --git a/netbox/circuits/graphql/schema.py b/netbox/circuits/graphql/schema.py
index ac8626cc5da..ac23421ce68 100644
--- a/netbox/circuits/graphql/schema.py
+++ b/netbox/circuits/graphql/schema.py
@@ -3,38 +3,31 @@ from typing import List
import strawberry
import strawberry_django
-from circuits import models
from .types import *
-@strawberry.type
+@strawberry.type(name="Query")
class CircuitsQuery:
- @strawberry.field
- def circuit(self, id: int) -> CircuitType:
- return models.Circuit.objects.get(pk=id)
+ circuit: CircuitType = strawberry_django.field()
circuit_list: List[CircuitType] = strawberry_django.field()
- @strawberry.field
- def circuit_termination(self, id: int) -> CircuitTerminationType:
- return models.CircuitTermination.objects.get(pk=id)
+ circuit_termination: CircuitTerminationType = strawberry_django.field()
circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field()
- @strawberry.field
- def circuit_type(self, id: int) -> CircuitTypeType:
- return models.CircuitType.objects.get(pk=id)
+ circuit_type: CircuitTypeType = strawberry_django.field()
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
- @strawberry.field
- def provider(self, id: int) -> ProviderType:
- return models.Provider.objects.get(pk=id)
+ circuit_group: CircuitGroupType = strawberry_django.field()
+ circuit_group_list: List[CircuitGroupType] = strawberry_django.field()
+
+ circuit_group_assignment: CircuitGroupAssignmentType = strawberry_django.field()
+ circuit_group_assignment_list: List[CircuitGroupAssignmentType] = strawberry_django.field()
+
+ provider: ProviderType = strawberry_django.field()
provider_list: List[ProviderType] = strawberry_django.field()
- @strawberry.field
- def provider_account(self, id: int) -> ProviderAccountType:
- return models.ProviderAccount.objects.get(pk=id)
+ provider_account: ProviderAccountType = strawberry_django.field()
provider_account_list: List[ProviderAccountType] = strawberry_django.field()
- @strawberry.field
- def provider_network(self, id: int) -> ProviderNetworkType:
- return models.ProviderNetwork.objects.get(pk=id)
+ provider_network: ProviderNetworkType = strawberry_django.field()
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py
index bae91e6b02b..45f0d065d5f 100644
--- a/netbox/circuits/graphql/types.py
+++ b/netbox/circuits/graphql/types.py
@@ -6,13 +6,15 @@ import strawberry_django
from circuits import models
from dcim.graphql.mixins import CabledObjectMixin
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
-from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType
+from netbox.graphql.types import BaseObjectType, NetBoxObjectType, ObjectType, OrganizationalObjectType
from tenancy.graphql.types import TenantType
from .filters import *
__all__ = (
'CircuitTerminationType',
'CircuitType',
+ 'CircuitGroupAssignmentType',
+ 'CircuitGroupType',
'CircuitTypeType',
'ProviderType',
'ProviderAccountType',
@@ -91,3 +93,22 @@ class CircuitType(NetBoxObjectType, ContactsMixin):
tenant: TenantType | None
terminations: List[CircuitTerminationType]
+
+
+@strawberry_django.type(
+ models.CircuitGroup,
+ fields='__all__',
+ filters=CircuitGroupFilter
+)
+class CircuitGroupType(OrganizationalObjectType):
+ tenant: TenantType | None
+
+
+@strawberry_django.type(
+ models.CircuitGroupAssignment,
+ fields='__all__',
+ filters=CircuitGroupAssignmentFilter
+)
+class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
+ group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
+ circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
diff --git a/netbox/circuits/migrations/0044_circuit_groups.py b/netbox/circuits/migrations/0044_circuit_groups.py
new file mode 100644
index 00000000000..98c3b8f3da2
--- /dev/null
+++ b/netbox/circuits/migrations/0044_circuit_groups.py
@@ -0,0 +1,88 @@
+import django.db.models.deletion
+import taggit.managers
+import utilities.json
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('circuits', '0043_circuittype_color'),
+ ('extras', '0119_notifications'),
+ ('tenancy', '0015_contactassignment_rename_content_type'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CircuitGroup',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('created', models.DateTimeField(auto_now_add=True, null=True)),
+ ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+ (
+ 'custom_field_data',
+ models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
+ ),
+ ('name', models.CharField(max_length=100, unique=True)),
+ ('slug', models.SlugField(max_length=100, unique=True)),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+ (
+ 'tenant',
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name='circuit_groups',
+ to='tenancy.tenant',
+ ),
+ ),
+ ],
+ options={
+ 'verbose_name': 'Circuit group',
+ 'verbose_name_plural': 'Circuit group',
+ 'ordering': ('name',),
+ },
+ ),
+ migrations.CreateModel(
+ name='CircuitGroupAssignment',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('created', models.DateTimeField(auto_now_add=True, null=True)),
+ ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+ (
+ 'custom_field_data',
+ models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
+ ),
+ ('priority', models.CharField(blank=True, max_length=50)),
+ (
+ 'circuit',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='assignments',
+ to='circuits.circuit',
+ ),
+ ),
+ (
+ 'group',
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='assignments',
+ to='circuits.circuitgroup',
+ ),
+ ),
+ ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+ ],
+ options={
+ 'verbose_name': 'Circuit group assignment',
+ 'verbose_name_plural': 'Circuit group assignments',
+ 'ordering': ('group', 'circuit', 'priority', 'pk'),
+ },
+ ),
+ migrations.AddConstraint(
+ model_name='circuitgroupassignment',
+ constraint=models.UniqueConstraint(
+ fields=('circuit', 'group'), name='circuits_circuitgroupassignment_unique_circuit_group'
+ ),
+ ),
+ ]
diff --git a/netbox/circuits/migrations/0045_circuit_distance.py b/netbox/circuits/migrations/0045_circuit_distance.py
new file mode 100644
index 00000000000..6c970339d73
--- /dev/null
+++ b/netbox/circuits/migrations/0045_circuit_distance.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.0.9 on 2024-09-26 22:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('circuits', '0044_circuit_groups'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='circuit',
+ name='_abs_distance',
+ field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
+ ),
+ migrations.AddField(
+ model_name='circuit',
+ name='distance',
+ field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
+ ),
+ migrations.AddField(
+ model_name='circuit',
+ name='distance_unit',
+ field=models.CharField(blank=True, max_length=50),
+ ),
+ ]
diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py
index fa21d7cd33d..2df83e97ee8 100644
--- a/netbox/circuits/models/circuits.py
+++ b/netbox/circuits/models/circuits.py
@@ -6,11 +6,14 @@ from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from dcim.models import CabledObjectModel
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
-from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ImageAttachmentsMixin, TagsMixin
+from netbox.models.mixins import DistanceMixin
+from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin
from utilities.fields import ColorField
__all__ = (
'Circuit',
+ 'CircuitGroup',
+ 'CircuitGroupAssignment',
'CircuitTermination',
'CircuitType',
)
@@ -26,16 +29,13 @@ class CircuitType(OrganizationalModel):
blank=True
)
- def get_absolute_url(self):
- return reverse('circuits:circuittype', args=[self.pk])
-
class Meta:
ordering = ('name',)
verbose_name = _('circuit type')
verbose_name_plural = _('circuit types')
-class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
+class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel):
"""
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
@@ -138,9 +138,6 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
def __str__(self):
return self.cid
- def get_absolute_url(self):
- return reverse('circuits:circuit', args=[self.pk])
-
def get_status_color(self):
return CircuitStatusChoices.colors.get(self.status)
@@ -151,6 +148,72 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
raise ValidationError({'provider_account': "The assigned account must belong to the assigned provider."})
+class CircuitGroup(OrganizationalModel):
+ """
+ An administrative grouping of Circuits.
+ """
+ tenant = models.ForeignKey(
+ to='tenancy.Tenant',
+ on_delete=models.PROTECT,
+ related_name='circuit_groups',
+ blank=True,
+ null=True
+ )
+
+ class Meta:
+ ordering = ('name',)
+ verbose_name = _('circuit group')
+ verbose_name_plural = _('circuit groups')
+
+ def __str__(self):
+ return self.name
+
+
+class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
+ """
+ Assignment of a Circuit to a CircuitGroup with an optional priority.
+ """
+ circuit = models.ForeignKey(
+ Circuit,
+ on_delete=models.CASCADE,
+ related_name='assignments'
+ )
+ group = models.ForeignKey(
+ CircuitGroup,
+ on_delete=models.CASCADE,
+ related_name='assignments'
+ )
+ priority = models.CharField(
+ verbose_name=_('priority'),
+ max_length=50,
+ choices=CircuitPriorityChoices,
+ blank=True
+ )
+ prerequisite_models = (
+ 'circuits.Circuit',
+ 'circuits.CircuitGroup',
+ )
+
+ class Meta:
+ ordering = ('group', 'circuit', 'priority', 'pk')
+ constraints = (
+ models.UniqueConstraint(
+ fields=('circuit', 'group'),
+ name='%(app_label)s_%(class)s_unique_circuit_group'
+ ),
+ )
+ verbose_name = _('Circuit group assignment')
+ verbose_name_plural = _('Circuit group assignments')
+
+ def __str__(self):
+ if self.priority:
+ return f"{self.group} ({self.get_priority_display()})"
+ return str(self.group)
+
+ def get_absolute_url(self):
+ return reverse('circuits:circuitgroupassignment', args=[self.pk])
+
+
class CircuitTermination(
CustomFieldsMixin,
CustomLinksMixin,
diff --git a/netbox/circuits/models/providers.py b/netbox/circuits/models/providers.py
index 31c8bccb297..f0fe77b1acf 100644
--- a/netbox/circuits/models/providers.py
+++ b/netbox/circuits/models/providers.py
@@ -1,6 +1,5 @@
from django.db import models
from django.db.models import Q
-from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from netbox.models import PrimaryModel
@@ -45,9 +44,6 @@ class Provider(ContactsMixin, PrimaryModel):
def __str__(self):
return self.name
- def get_absolute_url(self):
- return reverse('circuits:provider', args=[self.pk])
-
class ProviderAccount(ContactsMixin, PrimaryModel):
"""
@@ -91,9 +87,6 @@ class ProviderAccount(ContactsMixin, PrimaryModel):
return f'{self.account} ({self.name})'
return f'{self.account}'
- def get_absolute_url(self):
- return reverse('circuits:provideraccount', args=[self.pk])
-
class ProviderNetwork(PrimaryModel):
"""
@@ -128,6 +121,3 @@ class ProviderNetwork(PrimaryModel):
def __str__(self):
return self.name
-
- def get_absolute_url(self):
- return reverse('circuits:providernetwork', args=[self.pk])
diff --git a/netbox/circuits/search.py b/netbox/circuits/search.py
index f3fa359bae2..7a5711f0326 100644
--- a/netbox/circuits/search.py
+++ b/netbox/circuits/search.py
@@ -13,6 +13,17 @@ class CircuitIndex(SearchIndex):
display_attrs = ('provider', 'provider_account', 'type', 'status', 'tenant', 'description')
+@register_search
+class CircuitGroupIndex(SearchIndex):
+ model = models.CircuitGroup
+ fields = (
+ ('name', 100),
+ ('slug', 110),
+ ('description', 500),
+ )
+ display_attrs = ('description',)
+
+
@register_search
class CircuitTerminationIndex(SearchIndex):
model = models.CircuitTermination
diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py
index e1b99ff4257..e79212a1461 100644
--- a/netbox/circuits/tables/circuits.py
+++ b/netbox/circuits/tables/circuits.py
@@ -9,6 +9,8 @@ from netbox.tables import NetBoxTable, columns
from .columns import CommitRateColumn
__all__ = (
+ 'CircuitGroupAssignmentTable',
+ 'CircuitGroupTable',
'CircuitTable',
'CircuitTerminationTable',
'CircuitTypeTable',
@@ -74,19 +76,24 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
commit_rate = CommitRateColumn(
verbose_name=_('Commit Rate')
)
+ distance = columns.DistanceColumn()
comments = columns.MarkdownColumn(
- verbose_name=_('Comments'),
+ verbose_name=_('Comments')
)
tags = columns.TagColumn(
url_name='circuits:circuit_list'
)
+ assignments = columns.ManyToManyColumn(
+ verbose_name=_('Assignments'),
+ linkify_item=True
+ )
class Meta(NetBoxTable.Meta):
model = Circuit
fields = (
'pk', 'id', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'tenant_group',
'termination_a', 'termination_z', 'install_date', 'termination_date', 'commit_rate', 'description',
- 'comments', 'contacts', 'tags', 'created', 'last_updated',
+ 'comments', 'contacts', 'tags', 'created', 'last_updated', 'assignments',
)
default_columns = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
@@ -119,3 +126,55 @@ class CircuitTerminationTable(NetBoxTable):
'xconnect_id', 'pp_info', 'description', 'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'id', 'circuit', 'provider', 'term_side', 'description')
+
+
+class CircuitGroupTable(NetBoxTable):
+ name = tables.Column(
+ verbose_name=_('Name'),
+ linkify=True
+ )
+ circuit_group_assignment_count = columns.LinkedCountColumn(
+ viewname='circuits:circuitgroupassignment_list',
+ url_params={'group_id': 'pk'},
+ verbose_name=_('Circuits')
+ )
+ tags = columns.TagColumn(
+ url_name='circuits:circuitgroup_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = CircuitGroup
+ fields = (
+ 'pk', 'name', 'description', 'circuit_group_assignment_count', 'tags',
+ 'created', 'last_updated', 'actions',
+ )
+ default_columns = ('pk', 'name', 'description', 'circuit_group_assignment_count')
+
+
+class CircuitGroupAssignmentTable(NetBoxTable):
+ group = tables.Column(
+ verbose_name=_('Group'),
+ linkify=True
+ )
+ provider = tables.Column(
+ accessor='circuit__provider',
+ verbose_name=_('Provider'),
+ linkify=True
+ )
+ circuit = tables.Column(
+ verbose_name=_('Circuit'),
+ linkify=True
+ )
+ priority = tables.Column(
+ verbose_name=_('Priority'),
+ )
+ tags = columns.TagColumn(
+ url_name='circuits:circuitgroupassignment_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = CircuitGroupAssignment
+ fields = (
+ 'pk', 'id', 'group', 'provider', 'circuit', 'priority', 'created', 'last_updated', 'actions', 'tags',
+ )
+ default_columns = ('pk', 'group', 'provider', 'circuit', 'priority')
diff --git a/netbox/circuits/tables/providers.py b/netbox/circuits/tables/providers.py
index 54499aeaf86..d70c77e9c02 100644
--- a/netbox/circuits/tables/providers.py
+++ b/netbox/circuits/tables/providers.py
@@ -25,7 +25,7 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
account_count = columns.LinkedCountColumn(
accessor=tables.A('accounts__count'),
viewname='circuits:provideraccount_list',
- url_params={'account_id': 'pk'},
+ url_params={'provider_id': 'pk'},
verbose_name=_('Account Count')
)
asns = columns.ManyToManyColumn(
diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py
index d3745f2b155..1edcd531ba2 100644
--- a/netbox/circuits/tests/test_api.py
+++ b/netbox/circuits/tests/test_api.py
@@ -92,10 +92,11 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
class CircuitTest(APIViewTestCases.APIViewTestCase):
model = Circuit
- brief_fields = ['cid', 'description', 'display', 'id', 'url']
+ brief_fields = ['cid', 'description', 'display', 'id', 'provider', 'url']
bulk_update_data = {
'status': 'planned',
}
+ user_permissions = ('circuits.view_provider', 'circuits.view_circuittype')
@classmethod
def setUpTestData(cls):
@@ -150,6 +151,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
model = CircuitTermination
brief_fields = ['_occupied', 'cable', 'circuit', 'description', 'display', 'id', 'term_side', 'url']
+ user_permissions = ('circuits.view_circuit', )
@classmethod
def setUpTestData(cls):
@@ -206,9 +208,42 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
}
+class CircuitGroupTest(APIViewTestCases.APIViewTestCase):
+ model = CircuitGroup
+ brief_fields = ['display', 'id', 'name', 'url']
+ bulk_update_data = {
+ 'description': 'New description',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+ circuit_groups = (
+ CircuitGroup(name="Circuit Group 1", slug='circuit-group-1'),
+ CircuitGroup(name="Circuit Group 2", slug='circuit-group-2'),
+ CircuitGroup(name="Circuit Group 3", slug='circuit-group-3'),
+ )
+ CircuitGroup.objects.bulk_create(circuit_groups)
+
+ cls.create_data = [
+ {
+ 'name': 'Circuit Group 4',
+ 'slug': 'circuit-group-4',
+ },
+ {
+ 'name': 'Circuit Group 5',
+ 'slug': 'circuit-group-5',
+ },
+ {
+ 'name': 'Circuit Group 6',
+ 'slug': 'circuit-group-6',
+ },
+ ]
+
+
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
model = ProviderAccount
brief_fields = ['account', 'description', 'display', 'id', 'name', 'url']
+ user_permissions = ('circuits.view_provider',)
@classmethod
def setUpTestData(cls):
@@ -249,9 +284,82 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
}
+class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
+ model = CircuitGroupAssignment
+ brief_fields = ['circuit', 'display', 'group', 'id', 'priority', 'url']
+ bulk_update_data = {
+ 'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
+ }
+ user_permissions = ('circuits.view_circuit', 'circuits.view_circuitgroup')
+
+ @classmethod
+ def setUpTestData(cls):
+
+ circuit_groups = (
+ CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
+ CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
+ CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
+ CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
+ CircuitGroup(name='Circuit Group 5', slug='circuit-group-5'),
+ CircuitGroup(name='Circuit Group 6', slug='circuit-group-6'),
+ )
+ CircuitGroup.objects.bulk_create(circuit_groups)
+
+ provider = Provider.objects.create(name='Provider 1', slug='provider-1')
+ circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
+
+ circuits = (
+ Circuit(cid='Circuit 1', provider=provider, type=circuittype),
+ Circuit(cid='Circuit 2', provider=provider, type=circuittype),
+ Circuit(cid='Circuit 3', provider=provider, type=circuittype),
+ Circuit(cid='Circuit 4', provider=provider, type=circuittype),
+ Circuit(cid='Circuit 5', provider=provider, type=circuittype),
+ Circuit(cid='Circuit 6', provider=provider, type=circuittype),
+ )
+ Circuit.objects.bulk_create(circuits)
+
+ assignments = (
+ CircuitGroupAssignment(
+ group=circuit_groups[0],
+ circuit=circuits[0],
+ priority=CircuitPriorityChoices.PRIORITY_PRIMARY
+ ),
+ CircuitGroupAssignment(
+ group=circuit_groups[1],
+ circuit=circuits[1],
+ priority=CircuitPriorityChoices.PRIORITY_SECONDARY
+ ),
+ CircuitGroupAssignment(
+ group=circuit_groups[2],
+ circuit=circuits[2],
+ priority=CircuitPriorityChoices.PRIORITY_TERTIARY
+ ),
+ )
+ CircuitGroupAssignment.objects.bulk_create(assignments)
+
+ cls.create_data = [
+ {
+ 'group': circuit_groups[3].pk,
+ 'circuit': circuits[3].pk,
+ 'priority': CircuitPriorityChoices.PRIORITY_PRIMARY,
+ },
+ {
+ 'group': circuit_groups[4].pk,
+ 'circuit': circuits[4].pk,
+ 'priority': CircuitPriorityChoices.PRIORITY_SECONDARY,
+ },
+ {
+ 'group': circuit_groups[5].pk,
+ 'circuit': circuits[5].pk,
+ 'priority': CircuitPriorityChoices.PRIORITY_TERTIARY,
+ },
+ ]
+
+
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
model = ProviderNetwork
brief_fields = ['description', 'display', 'id', 'name', 'url']
+ user_permissions = ('circuits.view_provider', )
@classmethod
def setUpTestData(cls):
diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py
index df10c3929e3..93958298c6f 100644
--- a/netbox/circuits/tests/test_filtersets.py
+++ b/netbox/circuits/tests/test_filtersets.py
@@ -5,6 +5,7 @@ from circuits.filtersets import *
from circuits.models import *
from dcim.models import Cable, Region, Site, SiteGroup
from ipam.models import ASN, RIR
+from netbox.choices import DistanceUnitChoices
from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests
@@ -222,9 +223,9 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = (
- Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
- Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
- Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
+ Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1', distance=10, distance_unit=DistanceUnitChoices.UNIT_FOOT),
+ Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2', distance=20, distance_unit=DistanceUnitChoices.UNIT_METER),
+ Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED, distance=30, distance_unit=DistanceUnitChoices.UNIT_METER),
Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE),
@@ -289,6 +290,14 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ def test_distance(self):
+ params = {'distance': [10, 20]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_distance_unit(self):
+ params = {'distance_unit': DistanceUnitChoices.UNIT_FOOT}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -451,6 +460,136 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
+class CircuitGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = CircuitGroup.objects.all()
+ filterset = CircuitGroupFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ tenant_groups = (
+ TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
+ TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
+ TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
+ )
+ for tenantgroup in tenant_groups:
+ tenantgroup.save()
+
+ tenants = (
+ Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
+ Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
+ Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
+ )
+ Tenant.objects.bulk_create(tenants)
+
+ CircuitGroup.objects.bulk_create((
+ CircuitGroup(name='Circuit Group 1', slug='circuit-group-1', description='foobar1', tenant=tenants[0]),
+ CircuitGroup(name='Circuit Group 2', slug='circuit-group-2', description='foobar2', tenant=tenants[1]),
+ CircuitGroup(name='Circuit Group 3', slug='circuit-group-3', tenant=tenants[1]),
+ ))
+
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_name(self):
+ params = {'name': ['Circuit Group 1']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_slug(self):
+ params = {'slug': ['circuit-group-1']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_tenant(self):
+ tenants = Tenant.objects.all()[:2]
+ params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ params = {'tenant': [tenants[0].slug, tenants[1].slug]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
+ def test_tenant_group(self):
+ tenant_groups = TenantGroup.objects.all()[:2]
+ params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
+
+class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = CircuitGroupAssignment.objects.all()
+ filterset = CircuitGroupAssignmentFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+
+ circuit_groups = (
+ CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
+ CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
+ CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
+ CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
+ )
+ CircuitGroup.objects.bulk_create(circuit_groups)
+
+ providers = Provider.objects.bulk_create((
+ Provider(name='Provider 1', slug='provider-1'),
+ Provider(name='Provider 2', slug='provider-2'),
+ Provider(name='Provider 3', slug='provider-3'),
+ Provider(name='Provider 4', slug='provider-4'),
+ ))
+ circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
+
+ circuits = (
+ Circuit(cid='Circuit 1', provider=providers[0], type=circuittype),
+ Circuit(cid='Circuit 2', provider=providers[1], type=circuittype),
+ Circuit(cid='Circuit 3', provider=providers[2], type=circuittype),
+ Circuit(cid='Circuit 4', provider=providers[3], type=circuittype),
+ )
+ Circuit.objects.bulk_create(circuits)
+
+ assignments = (
+ CircuitGroupAssignment(
+ group=circuit_groups[0],
+ circuit=circuits[0],
+ priority=CircuitPriorityChoices.PRIORITY_PRIMARY
+ ),
+ CircuitGroupAssignment(
+ group=circuit_groups[1],
+ circuit=circuits[1],
+ priority=CircuitPriorityChoices.PRIORITY_SECONDARY
+ ),
+ CircuitGroupAssignment(
+ group=circuit_groups[2],
+ circuit=circuits[2],
+ priority=CircuitPriorityChoices.PRIORITY_TERTIARY
+ ),
+ )
+ CircuitGroupAssignment.objects.bulk_create(assignments)
+
+ def test_group_id(self):
+ groups = CircuitGroup.objects.filter(name__in=['Circuit Group 1', 'Circuit Group 2'])
+ params = {'group_id': [groups[0].pk, groups[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'group': [groups[0].slug, groups[1].slug]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_circuit(self):
+ circuits = Circuit.objects.all()[:2]
+ params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'circuit': [circuits[0].cid, circuits[1].cid]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_provider(self):
+ providers = Provider.objects.all()[:2]
+ params = {'provider_id': [providers[0].pk, providers[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'provider': [providers[0].slug, providers[1].slug]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ProviderNetwork.objects.all()
filterset = ProviderNetworkFilterSet
diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py
index 577548703b4..b06ade30b20 100644
--- a/netbox/circuits/tests/test_views.py
+++ b/netbox/circuits/tests/test_views.py
@@ -171,7 +171,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
cls.csv_update_data = (
- f"id,cid,description,status",
+ "id,cid,description,status",
f"{circuits[0].pk},Circuit 7,New description7,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
f"{circuits[1].pk},Circuit 8,New description8,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
f"{circuits[2].pk},Circuit 9,New description9,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
@@ -404,3 +404,109 @@ class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
response = self.client.get(reverse('circuits:circuittermination_trace', kwargs={'pk': circuittermination.pk}))
self.assertHttpStatus(response, 200)
+
+
+class CircuitGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
+ model = CircuitGroup
+
+ @classmethod
+ def setUpTestData(cls):
+
+ circuit_groups = (
+ CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
+ CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
+ CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
+ )
+ CircuitGroup.objects.bulk_create(circuit_groups)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'Circuit Group X',
+ 'slug': 'circuit-group-x',
+ 'description': 'A new Circuit Group',
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "name,slug",
+ "Circuit Group 4,circuit-group-4",
+ "Circuit Group 5,circuit-group-5",
+ "Circuit Group 6,circuit-group-6",
+ )
+
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{circuit_groups[0].pk},Circuit Group 7,New description7",
+ f"{circuit_groups[1].pk},Circuit Group 8,New description8",
+ f"{circuit_groups[2].pk},Circuit Group 9,New description9",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'Foo',
+ }
+
+
+class CircuitGroupAssignmentTestCase(
+ ViewTestCases.CreateObjectViewTestCase,
+ ViewTestCases.EditObjectViewTestCase,
+ ViewTestCases.DeleteObjectViewTestCase,
+ ViewTestCases.ListObjectsViewTestCase,
+ ViewTestCases.BulkEditObjectsViewTestCase,
+ ViewTestCases.BulkDeleteObjectsViewTestCase
+):
+ model = CircuitGroupAssignment
+
+ @classmethod
+ def setUpTestData(cls):
+
+ circuit_groups = (
+ CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
+ CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
+ CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
+ CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
+ )
+ CircuitGroup.objects.bulk_create(circuit_groups)
+
+ provider = Provider.objects.create(name='Provider 1', slug='provider-1')
+ circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
+
+ circuits = (
+ Circuit(cid='Circuit 1', provider=provider, type=circuittype),
+ Circuit(cid='Circuit 2', provider=provider, type=circuittype),
+ Circuit(cid='Circuit 3', provider=provider, type=circuittype),
+ Circuit(cid='Circuit 4', provider=provider, type=circuittype),
+ )
+ Circuit.objects.bulk_create(circuits)
+
+ assignments = (
+ CircuitGroupAssignment(
+ group=circuit_groups[0],
+ circuit=circuits[0],
+ priority=CircuitPriorityChoices.PRIORITY_PRIMARY
+ ),
+ CircuitGroupAssignment(
+ group=circuit_groups[1],
+ circuit=circuits[1],
+ priority=CircuitPriorityChoices.PRIORITY_SECONDARY
+ ),
+ CircuitGroupAssignment(
+ group=circuit_groups[2],
+ circuit=circuits[2],
+ priority=CircuitPriorityChoices.PRIORITY_TERTIARY
+ ),
+ )
+ CircuitGroupAssignment.objects.bulk_create(assignments)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'group': circuit_groups[3].pk,
+ 'circuit': circuits[3].pk,
+ 'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.bulk_edit_data = {
+ 'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
+ }
diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py
index 5c0ab99ee7a..2171d49bea4 100644
--- a/netbox/circuits/urls.py
+++ b/netbox/circuits/urls.py
@@ -55,4 +55,19 @@ urlpatterns = [
path('circuit-terminations/delete/', views.CircuitTerminationBulkDeleteView.as_view(), name='circuittermination_bulk_delete'),
path('circuit-terminations//', include(get_model_urls('circuits', 'circuittermination'))),
+ # Circuit Groups
+ path('circuit-groups/', views.CircuitGroupListView.as_view(), name='circuitgroup_list'),
+ path('circuit-groups/add/', views.CircuitGroupEditView.as_view(), name='circuitgroup_add'),
+ path('circuit-groups/import/', views.CircuitGroupBulkImportView.as_view(), name='circuitgroup_import'),
+ path('circuit-groups/edit/', views.CircuitGroupBulkEditView.as_view(), name='circuitgroup_bulk_edit'),
+ path('circuit-groups/delete/', views.CircuitGroupBulkDeleteView.as_view(), name='circuitgroup_bulk_delete'),
+ path('circuit-groups//', include(get_model_urls('circuits', 'circuitgroup'))),
+
+ # Circuit Group Assignments
+ path('circuit-group-assignments/', views.CircuitGroupAssignmentListView.as_view(), name='circuitgroupassignment_list'),
+ path('circuit-group-assignments/add/', views.CircuitGroupAssignmentEditView.as_view(), name='circuitgroupassignment_add'),
+ path('circuit-group-assignments/import/', views.CircuitGroupAssignmentBulkImportView.as_view(), name='circuitgroupassignment_import'),
+ path('circuit-group-assignments/edit/', views.CircuitGroupAssignmentBulkEditView.as_view(), name='circuitgroupassignment_bulk_edit'),
+ path('circuit-group-assignments/delete/', views.CircuitGroupAssignmentBulkDeleteView.as_view(), name='circuitgroupassignment_bulk_delete'),
+ path('circuit-group-assignments//', include(get_model_urls('circuits', 'circuitgroupassignment'))),
]
diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py
index b10b83b23dc..8218960c9d7 100644
--- a/netbox/circuits/views.py
+++ b/netbox/circuits/views.py
@@ -1,6 +1,7 @@
from django.contrib import messages
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.translation import gettext_lazy as _
from dcim.views import PathTraceView
from netbox.views import generic
@@ -326,7 +327,9 @@ class CircuitSwapTerminations(generic.ObjectEditView):
# Circuit must have at least one termination to swap
if not circuit.termination_a and not circuit.termination_z:
- messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
+ messages.error(request, _(
+ "No terminations have been defined for circuit {circuit}."
+ ).format(circuit=circuit))
return redirect('circuits:circuit', pk=circuit.pk)
return render(request, 'circuits/circuit_terminations_swap.html', {
@@ -374,7 +377,7 @@ class CircuitSwapTerminations(generic.ObjectEditView):
circuit.termination_z = None
circuit.save()
- messages.success(request, f"Swapped terminations for circuit {circuit}.")
+ messages.success(request, _("Swapped terminations for circuit {circuit}.").format(circuit=circuit))
return redirect('circuits:circuit', pk=circuit.pk)
return render(request, 'circuits/circuit_terminations_swap.html', {
@@ -440,3 +443,100 @@ class CircuitTerminationBulkDeleteView(generic.BulkDeleteView):
# Trace view
register_model_view(CircuitTermination, 'trace', kwargs={'model': CircuitTermination})(PathTraceView)
+
+
+#
+# Circuit Groups
+#
+
+class CircuitGroupListView(generic.ObjectListView):
+ queryset = CircuitGroup.objects.annotate(
+ circuit_group_assignment_count=count_related(CircuitGroupAssignment, 'group')
+ )
+ filterset = filtersets.CircuitGroupFilterSet
+ filterset_form = forms.CircuitGroupFilterForm
+ table = tables.CircuitGroupTable
+
+
+@register_model_view(CircuitGroup)
+class CircuitGroupView(GetRelatedModelsMixin, generic.ObjectView):
+ queryset = CircuitGroup.objects.all()
+
+ def get_extra_context(self, request, instance):
+ return {
+ 'related_models': self.get_related_models(request, instance),
+ }
+
+
+@register_model_view(CircuitGroup, 'edit')
+class CircuitGroupEditView(generic.ObjectEditView):
+ queryset = CircuitGroup.objects.all()
+ form = forms.CircuitGroupForm
+
+
+@register_model_view(CircuitGroup, 'delete')
+class CircuitGroupDeleteView(generic.ObjectDeleteView):
+ queryset = CircuitGroup.objects.all()
+
+
+class CircuitGroupBulkImportView(generic.BulkImportView):
+ queryset = CircuitGroup.objects.all()
+ model_form = forms.CircuitGroupImportForm
+
+
+class CircuitGroupBulkEditView(generic.BulkEditView):
+ queryset = CircuitGroup.objects.all()
+ filterset = filtersets.CircuitGroupFilterSet
+ table = tables.CircuitGroupTable
+ form = forms.CircuitGroupBulkEditForm
+
+
+class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
+ queryset = CircuitGroup.objects.all()
+ filterset = filtersets.CircuitGroupFilterSet
+ table = tables.CircuitGroupTable
+
+
+#
+# Circuit Groups
+#
+
+class CircuitGroupAssignmentListView(generic.ObjectListView):
+ queryset = CircuitGroupAssignment.objects.all()
+ filterset = filtersets.CircuitGroupAssignmentFilterSet
+ filterset_form = forms.CircuitGroupAssignmentFilterForm
+ table = tables.CircuitGroupAssignmentTable
+
+
+@register_model_view(CircuitGroupAssignment)
+class CircuitGroupAssignmentView(generic.ObjectView):
+ queryset = CircuitGroupAssignment.objects.all()
+
+
+@register_model_view(CircuitGroupAssignment, 'edit')
+class CircuitGroupAssignmentEditView(generic.ObjectEditView):
+ queryset = CircuitGroupAssignment.objects.all()
+ form = forms.CircuitGroupAssignmentForm
+
+
+@register_model_view(CircuitGroupAssignment, 'delete')
+class CircuitGroupAssignmentDeleteView(generic.ObjectDeleteView):
+ queryset = CircuitGroupAssignment.objects.all()
+
+
+class CircuitGroupAssignmentBulkImportView(generic.BulkImportView):
+ queryset = CircuitGroupAssignment.objects.all()
+ model_form = forms.CircuitGroupAssignmentImportForm
+
+
+class CircuitGroupAssignmentBulkEditView(generic.BulkEditView):
+ queryset = CircuitGroupAssignment.objects.all()
+ filterset = filtersets.CircuitGroupAssignmentFilterSet
+ table = tables.CircuitGroupAssignmentTable
+ form = forms.CircuitGroupAssignmentBulkEditForm
+
+
+class CircuitGroupAssignmentBulkDeleteView(generic.BulkDeleteView):
+ queryset = CircuitGroupAssignment.objects.all()
+ filterset = filtersets.CircuitGroupAssignmentFilterSet
+ table = tables.CircuitGroupAssignmentTable
diff --git a/netbox/core/api/nested_serializers.py b/netbox/core/api/nested_serializers.py
index 8e5d3ff6331..df7b41ca7c8 100644
--- a/netbox/core/api/nested_serializers.py
+++ b/netbox/core/api/nested_serializers.py
@@ -1,3 +1,5 @@
+import warnings
+
from rest_framework import serializers
from core.choices import JobStatusChoices
@@ -12,6 +14,12 @@ __all__ = (
'NestedJobSerializer',
)
+# TODO: Remove in v4.2
+warnings.warn(
+ "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+ DeprecationWarning
+)
+
class NestedDataSourceSerializer(WritableNestedSerializer):
diff --git a/netbox/core/api/schema.py b/netbox/core/api/schema.py
index bcc49d3fc3f..1ac822b8cc7 100644
--- a/netbox/core/api/schema.py
+++ b/netbox/core/api/schema.py
@@ -8,10 +8,8 @@ from drf_spectacular.plumbing import (
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
)
from drf_spectacular.types import OpenApiTypes
-from rest_framework import serializers
-from rest_framework.relations import ManyRelatedField
-from netbox.api.fields import ChoiceField, SerializedPKRelatedField
+from netbox.api.fields import ChoiceField
from netbox.api.serializers import WritableNestedSerializer
# see netbox.api.routers.NetBoxRouter
@@ -126,9 +124,18 @@ class NetBoxAutoSchema(AutoSchema):
return response_serializers
+ def _get_serializer_name(self, serializer, direction, bypass_extensions=False) -> str:
+ name = super()._get_serializer_name(serializer, direction, bypass_extensions)
+
+ # If this serializer is nested, prepend its name with "Brief"
+ if getattr(serializer, 'nested', False):
+ name = f'Brief{name}'
+
+ return name
+
def get_serializer_ref_name(self, serializer):
# from drf-yasg.utils
- """Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
+ """Get serializer's ref_name
:param serializer: Serializer instance
:return: Serializer's ``ref_name`` or ``None`` for inline serializer
:rtype: str or None
@@ -137,8 +144,6 @@ class NetBoxAutoSchema(AutoSchema):
serializer_name = type(serializer).__name__
if hasattr(serializer_meta, 'ref_name'):
ref_name = serializer_meta.ref_name
- elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
- ref_name = None
else:
ref_name = serializer_name
if ref_name.endswith('Serializer'):
diff --git a/netbox/core/api/serializers.py b/netbox/core/api/serializers.py
index d59745ccd7e..2dde6be9f90 100644
--- a/netbox/core/api/serializers.py
+++ b/netbox/core/api/serializers.py
@@ -1,4 +1,3 @@
from .serializers_.change_logging import *
from .serializers_.data import *
from .serializers_.jobs import *
-from .nested_serializers import *
diff --git a/netbox/core/api/serializers_/data.py b/netbox/core/api/serializers_/data.py
index eddd45c5831..2c155ba6bc5 100644
--- a/netbox/core/api/serializers_/data.py
+++ b/netbox/core/api/serializers_/data.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from core.choices import *
from core.models import DataFile, DataSource
from netbox.api.fields import ChoiceField, RelatedObjectCountField
@@ -27,8 +25,9 @@ class DataSourceSerializer(NetBoxModelSerializer):
class Meta:
model = DataSource
fields = [
- 'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
- 'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count',
+ 'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description',
+ 'parameters', 'ignore_rules', 'comments', 'custom_fields', 'created', 'last_updated', 'last_synced',
+ 'file_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
diff --git a/netbox/core/api/serializers_/jobs.py b/netbox/core/api/serializers_/jobs.py
index e5e07aa414b..544dddb5635 100644
--- a/netbox/core/api/serializers_/jobs.py
+++ b/netbox/core/api/serializers_/jobs.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from core.choices import *
from core.models import Job
from netbox.api.fields import ChoiceField, ContentTypeField
diff --git a/netbox/core/api/views.py b/netbox/core/api/views.py
index ff488e3cded..b3a024c026f 100644
--- a/netbox/core/api/views.py
+++ b/netbox/core/api/views.py
@@ -7,6 +7,8 @@ from rest_framework.routers import APIRootView
from rest_framework.viewsets import ReadOnlyModelViewSet
from core import filtersets
+from core.choices import DataSourceStatusChoices
+from core.jobs import SyncDataSourceJob
from core.models import *
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
@@ -36,7 +38,11 @@ class DataSourceViewSet(NetBoxModelViewSet):
if not request.user.has_perm('core.sync_datasource', obj=datasource):
raise PermissionDenied(_("This user does not have permission to synchronize this data source."))
- datasource.enqueue_sync_job(request)
+ # Enqueue the sync job & update the DataSource's status
+ SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
+ datasource.status = DataSourceStatusChoices.QUEUED
+ DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
+
serializer = serializers.DataSourceSerializer(datasource, context={'request': request})
return Response(serializer.data)
diff --git a/netbox/core/apps.py b/netbox/core/apps.py
index b1103469c19..1dfc7a65ea3 100644
--- a/netbox/core/apps.py
+++ b/netbox/core/apps.py
@@ -16,9 +16,9 @@ class CoreConfig(AppConfig):
name = "core"
def ready(self):
- from core.api import schema # noqa
+ from core.api import schema # noqa: F401
from netbox.models.features import register_models
- from . import data_backends, search
+ from . import data_backends, events, search # noqa: F401
# Register models
register_models(*self.get_models())
diff --git a/netbox/core/choices.py b/netbox/core/choices.py
index ee0febaff51..01a072ce1cf 100644
--- a/netbox/core/choices.py
+++ b/netbox/core/choices.py
@@ -59,6 +59,12 @@ class JobStatusChoices(ChoiceSet):
(STATUS_FAILED, _('Failed'), 'red'),
)
+ ENQUEUED_STATE_CHOICES = (
+ STATUS_PENDING,
+ STATUS_SCHEDULED,
+ STATUS_RUNNING,
+ )
+
TERMINAL_STATE_CHOICES = (
STATUS_COMPLETED,
STATUS_ERRORED,
diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py
index 2d3a7d8c8f2..770a3b25879 100644
--- a/netbox/core/data_backends.py
+++ b/netbox/core/data_backends.py
@@ -8,10 +8,13 @@ from urllib.parse import urlparse
from django import forms
from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext as _
from netbox.data_backends import DataBackend
from netbox.utils import register_data_backend
+from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS
+from utilities.socks import ProxyPoolManager
from .exceptions import SyncError
__all__ = (
@@ -31,7 +34,7 @@ class LocalBackend(DataBackend):
@contextmanager
def fetch(self):
- logger.debug(f"Data source type is local; skipping fetch")
+ logger.debug("Data source type is local; skipping fetch")
local_path = urlparse(self.url).path # Strip file:// scheme
yield local_path
@@ -67,11 +70,18 @@ class GitBackend(DataBackend):
# Initialize backend config
config = ConfigDict()
+ self.use_socks = False
# Apply HTTP proxy (if configured)
- if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'):
- if proxy := settings.HTTP_PROXIES.get(self.url_scheme):
- config.set("http", "proxy", proxy)
+ if settings.HTTP_PROXIES:
+ if proxy := settings.HTTP_PROXIES.get(self.url_scheme, None):
+ if urlparse(proxy).scheme not in HTTP_PROXY_SUPPORTED_SCHEMAS:
+ raise ImproperlyConfigured(f"Unsupported Git DataSource proxy scheme: {urlparse(proxy).scheme}")
+
+ if self.url_scheme in ('http', 'https'):
+ config.set("http", "proxy", proxy)
+ if urlparse(proxy).scheme in HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS:
+ self.use_socks = True
return config
@@ -84,11 +94,13 @@ class GitBackend(DataBackend):
clone_args = {
"branch": self.params.get('branch'),
"config": self.config,
- "depth": 1,
"errstream": porcelain.NoneStream(),
- "quiet": True,
}
+ # check if using socks for proxy - if so need to use custom pool_manager
+ if self.use_socks:
+ clone_args['pool_manager'] = ProxyPoolManager(settings.HTTP_PROXIES.get(self.url_scheme))
+
if self.url_scheme in ('http', 'https'):
if self.params.get('username'):
clone_args.update(
@@ -97,6 +109,9 @@ class GitBackend(DataBackend):
"password": self.params.get('password'),
}
)
+ if self.url_scheme:
+ clone_args["quiet"] = True
+ clone_args["depth"] = 1
logger.debug(f"Cloning git repo: {self.url}")
try:
diff --git a/netbox/core/events.py b/netbox/core/events.py
new file mode 100644
index 00000000000..384b61fd489
--- /dev/null
+++ b/netbox/core/events.py
@@ -0,0 +1,33 @@
+from django.utils.translation import gettext as _
+
+from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
+
+__all__ = (
+ 'JOB_COMPLETED',
+ 'JOB_ERRORED',
+ 'JOB_FAILED',
+ 'JOB_STARTED',
+ 'OBJECT_CREATED',
+ 'OBJECT_DELETED',
+ 'OBJECT_UPDATED',
+)
+
+# Object events
+OBJECT_CREATED = 'object_created'
+OBJECT_UPDATED = 'object_updated'
+OBJECT_DELETED = 'object_deleted'
+
+# Job events
+JOB_STARTED = 'job_started'
+JOB_COMPLETED = 'job_completed'
+JOB_FAILED = 'job_failed'
+JOB_ERRORED = 'job_errored'
+
+# Register core events
+EventType(OBJECT_CREATED, _('Object created')).register()
+EventType(OBJECT_UPDATED, _('Object updated')).register()
+EventType(OBJECT_DELETED, _('Object deleted'), destructive=True).register()
+EventType(JOB_STARTED, _('Job started')).register()
+EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
+EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
+EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()
diff --git a/netbox/core/filtersets.py b/netbox/core/filtersets.py
index f622e789cbb..21fdaa4abe2 100644
--- a/netbox/core/filtersets.py
+++ b/netbox/core/filtersets.py
@@ -1,4 +1,3 @@
-from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext as _
@@ -7,6 +6,7 @@ import django_filters
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
from netbox.utils import get_data_backend_choices
+from users.models import User
from utilities.filters import ContentTypeFilter
from .choices import *
from .models import *
@@ -141,12 +141,12 @@ class ObjectChangeFilterSet(BaseFilterSet):
queryset=ContentType.objects.all()
)
user_id = django_filters.ModelMultipleChoiceFilter(
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
label=_('User (ID)'),
)
user = django_filters.ModelMultipleChoiceFilter(
field_name='user__username',
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
to_field_name='username',
label=_('User name'),
)
diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py
index c629841ae31..ab4b869b739 100644
--- a/netbox/core/forms/filtersets.py
+++ b/netbox/core/forms/filtersets.py
@@ -1,5 +1,4 @@
from django import forms
-from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from core.choices import *
@@ -7,6 +6,7 @@ from core.models import *
from netbox.forms import NetBoxModelFilterSetForm
from netbox.forms.mixins import SavedFiltersMixin
from netbox.utils import get_data_backend_choices
+from users.models import User
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import (
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField,
@@ -121,7 +121,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
widget=DateTimePicker()
)
user = DynamicModelMultipleChoiceField(
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
required=False,
label=_('User')
)
@@ -150,7 +150,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
required=False
)
user_id = DynamicModelMultipleChoiceField(
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
required=False,
label=_('User')
)
diff --git a/netbox/core/graphql/mixins.py b/netbox/core/graphql/mixins.py
index 43f8761d18f..5195b52a0d8 100644
--- a/netbox/core/graphql/mixins.py
+++ b/netbox/core/graphql/mixins.py
@@ -15,7 +15,7 @@ __all__ = (
class ChangelogMixin:
@strawberry_django.field
- def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]:
+ def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: # noqa: F821
content_type = ContentType.objects.get_for_model(self)
object_changes = ObjectChange.objects.filter(
changed_object_type=content_type,
diff --git a/netbox/core/graphql/schema.py b/netbox/core/graphql/schema.py
index 34135cd47e2..a77c57c86b1 100644
--- a/netbox/core/graphql/schema.py
+++ b/netbox/core/graphql/schema.py
@@ -3,18 +3,13 @@ from typing import List
import strawberry
import strawberry_django
-from core import models
from .types import *
-@strawberry.type
+@strawberry.type(name="Query")
class CoreQuery:
- @strawberry.field
- def data_file(self, id: int) -> DataFileType:
- return models.DataFile.objects.get(pk=id)
+ data_file: DataFileType = strawberry_django.field()
data_file_list: List[DataFileType] = strawberry_django.field()
- @strawberry.field
- def data_source(self, id: int) -> DataSourceType:
- return models.DataSource.objects.get(pk=id)
+ data_source: DataSourceType = strawberry_django.field()
data_source_list: List[DataSourceType] = strawberry_django.field()
diff --git a/netbox/core/jobs.py b/netbox/core/jobs.py
index 264313e6204..d2b84639870 100644
--- a/netbox/core/jobs.py
+++ b/netbox/core/jobs.py
@@ -1,33 +1,33 @@
import logging
+from netbox.jobs import JobRunner
from netbox.search.backends import search_backend
-from .choices import *
+from .choices import DataSourceStatusChoices
from .exceptions import SyncError
from .models import DataSource
-from rq.timeouts import JobTimeoutException
logger = logging.getLogger(__name__)
-def sync_datasource(job, *args, **kwargs):
+class SyncDataSourceJob(JobRunner):
"""
Call sync() on a DataSource.
"""
- datasource = DataSource.objects.get(pk=job.object_id)
- try:
- job.start()
- datasource.sync()
+ class Meta:
+ name = 'Synchronization'
- # Update the search cache for DataFiles belonging to this source
- search_backend.cache(datasource.datafiles.iterator())
+ def run(self, *args, **kwargs):
+ datasource = DataSource.objects.get(pk=self.job.object_id)
- job.terminate()
+ try:
+ datasource.sync()
- except Exception as e:
- job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
- DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
- if type(e) in (SyncError, JobTimeoutException):
- logging.error(e)
- else:
+ # Update the search cache for DataFiles belonging to this source
+ search_backend.cache(datasource.datafiles.iterator())
+
+ except Exception as e:
+ DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
+ if type(e) is SyncError:
+ logging.error(e)
raise e
diff --git a/netbox/core/management/commands/nbshell.py b/netbox/core/management/commands/nbshell.py
index 7270c005a29..8f729d10acf 100644
--- a/netbox/core/management/commands/nbshell.py
+++ b/netbox/core/management/commands/nbshell.py
@@ -5,12 +5,16 @@ import sys
from django import get_version
from django.apps import apps
from django.conf import settings
-from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from core.models import ObjectType
+from users.models import User
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
+EXCLUDE_MODELS = (
+ 'extras.branch',
+ 'extras.stagedchange',
+)
BANNER_TEXT = """### NetBox interactive shell ({node})
### Python {python} | Django {django} | NetBox {netbox}
@@ -44,12 +48,16 @@ class Command(BaseCommand):
# Gather Django models and constants from each app
for app in APPS:
- self.django_models[app] = []
+ models = []
# Load models from each app
for model in apps.get_app_config(app).get_models():
- namespace[model.__name__] = model
- self.django_models[app].append(model.__name__)
+ app_label = model._meta.app_label
+ model_name = model._meta.model_name
+ if f'{app_label}.{model_name}' not in EXCLUDE_MODELS:
+ namespace[model.__name__] = model
+ models.append(model.__name__)
+ self.django_models[app] = sorted(models)
# Constants
try:
@@ -61,7 +69,7 @@ class Command(BaseCommand):
# Additional objects to include
namespace['ObjectType'] = ObjectType
- namespace['User'] = get_user_model()
+ namespace['User'] = User
# Load convenience commands
namespace.update({
diff --git a/netbox/core/management/commands/syncdatasource.py b/netbox/core/management/commands/syncdatasource.py
index aa81379526d..990b6eb2abc 100644
--- a/netbox/core/management/commands/syncdatasource.py
+++ b/netbox/core/management/commands/syncdatasource.py
@@ -26,7 +26,7 @@ class Command(BaseCommand):
if invalid_names := set(options['name']) - found_names:
raise CommandError(f"Invalid data source names: {', '.join(invalid_names)}")
else:
- raise CommandError(f"Must specify at least one data source, or set --all.")
+ raise CommandError("Must specify at least one data source, or set --all.")
if len(options['name']) > 1:
self.stdout.write(f"Syncing {len(datasources)} data sources.")
@@ -43,4 +43,4 @@ class Command(BaseCommand):
raise e
if len(options['name']) > 1:
- self.stdout.write(f"Finished.")
+ self.stdout.write("Finished.")
diff --git a/netbox/core/migrations/0012_job_object_type_optional.py b/netbox/core/migrations/0012_job_object_type_optional.py
new file mode 100644
index 00000000000..3c6664afce1
--- /dev/null
+++ b/netbox/core/migrations/0012_job_object_type_optional.py
@@ -0,0 +1,24 @@
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('core', '0011_move_objectchange'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='job',
+ name='object_type',
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='jobs',
+ to='contenttypes.contenttype'
+ ),
+ ),
+ ]
diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py
index 48fa2ff7130..39ee8fa575f 100644
--- a/netbox/core/models/data.py
+++ b/netbox/core/models/data.py
@@ -1,10 +1,10 @@
import hashlib
import logging
import os
-import yaml
from fnmatch import fnmatchcase
from urllib.parse import urlparse
+import yaml
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
@@ -12,7 +12,6 @@ from django.core.validators import RegexValidator
from django.db import models
from django.urls import reverse
from django.utils import timezone
-from django.utils.module_loading import import_string
from django.utils.translation import gettext as _
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
@@ -22,8 +21,6 @@ from netbox.registry import registry
from utilities.querysets import RestrictedQuerySet
from ..choices import *
from ..exceptions import SyncError
-from ..signals import post_sync, pre_sync
-from .jobs import Job
__all__ = (
'AutoSyncRecord',
@@ -87,9 +84,6 @@ class DataSource(JobsMixin, PrimaryModel):
def __str__(self):
return f'{self.name}'
- def get_absolute_url(self):
- return reverse('core:datasource', args=[self.pk])
-
@property
def docs_url(self):
return f'{settings.STATIC_URL}docs/models/{self._meta.app_label}/{self._meta.model_name}/'
@@ -128,7 +122,7 @@ class DataSource(JobsMixin, PrimaryModel):
# Ensure URL scheme matches selected type
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
raise ValidationError({
- 'source_url': f"URLs for local sources must start with file:// (or specify no scheme)"
+ 'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
})
def to_objectchange(self, action):
@@ -153,21 +147,6 @@ class DataSource(JobsMixin, PrimaryModel):
return objectchange
- def enqueue_sync_job(self, request):
- """
- Enqueue a background job to synchronize the DataSource by calling sync().
- """
- # Set the status to "syncing"
- self.status = DataSourceStatusChoices.QUEUED
- DataSource.objects.filter(pk=self.pk).update(status=self.status)
-
- # Enqueue a sync job
- return Job.enqueue(
- import_string('core.jobs.sync_datasource'),
- instance=self,
- user=request.user
- )
-
def get_backend(self):
backend_params = self.parameters or {}
return self.backend_class(self.source_url, **backend_params)
@@ -176,6 +155,8 @@ class DataSource(JobsMixin, PrimaryModel):
"""
Create/update/delete child DataFiles as necessary to synchronize with the remote source.
"""
+ from core.signals import post_sync, pre_sync
+
if self.status == DataSourceStatusChoices.SYNCING:
raise SyncError(_("Cannot initiate sync; syncing already in progress."))
@@ -217,7 +198,7 @@ class DataSource(JobsMixin, PrimaryModel):
logger.debug(f"Updated {updated_count} files")
# Bulk delete deleted files
- deleted_count, _ = DataFile.objects.filter(pk__in=deleted_file_ids).delete()
+ deleted_count, __ = DataFile.objects.filter(pk__in=deleted_file_ids).delete()
logger.debug(f"Deleted {deleted_count} files")
# Walk the local replication to find new files
diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py
index b9f0d0b913d..e1b5715ddff 100644
--- a/netbox/core/models/jobs.py
+++ b/netbox/core/models/jobs.py
@@ -13,9 +13,6 @@ from django.utils.translation import gettext as _
from core.choices import JobStatusChoices
from core.models import ObjectType
from core.signals import job_end, job_start
-from extras.constants import EVENT_JOB_END, EVENT_JOB_START
-from netbox.config import get_config
-from netbox.constants import RQ_QUEUE_DEFAULT
from utilities.querysets import RestrictedQuerySet
from utilities.rqworker import get_queue_for_model
@@ -32,6 +29,8 @@ class Job(models.Model):
to='contenttypes.ContentType',
related_name='jobs',
on_delete=models.CASCADE,
+ blank=True,
+ null=True
)
object_id = models.PositiveBigIntegerField(
blank=True,
@@ -117,10 +116,11 @@ class Job(models.Model):
def get_absolute_url(self):
# TODO: Employ dynamic registration
- if self.object_type.model == 'reportmodule':
- return reverse(f'extras:report_result', kwargs={'job_pk': self.pk})
- if self.object_type.model == 'scriptmodule':
- return reverse(f'extras:script_result', kwargs={'job_pk': self.pk})
+ if self.object_type:
+ if self.object_type.model == 'reportmodule':
+ return reverse('extras:report_result', kwargs={'job_pk': self.pk})
+ elif self.object_type.model == 'scriptmodule':
+ return reverse('extras:script_result', kwargs={'job_pk': self.pk})
return reverse('core:job', args=[self.pk])
def get_status_color(self):
@@ -153,7 +153,7 @@ class Job(models.Model):
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
- rq_queue_name = get_config().QUEUE_MAPPINGS.get(self.object_type.model, RQ_QUEUE_DEFAULT)
+ rq_queue_name = get_queue_for_model(self.object_type.model if self.object_type else None)
queue = django_rq.get_queue(rq_queue_name)
job = queue.fetch_job(str(self.job_id))
@@ -198,25 +198,34 @@ class Job(models.Model):
job_end.send(self)
@classmethod
- def enqueue(cls, func, instance, name='', user=None, schedule_at=None, interval=None, **kwargs):
+ def enqueue(cls, func, instance=None, name='', user=None, schedule_at=None, interval=None, immediate=False, **kwargs):
"""
Create a Job instance and enqueue a job using the given callable
Args:
func: The callable object to be enqueued for execution
- instance: The NetBox object to which this job pertains
+ instance: The NetBox object to which this job pertains (optional)
name: Name for the job (optional)
user: The user responsible for running the job
schedule_at: Schedule the job to be executed at the passed date and time
interval: Recurrence interval (in minutes)
+ immediate: Run the job immediately without scheduling it in the background. Should be used for interactive
+ management commands only.
"""
- object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False)
- rq_queue_name = get_queue_for_model(object_type.model)
+ if schedule_at and immediate:
+ raise ValueError(_("enqueue() cannot be called with values for both schedule_at and immediate."))
+
+ if instance:
+ object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False)
+ object_id = instance.pk
+ else:
+ object_type = object_id = None
+ rq_queue_name = get_queue_for_model(object_type.model if object_type else None)
queue = django_rq.get_queue(rq_queue_name)
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
job = Job.objects.create(
object_type=object_type,
- object_id=instance.pk,
+ object_id=object_id,
name=name,
status=status,
scheduled=schedule_at,
@@ -225,8 +234,16 @@ class Job(models.Model):
job_id=uuid.uuid4()
)
- if schedule_at:
+ # Run the job immediately, rather than enqueuing it as a background task. Note that this is a synchronous
+ # (blocking) operation, and execution will pause until the job completes.
+ if immediate:
+ func(job_id=str(job.job_id), job=job, **kwargs)
+
+ # Schedule the job to run at a specific date & time.
+ elif schedule_at:
queue.enqueue_at(schedule_at, func, job_id=str(job.job_id), job=job, **kwargs)
+
+ # Schedule the job to run asynchronously at this first available opportunity.
else:
queue.enqueue(func, job_id=str(job.job_id), job=job, **kwargs)
diff --git a/netbox/core/plugins.py b/netbox/core/plugins.py
new file mode 100644
index 00000000000..1fcb37f2bff
--- /dev/null
+++ b/netbox/core/plugins.py
@@ -0,0 +1,216 @@
+import datetime
+import importlib
+import importlib.util
+from dataclasses import dataclass, field
+from typing import Optional
+
+import requests
+from django.conf import settings
+from django.core.cache import cache
+
+from netbox.plugins import PluginConfig
+from utilities.datetime import datetime_from_timestamp
+
+USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}'
+CACHE_KEY_CATALOG_FEED = 'plugins-catalog-feed'
+
+
+@dataclass
+class PluginAuthor:
+ """
+ Identifying information for the author of a plugin.
+ """
+ name: str
+ org_id: str = ''
+ url: str = ''
+
+
+@dataclass
+class PluginVersion:
+ """
+ Details for a specific versioned release of a plugin.
+ """
+ date: datetime.datetime = None
+ version: str = ''
+ netbox_min_version: str = ''
+ netbox_max_version: str = ''
+ has_model: bool = False
+ is_certified: bool = False
+ is_feature: bool = False
+ is_integration: bool = False
+ is_netboxlabs_supported: bool = False
+
+
+@dataclass
+class Plugin:
+ """
+ The representation of a NetBox plugin in the catalog API.
+ """
+ id: str = ''
+ status: str = ''
+ title_short: str = ''
+ title_long: str = ''
+ tag_line: str = ''
+ description_short: str = ''
+ slug: str = ''
+ author: Optional[PluginAuthor] = None
+ created_at: datetime.datetime = None
+ updated_at: datetime.datetime = None
+ license_type: str = ''
+ homepage_url: str = ''
+ package_name_pypi: str = ''
+ config_name: str = ''
+ is_certified: bool = False
+ release_latest: PluginVersion = field(default_factory=PluginVersion)
+ release_recent_history: list[PluginVersion] = field(default_factory=list)
+ is_local: bool = False # extra field for locally installed plugins
+ is_installed: bool = False
+ installed_version: str = ''
+
+
+def get_local_plugins(plugins=None):
+ """
+ Return a dictionary of all locally-installed plugins, mapped by name.
+ """
+ plugins = plugins or {}
+ local_plugins = {}
+
+ # Gather all locally-installed plugins
+ for plugin_name in settings.PLUGINS:
+ plugin = importlib.import_module(plugin_name)
+ plugin_config: PluginConfig = plugin.config
+
+ local_plugins[plugin_config.name] = Plugin(
+ config_name=plugin_config.name,
+ title_short=plugin_config.verbose_name,
+ title_long=plugin_config.verbose_name,
+ tag_line=plugin_config.description,
+ description_short=plugin_config.description,
+ is_local=True,
+ is_installed=True,
+ installed_version=plugin_config.version,
+ )
+
+ # Update catalog entries for local plugins, or add them to the list if not listed
+ for k, v in local_plugins.items():
+ if k in plugins:
+ plugins[k].is_local = True
+ plugins[k].is_installed = True
+ plugins[k].installed_version = v.installed_version
+ else:
+ plugins[k] = v
+
+ return plugins
+
+
+def get_catalog_plugins():
+ """
+ Return a dictionary of all entries in the plugins catalog, mapped by name.
+ """
+ session = requests.Session()
+
+ # Disable catalog fetching for isolated deployments
+ if settings.ISOLATED_DEPLOYMENT:
+ return {}
+
+ def get_pages():
+ # TODO: pagination is currently broken in API
+ payload = {'page': '1', 'per_page': '50'}
+ first_page = session.get(
+ settings.PLUGIN_CATALOG_URL,
+ headers={'User-Agent': USER_AGENT_STRING},
+ proxies=settings.HTTP_PROXIES,
+ timeout=3,
+ params=payload
+ ).json()
+ yield first_page
+ num_pages = first_page['metadata']['pagination']['last_page']
+
+ for page in range(2, num_pages + 1):
+ payload['page'] = page
+ next_page = session.get(
+ settings.PLUGIN_CATALOG_URL,
+ headers={'User-Agent': USER_AGENT_STRING},
+ proxies=settings.HTTP_PROXIES,
+ timeout=3,
+ params=payload
+ ).json()
+ yield next_page
+
+ def make_plugin_dict():
+ plugins = {}
+
+ for page in get_pages():
+ for data in page['data']:
+
+ # Populate releases
+ releases = []
+ for version in data['release_recent_history']:
+ releases.append(
+ PluginVersion(
+ date=datetime_from_timestamp(version['date']),
+ version=version['version'],
+ netbox_min_version=version['netbox_min_version'],
+ netbox_max_version=version['netbox_max_version'],
+ has_model=version['has_model'],
+ is_certified=version['is_certified'],
+ is_feature=version['is_feature'],
+ is_integration=version['is_integration'],
+ is_netboxlabs_supported=version['is_netboxlabs_supported'],
+ )
+ )
+ releases = sorted(releases, key=lambda x: x.date, reverse=True)
+ latest_release = PluginVersion(
+ date=datetime_from_timestamp(data['release_latest']['date']),
+ version=data['release_latest']['version'],
+ netbox_min_version=data['release_latest']['netbox_min_version'],
+ netbox_max_version=data['release_latest']['netbox_max_version'],
+ has_model=data['release_latest']['has_model'],
+ is_certified=data['release_latest']['is_certified'],
+ is_feature=data['release_latest']['is_feature'],
+ is_integration=data['release_latest']['is_integration'],
+ is_netboxlabs_supported=data['release_latest']['is_netboxlabs_supported'],
+ )
+
+ # Populate author (if any)
+ if data['author']:
+ author = PluginAuthor(
+ name=data['author']['name'],
+ org_id=data['author']['org_id'],
+ url=data['author']['url'],
+ )
+ else:
+ author = None
+
+ # Populate plugin data
+ plugins[data['config_name']] = Plugin(
+ id=data['id'],
+ status=data['status'],
+ title_short=data['title_short'],
+ title_long=data['title_long'],
+ tag_line=data['tag_line'],
+ description_short=data['description_short'],
+ slug=data['slug'],
+ author=author,
+ created_at=datetime_from_timestamp(data['created_at']),
+ updated_at=datetime_from_timestamp(data['updated_at']),
+ license_type=data['license_type'],
+ homepage_url=data['homepage_url'],
+ package_name_pypi=data['package_name_pypi'],
+ config_name=data['config_name'],
+ is_certified=data['is_certified'],
+ release_latest=latest_release,
+ release_recent_history=releases,
+ )
+
+ return plugins
+
+ catalog_plugins = cache.get(CACHE_KEY_CATALOG_FEED, default={})
+ if not catalog_plugins:
+ try:
+ catalog_plugins = make_plugin_dict()
+ cache.set(CACHE_KEY_CATALOG_FEED, catalog_plugins, 3600)
+ except requests.exceptions.RequestException:
+ pass
+
+ return catalog_plugins
diff --git a/netbox/core/signals.py b/netbox/core/signals.py
index f884a27b46a..06432bf4ce4 100644
--- a/netbox/core/signals.py
+++ b/netbox/core/signals.py
@@ -1,9 +1,26 @@
-from django.db.models.signals import post_save
-from django.dispatch import Signal, receiver
+import logging
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ValidationError
+from django.db.models.fields.reverse_related import ManyToManyRel
+from django.db.models.signals import m2m_changed, post_save, pre_delete
+from django.dispatch import receiver, Signal
+from django.utils.translation import gettext_lazy as _
+from django_prometheus.models import model_deletes, model_inserts, model_updates
+
+from core.choices import ObjectChangeActionChoices
+from core.events import *
+from core.models import ObjectChange
+from extras.events import enqueue_event
+from extras.utils import run_validators
+from netbox.config import get_config
+from netbox.context import current_request, events_queue
+from netbox.models.features import ChangeLoggingMixin
+from utilities.exceptions import AbortRequest
from .models import ConfigRevision
__all__ = (
+ 'clear_events',
'job_end',
'job_start',
'post_sync',
@@ -18,6 +35,152 @@ job_end = Signal()
pre_sync = Signal()
post_sync = Signal()
+# Event signals
+clear_events = Signal()
+
+
+#
+# Change logging & event handling
+#
+
+@receiver((post_save, m2m_changed))
+def handle_changed_object(sender, instance, **kwargs):
+ """
+ Fires when an object is created or updated.
+ """
+ m2m_changed = False
+
+ if not hasattr(instance, 'to_objectchange'):
+ return
+
+ # Get the current request, or bail if not set
+ request = current_request.get()
+ if request is None:
+ return
+
+ # Determine the type of change being made
+ if kwargs.get('created'):
+ event_type = OBJECT_CREATED
+ elif 'created' in kwargs:
+ event_type = OBJECT_UPDATED
+ elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']:
+ # m2m_changed with objects added or removed
+ m2m_changed = True
+ event_type = OBJECT_UPDATED
+ else:
+ return
+
+ # Create/update an ObjectChange record for this change
+ action = {
+ OBJECT_CREATED: ObjectChangeActionChoices.ACTION_CREATE,
+ OBJECT_UPDATED: ObjectChangeActionChoices.ACTION_UPDATE,
+ OBJECT_DELETED: ObjectChangeActionChoices.ACTION_DELETE,
+ }[event_type]
+ objectchange = instance.to_objectchange(action)
+ # If this is a many-to-many field change, check for a previous ObjectChange instance recorded
+ # for this object by this request and update it
+ if m2m_changed and (
+ prev_change := ObjectChange.objects.filter(
+ changed_object_type=ContentType.objects.get_for_model(instance),
+ changed_object_id=instance.pk,
+ request_id=request.id
+ ).first()
+ ):
+ prev_change.postchange_data = objectchange.postchange_data
+ prev_change.save()
+ elif objectchange and objectchange.has_changes:
+ objectchange.user = request.user
+ objectchange.request_id = request.id
+ objectchange.save()
+
+ # Ensure that we're working with fresh M2M assignments
+ if m2m_changed:
+ instance.refresh_from_db()
+
+ # Enqueue the object for event processing
+ queue = events_queue.get()
+ enqueue_event(queue, instance, request.user, request.id, event_type)
+ events_queue.set(queue)
+
+ # Increment metric counters
+ if event_type == OBJECT_CREATED:
+ model_inserts.labels(instance._meta.model_name).inc()
+ elif event_type == OBJECT_UPDATED:
+ model_updates.labels(instance._meta.model_name).inc()
+
+
+@receiver(pre_delete)
+def handle_deleted_object(sender, instance, **kwargs):
+ """
+ Fires when an object is deleted.
+ """
+ # Run any deletion protection rules for the object. Note that this must occur prior
+ # 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, [])
+ try:
+ run_validators(instance, validators)
+ except ValidationError as e:
+ raise AbortRequest(
+ _("Deletion is prevented by a protection rule: {message}").format(message=e)
+ )
+
+ # Get the current request, or bail if not set
+ request = current_request.get()
+ if request is None:
+ return
+
+ # Record an ObjectChange if applicable
+ if hasattr(instance, 'to_objectchange'):
+ if hasattr(instance, 'snapshot') and not getattr(instance, '_prechange_snapshot', None):
+ instance.snapshot()
+ objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
+ objectchange.user = request.user
+ objectchange.request_id = request.id
+ objectchange.save()
+
+ # Django does not automatically send an m2m_changed signal for the reverse direction of a
+ # many-to-many relationship (see https://code.djangoproject.com/ticket/17688), so we need to
+ # trigger one manually. We do this by checking for any reverse M2M relationships on the
+ # instance being deleted, and explicitly call .remove() on the remote M2M field to delete
+ # the association. This triggers an m2m_changed signal with the `post_remove` action type
+ # for the forward direction of the relationship, ensuring that the change is recorded.
+ for relation in instance._meta.related_objects:
+ if type(relation) is not ManyToManyRel:
+ continue
+ related_model = relation.related_model
+ related_field_name = relation.remote_field.name
+ if not issubclass(related_model, ChangeLoggingMixin):
+ # We only care about triggering the m2m_changed signal for models which support
+ # change logging
+ continue
+ for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
+ obj.snapshot() # Ensure the change record includes the "before" state
+ getattr(obj, related_field_name).remove(instance)
+
+ # Enqueue the object for event processing
+ queue = events_queue.get()
+ enqueue_event(queue, instance, request.user, request.id, OBJECT_DELETED)
+ events_queue.set(queue)
+
+ # Increment metric counters
+ model_deletes.labels(instance._meta.model_name).inc()
+
+
+@receiver(clear_events)
+def clear_events_queue(sender, **kwargs):
+ """
+ Delete any queued events (e.g. because of an aborted bulk transaction)
+ """
+ logger = logging.getLogger('events')
+ logger.info(f"Clearing {len(events_queue.get())} queued events ({sender})")
+ events_queue.set({})
+
+
+#
+# DataSource handlers
+#
@receiver(post_sync)
def auto_sync(instance, **kwargs):
diff --git a/netbox/core/tables/change_logging.py b/netbox/core/tables/change_logging.py
index 423e459e5f6..aced0e8a637 100644
--- a/netbox/core/tables/change_logging.py
+++ b/netbox/core/tables/change_logging.py
@@ -48,6 +48,6 @@ class ObjectChangeTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = ObjectChange
fields = (
- 'pk', 'id', 'time', 'user_name', 'full_name', 'action', 'changed_object_type', 'object_repr', 'request_id',
+ 'pk', 'time', 'user_name', 'full_name', 'action', 'changed_object_type', 'object_repr', 'request_id',
'actions',
)
diff --git a/netbox/core/tables/config.py b/netbox/core/tables/config.py
index 9d4cb63935d..018d89edf2f 100644
--- a/netbox/core/tables/config.py
+++ b/netbox/core/tables/config.py
@@ -19,6 +19,7 @@ REVISION_BUTTONS = """
class ConfigRevisionTable(NetBoxTable):
is_active = columns.BooleanColumn(
verbose_name=_('Is Active'),
+ false_mark=None
)
actions = columns.ActionsColumn(
actions=('delete',),
diff --git a/netbox/core/tables/plugins.py b/netbox/core/tables/plugins.py
index 21e90cd6b46..96c612366d6 100644
--- a/netbox/core/tables/plugins.py
+++ b/netbox/core/tables/plugins.py
@@ -1,39 +1,84 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
-from netbox.tables import BaseTable
+
+from netbox.tables import BaseTable, columns
__all__ = (
- 'PluginTable',
+ 'CatalogPluginTable',
+ 'PluginVersionTable',
)
-class PluginTable(BaseTable):
- name = tables.Column(
- accessor=tables.A('verbose_name'),
- verbose_name=_('Name')
- )
+class PluginVersionTable(BaseTable):
version = tables.Column(
verbose_name=_('Version')
)
- package = tables.Column(
- accessor=tables.A('name'),
- verbose_name=_('Package')
+ last_updated = columns.DateTimeColumn(
+ accessor=tables.A('date'),
+ timespec='minutes',
+ verbose_name=_('Last Updated')
)
- author = tables.Column(
- verbose_name=_('Author')
+ min_version = tables.Column(
+ accessor=tables.A('netbox_min_version'),
+ verbose_name=_('Minimum NetBox Version')
)
- author_email = tables.Column(
- verbose_name=_('Author Email')
- )
- description = tables.Column(
- verbose_name=_('Description')
+ max_version = tables.Column(
+ accessor=tables.A('netbox_max_version'),
+ verbose_name=_('Maximum NetBox Version')
)
class Meta(BaseTable.Meta):
- empty_text = _('No plugins found')
+ empty_text = _('No plugin data found')
fields = (
- 'name', 'version', 'package', 'author', 'author_email', 'description',
+ 'version', 'last_updated', 'min_version', 'max_version',
)
default_columns = (
- 'name', 'version', 'package', 'description',
+ 'version', 'last_updated', 'min_version', 'max_version',
)
+ orderable = False
+
+
+class CatalogPluginTable(BaseTable):
+ title_long = tables.Column(
+ linkify=('core:plugin', [tables.A('config_name')]),
+ verbose_name=_('Name')
+ )
+ author = tables.Column(
+ accessor=tables.A('author__name'),
+ verbose_name=_('Author')
+ )
+ is_local = columns.BooleanColumn(
+ verbose_name=_('Local')
+ )
+ is_installed = columns.BooleanColumn(
+ verbose_name=_('Installed')
+ )
+ is_certified = columns.BooleanColumn(
+ verbose_name=_('Certified')
+ )
+ created_at = columns.DateTimeColumn(
+ verbose_name=_('Published')
+ )
+ updated_at = columns.DateTimeColumn(
+ verbose_name=_('Updated')
+ )
+ installed_version = tables.Column(
+ verbose_name=_('Installed Version')
+ )
+ latest_version = tables.Column(
+ accessor=tables.A('release_latest__version'),
+ verbose_name=_('Latest Version')
+ )
+
+ class Meta(BaseTable.Meta):
+ empty_text = _('No plugin data found')
+ fields = (
+ 'title_long', 'author', 'is_local', 'is_installed', 'is_certified', 'created_at', 'updated_at',
+ 'installed_version', 'latest_version',
+ )
+ default_columns = (
+ 'title_long', 'author', 'is_local', 'is_installed', 'is_certified', 'installed_version', 'latest_version',
+ )
+ # List installed plugins first, then certified plugins, then
+ # everything else (with each tranche ordered alphabetically)
+ order_by = ('-is_installed', '-is_certified', 'name')
diff --git a/netbox/core/tests/test_api.py b/netbox/core/tests/test_api.py
index 44db21bff9c..eeb3bd9c45f 100644
--- a/netbox/core/tests/test_api.py
+++ b/netbox/core/tests/test_api.py
@@ -57,6 +57,7 @@ class DataFileTest(
):
model = DataFile
brief_fields = ['display', 'id', 'path', 'url']
+ user_permissions = ('core.view_datasource', )
@classmethod
def setUpTestData(cls):
diff --git a/netbox/core/urls.py b/netbox/core/urls.py
index 58e96d735bb..fd6ec89962a 100644
--- a/netbox/core/urls.py
+++ b/netbox/core/urls.py
@@ -49,4 +49,8 @@ urlpatterns = (
# System
path('system/', views.SystemView.as_view(), name='system'),
+
+ # Plugins
+ path('plugins/', views.PluginListView.as_view(), name='plugin_list'),
+ path('plugins//', views.PluginView.as_view(), name='plugin'),
)
diff --git a/netbox/core/views.py b/netbox/core/views.py
index a976c1ec6d1..3c531962618 100644
--- a/netbox/core/views.py
+++ b/netbox/core/views.py
@@ -2,7 +2,6 @@ import json
import platform
from django import __version__ as DJANGO_VERSION
-from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import UserPassesTestMixin
@@ -32,10 +31,15 @@ from netbox.views.generic.mixins import TableMixin
from utilities.data import shallow_compare_dict
from utilities.forms import ConfirmationForm
from utilities.htmx import htmx_partial
+from utilities.json import ConfigJSONEncoder
from utilities.query import count_related
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables
+from .choices import DataSourceStatusChoices
+from .jobs import SyncDataSourceJob
from .models import *
+from .plugins import get_catalog_plugins, get_local_plugins
+from .tables import CatalogPluginTable, PluginVersionTable
#
@@ -75,9 +79,16 @@ class DataSourceSyncView(BaseObjectView):
def post(self, request, pk):
datasource = get_object_or_404(self.queryset, pk=pk)
- job = datasource.enqueue_sync_job(request)
- messages.success(request, f"Queued job #{job.pk} to sync {datasource}")
+ # Enqueue the sync job & update the DataSource's status
+ job = SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
+ datasource.status = DataSourceStatusChoices.QUEUED
+ DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
+
+ messages.success(
+ request,
+ _("Queued job #{id} to sync {datasource}").format(id=job.pk, datasource=datasource)
+ )
return redirect(datasource.get_absolute_url())
@@ -305,7 +316,7 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
candidate_config = get_object_or_404(ConfigRevision, pk=pk)
candidate_config.activate()
- messages.success(request, f"Restored configuration revision #{pk}")
+ messages.success(request, _("Restored configuration revision #{id}").format(id=pk))
return redirect(candidate_config.get_absolute_url())
@@ -449,9 +460,9 @@ class BackgroundTaskDeleteView(BaseRQView):
# Remove job id from queue and delete the actual job
queue.connection.lrem(queue.key, 0, job.id)
job.delete()
- messages.success(request, f'Deleted job {job_id}')
+ messages.success(request, _('Job {id} has been deleted.').format(id=job_id))
else:
- messages.error(request, f'Error deleting job: {form.errors[0]}')
+ messages.error(request, _('Error deleting job {id}: {error}').format(id=job_id, error=form.errors[0]))
return redirect(reverse('core:background_queue_list'))
@@ -464,13 +475,13 @@ class BackgroundTaskRequeueView(BaseRQView):
try:
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
except NoSuchJobError:
- raise Http404(_("Job {job_id} not found").format(job_id=job_id))
+ raise Http404(_("Job {id} not found.").format(id=job_id))
queue_index = QUEUES_MAP[job.origin]
queue = get_queue_by_index(queue_index)
requeue_job(job_id, connection=queue.connection, serializer=queue.serializer)
- messages.success(request, f'You have successfully requeued: {job_id}')
+ messages.success(request, _('Job {id} has been re-enqueued.').format(id=job_id))
return redirect(reverse('core:background_task', args=[job_id]))
@@ -482,7 +493,7 @@ class BackgroundTaskEnqueueView(BaseRQView):
try:
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
except NoSuchJobError:
- raise Http404(_("Job {job_id} not found").format(job_id=job_id))
+ raise Http404(_("Job {id} not found.").format(id=job_id))
queue_index = QUEUES_MAP[job.origin]
queue = get_queue_by_index(queue_index)
@@ -505,7 +516,7 @@ class BackgroundTaskEnqueueView(BaseRQView):
registry = ScheduledJobRegistry(queue.name, queue.connection)
registry.remove(job)
- messages.success(request, f'You have successfully enqueued: {job_id}')
+ messages.success(request, _('Job {id} has been enqueued.').format(id=job_id))
return redirect(reverse('core:background_task', args=[job_id]))
@@ -522,11 +533,11 @@ class BackgroundTaskStopView(BaseRQView):
queue_index = QUEUES_MAP[job.origin]
queue = get_queue_by_index(queue_index)
- stopped, _ = stop_jobs(queue, job_id)
- if len(stopped) == 1:
- messages.success(request, f'You have successfully stopped {job_id}')
+ stopped_jobs = stop_jobs(queue, job_id)[0]
+ if len(stopped_jobs) == 1:
+ messages.success(request, _('Job {id} has been stopped.').format(id=job_id))
else:
- messages.error(request, f'Failed to stop {job_id}')
+ messages.error(request, _('Failed to stop job {id}').format(id=job_id))
return redirect(reverse('core:background_task', args=[job_id]))
@@ -581,7 +592,7 @@ class WorkerView(BaseRQView):
#
-# Plugins
+# System
#
class SystemView(UserPassesTestMixin, View):
@@ -614,39 +625,97 @@ class SystemView(UserPassesTestMixin, View):
'rq_worker_count': Worker.count(get_connection('default')),
}
- # Plugins
- plugins = [
- # Look up app config by package name
- apps.get_app_config(plugin.rsplit('.', 1)[-1]) for plugin in settings.PLUGINS
- ]
-
# Configuration
try:
config = ConfigRevision.objects.get(pk=cache.get('config_version'))
except ConfigRevision.DoesNotExist:
# Fall back to using the active config data if no record is found
- config = ConfigRevision(data=get_config().defaults)
+ config = get_config()
# Raw data export
if 'export' in request.GET:
+ stats['netbox_release'] = stats['netbox_release'].asdict()
+ params = [param.name for param in PARAMS]
data = {
**stats,
- 'plugins': {
- plugin.name: plugin.version for plugin in plugins
- },
+ 'plugins': settings.PLUGINS,
'config': {
- k: config.data[k] for k in sorted(config.data)
+ k: getattr(config, k) for k in sorted(params)
},
}
- response = HttpResponse(json.dumps(data, indent=4), content_type='text/json')
+ response = HttpResponse(json.dumps(data, cls=ConfigJSONEncoder, indent=4), content_type='text/json')
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
return response
- plugins_table = tables.PluginTable(plugins, orderable=False)
- plugins_table.configure(request)
+ # Serialize any CustomValidator classes
+ if hasattr(config, 'CUSTOM_VALIDATORS') and config.CUSTOM_VALIDATORS:
+ config.CUSTOM_VALIDATORS = json.dumps(config.CUSTOM_VALIDATORS, cls=ConfigJSONEncoder, indent=4)
return render(request, 'core/system.html', {
'stats': stats,
- 'plugins_table': plugins_table,
'config': config,
})
+
+
+#
+# Plugins
+#
+
+class BasePluginView(UserPassesTestMixin, View):
+ CACHE_KEY_CATALOG_ERROR = 'plugins-catalog-error'
+
+ def test_func(self):
+ return self.request.user.is_staff
+
+ def get_cached_plugins(self, request):
+ catalog_plugins = {}
+ catalog_plugins_error = cache.get(self.CACHE_KEY_CATALOG_ERROR, default=False)
+ if not catalog_plugins_error:
+ catalog_plugins = get_catalog_plugins()
+ if not catalog_plugins:
+ # Cache for 5 minutes to avoid spamming connection
+ cache.set(self.CACHE_KEY_CATALOG_ERROR, True, 300)
+ messages.warning(request, _("Plugins catalog could not be loaded"))
+
+ return get_local_plugins(catalog_plugins)
+
+
+class PluginListView(BasePluginView):
+
+ def get(self, request):
+ q = request.GET.get('q', None)
+
+ plugins = self.get_cached_plugins(request).values()
+ if q:
+ plugins = [obj for obj in plugins if q.casefold() in obj.title_short.casefold()]
+
+ table = CatalogPluginTable(plugins, user=request.user)
+ table.configure(request)
+
+ # If this is an HTMX request, return only the rendered table HTML
+ if htmx_partial(request):
+ return render(request, 'htmx/table.html', {
+ 'table': table,
+ })
+
+ return render(request, 'core/plugin_list.html', {
+ 'table': table,
+ })
+
+
+class PluginView(BasePluginView):
+
+ def get(self, request, name):
+
+ plugins = self.get_cached_plugins(request)
+ if name not in plugins:
+ raise Http404(_("Plugin {name} not found").format(name=name))
+ plugin = plugins[name]
+
+ table = PluginVersionTable(plugin.release_recent_history, user=request.user)
+ table.configure(request)
+
+ return render(request, 'core/plugin.html', {
+ 'plugin': plugin,
+ 'table': table,
+ })
diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py
index 6aff1bdc915..4b8f0db4a46 100644
--- a/netbox/dcim/api/nested_serializers.py
+++ b/netbox/dcim/api/nested_serializers.py
@@ -1,9 +1,15 @@
+import warnings
+
from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from dcim import models
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
+from .serializers_.nested import (
+ NestedDeviceBaySerializer, NestedDeviceSerializer, NestedInterfaceSerializer, NestedInterfaceTemplateSerializer,
+ NestedLocationSerializer, NestedModuleBaySerializer, NestedRegionSerializer, NestedSiteGroupSerializer,
+)
__all__ = [
'NestedCableSerializer',
@@ -48,35 +54,17 @@ __all__ = [
'NestedVirtualDeviceContextSerializer',
]
+# TODO: Remove in v4.2
+warnings.warn(
+ "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+ DeprecationWarning
+)
+
#
# Regions/sites
#
-@extend_schema_serializer(
- exclude_fields=('site_count',),
-)
-class NestedRegionSerializer(WritableNestedSerializer):
- site_count = serializers.IntegerField(read_only=True)
- _depth = serializers.IntegerField(source='level', read_only=True)
-
- class Meta:
- model = models.Region
- fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'site_count', '_depth']
-
-
-@extend_schema_serializer(
- exclude_fields=('site_count',),
-)
-class NestedSiteGroupSerializer(WritableNestedSerializer):
- site_count = serializers.IntegerField(read_only=True)
- _depth = serializers.IntegerField(source='level', read_only=True)
-
- class Meta:
- model = models.SiteGroup
- fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'site_count', '_depth']
-
-
class NestedSiteSerializer(WritableNestedSerializer):
class Meta:
@@ -88,18 +76,6 @@ class NestedSiteSerializer(WritableNestedSerializer):
# Racks
#
-@extend_schema_serializer(
- exclude_fields=('rack_count',),
-)
-class NestedLocationSerializer(WritableNestedSerializer):
- rack_count = serializers.IntegerField(read_only=True)
- _depth = serializers.IntegerField(source='level', read_only=True)
-
- class Meta:
- model = models.Location
- fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'rack_count', '_depth']
-
-
@extend_schema_serializer(
exclude_fields=('rack_count',),
)
@@ -200,13 +176,6 @@ class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display_url', 'display', 'name']
-class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
-
- class Meta:
- model = models.InterfaceTemplate
- fields = ['id', 'url', 'display_url', 'display', 'name']
-
-
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
class Meta:
@@ -271,13 +240,6 @@ class NestedPlatformSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
-class NestedDeviceSerializer(WritableNestedSerializer):
-
- class Meta:
- model = models.Device
- fields = ['id', 'url', 'display_url', 'display', 'name']
-
-
class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
class Meta:
@@ -285,13 +247,6 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display_url', 'display', 'name']
-class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
-
- class Meta:
- model = models.Module
- fields = ['id', 'url', 'display_url', 'display', 'serial']
-
-
class NestedModuleSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
module_bay = ModuleNestedModuleBaySerializer(read_only=True)
@@ -338,15 +293,6 @@ class NestedPowerPortSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
-class NestedInterfaceSerializer(WritableNestedSerializer):
- device = NestedDeviceSerializer(read_only=True)
- _occupied = serializers.BooleanField(required=False, read_only=True)
-
- class Meta:
- model = models.Interface
- fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
-
-
class NestedRearPortSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
_occupied = serializers.BooleanField(required=False, read_only=True)
@@ -365,22 +311,6 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
-class NestedModuleBaySerializer(WritableNestedSerializer):
- installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
-
- class Meta:
- model = models.ModuleBay
- fields = ['id', 'url', 'display_url', 'display', 'installed_module', 'name']
-
-
-class NestedDeviceBaySerializer(WritableNestedSerializer):
- device = NestedDeviceSerializer(read_only=True)
-
- class Meta:
- model = models.DeviceBay
- fields = ['id', 'url', 'display_url', 'display', 'device', 'name']
-
-
class NestedInventoryItemSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True)
diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index 4f8bbac1769..30aa2e1a0a7 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -11,4 +11,3 @@ from .serializers_.devices import *
from .serializers_.device_components import *
from .serializers_.power import *
from .serializers_.rackunits import *
-from .nested_serializers import *
diff --git a/netbox/dcim/api/serializers_/base.py b/netbox/dcim/api/serializers_/base.py
index 48f4967e363..1dca773b21d 100644
--- a/netbox/dcim/api/serializers_/base.py
+++ b/netbox/dcim/api/serializers_/base.py
@@ -13,7 +13,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
"""
Legacy serializer for pre-v3.3 connections
"""
- connected_endpoints_type = serializers.SerializerMethodField(read_only=True)
+ connected_endpoints_type = serializers.SerializerMethodField(read_only=True, allow_null=True)
connected_endpoints = serializers.SerializerMethodField(read_only=True)
connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True)
@@ -22,7 +22,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
if endpoints := obj.connected_endpoints:
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
- @extend_schema_field(serializers.ListField)
+ @extend_schema_field(serializers.ListField(allow_null=True))
def get_connected_endpoints(self, obj):
"""
Return the appropriate serializer for the type of connected object.
diff --git a/netbox/dcim/api/serializers_/cables.py b/netbox/dcim/api/serializers_/cables.py
index 53dc3a0d671..397e5cd169f 100644
--- a/netbox/dcim/api/serializers_/cables.py
+++ b/netbox/dcim/api/serializers_/cables.py
@@ -89,7 +89,7 @@ class CablePathSerializer(serializers.ModelSerializer):
class CabledObjectSerializer(serializers.ModelSerializer):
cable = CableSerializer(nested=True, read_only=True, allow_null=True)
cable_end = serializers.CharField(read_only=True)
- link_peers_type = serializers.SerializerMethodField(read_only=True)
+ link_peers_type = serializers.SerializerMethodField(read_only=True, allow_null=True)
link_peers = serializers.SerializerMethodField(read_only=True)
_occupied = serializers.SerializerMethodField(read_only=True)
diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py
index ab3944723b7..e285ce34937 100644
--- a/netbox/dcim/api/serializers_/device_components.py
+++ b/netbox/dcim/api/serializers_/device_components.py
@@ -15,7 +15,7 @@ from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelated
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from utilities.api import get_serializer_for_model
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
-from wireless.api.nested_serializers import NestedWirelessLinkSerializer
+from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
from wireless.choices import *
from wireless.models import WirelessLAN
@@ -23,8 +23,8 @@ from .base import ConnectedEndpointsSerializer
from .cables import CabledObjectSerializer
from .devices import DeviceSerializer, ModuleSerializer, VirtualDeviceContextSerializer
from .manufacturers import ManufacturerSerializer
+from .nested import NestedInterfaceSerializer
from .roles import InventoryItemRoleSerializer
-from ..nested_serializers import *
__all__ = (
'ConsolePortSerializer',
@@ -155,7 +155,7 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
class Meta:
model = PowerOutlet
fields = [
- 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port',
+ 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'power_port',
'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
'created', 'last_updated', '_occupied',
@@ -297,6 +297,13 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
class ModuleBaySerializer(NetBoxModelSerializer):
device = DeviceSerializer(nested=True)
+ module = ModuleSerializer(
+ nested=True,
+ fields=('id', 'url', 'display'),
+ required=False,
+ allow_null=True,
+ default=None
+ )
installed_module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'serial', 'description'),
@@ -307,7 +314,7 @@ class ModuleBaySerializer(NetBoxModelSerializer):
class Meta:
model = ModuleBay
fields = [
- 'id', 'url', 'display_url', 'display', 'device', 'name', 'installed_module', 'label', 'position',
+ 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'installed_module', 'label', 'position',
'description', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
@@ -338,11 +345,12 @@ class InventoryItemSerializer(NetBoxModelSerializer):
)
component = serializers.SerializerMethodField(read_only=True, allow_null=True)
_depth = serializers.IntegerField(source='level', read_only=True)
+ status = ChoiceField(choices=InventoryItemStatusChoices, required=False)
class Meta:
model = InventoryItem
fields = [
- 'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer',
+ 'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'status', 'role', 'manufacturer',
'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type', 'component_id',
'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
]
diff --git a/netbox/dcim/api/serializers_/devices.py b/netbox/dcim/api/serializers_/devices.py
index f4e72f08279..7a01b49e815 100644
--- a/netbox/dcim/api/serializers_/devices.py
+++ b/netbox/dcim/api/serializers_/devices.py
@@ -16,9 +16,9 @@ from .devicetypes import *
from .platforms import PlatformSerializer
from .racks import RackSerializer
from .roles import DeviceRoleSerializer
+from .nested import NestedDeviceBaySerializer, NestedDeviceSerializer, NestedModuleBaySerializer
from .sites import LocationSerializer, SiteSerializer
from .virtualchassis import VirtualChassisSerializer
-from ..nested_serializers import *
__all__ = (
'DeviceSerializer',
@@ -87,7 +87,7 @@ class DeviceSerializer(NetBoxModelSerializer):
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
- @extend_schema_field(NestedDeviceSerializer)
+ @extend_schema_field(NestedDeviceSerializer(allow_null=True))
def get_parent_device(self, obj):
try:
device_bay = obj.parent_bay
diff --git a/netbox/dcim/api/serializers_/devicetype_components.py b/netbox/dcim/api/serializers_/devicetype_components.py
index b03fbc63452..04f6395a6db 100644
--- a/netbox/dcim/api/serializers_/devicetype_components.py
+++ b/netbox/dcim/api/serializers_/devicetype_components.py
@@ -14,8 +14,8 @@ from utilities.api import get_serializer_for_model
from wireless.choices import *
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
from .manufacturers import ManufacturerSerializer
+from .nested import NestedInterfaceTemplateSerializer
from .roles import InventoryItemRoleSerializer
-from ..nested_serializers import *
__all__ = (
'ConsolePortTemplateSerializer',
@@ -253,13 +253,22 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
class ModuleBayTemplateSerializer(ValidatedModelSerializer):
device_type = DeviceTypeSerializer(
- nested=True
+ nested=True,
+ required=False,
+ allow_null=True,
+ default=None
+ )
+ module_type = ModuleTypeSerializer(
+ nested=True,
+ required=False,
+ allow_null=True,
+ default=None
)
class Meta:
model = ModuleBayTemplate
fields = [
- 'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description',
+ 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'position', 'description',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
diff --git a/netbox/dcim/api/serializers_/devicetypes.py b/netbox/dcim/api/serializers_/devicetypes.py
index a755247db10..0ce2af2f89a 100644
--- a/netbox/dcim/api/serializers_/devicetypes.py
+++ b/netbox/dcim/api/serializers_/devicetypes.py
@@ -7,6 +7,7 @@ from dcim.choices import *
from dcim.models import DeviceType, ModuleType
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
+from netbox.choices import *
from .manufacturers import ManufacturerSerializer
from .platforms import PlatformSerializer
@@ -62,13 +63,27 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
class ModuleTypeSerializer(NetBoxModelSerializer):
- manufacturer = ManufacturerSerializer(nested=True)
- weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
+ manufacturer = ManufacturerSerializer(
+ nested=True
+ )
+ weight_unit = ChoiceField(
+ choices=WeightUnitChoices,
+ allow_blank=True,
+ required=False,
+ allow_null=True
+ )
+ airflow = ChoiceField(
+ choices=ModuleAirflowChoices,
+ allow_blank=True,
+ required=False,
+ allow_null=True
+ )
class Meta:
model = ModuleType
fields = [
- 'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit',
- 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+ 'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'part_number', 'airflow',
+ 'weight', 'weight_unit', 'description', 'comments', 'tags', 'custom_fields',
+ 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description')
diff --git a/netbox/dcim/api/serializers_/manufacturers.py b/netbox/dcim/api/serializers_/manufacturers.py
index 61158e0f75d..1a1eea6ec35 100644
--- a/netbox/dcim/api/serializers_/manufacturers.py
+++ b/netbox/dcim/api/serializers_/manufacturers.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from dcim.models import Manufacturer
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
diff --git a/netbox/dcim/api/serializers_/nested.py b/netbox/dcim/api/serializers_/nested.py
new file mode 100644
index 00000000000..ea346cc63fa
--- /dev/null
+++ b/netbox/dcim/api/serializers_/nested.py
@@ -0,0 +1,97 @@
+from drf_spectacular.utils import extend_schema_serializer
+from rest_framework import serializers
+
+from netbox.api.serializers import WritableNestedSerializer
+from dcim import models
+
+__all__ = (
+ 'NestedDeviceBaySerializer',
+ 'NestedDeviceSerializer',
+ 'NestedInterfaceSerializer',
+ 'NestedInterfaceTemplateSerializer',
+ 'NestedLocationSerializer',
+ 'NestedModuleBaySerializer',
+ 'NestedRegionSerializer',
+ 'NestedSiteGroupSerializer',
+)
+
+
+@extend_schema_serializer(
+ exclude_fields=('site_count',),
+)
+class NestedRegionSerializer(WritableNestedSerializer):
+ site_count = serializers.IntegerField(read_only=True)
+ _depth = serializers.IntegerField(source='level', read_only=True)
+
+ class Meta:
+ model = models.Region
+ fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'site_count', '_depth']
+
+
+@extend_schema_serializer(
+ exclude_fields=('site_count',),
+)
+class NestedSiteGroupSerializer(WritableNestedSerializer):
+ site_count = serializers.IntegerField(read_only=True)
+ _depth = serializers.IntegerField(source='level', read_only=True)
+
+ class Meta:
+ model = models.SiteGroup
+ fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'site_count', '_depth']
+
+
+@extend_schema_serializer(
+ exclude_fields=('rack_count',),
+)
+class NestedLocationSerializer(WritableNestedSerializer):
+ rack_count = serializers.IntegerField(read_only=True)
+ _depth = serializers.IntegerField(source='level', read_only=True)
+
+ class Meta:
+ model = models.Location
+ fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'rack_count', '_depth']
+
+
+class NestedDeviceSerializer(WritableNestedSerializer):
+
+ class Meta:
+ model = models.Device
+ fields = ['id', 'url', 'display_url', 'display', 'name']
+
+
+class NestedInterfaceSerializer(WritableNestedSerializer):
+ device = NestedDeviceSerializer(read_only=True)
+ _occupied = serializers.BooleanField(required=False, read_only=True)
+
+ class Meta:
+ model = models.Interface
+ fields = ['id', 'url', 'display_url', 'display', 'device', 'name', 'cable', '_occupied']
+
+
+class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
+
+ class Meta:
+ model = models.InterfaceTemplate
+ fields = ['id', 'url', 'display', 'name']
+
+
+class NestedDeviceBaySerializer(WritableNestedSerializer):
+ device = NestedDeviceSerializer(read_only=True)
+
+ class Meta:
+ model = models.DeviceBay
+ fields = ['id', 'url', 'display_url', 'display', 'device', 'name']
+
+
+class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
+
+ class Meta:
+ model = models.Module
+ fields = ['id', 'url', 'display_url', 'display', 'serial']
+
+
+class NestedModuleBaySerializer(WritableNestedSerializer):
+
+ class Meta:
+ model = models.ModuleBay
+ fields = ['id', 'url', 'display_url', 'display', 'name']
diff --git a/netbox/dcim/api/serializers_/platforms.py b/netbox/dcim/api/serializers_/platforms.py
index 3c846f8fd11..2f47457019f 100644
--- a/netbox/dcim/api/serializers_/platforms.py
+++ b/netbox/dcim/api/serializers_/platforms.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from dcim.models import Platform
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
from netbox.api.fields import RelatedObjectCountField
diff --git a/netbox/dcim/api/serializers_/power.py b/netbox/dcim/api/serializers_/power.py
index fc65a0732f4..4c2cf54fbcb 100644
--- a/netbox/dcim/api/serializers_/power.py
+++ b/netbox/dcim/api/serializers_/power.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from dcim.choices import *
from dcim.models import PowerFeed, PowerPanel
from netbox.api.fields import ChoiceField, RelatedObjectCountField
diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py
index d8d73800195..1378c265a85 100644
--- a/netbox/dcim/api/serializers_/racks.py
+++ b/netbox/dcim/api/serializers_/racks.py
@@ -3,12 +3,14 @@ from rest_framework import serializers
from dcim.choices import *
from dcim.constants import *
-from dcim.models import Rack, RackReservation, RackRole
+from dcim.models import Rack, RackReservation, RackRole, RackType
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
+from netbox.choices import *
from netbox.config import ConfigItem
from tenancy.api.serializers_.tenants import TenantSerializer
from users.api.serializers_.users import UserSerializer
+from .manufacturers import ManufacturerSerializer
from .sites import LocationSerializer, SiteSerializer
__all__ = (
@@ -16,6 +18,7 @@ __all__ = (
'RackReservationSerializer',
'RackRoleSerializer',
'RackSerializer',
+ 'RackTypeSerializer',
)
@@ -33,18 +36,89 @@ class RackRoleSerializer(NetBoxModelSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
-class RackSerializer(NetBoxModelSerializer):
- site = SiteSerializer(nested=True)
- location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
- tenant = TenantSerializer(nested=True, required=False, allow_null=True)
- status = ChoiceField(choices=RackStatusChoices, required=False)
- role = RackRoleSerializer(nested=True, required=False, allow_null=True)
- type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False, allow_null=True)
- facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'),
- default=None)
- width = ChoiceField(choices=RackWidthChoices, required=False)
- outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
- weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
+class RackBaseSerializer(NetBoxModelSerializer):
+ form_factor = ChoiceField(
+ choices=RackFormFactorChoices,
+ allow_blank=True,
+ required=False,
+ allow_null=True
+ )
+ width = ChoiceField(
+ choices=RackWidthChoices,
+ required=False
+ )
+ outer_unit = ChoiceField(
+ choices=RackDimensionUnitChoices,
+ allow_blank=True,
+ required=False,
+ allow_null=True
+ )
+ weight_unit = ChoiceField(
+ choices=WeightUnitChoices,
+ allow_blank=True,
+ required=False,
+ allow_null=True
+ )
+
+
+class RackTypeSerializer(RackBaseSerializer):
+ manufacturer = ManufacturerSerializer(
+ nested=True
+ )
+
+ class Meta:
+ model = RackType
+ fields = [
+ 'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor',
+ 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
+ 'max_weight', 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags',
+ 'custom_fields', 'created', 'last_updated',
+ ]
+ brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description')
+
+
+class RackSerializer(RackBaseSerializer):
+ site = SiteSerializer(
+ nested=True
+ )
+ location = LocationSerializer(
+ nested=True,
+ required=False,
+ allow_null=True,
+ default=None
+ )
+ tenant = TenantSerializer(
+ nested=True,
+ required=False,
+ allow_null=True
+ )
+ status = ChoiceField(
+ choices=RackStatusChoices,
+ required=False
+ )
+ airflow = ChoiceField(
+ choices=RackAirflowChoices,
+ allow_blank=True,
+ required=False
+ )
+ role = RackRoleSerializer(
+ nested=True,
+ required=False,
+ allow_null=True
+ )
+ facility_id = serializers.CharField(
+ max_length=50,
+ allow_blank=True,
+ allow_null=True,
+ label=_('Facility ID'),
+ default=None
+ )
+ rack_type = RackTypeSerializer(
+ nested=True,
+ required=False,
+ allow_null=True,
+ default=None
+ )
# Related object counts
device_count = RelatedObjectCountField('devices')
@@ -54,9 +128,10 @@ class RackSerializer(NetBoxModelSerializer):
model = Rack
fields = [
'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status',
- 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight',
- 'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description',
- 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
+ 'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'weight',
+ 'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
+ 'airflow', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
+ 'powerfeed_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
diff --git a/netbox/dcim/api/serializers_/roles.py b/netbox/dcim/api/serializers_/roles.py
index e9c9d35639a..8f922da1079 100644
--- a/netbox/dcim/api/serializers_/roles.py
+++ b/netbox/dcim/api/serializers_/roles.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from dcim.models import DeviceRole, InventoryItemRole
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
from netbox.api.fields import RelatedObjectCountField
diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py
index f45a1949da7..dc91f5dc766 100644
--- a/netbox/dcim/api/serializers_/sites.py
+++ b/netbox/dcim/api/serializers_/sites.py
@@ -8,7 +8,7 @@ from ipam.models import ASN
from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
-from ..nested_serializers import *
+from .nested import NestedLocationSerializer, NestedRegionSerializer, NestedSiteGroupSerializer
__all__ = (
'LocationSerializer',
diff --git a/netbox/dcim/api/serializers_/virtualchassis.py b/netbox/dcim/api/serializers_/virtualchassis.py
index 19e94ba8dd3..a93d2833f01 100644
--- a/netbox/dcim/api/serializers_/virtualchassis.py
+++ b/netbox/dcim/api/serializers_/virtualchassis.py
@@ -2,7 +2,7 @@ from rest_framework import serializers
from dcim.models import VirtualChassis
from netbox.api.serializers import NetBoxModelSerializer
-from ..nested_serializers import *
+from .nested import NestedDeviceSerializer
__all__ = (
'VirtualChassisSerializer',
diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py
index 36a0c99a599..d099b392a3c 100644
--- a/netbox/dcim/api/urls.py
+++ b/netbox/dcim/api/urls.py
@@ -12,6 +12,7 @@ router.register('sites', views.SiteViewSet)
# Racks
router.register('locations', views.LocationViewSet)
+router.register('rack-types', views.RackTypeViewSet)
router.register('rack-roles', views.RackRoleViewSet)
router.register('racks', views.RackViewSet)
router.register('rack-reservations', views.RackReservationViewSet)
diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py
index be7a9c30645..87aa7535c35 100644
--- a/netbox/dcim/api/views.py
+++ b/netbox/dcim/api/views.py
@@ -161,6 +161,16 @@ class RackRoleViewSet(NetBoxModelViewSet):
filterset_class = filtersets.RackRoleFilterSet
+#
+# Rack Types
+#
+
+class RackTypeViewSet(NetBoxModelViewSet):
+ queryset = RackType.objects.all()
+ serializer_class = serializers.RackTypeSerializer
+ filterset_class = filtersets.RackTypeFilterSet
+
+
#
# Racks
#
diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py
index 4df66e36747..9653d3b93a9 100644
--- a/netbox/dcim/apps.py
+++ b/netbox/dcim/apps.py
@@ -10,7 +10,7 @@ class DCIMConfig(AppConfig):
def ready(self):
from netbox.models.features import register_models
from utilities.counters import connect_counters
- from . import signals, search
+ from . import signals, search # noqa: F401
from .models import CableTermination, Device, DeviceType, VirtualChassis
# Register models
diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py
index fe8d8a158f6..fee587e6b6c 100644
--- a/netbox/dcim/choices.py
+++ b/netbox/dcim/choices.py
@@ -51,7 +51,7 @@ class LocationStatusChoices(ChoiceSet):
# Racks
#
-class RackTypeChoices(ChoiceSet):
+class RackFormFactorChoices(ChoiceSet):
TYPE_2POST = '2-post-frame'
TYPE_4POST = '4-post-frame'
@@ -127,6 +127,17 @@ class RackElevationDetailRenderChoices(ChoiceSet):
)
+class RackAirflowChoices(ChoiceSet):
+
+ FRONT_TO_REAR = 'front-to-rear'
+ REAR_TO_FRONT = 'rear-to-front'
+
+ CHOICES = (
+ (FRONT_TO_REAR, _('Front to rear')),
+ (REAR_TO_FRONT, _('Rear to front')),
+ )
+
+
#
# DeviceTypes
#
@@ -186,6 +197,9 @@ class DeviceAirflowChoices(ChoiceSet):
AIRFLOW_LEFT_TO_RIGHT = 'left-to-right'
AIRFLOW_RIGHT_TO_LEFT = 'right-to-left'
AIRFLOW_SIDE_TO_REAR = 'side-to-rear'
+ AIRFLOW_REAR_TO_SIDE = 'rear-to-side'
+ AIRFLOW_BOTTOM_TO_TOP = 'bottom-to-top'
+ AIRFLOW_TOP_TO_BOTTOM = 'top-to-bottom'
AIRFLOW_PASSIVE = 'passive'
AIRFLOW_MIXED = 'mixed'
@@ -195,6 +209,9 @@ class DeviceAirflowChoices(ChoiceSet):
(AIRFLOW_LEFT_TO_RIGHT, _('Left to right')),
(AIRFLOW_RIGHT_TO_LEFT, _('Right to left')),
(AIRFLOW_SIDE_TO_REAR, _('Side to rear')),
+ (AIRFLOW_REAR_TO_SIDE, _('Rear to side')),
+ (AIRFLOW_BOTTOM_TO_TOP, _('Bottom to top')),
+ (AIRFLOW_TOP_TO_BOTTOM, _('Top to bottom')),
(AIRFLOW_PASSIVE, _('Passive')),
(AIRFLOW_MIXED, _('Mixed')),
)
@@ -224,6 +241,25 @@ class ModuleStatusChoices(ChoiceSet):
]
+class ModuleAirflowChoices(ChoiceSet):
+
+ FRONT_TO_REAR = 'front-to-rear'
+ REAR_TO_FRONT = 'rear-to-front'
+ LEFT_TO_RIGHT = 'left-to-right'
+ RIGHT_TO_LEFT = 'right-to-left'
+ SIDE_TO_REAR = 'side-to-rear'
+ PASSIVE = 'passive'
+
+ CHOICES = (
+ (FRONT_TO_REAR, _('Front to rear')),
+ (REAR_TO_FRONT, _('Rear to front')),
+ (LEFT_TO_RIGHT, _('Left to right')),
+ (RIGHT_TO_LEFT, _('Right to left')),
+ (SIDE_TO_REAR, _('Side to rear')),
+ (PASSIVE, _('Passive')),
+ )
+
+
#
# ConsolePorts
#
@@ -366,6 +402,7 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_NEMA_L1560P = 'nema-l15-60p'
TYPE_NEMA_L2120P = 'nema-l21-20p'
TYPE_NEMA_L2130P = 'nema-l21-30p'
+ TYPE_NEMA_L2220P = 'nema-l22-20p'
TYPE_NEMA_L2230P = 'nema-l22-30p'
# California style
TYPE_CS6361C = 'cs6361c'
@@ -487,6 +524,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_NEMA_L1560P, 'NEMA L15-60P'),
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
+ (TYPE_NEMA_L2220P, 'NEMA L22-20P'),
(TYPE_NEMA_L2230P, 'NEMA L22-30P'),
)),
(_('California Style'), (
@@ -619,6 +657,7 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_NEMA_L1560R = 'nema-l15-60r'
TYPE_NEMA_L2120R = 'nema-l21-20r'
TYPE_NEMA_L2130R = 'nema-l21-30r'
+ TYPE_NEMA_L2220R = 'nema-l22-20r'
TYPE_NEMA_L2230R = 'nema-l22-30r'
# California style
TYPE_CS6360C = 'CS6360C'
@@ -651,6 +690,7 @@ class PowerOutletTypeChoices(ChoiceSet):
# Direct current (DC)
TYPE_DC = 'dc-terminal'
# Proprietary
+ TYPE_EATON_C39 = 'eaton-c39'
TYPE_HDOT_CX = 'hdot-cx'
TYPE_SAF_D_GRID = 'saf-d-grid'
TYPE_NEUTRIK_POWERCON_20A = 'neutrik-powercon-20a'
@@ -733,6 +773,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_NEMA_L1560R, 'NEMA L15-60R'),
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
+ (TYPE_NEMA_L2220R, 'NEMA L22-20R'),
(TYPE_NEMA_L2230R, 'NEMA L22-30R'),
)),
(_('California Style'), (
@@ -771,6 +812,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_DC, 'DC Terminal'),
)),
(_('Proprietary'), (
+ (TYPE_EATON_C39, 'Eaton C39'),
(TYPE_HDOT_CX, 'HDOT Cx'),
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
(TYPE_NEUTRIK_POWERCON_20A, 'Neutrik powerCON (20A)'),
@@ -827,6 +869,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_100ME_LFX = '100base-lfx'
TYPE_100ME_FIXED = '100base-tx'
TYPE_100ME_T1 = '100base-t1'
+ TYPE_100ME_SFP = '100base-x-sfp'
TYPE_1GE_FIXED = '1000base-t'
TYPE_1GE_TX_FIXED = '1000base-tx'
TYPE_1GE_GBIC = '1000base-x-gbic'
@@ -886,7 +929,9 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_80211AD = 'ieee802.11ad'
TYPE_80211AX = 'ieee802.11ax'
TYPE_80211AY = 'ieee802.11ay'
+ TYPE_80211BE = 'ieee802.11be'
TYPE_802151 = 'ieee802.15.1'
+ TYPE_802154 = 'ieee802.15.4'
TYPE_OTHER_WIRELESS = 'other-wireless'
# Cellular
@@ -998,6 +1043,7 @@ class InterfaceTypeChoices(ChoiceSet):
(
_('Ethernet (modular)'),
(
+ (TYPE_100ME_SFP, 'SFP (100ME)'),
(TYPE_1GE_GBIC, 'GBIC (1GE)'),
(TYPE_1GE_SFP, 'SFP (1GE)'),
(TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'),
@@ -1057,7 +1103,9 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_80211AD, 'IEEE 802.11ad'),
(TYPE_80211AX, 'IEEE 802.11ax'),
(TYPE_80211AY, 'IEEE 802.11ay'),
+ (TYPE_80211BE, 'IEEE 802.11be'),
(TYPE_802151, 'IEEE 802.15.1 (Bluetooth)'),
+ (TYPE_802154, 'IEEE 802.15.4 (LR-WPAN)'),
(TYPE_OTHER_WIRELESS, 'Other (Wireless)'),
)
),
@@ -1315,6 +1363,14 @@ class PortTypeChoices(ChoiceSet):
TYPE_URM_P2 = 'urm-p2'
TYPE_URM_P4 = 'urm-p4'
TYPE_URM_P8 = 'urm-p8'
+ TYPE_USB_A = 'usb-a'
+ TYPE_USB_B = 'usb-b'
+ TYPE_USB_C = 'usb-c'
+ TYPE_USB_MINI_A = 'usb-mini-a'
+ TYPE_USB_MINI_B = 'usb-mini-b'
+ TYPE_USB_MICRO_A = 'usb-micro-a'
+ TYPE_USB_MICRO_B = 'usb-micro-b'
+ TYPE_USB_MICRO_AB = 'usb-micro-ab'
TYPE_OTHER = 'other'
CHOICES = (
@@ -1374,6 +1430,19 @@ class PortTypeChoices(ChoiceSet):
(TYPE_SPLICE, 'Splice'),
),
),
+ (
+ _('USB'),
+ (
+ (TYPE_USB_A, 'USB Type A'),
+ (TYPE_USB_B, 'USB Type B'),
+ (TYPE_USB_C, 'USB Type C'),
+ (TYPE_USB_MINI_A, 'USB Mini A'),
+ (TYPE_USB_MINI_B, 'USB Mini B'),
+ (TYPE_USB_MICRO_A, 'USB Micro A'),
+ (TYPE_USB_MICRO_B, 'USB Micro B'),
+ (TYPE_USB_MICRO_AB, 'USB Micro AB'),
+ ),
+ ),
(
_('Other'),
(
@@ -1412,6 +1481,7 @@ class CableTypeChoices(ChoiceSet):
TYPE_SMF_OS2 = 'smf-os2'
TYPE_AOC = 'aoc'
TYPE_POWER = 'power'
+ TYPE_USB = 'usb'
CHOICES = (
(
@@ -1444,6 +1514,7 @@ class CableTypeChoices(ChoiceSet):
(TYPE_AOC, 'Active Optical Cabling (AOC)'),
),
),
+ (TYPE_USB, _('USB')),
(TYPE_POWER, _('Power')),
)
@@ -1483,24 +1554,6 @@ class CableLengthUnitChoices(ChoiceSet):
)
-class WeightUnitChoices(ChoiceSet):
-
- # Metric
- UNIT_KILOGRAM = 'kg'
- UNIT_GRAM = 'g'
-
- # Imperial
- UNIT_POUND = 'lb'
- UNIT_OUNCE = 'oz'
-
- CHOICES = (
- (UNIT_KILOGRAM, _('Kilograms')),
- (UNIT_GRAM, _('Grams')),
- (UNIT_POUND, _('Pounds')),
- (UNIT_OUNCE, _('Ounces')),
- )
-
-
#
# CableTerminations
#
@@ -1585,3 +1638,27 @@ class VirtualDeviceContextStatusChoices(ChoiceSet):
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_OFFLINE, _('Offline'), 'red'),
]
+
+
+#
+# InventoryItem
+#
+
+class InventoryItemStatusChoices(ChoiceSet):
+ key = 'InventoryItem.status'
+
+ STATUS_OFFLINE = 'offline'
+ STATUS_ACTIVE = 'active'
+ STATUS_PLANNED = 'planned'
+ STATUS_STAGED = 'staged'
+ STATUS_FAILED = 'failed'
+ STATUS_DECOMMISSIONING = 'decommissioning'
+
+ CHOICES = [
+ (STATUS_OFFLINE, _('Offline'), 'gray'),
+ (STATUS_ACTIVE, _('Active'), 'green'),
+ (STATUS_PLANNED, _('Planned'), 'cyan'),
+ (STATUS_STAGED, _('Staged'), 'blue'),
+ (STATUS_FAILED, _('Failed'), 'red'),
+ (STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'),
+ ]
diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py
index 303fc234413..ba3e6464b2c 100644
--- a/netbox/dcim/constants.py
+++ b/netbox/dcim/constants.py
@@ -49,7 +49,9 @@ WIRELESS_IFACE_TYPES = [
InterfaceTypeChoices.TYPE_80211AD,
InterfaceTypeChoices.TYPE_80211AX,
InterfaceTypeChoices.TYPE_80211AY,
+ InterfaceTypeChoices.TYPE_80211BE,
InterfaceTypeChoices.TYPE_802151,
+ InterfaceTypeChoices.TYPE_802154,
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
]
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index a4d75654e0e..04ac3a3d201 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -1,5 +1,4 @@
import django_filters
-from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
@@ -16,11 +15,12 @@ from netbox.filtersets import (
)
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from tenancy.models import *
+from users.models import User
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
)
-from virtualization.models import Cluster
+from virtualization.models import Cluster, ClusterGroup
from vpn.models import L2VPN
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
from wireless.models import WirelessLAN, WirelessLink
@@ -69,6 +69,7 @@ __all__ = (
'RackFilterSet',
'RackReservationFilterSet',
'RackRoleFilterSet',
+ 'RackTypeFilterSet',
'RearPortFilterSet',
'RearPortTemplateFilterSet',
'RegionFilterSet',
@@ -270,7 +271,7 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
class Meta:
model = Location
- fields = ('id', 'name', 'slug', 'status', 'facility', 'description')
+ fields = ('id', 'name', 'slug', 'facility', 'description')
def search(self, queryset, name, value):
if not value.strip():
@@ -289,6 +290,41 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
fields = ('id', 'name', 'slug', 'color', 'description')
+class RackTypeFilterSet(NetBoxModelFilterSet):
+ manufacturer_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=Manufacturer.objects.all(),
+ label=_('Manufacturer (ID)'),
+ )
+ manufacturer = django_filters.ModelMultipleChoiceFilter(
+ field_name='manufacturer__slug',
+ queryset=Manufacturer.objects.all(),
+ to_field_name='slug',
+ label=_('Manufacturer (slug)'),
+ )
+ form_factor = django_filters.MultipleChoiceFilter(
+ choices=RackFormFactorChoices
+ )
+ width = django_filters.MultipleChoiceFilter(
+ choices=RackWidthChoices
+ )
+
+ class Meta:
+ model = RackType
+ fields = (
+ 'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
+ 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
+ )
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(model__icontains=value) |
+ Q(description__icontains=value) |
+ Q(comments__icontains=value)
+ )
+
+
class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
@@ -339,12 +375,33 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
to_field_name='slug',
label=_('Location (slug)'),
)
+ manufacturer_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='rack_type__manufacturer',
+ queryset=Manufacturer.objects.all(),
+ label=_('Manufacturer (ID)'),
+ )
+ manufacturer = django_filters.ModelMultipleChoiceFilter(
+ field_name='rack_type__manufacturer__slug',
+ queryset=Manufacturer.objects.all(),
+ to_field_name='slug',
+ label=_('Manufacturer (slug)'),
+ )
+ rack_type = django_filters.ModelMultipleChoiceFilter(
+ field_name='rack_type__slug',
+ queryset=RackType.objects.all(),
+ to_field_name='slug',
+ label=_('Rack type (slug)'),
+ )
+ rack_type_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=RackType.objects.all(),
+ label=_('Rack type (ID)'),
+ )
status = django_filters.MultipleChoiceFilter(
choices=RackStatusChoices,
null_value=None
)
- type = django_filters.MultipleChoiceFilter(
- choices=RackTypeChoices
+ form_factor = django_filters.MultipleChoiceFilter(
+ choices=RackFormFactorChoices
)
width = django_filters.MultipleChoiceFilter(
choices=RackWidthChoices
@@ -367,7 +424,8 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
model = Rack
fields = (
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
- 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
+ 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit',
+ 'description',
)
def search(self, queryset, name, value):
@@ -439,12 +497,12 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
label=_('Location (slug)'),
)
user_id = django_filters.ModelMultipleChoiceFilter(
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
label=_('User (ID)'),
)
user = django_filters.ModelMultipleChoiceFilter(
field_name='user__username',
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
to_field_name='username',
label=_('User (name)'),
)
@@ -652,7 +710,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
class Meta:
model = ModuleType
- fields = ('id', 'model', 'part_number', 'weight', 'weight_unit', 'description')
+ fields = ('id', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'description')
def search(self, queryset, name, value):
if not value.strip():
@@ -800,7 +858,7 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
-class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
+class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta:
model = ModuleBayTemplate
@@ -1012,6 +1070,17 @@ class DeviceFilterSet(
queryset=Cluster.objects.all(),
label=_('VM cluster (ID)'),
)
+ cluster_group = django_filters.ModelMultipleChoiceFilter(
+ field_name='cluster__group__slug',
+ queryset=ClusterGroup.objects.all(),
+ to_field_name='slug',
+ label=_('Cluster group (slug)'),
+ )
+ cluster_group_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='cluster__group',
+ queryset=ClusterGroup.objects.all(),
+ label=_('Cluster group (ID)'),
+ )
model = django_filters.ModelMultipleChoiceFilter(
field_name='device_type__slug',
queryset=DeviceType.objects.all(),
@@ -1253,11 +1322,11 @@ class ModuleFilterSet(NetBoxModelFilterSet):
to_field_name='model',
label=_('Module type (model)'),
)
- module_bay_id = django_filters.ModelMultipleChoiceFilter(
- field_name='module_bay',
+ module_bay_id = TreeNodeMultipleChoiceFilter(
queryset=ModuleBay.objects.all(),
- to_field_name='id',
- label=_('Module Bay (ID)')
+ field_name='module_bay',
+ lookup_expr='in',
+ label=_('Module bay (ID)'),
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
@@ -1372,12 +1441,12 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
to_field_name='model',
label=_('Device type (model)'),
)
- role_id = django_filters.ModelMultipleChoiceFilter(
+ device_role_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__role',
queryset=DeviceRole.objects.all(),
label=_('Device role (ID)'),
)
- role = django_filters.ModelMultipleChoiceFilter(
+ device_role = django_filters.ModelMultipleChoiceFilter(
field_name='device__role__slug',
queryset=DeviceRole.objects.all(),
to_field_name='slug',
@@ -1394,6 +1463,10 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
to_field_name='name',
label=_('Virtual Chassis'),
)
+ device_status = django_filters.MultipleChoiceFilter(
+ choices=DeviceStatusChoices,
+ field_name='device__status',
+ )
def search(self, queryset, name, value):
if not value.strip():
@@ -1521,7 +1594,7 @@ class PowerOutletFilterSet(
class Meta:
model = PowerOutlet
fields = (
- 'id', 'name', 'label', 'feed_leg', 'description', 'mark_connected', 'cable_end',
+ 'id', 'name', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
)
@@ -1724,7 +1797,11 @@ class RearPortFilterSet(
)
-class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
+class ModuleBayFilterSet(ModularDeviceComponentFilterSet, NetBoxModelFilterSet):
+ parent_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=ModuleBay.objects.all(),
+ label=_('Parent module bay (ID)'),
+ )
installed_module_id = django_filters.ModelMultipleChoiceFilter(
field_name='installed_module',
queryset=ModuleBay.objects.all(),
@@ -1783,10 +1860,14 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
serial = MultiValueCharFilter(
lookup_expr='iexact'
)
+ status = django_filters.MultipleChoiceFilter(
+ choices=InventoryItemStatusChoices,
+ null_value=None
+ )
class Meta:
model = InventoryItem
- fields = ('id', 'name', 'label', 'part_id', 'asset_tag', 'description', 'discovered')
+ fields = ('id', 'name', 'label', 'part_id', 'asset_tag', 'status', 'description', 'discovered')
def search(self, queryset, name, value):
if not value.strip():
diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py
index 2939b986e82..0ea3aee6344 100644
--- a/netbox/dcim/forms/bulk_create.py
+++ b/netbox/dcim/forms/bulk_create.py
@@ -69,7 +69,7 @@ class PowerPortBulkCreateForm(
class PowerOutletBulkCreateForm(
- form_from_model(PowerOutlet, ['type', 'feed_leg', 'mark_connected']),
+ form_from_model(PowerOutlet, ['type', 'color', 'feed_leg', 'mark_connected']),
DeviceBulkAddComponentForm
):
model = PowerOutlet
diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py
index 25b049e6d52..ac20173f23e 100644
--- a/netbox/dcim/forms/bulk_edit.py
+++ b/netbox/dcim/forms/bulk_edit.py
@@ -1,6 +1,5 @@
from django import forms
from django.conf import settings
-from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneFormField
@@ -9,11 +8,13 @@ from dcim.constants import *
from dcim.models import *
from extras.models import ConfigTemplate
from ipam.models import ASN, VLAN, VLANGroup, VRF
+from netbox.choices import *
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
+from users.models import User
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
-from utilities.forms.rendering import FieldSet
+from utilities.forms.rendering import FieldSet, InlineFields
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
from wireless.models import WirelessLAN, WirelessLANGroup
from wireless.choices import WirelessRoleChoices
@@ -52,6 +53,7 @@ __all__ = (
'RackBulkEditForm',
'RackReservationBulkEditForm',
'RackRoleBulkEditForm',
+ 'RackTypeBulkEditForm',
'RearPortBulkEditForm',
'RearPortTemplateBulkEditForm',
'RegionBulkEditForm',
@@ -218,6 +220,95 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = ('color', 'description')
+class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
+ manufacturer = DynamicModelChoiceField(
+ label=_('Manufacturer'),
+ queryset=Manufacturer.objects.all(),
+ required=False
+ )
+ form_factor = forms.ChoiceField(
+ label=_('Form factor'),
+ choices=add_blank_choice(RackFormFactorChoices),
+ required=False
+ )
+ width = forms.ChoiceField(
+ label=_('Width'),
+ choices=add_blank_choice(RackWidthChoices),
+ required=False
+ )
+ u_height = forms.IntegerField(
+ required=False,
+ label=_('Height (U)')
+ )
+ starting_unit = forms.IntegerField(
+ required=False,
+ min_value=1
+ )
+ desc_units = forms.NullBooleanField(
+ required=False,
+ widget=BulkEditNullBooleanSelect,
+ label=_('Descending units')
+ )
+ outer_width = forms.IntegerField(
+ label=_('Outer width'),
+ required=False,
+ min_value=1
+ )
+ outer_depth = forms.IntegerField(
+ label=_('Outer depth'),
+ required=False,
+ min_value=1
+ )
+ outer_unit = forms.ChoiceField(
+ label=_('Outer unit'),
+ choices=add_blank_choice(RackDimensionUnitChoices),
+ required=False
+ )
+ mounting_depth = forms.IntegerField(
+ label=_('Mounting depth'),
+ required=False,
+ min_value=1
+ )
+ weight = forms.DecimalField(
+ label=_('Weight'),
+ min_value=0,
+ required=False
+ )
+ max_weight = forms.IntegerField(
+ label=_('Max weight'),
+ min_value=0,
+ required=False
+ )
+ weight_unit = forms.ChoiceField(
+ label=_('Weight unit'),
+ choices=add_blank_choice(WeightUnitChoices),
+ required=False,
+ initial=''
+ )
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
+ )
+ comments = CommentField()
+
+ model = RackType
+ fieldsets = (
+ FieldSet('manufacturer', 'description', 'form_factor', 'width', 'u_height', name=_('Rack Type')),
+ FieldSet(
+ InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
+ InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
+ 'mounting_depth',
+ name=_('Dimensions')
+ ),
+ FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
+ )
+ nullable_fields = (
+ 'outer_width', 'outer_depth', 'outer_unit', 'weight',
+ 'max_weight', 'weight_unit', 'description', 'comments',
+ )
+
+
class RackBulkEditForm(NetBoxModelBulkEditForm):
region = DynamicModelChoiceField(
label=_('Region'),
@@ -278,9 +369,9 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
max_length=50,
required=False
)
- type = forms.ChoiceField(
- label=_('Type'),
- choices=add_blank_choice(RackTypeChoices),
+ form_factor = forms.ChoiceField(
+ label=_('Form factor'),
+ choices=add_blank_choice(RackFormFactorChoices),
required=False
)
width = forms.ChoiceField(
@@ -317,6 +408,11 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
required=False,
min_value=1
)
+ airflow = forms.ChoiceField(
+ label=_('Airflow'),
+ choices=add_blank_choice(RackAirflowChoices),
+ required=False
+ )
weight = forms.DecimalField(
label=_('Weight'),
min_value=0,
@@ -345,8 +441,8 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'description', name=_('Rack')),
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
FieldSet(
- 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
- name=_('Hardware')
+ 'form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'outer_width', 'outer_depth', 'outer_unit',
+ 'mounting_depth', name=_('Hardware')
),
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
)
@@ -359,9 +455,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
user = forms.ModelChoiceField(
label=_('User'),
- queryset=get_user_model().objects.order_by(
- 'username'
- ),
+ queryset=User.objects.order_by('username'),
required=False
)
tenant = DynamicModelChoiceField(
@@ -471,6 +565,11 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
label=_('Part number'),
required=False
)
+ airflow = forms.ChoiceField(
+ label=_('Airflow'),
+ choices=add_blank_choice(ModuleAirflowChoices),
+ required=False
+ )
weight = forms.DecimalField(
label=_('Weight'),
min_value=0,
@@ -492,7 +591,11 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
model = ModuleType
fieldsets = (
FieldSet('manufacturer', 'part_number', 'description', name=_('Module Type')),
- FieldSet('weight', 'weight_unit', name=_('Weight')),
+ FieldSet(
+ 'airflow',
+ InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
+ name=_('Chassis')
+ ),
)
nullable_fields = ('part_number', 'weight', 'weight_unit', 'description', 'comments')
@@ -1188,12 +1291,17 @@ class ComponentBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
+ def __init__(self, *args, initial=None, **kwargs):
+ try:
+ self.device_id = int(initial.get('device'))
+ except (TypeError, ValueError):
+ self.device_id = None
+
+ super().__init__(*args, initial=initial, **kwargs)
# Limit module queryset to Modules which belong to the parent Device
- if 'device' in self.initial:
- device = Device.objects.filter(pk=self.initial['device']).first()
+ if self.device_id:
+ device = Device.objects.filter(pk=self.device_id).first()
self.fields['module'].queryset = Module.objects.filter(device=device)
else:
self.fields['module'].choices = ()
@@ -1201,8 +1309,8 @@ class ComponentBulkEditForm(NetBoxModelBulkEditForm):
class ConsolePortBulkEditForm(
- form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']),
- ComponentBulkEditForm
+ ComponentBulkEditForm,
+ form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),
@@ -1218,8 +1326,8 @@ class ConsolePortBulkEditForm(
class ConsoleServerPortBulkEditForm(
- form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']),
- ComponentBulkEditForm
+ ComponentBulkEditForm,
+ form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),
@@ -1235,8 +1343,8 @@ class ConsoleServerPortBulkEditForm(
class PowerPortBulkEditForm(
- form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']),
- ComponentBulkEditForm
+ ComponentBulkEditForm,
+ form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),
@@ -1253,8 +1361,8 @@ class PowerPortBulkEditForm(
class PowerOutletBulkEditForm(
- form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']),
- ComponentBulkEditForm
+ ComponentBulkEditForm,
+ form_from_model(PowerOutlet, ['label', 'type', 'color', 'feed_leg', 'power_port', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),
@@ -1264,7 +1372,7 @@ class PowerOutletBulkEditForm(
model = PowerOutlet
fieldsets = (
- FieldSet('module', 'type', 'label', 'description', 'mark_connected'),
+ FieldSet('module', 'type', 'label', 'description', 'mark_connected', 'color'),
FieldSet('feed_leg', 'power_port', name=_('Power')),
)
nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description')
@@ -1273,8 +1381,8 @@ class PowerOutletBulkEditForm(
super().__init__(*args, **kwargs)
# Limit power_port queryset to PowerPorts which belong to the parent Device
- if 'device' in self.initial:
- device = Device.objects.filter(pk=self.initial['device']).first()
+ if self.device_id:
+ device = Device.objects.filter(pk=self.device_id).first()
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
else:
self.fields['power_port'].choices = ()
@@ -1282,12 +1390,12 @@ class PowerOutletBulkEditForm(
class InterfaceBulkEditForm(
+ ComponentBulkEditForm,
form_from_model(Interface, [
'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only',
'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width',
'tx_power', 'wireless_lans'
- ]),
- ComponentBulkEditForm
+ ])
):
enabled = forms.NullBooleanField(
label=_('Enabled'),
@@ -1416,8 +1524,8 @@ class InterfaceBulkEditForm(
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- if 'device' in self.initial:
- device = Device.objects.filter(pk=self.initial['device']).first()
+ if self.device_id:
+ device = Device.objects.filter(pk=self.device_id).first()
# Restrict parent/bridge/LAG interface assignment by device
self.fields['parent'].widget.add_query_param('virtual_chassis_member_id', device.pk)
@@ -1480,8 +1588,8 @@ class InterfaceBulkEditForm(
class FrontPortBulkEditForm(
- form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']),
- ComponentBulkEditForm
+ ComponentBulkEditForm,
+ form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),
@@ -1497,8 +1605,8 @@ class FrontPortBulkEditForm(
class RearPortBulkEditForm(
- form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']),
- ComponentBulkEditForm
+ ComponentBulkEditForm,
+ form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description'])
):
mark_connected = forms.NullBooleanField(
label=_('Mark connected'),
@@ -1554,10 +1662,16 @@ class InventoryItemBulkEditForm(
queryset=Manufacturer.objects.all(),
required=False
)
+ status = forms.ChoiceField(
+ label=_('Status'),
+ choices=add_blank_choice(InventoryItemStatusChoices),
+ required=False,
+ initial=''
+ )
model = InventoryItem
fieldsets = (
- FieldSet('device', 'label', 'role', 'manufacturer', 'part_id', 'description'),
+ FieldSet('device', 'label', 'role', 'manufacturer', 'part_id', 'status', 'description'),
)
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
index 5a64cad02fb..e34db001697 100644
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -9,7 +9,8 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from extras.models import ConfigTemplate
-from ipam.models import VRF
+from ipam.models import VRF, IPAddress
+from netbox.choices import *
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms.fields import (
@@ -45,6 +46,7 @@ __all__ = (
'RackImportForm',
'RackReservationImportForm',
'RackRoleImportForm',
+ 'RackTypeImportForm',
'RearPortImportForm',
'RegionImportForm',
'SiteImportForm',
@@ -174,9 +176,54 @@ class RackRoleImportForm(NetBoxModelImportForm):
class Meta:
model = RackRole
fields = ('name', 'slug', 'color', 'description', 'tags')
- help_texts = {
- 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'),
- }
+
+
+class RackTypeImportForm(NetBoxModelImportForm):
+ manufacturer = forms.ModelChoiceField(
+ label=_('Manufacturer'),
+ queryset=Manufacturer.objects.all(),
+ to_field_name='name',
+ help_text=_('The manufacturer of this rack type')
+ )
+ form_factor = CSVChoiceField(
+ label=_('Type'),
+ choices=RackFormFactorChoices,
+ required=False,
+ help_text=_('Form factor')
+ )
+ starting_unit = forms.IntegerField(
+ required=False,
+ min_value=1,
+ help_text=_('The lowest-numbered position in the rack')
+ )
+ width = forms.ChoiceField(
+ label=_('Width'),
+ choices=RackWidthChoices,
+ help_text=_('Rail-to-rail width (in inches)')
+ )
+ outer_unit = CSVChoiceField(
+ label=_('Outer unit'),
+ choices=RackDimensionUnitChoices,
+ required=False,
+ help_text=_('Unit for outer dimensions')
+ )
+ weight_unit = CSVChoiceField(
+ label=_('Weight unit'),
+ choices=WeightUnitChoices,
+ required=False,
+ help_text=_('Unit for rack weights')
+ )
+
+ class Meta:
+ model = RackType
+ fields = (
+ 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
+ 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
+ 'weight_unit', 'description', 'comments', 'tags',
+ )
+
+ def __init__(self, data=None, *args, **kwargs):
+ super().__init__(data, *args, **kwargs)
class RackImportForm(NetBoxModelImportForm):
@@ -210,11 +257,11 @@ class RackImportForm(NetBoxModelImportForm):
to_field_name='name',
help_text=_('Name of assigned role')
)
- type = CSVChoiceField(
+ form_factor = CSVChoiceField(
label=_('Type'),
- choices=RackTypeChoices,
+ choices=RackFormFactorChoices,
required=False,
- help_text=_('Rack type')
+ help_text=_('Form factor')
)
width = forms.ChoiceField(
label=_('Width'),
@@ -227,6 +274,12 @@ class RackImportForm(NetBoxModelImportForm):
required=False,
help_text=_('Unit for outer dimensions')
)
+ airflow = CSVChoiceField(
+ label=_('Airflow'),
+ choices=RackAirflowChoices,
+ required=False,
+ help_text=_('Airflow direction')
+ )
weight_unit = CSVChoiceField(
label=_('Weight unit'),
choices=WeightUnitChoices,
@@ -237,9 +290,9 @@ class RackImportForm(NetBoxModelImportForm):
class Meta:
model = Rack
fields = (
- 'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag',
- 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight',
- 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
+ 'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'form_factor', 'serial', 'asset_tag',
+ 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow',
+ 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
)
def __init__(self, data=None, *args, **kwargs):
@@ -315,13 +368,13 @@ class ManufacturerImportForm(NetBoxModelImportForm):
class DeviceTypeImportForm(NetBoxModelImportForm):
- manufacturer = forms.ModelChoiceField(
+ manufacturer = CSVModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
to_field_name='name',
help_text=_('The manufacturer which produces this device type')
)
- default_platform = forms.ModelChoiceField(
+ default_platform = CSVModelChoiceField(
label=_('Default platform'),
queryset=Platform.objects.all(),
to_field_name='name',
@@ -354,6 +407,12 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
queryset=Manufacturer.objects.all(),
to_field_name='name'
)
+ airflow = CSVChoiceField(
+ label=_('Airflow'),
+ choices=ModuleAirflowChoices,
+ required=False,
+ help_text=_('Airflow direction')
+ )
weight = forms.DecimalField(
label=_('Weight'),
required=False,
@@ -368,7 +427,7 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
class Meta:
model = ModuleType
- fields = ['manufacturer', 'model', 'part_number', 'description', 'weight', 'weight_unit', 'comments', 'tags']
+ fields = ['manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'comments', 'tags']
class DeviceRoleImportForm(NetBoxModelImportForm):
@@ -384,9 +443,6 @@ class DeviceRoleImportForm(NetBoxModelImportForm):
class Meta:
model = DeviceRole
fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags')
- help_texts = {
- 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'),
- }
class PlatformImportForm(NetBoxModelImportForm):
@@ -743,7 +799,7 @@ class PowerOutletImportForm(NetBoxModelImportForm):
class Meta:
model = PowerOutlet
- fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description', 'tags')
+ fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'power_port', 'feed_leg', 'description', 'tags')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1048,11 +1104,16 @@ class InventoryItemImportForm(NetBoxModelImportForm):
required=False,
help_text=_('Component Name')
)
+ status = CSVChoiceField(
+ label=_('Status'),
+ choices=InventoryItemStatusChoices,
+ help_text=_('Operational status')
+ )
class Meta:
model = InventoryItem
fields = (
- 'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
+ 'device', 'name', 'label', 'status', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', 'discovered',
'description', 'tags', 'component_type', 'component_name',
)
@@ -1104,9 +1165,6 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm):
class Meta:
model = InventoryItemRole
fields = ('name', 'slug', 'color', 'description')
- help_texts = {
- 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'),
- }
#
@@ -1183,9 +1241,6 @@ class CableImportForm(NetBoxModelImportForm):
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
]
- help_texts = {
- 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'),
- }
def _clean_side(self, side):
"""
@@ -1386,9 +1441,33 @@ class VirtualDeviceContextImportForm(NetBoxModelImportForm):
label=_('Status'),
choices=VirtualDeviceContextStatusChoices,
)
+ primary_ip4 = CSVModelChoiceField(
+ label=_('Primary IPv4'),
+ queryset=IPAddress.objects.all(),
+ required=False,
+ to_field_name='address',
+ help_text=_('IPv4 address with mask, e.g. 1.2.3.4/24')
+ )
+ primary_ip6 = CSVModelChoiceField(
+ label=_('Primary IPv6'),
+ queryset=IPAddress.objects.all(),
+ required=False,
+ to_field_name='address',
+ help_text=_('IPv6 address with prefix length, e.g. 2001:db8::1/64')
+ )
class Meta:
fields = [
- 'name', 'device', 'status', 'tenant', 'identifier', 'comments',
+ 'name', 'device', 'status', 'tenant', 'identifier', 'comments', 'primary_ip4', 'primary_ip6',
]
model = VirtualDeviceContext
+
+ def __init__(self, data=None, *args, **kwargs):
+ super().__init__(data, *args, **kwargs)
+
+ if data:
+
+ # Limit primary_ip4/ip6 querysets by assigned device
+ params = {f"interface__device__{self.fields['device'].to_field_name}": data.get('device')}
+ self.fields['primary_ip4'].queryset = self.fields['primary_ip4'].queryset.filter(**params)
+ self.fields['primary_ip6'].queryset = self.fields['primary_ip6'].queryset.filter(**params)
diff --git a/netbox/dcim/forms/common.py b/netbox/dcim/forms/common.py
index 3be4d08e8ec..4341ec04110 100644
--- a/netbox/dcim/forms/common.py
+++ b/netbox/dcim/forms/common.py
@@ -70,6 +70,18 @@ class InterfaceCommonForm(forms.Form):
class ModuleCommonForm(forms.Form):
+ def _get_module_bay_tree(self, module_bay):
+ module_bays = []
+ while module_bay:
+ module_bays.append(module_bay)
+ if module_bay.module:
+ module_bay = module_bay.module.module_bay
+ else:
+ module_bay = None
+
+ module_bays.reverse()
+ return module_bays
+
def clean(self):
super().clean()
@@ -88,6 +100,8 @@ class ModuleCommonForm(forms.Form):
self.instance._disable_replication = True
return
+ module_bays = self._get_module_bay_tree(module_bay)
+
for templates, component_attribute in [
("consoleporttemplates", "consoleports"),
("consoleserverporttemplates", "consoleserverports"),
@@ -104,13 +118,24 @@ class ModuleCommonForm(forms.Form):
# Get the templates for the module type.
for template in getattr(module_type, templates).all():
+ resolved_name = template.name
# Installing modules with placeholders require that the bay has a position value
- if MODULE_TOKEN in template.name and not module_bay.position:
- raise forms.ValidationError(
- _("Cannot install module with placeholder values in a module bay with no position defined.")
- )
+ if MODULE_TOKEN in template.name:
+ if not module_bay.position:
+ raise forms.ValidationError(
+ _("Cannot install module with placeholder values in a module bay with no position defined.")
+ )
+
+ if len(module_bays) != template.name.count(MODULE_TOKEN):
+ raise forms.ValidationError(
+ _("Cannot install module with placeholder values in a module bay tree {level} in tree but {tokens} placeholders given.").format(
+ level=len(module_bays), tokens=template.name.count(MODULE_TOKEN)
+ )
+ )
+
+ for module_bay in module_bays:
+ resolved_name = resolved_name.replace(MODULE_TOKEN, module_bay.position, 1)
- resolved_name = template.name.replace(MODULE_TOKEN, module_bay.position)
existing_item = installed_components.get(resolved_name)
# It is not possible to adopt components already belonging to a module
diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py
index 44bea047ac9..324f8ecfdad 100644
--- a/netbox/dcim/forms/connections.py
+++ b/netbox/dcim/forms/connections.py
@@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
from circuits.models import Circuit, CircuitTermination
from dcim.models import *
-from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.fields import DynamicModelMultipleChoiceField
from .model_forms import CableForm
@@ -19,7 +19,7 @@ def get_cable_form(a_type, b_type):
# Device component
if hasattr(term_cls, 'device'):
- attrs[f'termination_{cable_end}_device'] = DynamicModelChoiceField(
+ attrs[f'termination_{cable_end}_device'] = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
label=_('Device'),
required=False,
@@ -33,6 +33,7 @@ def get_cable_form(a_type, b_type):
label=term_cls._meta.verbose_name.title(),
context={
'disabled': '_occupied',
+ 'parent': 'device',
},
query_params={
'device_id': f'$termination_{cable_end}_device',
@@ -43,7 +44,7 @@ def get_cable_form(a_type, b_type):
# PowerFeed
elif term_cls == PowerFeed:
- attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelChoiceField(
+ attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelMultipleChoiceField(
queryset=PowerPanel.objects.all(),
label=_('Power Panel'),
required=False,
@@ -57,6 +58,7 @@ def get_cable_form(a_type, b_type):
label=_('Power Feed'),
context={
'disabled': '_occupied',
+ 'parent': 'powerpanel',
},
query_params={
'power_panel_id': f'$termination_{cable_end}_powerpanel',
@@ -66,7 +68,7 @@ def get_cable_form(a_type, b_type):
# CircuitTermination
elif term_cls == CircuitTermination:
- attrs[f'termination_{cable_end}_circuit'] = DynamicModelChoiceField(
+ attrs[f'termination_{cable_end}_circuit'] = DynamicModelMultipleChoiceField(
queryset=Circuit.objects.all(),
label=_('Circuit'),
selector=True,
@@ -79,6 +81,7 @@ def get_cable_form(a_type, b_type):
label=_('Side'),
context={
'disabled': '_occupied',
+ 'parent': 'circuit',
},
query_params={
'circuit_id': f'$termination_{cable_end}_circuit',
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index 0a28a4ec445..13478263ec9 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -1,5 +1,4 @@
from django import forms
-from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
@@ -8,12 +7,15 @@ from dcim.models import *
from extras.forms import LocalConfigContextFilterForm
from extras.models import ConfigTemplate
from ipam.models import ASN, VRF
+from netbox.choices import *
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
+from users.models import User
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
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
from vpn.models import L2VPN
from wireless.choices import *
@@ -34,7 +36,6 @@ __all__ = (
'LocationFilterForm',
'ManufacturerFilterForm',
'ModuleFilterForm',
- 'ModuleFilterForm',
'ModuleBayFilterForm',
'ModuleTypeFilterForm',
'PlatformFilterForm',
@@ -47,6 +48,7 @@ __all__ = (
'RackElevationFilterForm',
'RackReservationFilterForm',
'RackRoleFilterForm',
+ 'RackTypeFilterForm',
'RearPortFilterForm',
'RegionFilterForm',
'SiteFilterForm',
@@ -128,6 +130,11 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
},
label=_('Device')
)
+ device_status = forms.MultipleChoiceField(
+ choices=DeviceStatusChoices,
+ required=False,
+ label=_('Device Status'),
+ )
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
@@ -194,7 +201,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
model = Location
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
- FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', name=_('Attributes')),
+ FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', 'facility', name=_('Attributes')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
@@ -231,6 +238,10 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
choices=LocationStatusChoices,
required=False
)
+ facility = forms.CharField(
+ label=_('Facility'),
+ required=False
+ )
tag = TagFilterField(model)
@@ -239,16 +250,82 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model)
-class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
+class RackBaseFilterForm(NetBoxModelFilterSetForm):
+ form_factor = forms.MultipleChoiceField(
+ label=_('Form factor'),
+ choices=RackFormFactorChoices,
+ required=False
+ )
+ width = forms.MultipleChoiceField(
+ label=_('Width'),
+ choices=RackWidthChoices,
+ required=False
+ )
+ u_height = forms.IntegerField(
+ required=False,
+ min_value=1
+ )
+ starting_unit = forms.IntegerField(
+ required=False,
+ min_value=1
+ )
+ desc_units = forms.NullBooleanField(
+ required=False,
+ label=_('Descending units'),
+ widget=forms.Select(
+ choices=BOOLEAN_WITH_BLANK_CHOICES
+ )
+ )
+ airflow = forms.MultipleChoiceField(
+ label=_('Airflow'),
+ choices=add_blank_choice(RackAirflowChoices),
+ required=False
+ )
+ weight = forms.DecimalField(
+ label=_('Weight'),
+ required=False,
+ min_value=1
+ )
+ max_weight = forms.IntegerField(
+ label=_('Max weight'),
+ required=False,
+ min_value=1
+ )
+ weight_unit = forms.ChoiceField(
+ label=_('Weight unit'),
+ choices=add_blank_choice(WeightUnitChoices),
+ required=False
+ )
+
+
+class RackTypeFilterForm(RackBaseFilterForm):
+ model = RackType
+ fieldsets = (
+ FieldSet('q', 'filter_id', 'tag'),
+ FieldSet('form_factor', 'width', 'u_height', name=_('Rack Type')),
+ FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
+ FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
+ )
+ selector_fields = ('filter_id', 'q', 'manufacturer_id')
+ manufacturer_id = DynamicModelMultipleChoiceField(
+ queryset=Manufacturer.objects.all(),
+ required=False,
+ label=_('Manufacturer')
+ )
+ tag = TagFilterField(model)
+
+
+class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
model = Rack
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_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('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('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
region_id = DynamicModelMultipleChoiceField(
@@ -283,22 +360,25 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
choices=RackStatusChoices,
required=False
)
- type = forms.MultipleChoiceField(
- label=_('Type'),
- choices=RackTypeChoices,
- required=False
- )
- width = forms.MultipleChoiceField(
- label=_('Width'),
- choices=RackWidthChoices,
- required=False
- )
role_id = DynamicModelMultipleChoiceField(
queryset=RackRole.objects.all(),
required=False,
null_option='None',
label=_('Role')
)
+ manufacturer_id = DynamicModelMultipleChoiceField(
+ queryset=Manufacturer.objects.all(),
+ required=False,
+ label=_('Manufacturer')
+ )
+ rack_type_id = DynamicModelMultipleChoiceField(
+ queryset=RackType.objects.all(),
+ required=False,
+ query_params={
+ 'manufacturer_id': '$manufacturer_id'
+ },
+ label=_('Rack type')
+ )
serial = forms.CharField(
label=_('Serial'),
required=False
@@ -308,21 +388,6 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
required=False
)
tag = TagFilterField(model)
- weight = forms.DecimalField(
- label=_('Weight'),
- required=False,
- min_value=1
- )
- max_weight = forms.IntegerField(
- label=_('Max weight'),
- required=False,
- min_value=1
- )
- weight_unit = forms.ChoiceField(
- label=_('Weight unit'),
- choices=add_blank_choice(WeightUnitChoices),
- required=False
- )
class RackElevationFilterForm(RackFilterForm):
@@ -392,7 +457,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
label=_('Rack')
)
user_id = DynamicModelMultipleChoiceField(
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
required=False,
label=_('User')
)
@@ -540,7 +605,7 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
model = ModuleType
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
- FieldSet('manufacturer_id', 'part_number', name=_('Hardware')),
+ FieldSet('manufacturer_id', 'part_number', 'airflow', name=_('Hardware')),
FieldSet(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', name=_('Components')
@@ -600,6 +665,11 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
)
)
tag = TagFilterField(model)
+ airflow = forms.MultipleChoiceField(
+ label=_('Airflow'),
+ choices=add_blank_choice(ModuleAirflowChoices),
+ required=False
+ )
weight = forms.DecimalField(
label=_('Weight'),
required=False
@@ -655,6 +725,7 @@ class DeviceFilterForm(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
name=_('Components')
),
+ FieldSet('cluster_group_id', 'cluster_id', name=_('Cluster')),
FieldSet(
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
'has_virtual_device_context',
@@ -821,6 +892,16 @@ class DeviceFilterForm(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
+ cluster_id = DynamicModelMultipleChoiceField(
+ queryset=Cluster.objects.all(),
+ required=False,
+ label=_('Cluster')
+ )
+ cluster_group_id = DynamicModelMultipleChoiceField(
+ queryset=ClusterGroup.objects.all(),
+ required=False,
+ label=_('Cluster group')
+ )
tag = TagFilterField(model)
@@ -1157,7 +1238,9 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
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('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+ FieldSet(
+ 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
+ ),
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
)
type = forms.MultipleChoiceField(
@@ -1179,7 +1262,10 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
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('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+ FieldSet(
+ 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
+ name=_('Device')
+ ),
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
)
type = forms.MultipleChoiceField(
@@ -1201,7 +1287,9 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
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('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+ FieldSet(
+ 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
+ ),
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
)
type = forms.MultipleChoiceField(
@@ -1216,9 +1304,12 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
- FieldSet('name', 'label', 'type', name=_('Attributes')),
+ FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
- FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+ FieldSet(
+ 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
+ name=_('Device')
+ ),
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
)
type = forms.MultipleChoiceField(
@@ -1227,6 +1318,10 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
required=False
)
tag = TagFilterField(model)
+ color = ColorField(
+ label=_('Color'),
+ required=False
+ )
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
@@ -1238,7 +1333,10 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
- FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id', name=_('Device')),
+ FieldSet(
+ 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', 'vdc_id',
+ name=_('Device')
+ ),
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
)
selector_fields = ('filter_id', 'q', 'device_id')
@@ -1346,7 +1444,9 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
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('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+ FieldSet(
+ 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
+ ),
FieldSet('cabled', 'occupied', name=_('Cable')),
)
model = FrontPort
@@ -1368,7 +1468,10 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
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('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+ FieldSet(
+ 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
+ name=_('Device')
+ ),
FieldSet('cabled', 'occupied', name=_('Cable')),
)
type = forms.MultipleChoiceField(
@@ -1389,7 +1492,10 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
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('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+ FieldSet(
+ 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
+ name=_('Device')
+ ),
)
tag = TagFilterField(model)
position = forms.CharField(
@@ -1404,7 +1510,10 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
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('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+ FieldSet(
+ 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
+ name=_('Device')
+ ),
)
tag = TagFilterField(model)
@@ -1418,7 +1527,10 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
name=_('Attributes')
),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
- FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
+ FieldSet(
+ 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
+ name=_('Device')
+ ),
)
role_id = DynamicModelMultipleChoiceField(
queryset=InventoryItemRole.objects.all(),
@@ -1445,6 +1557,11 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
+ status = forms.MultipleChoiceField(
+ label=_('Status'),
+ choices=InventoryItemStatusChoices,
+ required=False
+ )
tag = TagFilterField(model)
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
index d493687f959..095882d13f1 100644
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -1,5 +1,4 @@
from django import forms
-from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneFormField
@@ -11,7 +10,8 @@ from extras.models import ConfigTemplate
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
-from utilities.forms import add_blank_choice
+from users.models import User
+from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
)
@@ -57,6 +57,7 @@ __all__ = (
'RackForm',
'RackReservationForm',
'RackRoleForm',
+ 'RackTypeForm',
'RearPortForm',
'RearPortTemplateForm',
'RegionForm',
@@ -201,6 +202,37 @@ class RackRoleForm(NetBoxModelForm):
]
+class RackTypeForm(NetBoxModelForm):
+ manufacturer = DynamicModelChoiceField(
+ label=_('Manufacturer'),
+ queryset=Manufacturer.objects.all()
+ )
+ comments = CommentField()
+ slug = SlugField(
+ label=_('Slug'),
+ slug_source='model'
+ )
+
+ fieldsets = (
+ FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')),
+ FieldSet(
+ 'width', 'u_height',
+ InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
+ InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
+ 'mounting_depth', name=_('Dimensions')
+ ),
+ FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
+ )
+
+ class Meta:
+ model = RackType
+ fields = [
+ 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
+ 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
+ 'description', 'comments', 'tags',
+ ]
+
+
class RackForm(TenancyForm, NetBoxModelForm):
site = DynamicModelChoiceField(
label=_('Site'),
@@ -220,28 +252,54 @@ class RackForm(TenancyForm, NetBoxModelForm):
queryset=RackRole.objects.all(),
required=False
)
+ rack_type = DynamicModelChoiceField(
+ label=_('Rack Type'),
+ queryset=RackType.objects.all(),
+ required=False,
+ help_text=_("Select a pre-defined rack type, or set physical characteristics below.")
+ )
comments = CommentField()
fieldsets = (
- FieldSet('site', 'location', 'name', 'status', 'role', 'description', 'tags', name=_('Rack')),
+ FieldSet('site', 'location', 'name', 'status', 'role', 'rack_type', 'description', 'airflow', 'tags', name=_('Rack')),
FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
- FieldSet(
- 'type', 'width', 'starting_unit', 'u_height',
- InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
- InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
- 'mounting_depth', 'desc_units', name=_('Dimensions')
- ),
)
class Meta:
model = Rack
fields = [
'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
- 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth',
- 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
+ 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
+ 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit',
+ 'description', 'comments', 'tags',
]
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Mimic HTMXSelect()
+ self.fields['rack_type'].widget.attrs.update({
+ 'hx-get': '.',
+ 'hx-include': '#form_fields',
+ 'hx-target': '#form_fields',
+ })
+
+ # Omit RackType-defined fields if rack_type is set
+ if get_field_value(self, 'rack_type'):
+ for field_name in Rack.RACKTYPE_FIELDS:
+ del self.fields[field_name]
+ else:
+ self.fieldsets = (
+ *self.fieldsets,
+ FieldSet(
+ 'form_factor', 'width', 'starting_unit', 'u_height',
+ InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
+ InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
+ 'mounting_depth', 'desc_units', name=_('Dimensions')
+ ),
+ )
+
class RackReservationForm(TenancyForm, NetBoxModelForm):
rack = DynamicModelChoiceField(
@@ -256,9 +314,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
)
user = forms.ModelChoiceField(
label=_('User'),
- queryset=get_user_model().objects.order_by(
- 'username'
- )
+ queryset=User.objects.order_by('username')
)
comments = CommentField()
@@ -343,13 +399,14 @@ class ModuleTypeForm(NetBoxModelForm):
fieldsets = (
FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
- FieldSet('weight', 'weight_unit', name=_('Weight'))
+ FieldSet('airflow', 'weight', 'weight_unit', name=_('Chassis'))
)
class Meta:
model = ModuleType
fields = [
- 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', 'comments', 'tags',
+ 'manufacturer', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'description',
+ 'comments', 'tags',
]
@@ -897,7 +954,7 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
queryset=PowerPortTemplate.objects.all(),
required=False,
query_params={
- 'devicetype_id': '$device_type',
+ 'device_type_id': '$device_type',
}
)
@@ -918,8 +975,8 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
queryset=InterfaceTemplate.objects.all(),
required=False,
query_params={
- 'devicetype_id': '$device_type',
- 'moduletype_id': '$module_type',
+ 'device_type_id': '$device_type',
+ 'module_type_id': '$module_type',
}
)
@@ -944,8 +1001,8 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
queryset=RearPortTemplate.objects.all(),
required=False,
query_params={
- 'devicetype_id': '$device_type',
- 'moduletype_id': '$module_type',
+ 'device_type_id': '$device_type',
+ 'module_type_id': '$module_type',
}
)
@@ -976,15 +1033,15 @@ class RearPortTemplateForm(ModularComponentTemplateForm):
]
-class ModuleBayTemplateForm(ComponentTemplateForm):
+class ModuleBayTemplateForm(ModularComponentTemplateForm):
fieldsets = (
- FieldSet('device_type', 'name', 'label', 'position', 'description'),
+ FieldSet('device_type', 'module_type', 'name', 'label', 'position', 'description'),
)
class Meta:
model = ModuleBayTemplate
fields = [
- 'device_type', 'name', 'label', 'position', 'description',
+ 'device_type', 'module_type', 'name', 'label', 'position', 'description',
]
@@ -1006,7 +1063,7 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
queryset=InventoryItemTemplate.objects.all(),
required=False,
query_params={
- 'devicetype_id': '$device_type'
+ 'device_type_id': '$device_type'
}
)
role = DynamicModelChoiceField(
@@ -1228,7 +1285,7 @@ class PowerOutletForm(ModularDeviceComponentForm):
fieldsets = (
FieldSet(
- 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
+ 'device', 'module', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'mark_connected', 'description',
'tags',
),
)
@@ -1236,7 +1293,7 @@ class PowerOutletForm(ModularDeviceComponentForm):
class Meta:
model = PowerOutlet
fields = [
- 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
+ 'device', 'module', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'mark_connected', 'description',
'tags',
]
@@ -1294,7 +1351,8 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
- label=_('VLAN group')
+ label=_('VLAN group'),
+ help_text=_("Filter VLANs available for assignment by group.")
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
@@ -1396,15 +1454,15 @@ class RearPortForm(ModularDeviceComponentForm):
]
-class ModuleBayForm(DeviceComponentForm):
+class ModuleBayForm(ModularDeviceComponentForm):
fieldsets = (
- FieldSet('device', 'name', 'label', 'position', 'description', 'tags',),
+ FieldSet('device', 'module', 'name', 'label', 'position', 'description', 'tags',),
)
class Meta:
model = ModuleBay
fields = [
- 'device', 'name', 'label', 'position', 'description', 'tags',
+ 'device', 'module', 'name', 'label', 'position', 'description', 'tags',
]
@@ -1518,7 +1576,7 @@ class InventoryItemForm(DeviceComponentForm):
)
fieldsets = (
- FieldSet('device', 'parent', 'name', 'label', 'role', 'description', 'tags', name=_('Inventory Item')),
+ FieldSet('device', 'parent', 'name', 'label', 'status', 'role', 'description', 'tags', name=_('Inventory Item')),
FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')),
FieldSet(
TabbedGroups(
@@ -1538,7 +1596,7 @@ class InventoryItemForm(DeviceComponentForm):
model = InventoryItem
fields = [
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
- 'description', 'tags',
+ 'status', 'description', 'tags',
]
def __init__(self, *args, **kwargs):
diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py
index f811700b47c..d18c7ed1448 100644
--- a/netbox/dcim/forms/object_create.py
+++ b/netbox/dcim/forms/object_create.py
@@ -261,8 +261,8 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
# TODO: Clean up the application of HTMXSelect attributes
attrs={
'hx-get': '.',
- 'hx-include': f'#form_fields',
- 'hx-target': f'#form_fields',
+ 'hx-include': '#form_fields',
+ 'hx-target': '#form_fields',
}
)
)
diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py
index 2ae5e777177..8c256aecb43 100644
--- a/netbox/dcim/graphql/filters.py
+++ b/netbox/dcim/graphql/filters.py
@@ -38,6 +38,7 @@ __all__ = (
'RackFilter',
'RackReservationFilter',
'RackRoleFilter',
+ 'RackTypeFilter',
'RearPortFilter',
'RearPortTemplateFilter',
'RegionFilter',
@@ -234,6 +235,12 @@ class PowerPortTemplateFilter(BaseFilterMixin):
pass
+@strawberry_django.filter(models.RackType, lookups=True)
+@autotype_decorator(filtersets.RackTypeFilterSet)
+class RackTypeFilter(BaseFilterMixin):
+ pass
+
+
@strawberry_django.filter(models.Rack, lookups=True)
@autotype_decorator(filtersets.RackFilterSet)
class RackFilter(BaseFilterMixin):
diff --git a/netbox/dcim/graphql/mixins.py b/netbox/dcim/graphql/mixins.py
index 589af50c816..2e5ab7ea798 100644
--- a/netbox/dcim/graphql/mixins.py
+++ b/netbox/dcim/graphql/mixins.py
@@ -1,7 +1,6 @@
from typing import Annotated, List, Union
import strawberry
-import strawberry_django
__all__ = (
'CabledObjectMixin',
@@ -11,18 +10,18 @@ __all__ = (
@strawberry.type
class CabledObjectMixin:
- cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None
+ cable: Annotated["CableType", strawberry.lazy('dcim.graphql.types')] | None # noqa: F821
link_peers: List[Annotated[Union[
- Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
- Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
- Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
- Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
- Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
- Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
- Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
- Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
- Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
+ Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
+ Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
], strawberry.union("LinkPeerType")]]
@@ -30,14 +29,14 @@ class CabledObjectMixin:
class PathEndpointMixin:
connected_endpoints: List[Annotated[Union[
- Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')],
- Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')],
- Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')],
- Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')],
- Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
- Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')],
- Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
- Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
- Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
- Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
+ Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
+ Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
+ Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')], # noqa: F821
+ Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')], # noqa: F821
], strawberry.union("ConnectedEndpointType")]]
diff --git a/netbox/dcim/graphql/schema.py b/netbox/dcim/graphql/schema.py
index c3962a87af6..65818fb20de 100644
--- a/netbox/dcim/graphql/schema.py
+++ b/netbox/dcim/graphql/schema.py
@@ -3,208 +3,130 @@ from typing import List
import strawberry
import strawberry_django
-from dcim import models
from .types import *
-@strawberry.type
+@strawberry.type(name="Query")
class DCIMQuery:
- @strawberry.field
- def cable(self, id: int) -> CableType:
- return models.Cable.objects.get(pk=id)
+ cable: CableType = strawberry_django.field()
cable_list: List[CableType] = strawberry_django.field()
- @strawberry.field
- def console_port(self, id: int) -> ConsolePortType:
- return models.ConsolePort.objects.get(pk=id)
+ console_port: ConsolePortType = strawberry_django.field()
console_port_list: List[ConsolePortType] = strawberry_django.field()
- @strawberry.field
- def console_port_template(self, id: int) -> ConsolePortTemplateType:
- return models.ConsolePortTemplate.objects.get(pk=id)
+ console_port_template: ConsolePortTemplateType = strawberry_django.field()
console_port_template_list: List[ConsolePortTemplateType] = strawberry_django.field()
- @strawberry.field
- def console_server_port(self, id: int) -> ConsoleServerPortType:
- return models.ConsoleServerPort.objects.get(pk=id)
+ console_server_port: ConsoleServerPortType = strawberry_django.field()
console_server_port_list: List[ConsoleServerPortType] = strawberry_django.field()
- @strawberry.field
- def console_server_port_template(self, id: int) -> ConsoleServerPortTemplateType:
- return models.ConsoleServerPortTemplate.objects.get(pk=id)
+ console_server_port_template: ConsoleServerPortTemplateType = strawberry_django.field()
console_server_port_template_list: List[ConsoleServerPortTemplateType] = strawberry_django.field()
- @strawberry.field
- def device(self, id: int) -> DeviceType:
- return models.Device.objects.get(pk=id)
+ device: DeviceType = strawberry_django.field()
device_list: List[DeviceType] = strawberry_django.field()
- @strawberry.field
- def device_bay(self, id: int) -> DeviceBayType:
- return models.DeviceBay.objects.get(pk=id)
+ device_bay: DeviceBayType = strawberry_django.field()
device_bay_list: List[DeviceBayType] = strawberry_django.field()
- @strawberry.field
- def device_bay_template(self, id: int) -> DeviceBayTemplateType:
- return models.DeviceBayTemplate.objects.get(pk=id)
+ device_bay_template: DeviceBayTemplateType = strawberry_django.field()
device_bay_template_list: List[DeviceBayTemplateType] = strawberry_django.field()
- @strawberry.field
- def device_role(self, id: int) -> DeviceRoleType:
- return models.DeviceRole.objects.get(pk=id)
+ device_role: DeviceRoleType = strawberry_django.field()
device_role_list: List[DeviceRoleType] = strawberry_django.field()
- @strawberry.field
- def device_type(self, id: int) -> DeviceTypeType:
- return models.DeviceType.objects.get(pk=id)
+ device_type: DeviceTypeType = strawberry_django.field()
device_type_list: List[DeviceTypeType] = strawberry_django.field()
- @strawberry.field
- def front_port(self, id: int) -> FrontPortType:
- return models.FrontPort.objects.get(pk=id)
+ front_port: FrontPortType = strawberry_django.field()
front_port_list: List[FrontPortType] = strawberry_django.field()
- @strawberry.field
- def front_port_template(self, id: int) -> FrontPortTemplateType:
- return models.FrontPortTemplate.objects.get(pk=id)
+ front_port_template: FrontPortTemplateType = strawberry_django.field()
front_port_template_list: List[FrontPortTemplateType] = strawberry_django.field()
- @strawberry.field
- def interface(self, id: int) -> InterfaceType:
- return models.Interface.objects.get(pk=id)
+ interface: InterfaceType = strawberry_django.field()
interface_list: List[InterfaceType] = strawberry_django.field()
- @strawberry.field
- def interface_template(self, id: int) -> InterfaceTemplateType:
- return models.InterfaceTemplate.objects.get(pk=id)
+ interface_template: InterfaceTemplateType = strawberry_django.field()
interface_template_list: List[InterfaceTemplateType] = strawberry_django.field()
- @strawberry.field
- def inventory_item(self, id: int) -> InventoryItemType:
- return models.InventoryItem.objects.get(pk=id)
+ inventory_item: InventoryItemType = strawberry_django.field()
inventory_item_list: List[InventoryItemType] = strawberry_django.field()
- @strawberry.field
- def inventory_item_role(self, id: int) -> InventoryItemRoleType:
- return models.InventoryItemRole.objects.get(pk=id)
+ inventory_item_role: InventoryItemRoleType = strawberry_django.field()
inventory_item_role_list: List[InventoryItemRoleType] = strawberry_django.field()
- @strawberry.field
- def inventory_item_template(self, id: int) -> InventoryItemTemplateType:
- return models.InventoryItemTemplate.objects.get(pk=id)
+ inventory_item_template: InventoryItemTemplateType = strawberry_django.field()
inventory_item_template_list: List[InventoryItemTemplateType] = strawberry_django.field()
- @strawberry.field
- def location(self, id: int) -> LocationType:
- return models.Location.objects.get(pk=id)
+ location: LocationType = strawberry_django.field()
location_list: List[LocationType] = strawberry_django.field()
- @strawberry.field
- def manufacturer(self, id: int) -> ManufacturerType:
- return models.Manufacturer.objects.get(pk=id)
+ manufacturer: ManufacturerType = strawberry_django.field()
manufacturer_list: List[ManufacturerType] = strawberry_django.field()
- @strawberry.field
- def module(self, id: int) -> ModuleType:
- return models.Module.objects.get(pk=id)
+ module: ModuleType = strawberry_django.field()
module_list: List[ModuleType] = strawberry_django.field()
- @strawberry.field
- def module_bay(self, id: int) -> ModuleBayType:
- return models.ModuleBay.objects.get(pk=id)
+ module_bay: ModuleBayType = strawberry_django.field()
module_bay_list: List[ModuleBayType] = strawberry_django.field()
- @strawberry.field
- def module_bay_template(self, id: int) -> ModuleBayTemplateType:
- return models.ModuleBayTemplate.objects.get(pk=id)
+ module_bay_template: ModuleBayTemplateType = strawberry_django.field()
module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field()
- @strawberry.field
- def module_type(self, id: int) -> ModuleTypeType:
- return models.ModuleType.objects.get(pk=id)
+ module_type: ModuleTypeType = strawberry_django.field()
module_type_list: List[ModuleTypeType] = strawberry_django.field()
- @strawberry.field
- def platform(self, id: int) -> PlatformType:
- return models.Platform.objects.get(pk=id)
+ platform: PlatformType = strawberry_django.field()
platform_list: List[PlatformType] = strawberry_django.field()
- @strawberry.field
- def power_feed(self, id: int) -> PowerFeedType:
- return models.PowerFeed.objects.get(pk=id)
+ power_feed: PowerFeedType = strawberry_django.field()
power_feed_list: List[PowerFeedType] = strawberry_django.field()
- @strawberry.field
- def power_outlet(self, id: int) -> PowerOutletType:
- return models.PowerOutlet.objects.get(pk=id)
+ power_outlet: PowerOutletType = strawberry_django.field()
power_outlet_list: List[PowerOutletType] = strawberry_django.field()
- @strawberry.field
- def power_outlet_template(self, id: int) -> PowerOutletTemplateType:
- return models.PowerOutletTemplate.objects.get(pk=id)
+ power_outlet_template: PowerOutletTemplateType = strawberry_django.field()
power_outlet_template_list: List[PowerOutletTemplateType] = strawberry_django.field()
- @strawberry.field
- def power_panel(self, id: int) -> PowerPanelType:
- return models.PowerPanel.objects.get(id=id)
+ power_panel: PowerPanelType = strawberry_django.field()
power_panel_list: List[PowerPanelType] = strawberry_django.field()
- @strawberry.field
- def power_port(self, id: int) -> PowerPortType:
- return models.PowerPort.objects.get(id=id)
+ power_port: PowerPortType = strawberry_django.field()
power_port_list: List[PowerPortType] = strawberry_django.field()
- @strawberry.field
- def power_port_template(self, id: int) -> PowerPortTemplateType:
- return models.PowerPortTemplate.objects.get(id=id)
+ power_port_template: PowerPortTemplateType = strawberry_django.field()
power_port_template_list: List[PowerPortTemplateType] = strawberry_django.field()
- @strawberry.field
- def rack(self, id: int) -> RackType:
- return models.Rack.objects.get(id=id)
+ rack_type: RackTypeType = strawberry_django.field()
+ rack_type_list: List[RackTypeType] = strawberry_django.field()
+
+ rack: RackType = strawberry_django.field()
rack_list: List[RackType] = strawberry_django.field()
- @strawberry.field
- def rack_reservation(self, id: int) -> RackReservationType:
- return models.RackReservation.objects.get(id=id)
+ rack_reservation: RackReservationType = strawberry_django.field()
rack_reservation_list: List[RackReservationType] = strawberry_django.field()
- @strawberry.field
- def rack_role(self, id: int) -> RackRoleType:
- return models.RackRole.objects.get(id=id)
+ rack_role: RackRoleType = strawberry_django.field()
rack_role_list: List[RackRoleType] = strawberry_django.field()
- @strawberry.field
- def rear_port(self, id: int) -> RearPortType:
- return models.RearPort.objects.get(id=id)
+ rear_port: RearPortType = strawberry_django.field()
rear_port_list: List[RearPortType] = strawberry_django.field()
- @strawberry.field
- def rear_port_template(self, id: int) -> RearPortTemplateType:
- return models.RearPortTemplate.objects.get(id=id)
+ rear_port_template: RearPortTemplateType = strawberry_django.field()
rear_port_template_list: List[RearPortTemplateType] = strawberry_django.field()
- @strawberry.field
- def region(self, id: int) -> RegionType:
- return models.Region.objects.get(id=id)
+ region: RegionType = strawberry_django.field()
region_list: List[RegionType] = strawberry_django.field()
- @strawberry.field
- def site(self, id: int) -> SiteType:
- return models.Site.objects.get(id=id)
+ site: SiteType = strawberry_django.field()
site_list: List[SiteType] = strawberry_django.field()
- @strawberry.field
- def site_group(self, id: int) -> SiteGroupType:
- return models.SiteGroup.objects.get(id=id)
+ site_group: SiteGroupType = strawberry_django.field()
site_group_list: List[SiteGroupType] = strawberry_django.field()
- @strawberry.field
- def virtual_chassis(self, id: int) -> VirtualChassisType:
- return models.VirtualChassis.objects.get(id=id)
+ virtual_chassis: VirtualChassisType = strawberry_django.field()
virtual_chassis_list: List[VirtualChassisType] = strawberry_django.field()
- @strawberry.field
- def virtual_device_context(self, id: int) -> VirtualDeviceContextType:
- return models.VirtualDeviceContext.objects.get(id=id)
+ virtual_device_context: VirtualDeviceContextType = strawberry_django.field()
virtual_device_context_list: List[VirtualDeviceContextType] = strawberry_django.field()
diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py
index 8b4613f1437..b951aead07d 100644
--- a/netbox/dcim/graphql/types.py
+++ b/netbox/dcim/graphql/types.py
@@ -50,6 +50,7 @@ __all__ = (
'RackType',
'RackReservationType',
'RackRoleType',
+ 'RackTypeType',
'RearPortType',
'RearPortTemplateType',
'RegionType',
@@ -495,12 +496,18 @@ class ModuleType(NetBoxObjectType):
@strawberry_django.type(
models.ModuleBay,
- fields='__all__',
+ # fields='__all__',
+ exclude=('parent',),
filters=ModuleBayFilter
)
-class ModuleBayType(ComponentType):
+class ModuleBayType(ModularComponentType):
installed_module: Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')] | None
+ children: List[Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]]
+
+ @strawberry_django.field
+ def parent(self) -> Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')] | None:
+ return self.parent
@strawberry_django.type(
@@ -508,7 +515,7 @@ class ModuleBayType(ComponentType):
fields='__all__',
filters=ModuleBayTemplateFilter
)
-class ModuleBayTemplateType(ComponentTemplateType):
+class ModuleBayTemplateType(ModularComponentTemplateType):
_name: str
@@ -561,6 +568,7 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin):
)
class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
power_port: Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')] | None
+ color: str
@strawberry_django.type(
@@ -606,6 +614,15 @@ class PowerPortTemplateType(ModularComponentTemplateType):
poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
+@strawberry_django.type(
+ models.RackType,
+ fields='__all__',
+ filters=RackTypeFilter
+)
+class RackTypeType(NetBoxObjectType):
+ manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
+
+
@strawberry_django.type(
models.Rack,
fields='__all__',
@@ -618,6 +635,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
role: Annotated["RackRoleType", strawberry.lazy('dcim.graphql.types')] | None
+ rack_type: Annotated["RackTypeType", strawberry.lazy('dcim.graphql.types')] | None
reservations: List[Annotated["RackReservationType", strawberry.lazy('dcim.graphql.types')]]
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
powerfeeds: List[Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')]]
diff --git a/netbox/dcim/management/commands/trace_paths.py b/netbox/dcim/management/commands/trace_paths.py
index d34a428e4fc..592aeb6a7be 100644
--- a/netbox/dcim/management/commands/trace_paths.py
+++ b/netbox/dcim/management/commands/trace_paths.py
@@ -60,7 +60,7 @@ class Command(BaseCommand):
self.stdout.write((self.style.SUCCESS(f' Deleted {deleted_count} paths')))
# Reinitialize the model's PK sequence
- self.stdout.write(f'Resetting database sequence for CablePath model')
+ self.stdout.write('Resetting database sequence for CablePath model')
sequence_sql = connection.ops.sequence_reset_sql(no_style(), [CablePath])
with connection.cursor() as cursor:
for sql in sequence_sql:
diff --git a/netbox/dcim/migrations/0188_racktype.py b/netbox/dcim/migrations/0188_racktype.py
new file mode 100644
index 00000000000..aa45246e5c7
--- /dev/null
+++ b/netbox/dcim/migrations/0188_racktype.py
@@ -0,0 +1,98 @@
+import django.core.validators
+import django.db.models.deletion
+import taggit.managers
+from django.db import migrations, models
+
+import utilities.fields
+import utilities.json
+import utilities.ordering
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0118_customfield_uniqueness'),
+ ('dcim', '0187_alter_device_vc_position'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='RackType',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('created', models.DateTimeField(auto_now_add=True, null=True)),
+ ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+ ('custom_field_data', models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=utilities.json.CustomFieldJSONEncoder
+ )),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ('comments', models.TextField(blank=True)),
+ ('weight', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True)),
+ ('weight_unit', models.CharField(blank=True, max_length=50)),
+ ('_abs_weight', models.PositiveBigIntegerField(blank=True, null=True)),
+ ('manufacturer', models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name='rack_types',
+ to='dcim.manufacturer'
+ )),
+ ('model', models.CharField(max_length=100)),
+ ('slug', models.SlugField(max_length=100, unique=True)),
+ ('form_factor', models.CharField(max_length=50)),
+ ('width', models.PositiveSmallIntegerField(default=19)),
+ ('u_height', models.PositiveSmallIntegerField(
+ default=42,
+ validators=[
+ django.core.validators.MinValueValidator(1),
+ django.core.validators.MaxValueValidator(100),
+ ]
+ )),
+ ('starting_unit', models.PositiveSmallIntegerField(
+ default=1,
+ validators=[django.core.validators.MinValueValidator(1)]
+ )),
+ ('desc_units', models.BooleanField(default=False)),
+ ('outer_width', models.PositiveSmallIntegerField(blank=True, null=True)),
+ ('outer_depth', models.PositiveSmallIntegerField(blank=True, null=True)),
+ ('outer_unit', models.CharField(blank=True, max_length=50)),
+ ('max_weight', models.PositiveIntegerField(blank=True, null=True)),
+ ('_abs_max_weight', models.PositiveBigIntegerField(blank=True, null=True)),
+ ('mounting_depth', models.PositiveSmallIntegerField(blank=True, null=True)),
+ ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+ ],
+ options={
+ 'verbose_name': 'racktype',
+ 'verbose_name_plural': 'racktypes',
+ 'ordering': ('manufacturer', 'model'),
+ },
+ ),
+ migrations.RenameField(
+ model_name='rack',
+ old_name='type',
+ new_name='form_factor',
+ ),
+ migrations.AddField(
+ model_name='rack',
+ name='rack_type',
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name='racks',
+ to='dcim.racktype',
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name='racktype',
+ constraint=models.UniqueConstraint(
+ fields=('manufacturer', 'model'), name='dcim_racktype_unique_manufacturer_model'
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name='racktype',
+ constraint=models.UniqueConstraint(
+ fields=('manufacturer', 'slug'), name='dcim_racktype_unique_manufacturer_slug'
+ ),
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0189_moduletype_rack_airflow.py b/netbox/dcim/migrations/0189_moduletype_rack_airflow.py
new file mode 100644
index 00000000000..31787b67d47
--- /dev/null
+++ b/netbox/dcim/migrations/0189_moduletype_rack_airflow.py
@@ -0,0 +1,21 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0188_racktype'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='moduletype',
+ name='airflow',
+ field=models.CharField(blank=True, max_length=50),
+ ),
+ migrations.AddField(
+ model_name='rack',
+ name='airflow',
+ field=models.CharField(blank=True, max_length=50),
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0190_nested_modules.py b/netbox/dcim/migrations/0190_nested_modules.py
new file mode 100644
index 00000000000..9cef40efbb1
--- /dev/null
+++ b/netbox/dcim/migrations/0190_nested_modules.py
@@ -0,0 +1,74 @@
+import django.db.models.deletion
+import mptt.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0189_moduletype_rack_airflow'),
+ ('extras', '0121_customfield_related_object_filter'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='modulebaytemplate',
+ options={'ordering': ('device_type', 'module_type', '_name')},
+ ),
+ migrations.RemoveConstraint(
+ model_name='modulebay',
+ name='dcim_modulebay_unique_device_name',
+ ),
+ migrations.AddField(
+ model_name='modulebay',
+ name='level',
+ field=models.PositiveIntegerField(default=0, editable=False),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='modulebay',
+ name='lft',
+ field=models.PositiveIntegerField(default=0, editable=False),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='modulebay',
+ name='module',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.module'),
+ ),
+ migrations.AddField(
+ model_name='modulebay',
+ name='parent',
+ field=mptt.fields.TreeForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.modulebay'),
+ ),
+ migrations.AddField(
+ model_name='modulebay',
+ name='rght',
+ field=models.PositiveIntegerField(default=0, editable=False),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='modulebay',
+ name='tree_id',
+ field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='modulebaytemplate',
+ name='module_type',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.moduletype'),
+ ),
+ migrations.AlterField(
+ model_name='modulebaytemplate',
+ name='device_type',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='dcim.devicetype'),
+ ),
+ migrations.AddConstraint(
+ model_name='modulebay',
+ constraint=models.UniqueConstraint(fields=('device', 'module', 'name'), name='dcim_modulebay_unique_device_module_name'),
+ ),
+ migrations.AddConstraint(
+ model_name='modulebaytemplate',
+ constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_modulebaytemplate_unique_module_type_name'),
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0191_module_bay_rebuild.py b/netbox/dcim/migrations/0191_module_bay_rebuild.py
new file mode 100644
index 00000000000..2600632131f
--- /dev/null
+++ b/netbox/dcim/migrations/0191_module_bay_rebuild.py
@@ -0,0 +1,26 @@
+from django.db import migrations
+import mptt
+import mptt.managers
+
+
+def rebuild_mptt(apps, schema_editor):
+ manager = mptt.managers.TreeManager()
+ ModuleBay = apps.get_model('dcim', 'ModuleBay')
+ manager.model = ModuleBay
+ mptt.register(ModuleBay)
+ manager.contribute_to_class(ModuleBay, 'objects')
+ manager.rebuild()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0190_nested_modules'),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ code=rebuild_mptt,
+ reverse_code=migrations.RunPython.noop
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0192_inventoryitem_status.py b/netbox/dcim/migrations/0192_inventoryitem_status.py
new file mode 100644
index 00000000000..335ab2ca724
--- /dev/null
+++ b/netbox/dcim/migrations/0192_inventoryitem_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.9 on 2024-09-26 20:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0191_module_bay_rebuild'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='inventoryitem',
+ name='status',
+ field=models.CharField(default='active', max_length=50),
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0193_poweroutlet_color.py b/netbox/dcim/migrations/0193_poweroutlet_color.py
new file mode 100644
index 00000000000..0a6c08b480c
--- /dev/null
+++ b/netbox/dcim/migrations/0193_poweroutlet_color.py
@@ -0,0 +1,19 @@
+# Generated by Django 5.0.9 on 2024-09-26 19:31
+
+import utilities.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0192_inventoryitem_status'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='poweroutlet',
+ name='color',
+ field=utilities.fields.ColorField(blank=True, max_length=6),
+ ),
+ ]
diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py
index 7afead829f4..96861a13e97 100644
--- a/netbox/dcim/models/cables.py
+++ b/netbox/dcim/models/cables.py
@@ -6,7 +6,6 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Sum
from django.dispatch import Signal
-from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from core.models import ObjectType
@@ -88,6 +87,8 @@ class Cable(PrimaryModel):
null=True
)
+ clone_fields = ('tenant', 'type',)
+
class Meta:
ordering = ('pk',)
verbose_name = _('cable')
@@ -114,9 +115,6 @@ class Cable(PrimaryModel):
pk = self.pk or self._pk
return self.label or f'#{pk}'
- def get_absolute_url(self):
- return reverse('dcim:cable', args=[self.pk])
-
@property
def a_terminations(self):
if hasattr(self, '_a_terminations'):
@@ -162,7 +160,7 @@ class Cable(PrimaryModel):
if self.length is not None and not self.length_unit:
raise ValidationError(_("Must specify a unit when setting a cable length"))
- if self.pk is None and (not self.a_terminations or not self.b_terminations):
+ if self._state.adding and (not self.a_terminations or not self.b_terminations):
raise ValidationError(_("Must define A and B terminations when creating a new cable."))
if self._terminations_modified:
@@ -364,11 +362,11 @@ class CableTermination(ChangeLoggedModel):
def delete(self, *args, **kwargs):
# Delete the cable association on the terminating object
- termination_model = self.termination._meta.model
- termination_model.objects.filter(pk=self.termination_id).update(
- cable=None,
- cable_end=''
- )
+ termination = self.termination._meta.model.objects.get(pk=self.termination_id)
+ termination.snapshot()
+ termination.cable = None
+ termination.cable_end = ''
+ termination.save()
super().delete(*args, **kwargs)
@@ -664,6 +662,14 @@ class CablePath(models.Model):
rear_port_id=remote_terminations[0].pk,
rear_port_position__in=position_stack.pop()
)
+ # If all rear ports have a single position, we can just get the front ports
+ elif all([rp.positions == 1 for rp in remote_terminations]):
+ front_ports = FrontPort.objects.filter(rear_port_id__in=[rp.pk for rp in remote_terminations])
+
+ if len(front_ports) != len(remote_terminations):
+ # Some rear ports does not have a front port
+ is_split = True
+ break
else:
# No position indicated: path has split, so we stop at the RearPorts
is_split = True
diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py
index dacd7ec3ed9..3a71c424d61 100644
--- a/netbox/dcim/models/device_component_templates.py
+++ b/netbox/dcim/models/device_component_templates.py
@@ -98,7 +98,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
def clean(self):
super().clean()
- if self.pk is not None and self._original_device_type != self.device_type_id:
+ if not self._state.adding and self._original_device_type != self.device_type_id:
raise ValidationError({
"device_type": _("Component templates cannot be moved to a different device type.")
})
@@ -158,14 +158,40 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
_("A component template must be associated with either a device type or a module type.")
)
+ def _get_module_tree(self, module):
+ modules = []
+ while module:
+ modules.append(module)
+ if module.module_bay:
+ module = module.module_bay.module
+ else:
+ module = None
+
+ modules.reverse()
+ return modules
+
def resolve_name(self, module):
+ if MODULE_TOKEN not in self.name:
+ return self.name
+
if module:
- return self.name.replace(MODULE_TOKEN, module.module_bay.position)
+ modules = self._get_module_tree(module)
+ name = self.name
+ for module in modules:
+ name = name.replace(MODULE_TOKEN, module.module_bay.position, 1)
+ return name
return self.name
def resolve_label(self, module):
+ if MODULE_TOKEN not in self.label:
+ return self.label
+
if module:
- return self.label.replace(MODULE_TOKEN, module.module_bay.position)
+ modules = self._get_module_tree(module)
+ label = self.label
+ for module in modules:
+ label = label.replace(MODULE_TOKEN, module.module_bay.position, 1)
+ return label
return self.label
@@ -628,7 +654,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
}
-class ModuleBayTemplate(ComponentTemplateModel):
+class ModuleBayTemplate(ModularComponentTemplateModel):
"""
A template for a ModuleBay to be created for a new parent Device.
"""
@@ -641,16 +667,16 @@ class ModuleBayTemplate(ComponentTemplateModel):
component_model = ModuleBay
- class Meta(ComponentTemplateModel.Meta):
+ class Meta(ModularComponentTemplateModel.Meta):
verbose_name = _('module bay template')
verbose_name_plural = _('module bay templates')
- def instantiate(self, device):
+ def instantiate(self, **kwargs):
return self.component_model(
- device=device,
name=self.name,
label=self.label,
- position=self.position
+ position=self.position,
+ **kwargs
)
instantiate.do_not_call_in_templates = True
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index 9438b741f58..1a86a250c08 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -5,7 +5,6 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Sum
-from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
@@ -22,7 +21,6 @@ from utilities.tracking import TrackingModelMixin
from wireless.choices import *
from wireless.utils import get_channel_attr
-
__all__ = (
'BaseInterface',
'CabledObjectModel',
@@ -301,9 +299,6 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
verbose_name = _('console port')
verbose_name_plural = _('console ports')
- def get_absolute_url(self):
- return reverse('dcim:consoleport', kwargs={'pk': self.pk})
-
class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
"""
@@ -330,9 +325,6 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint,
verbose_name = _('console server port')
verbose_name_plural = _('console server ports')
- def get_absolute_url(self):
- return reverse('dcim:consoleserverport', kwargs={'pk': self.pk})
-
#
# Power components
@@ -370,9 +362,6 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracking
verbose_name = _('power port')
verbose_name_plural = _('power ports')
- def get_absolute_url(self):
- return reverse('dcim:powerport', kwargs={'pk': self.pk})
-
def clean(self):
super().clean()
@@ -481,6 +470,10 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
blank=True,
help_text=_('Phase (for three-phase feeds)')
)
+ color = ColorField(
+ verbose_name=_('color'),
+ blank=True
+ )
clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
@@ -488,9 +481,6 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
verbose_name = _('power outlet')
verbose_name_plural = _('power outlets')
- def get_absolute_url(self):
- return reverse('dcim:poweroutlet', kwargs={'pk': self.pk})
-
def clean(self):
super().clean()
@@ -561,7 +551,7 @@ class BaseInterface(models.Model):
self.untagged_vlan = None
# Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
- if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED:
+ if not self._state.adding and self.mode != InterfaceModeChoices.MODE_TAGGED:
self.tagged_vlans.clear()
return super().save(*args, **kwargs)
@@ -746,9 +736,6 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
verbose_name = _('interface')
verbose_name_plural = _('interfaces')
- def get_absolute_url(self):
- return reverse('dcim:interface', kwargs={'pk': self.pk})
-
def clean(self):
super().clean()
@@ -1007,9 +994,6 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
verbose_name = _('front port')
verbose_name_plural = _('front ports')
- def get_absolute_url(self):
- return reverse('dcim:frontport', kwargs={'pk': self.pk})
-
def clean(self):
super().clean()
@@ -1065,14 +1049,11 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
verbose_name = _('rear port')
verbose_name_plural = _('rear ports')
- def get_absolute_url(self):
- return reverse('dcim:rearport', kwargs={'pk': self.pk})
-
def clean(self):
super().clean()
# Check that positions count is greater than or equal to the number of associated FrontPorts
- if self.pk:
+ if not self._state.adding:
frontport_count = self.frontports.count()
if self.positions < frontport_count:
raise ValidationError({
@@ -1087,10 +1068,19 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
# Bays
#
-class ModuleBay(ComponentModel, TrackingModelMixin):
+class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
"""
An empty space within a Device which can house a child device
"""
+ parent = TreeForeignKey(
+ to='self',
+ on_delete=models.CASCADE,
+ related_name='children',
+ blank=True,
+ null=True,
+ editable=False,
+ db_index=True
+ )
position = models.CharField(
verbose_name=_('position'),
max_length=30,
@@ -1098,14 +1088,41 @@ class ModuleBay(ComponentModel, TrackingModelMixin):
help_text=_('Identifier to reference when renaming installed components')
)
+ objects = TreeManager()
+
clone_fields = ('device',)
- class Meta(ComponentModel.Meta):
+ class Meta(ModularComponentModel.Meta):
+ constraints = (
+ models.UniqueConstraint(
+ fields=('device', 'module', 'name'),
+ name='%(app_label)s_%(class)s_unique_device_module_name'
+ ),
+ )
verbose_name = _('module bay')
verbose_name_plural = _('module bays')
- def get_absolute_url(self):
- return reverse('dcim:modulebay', kwargs={'pk': self.pk})
+ class MPTTMeta:
+ order_insertion_by = ('module',)
+
+ def clean(self):
+ super().clean()
+
+ # Check for recursion
+ if module := self.module:
+ module_bays = [self.pk]
+ modules = []
+ while module:
+ if module.pk in modules or module.module_bay.pk in module_bays:
+ raise ValidationError(_("A module bay cannot belong to a module installed within it."))
+ modules.append(module.pk)
+ module_bays.append(module.module_bay.pk)
+ module = module.module_bay.module if module.module_bay else None
+
+ def save(self, *args, **kwargs):
+ if self.module:
+ self.parent = self.module.module_bay
+ super().save(*args, **kwargs)
class DeviceBay(ComponentModel, TrackingModelMixin):
@@ -1126,9 +1143,6 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
verbose_name = _('device bay')
verbose_name_plural = _('device bays')
- def get_absolute_url(self):
- return reverse('dcim:devicebay', kwargs={'pk': self.pk})
-
def clean(self):
super().clean()
@@ -1172,9 +1186,6 @@ class InventoryItemRole(OrganizationalModel):
verbose_name = _('inventory item role')
verbose_name_plural = _('inventory item roles')
- def get_absolute_url(self):
- return reverse('dcim:inventoryitemrole', args=[self.pk])
-
class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
"""
@@ -1205,6 +1216,12 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
ct_field='component_type',
fk_field='component_id'
)
+ status = models.CharField(
+ verbose_name=_('status'),
+ max_length=50,
+ choices=InventoryItemStatusChoices,
+ default=InventoryItemStatusChoices.STATUS_ACTIVE
+ )
role = models.ForeignKey(
to='dcim.InventoryItemRole',
on_delete=models.PROTECT,
@@ -1246,7 +1263,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
objects = TreeManager()
- clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',)
+ clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id')
class Meta:
ordering = ('device__id', 'parent__id', '_name')
@@ -1262,9 +1279,6 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
verbose_name = _('inventory item')
verbose_name_plural = _('inventory items')
- def get_absolute_url(self):
- return reverse('dcim:inventoryitem', kwargs={'pk': self.pk})
-
def clean(self):
super().clean()
@@ -1275,7 +1289,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
})
# Validation for moving InventoryItems
- if self.pk:
+ if not self._state.adding:
# Cannot move an InventoryItem to another device if it has a parent
if self.parent and self.parent.device != self.device:
raise ValidationError({
@@ -1295,3 +1309,6 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
raise ValidationError({
"device": _("Cannot assign inventory item to component on another device")
})
+
+ def get_status_color(self):
+ return InventoryItemStatusChoices.colors.get(self.status)
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index abc9e0b0857..e472303a6aa 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -21,11 +21,12 @@ from extras.querysets import ConfigContextModelQuerySet
from netbox.choices import ColorChoices
from netbox.config import ConfigItem
from netbox.models import OrganizationalModel, PrimaryModel
+from netbox.models.mixins import WeightMixin
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
from utilities.tracking import TrackingModelMixin
from .device_components import *
-from .mixins import RenderConfigMixin, WeightMixin
+from .mixins import RenderConfigMixin
__all__ = (
@@ -54,9 +55,6 @@ class Manufacturer(ContactsMixin, OrganizationalModel):
verbose_name = _('manufacturer')
verbose_name_plural = _('manufacturers')
- def get_absolute_url(self):
- return reverse('dcim:manufacturer', args=[self.pk])
-
class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
"""
@@ -217,11 +215,8 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
self._original_front_image = self.__dict__.get('front_image')
self._original_rear_image = self.__dict__.get('rear_image')
- def get_absolute_url(self):
- return reverse('dcim:devicetype', args=[self.pk])
-
@property
- def get_full_name(self):
+ def full_name(self):
return f"{self.manufacturer} {self.model}"
def to_yaml(self):
@@ -293,7 +288,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
# room to expand within their racks. This validation will impose a very high performance penalty when there are
# many instances to check, but increasing the u_height of a DeviceType should be a very rare occurrence.
- if self.pk and self.u_height > self._original_u_height:
+ if not self._state.adding and self.u_height > self._original_u_height:
for d in Device.objects.filter(device_type=self, position__isnull=False):
face_required = None if self.is_full_depth else d.face
u_available = d.rack.get_available_units(
@@ -310,7 +305,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
})
# If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position.
- elif self.pk and self._original_u_height > 0 and self.u_height == 0:
+ elif not self._state.adding and self._original_u_height > 0 and self.u_height == 0:
racked_instance_count = Device.objects.filter(
device_type=self,
position__isnull=False
@@ -388,8 +383,14 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
blank=True,
help_text=_('Discrete part number (optional)')
)
+ airflow = models.CharField(
+ verbose_name=_('airflow'),
+ max_length=50,
+ choices=ModuleAirflowChoices,
+ blank=True
+ )
- clone_fields = ('manufacturer', 'weight', 'weight_unit',)
+ clone_fields = ('manufacturer', 'weight', 'weight_unit', 'airflow')
prerequisite_models = (
'dcim.Manufacturer',
)
@@ -408,8 +409,9 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
def __str__(self):
return self.model
- def get_absolute_url(self):
- return reverse('dcim:moduletype', args=[self.pk])
+ @property
+ def full_name(self):
+ return f"{self.manufacturer} {self.model}"
def to_yaml(self):
data = {
@@ -487,9 +489,6 @@ class DeviceRole(OrganizationalModel):
verbose_name = _('device role')
verbose_name_plural = _('device roles')
- def get_absolute_url(self):
- return reverse('dcim:devicerole', args=[self.pk])
-
class Platform(OrganizationalModel):
"""
@@ -517,9 +516,6 @@ class Platform(OrganizationalModel):
verbose_name = _('platform')
verbose_name_plural = _('platforms')
- def get_absolute_url(self):
- return reverse('dcim:platform', args=[self.pk])
-
def update_interface_bridges(device, interface_templates, module=None):
"""
@@ -813,9 +809,6 @@ class Device(
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
return super().__str__()
- def get_absolute_url(self):
- return reverse('dcim:device', args=[self.pk])
-
def clean(self):
super().clean()
@@ -973,6 +966,13 @@ class Device(
'vc_position': _("A device assigned to a virtual chassis must have its position defined.")
})
+ if hasattr(self, 'vc_master_for') and self.vc_master_for and self.vc_master_for != self.virtual_chassis:
+ raise ValidationError({
+ 'virtual_chassis': _('Device cannot be removed from virtual chassis {virtual_chassis} because it is currently designated as its master.').format(
+ virtual_chassis=self.vc_master_for
+ )
+ })
+
def _instantiate_components(self, queryset, bulk_create=True):
"""
Instantiate components for the device from the specified component templates.
@@ -1036,7 +1036,8 @@ class Device(
self._instantiate_components(self.device_type.interfacetemplates.all())
self._instantiate_components(self.device_type.rearporttemplates.all())
self._instantiate_components(self.device_type.frontporttemplates.all())
- self._instantiate_components(self.device_type.modulebaytemplates.all())
+ # Disable bulk_create to accommodate MPTT
+ self._instantiate_components(self.device_type.modulebaytemplates.all(), bulk_create=False)
self._instantiate_components(self.device_type.devicebaytemplates.all())
# Disable bulk_create to accommodate MPTT
self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False)
@@ -1181,9 +1182,6 @@ class Module(PrimaryModel, ConfigContextModel):
def __str__(self):
return f'{self.module_bay.name}: {self.module_type} ({self.pk})'
- def get_absolute_url(self):
- return reverse('dcim:module', args=[self.pk])
-
def get_status_color(self):
return ModuleStatusChoices.colors.get(self.status)
@@ -1197,6 +1195,17 @@ class Module(PrimaryModel, ConfigContextModel):
)
)
+ # Check for recursion
+ module = self
+ module_bays = []
+ modules = []
+ while module:
+ if module.pk in modules or module.module_bay.pk in module_bays:
+ raise ValidationError(_("A module bay cannot belong to a module installed within it."))
+ modules.append(module.pk)
+ module_bays.append(module.module_bay.pk)
+ module = module.module_bay.module if module.module_bay else None
+
def save(self, *args, **kwargs):
is_new = self.pk is None
@@ -1218,7 +1227,8 @@ class Module(PrimaryModel, ConfigContextModel):
("powerporttemplates", "powerports", PowerPort),
("poweroutlettemplates", "poweroutlets", PowerOutlet),
("rearporttemplates", "rearports", RearPort),
- ("frontporttemplates", "frontports", FrontPort)
+ ("frontporttemplates", "frontports", FrontPort),
+ ("modulebaytemplates", "modulebays", ModuleBay),
]:
create_instances = []
update_instances = []
@@ -1247,17 +1257,22 @@ class Module(PrimaryModel, ConfigContextModel):
if not disable_replication:
create_instances.append(template_instance)
- component_model.objects.bulk_create(create_instances)
- # Emit the post_save signal for each newly created object
- for component in create_instances:
- post_save.send(
- sender=component_model,
- instance=component,
- created=True,
- raw=False,
- using='default',
- update_fields=None
- )
+ if component_model is not ModuleBay:
+ component_model.objects.bulk_create(create_instances)
+ # Emit the post_save signal for each newly created object
+ for component in create_instances:
+ post_save.send(
+ sender=component_model,
+ instance=component,
+ created=True,
+ raw=False,
+ using='default',
+ update_fields=None
+ )
+ else:
+ # ModuleBays must be saved individually for MPTT
+ for instance in create_instances:
+ instance.save()
update_fields = ['module']
component_model.objects.bulk_update(update_instances, update_fields)
@@ -1315,15 +1330,12 @@ class VirtualChassis(PrimaryModel):
def __str__(self):
return self.name
- def get_absolute_url(self):
- return reverse('dcim:virtualchassis', kwargs={'pk': self.pk})
-
def clean(self):
super().clean()
# Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new
# VirtualChassis.)
- if self.pk and self.master and self.master not in self.members.all():
+ if not self._state.adding and self.master and self.master not in self.members.all():
raise ValidationError({
'master': _("The selected master ({master}) is not assigned to this virtual chassis.").format(
master=self.master
@@ -1417,9 +1429,6 @@ class VirtualDeviceContext(PrimaryModel):
def __str__(self):
return self.name
- def get_absolute_url(self):
- return reverse('dcim:virtualdevicecontext', kwargs={'pk': self.pk})
-
def get_status_color(self):
return VirtualDeviceContextStatusChoices.colors.get(self.status)
diff --git a/netbox/dcim/models/mixins.py b/netbox/dcim/models/mixins.py
index d4a05699cac..c9be451a05a 100644
--- a/netbox/dcim/models/mixins.py
+++ b/netbox/dcim/models/mixins.py
@@ -1,56 +1,10 @@
-from django.core.exceptions import ValidationError
from django.db import models
-from django.utils.translation import gettext_lazy as _
-from dcim.choices import *
-from utilities.conversion import to_grams
__all__ = (
'RenderConfigMixin',
- 'WeightMixin',
)
-class WeightMixin(models.Model):
- weight = models.DecimalField(
- verbose_name=_('weight'),
- max_digits=8,
- decimal_places=2,
- blank=True,
- null=True
- )
- weight_unit = models.CharField(
- verbose_name=_('weight unit'),
- max_length=50,
- choices=WeightUnitChoices,
- blank=True,
- )
- # Stores the normalized weight (in grams) for database ordering
- _abs_weight = models.PositiveBigIntegerField(
- blank=True,
- null=True
- )
-
- class Meta:
- abstract = True
-
- def save(self, *args, **kwargs):
-
- # Store the given weight (if any) in grams for use in database ordering
- if self.weight and self.weight_unit:
- self._abs_weight = to_grams(self.weight, self.weight_unit)
- else:
- self._abs_weight = None
-
- super().save(*args, **kwargs)
-
- def clean(self):
- super().clean()
-
- # Validate weight and weight_unit
- if self.weight and not self.weight_unit:
- raise ValidationError(_("Must specify a unit when setting a weight"))
-
-
class RenderConfigMixin(models.Model):
config_template = models.ForeignKey(
to='extras.ConfigTemplate',
diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py
index 826eaae9ce3..d0c6b18b664 100644
--- a/netbox/dcim/models/power.py
+++ b/netbox/dcim/models/power.py
@@ -1,7 +1,6 @@
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
-from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
@@ -58,9 +57,6 @@ class PowerPanel(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
def __str__(self):
return self.name
- def get_absolute_url(self):
- return reverse('dcim:powerpanel', args=[self.pk])
-
def clean(self):
super().clean()
@@ -167,9 +163,6 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
def __str__(self):
return self.name
- def get_absolute_url(self):
- return reverse('dcim:powerfeed', args=[self.pk])
-
def clean(self):
super().clean()
diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py
index 289c38133a3..ae5513feaa9 100644
--- a/netbox/dcim/models/racks.py
+++ b/netbox/dcim/models/racks.py
@@ -8,7 +8,6 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Count
-from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
@@ -16,22 +15,199 @@ from dcim.constants import *
from dcim.svg import RackElevationSVG
from netbox.choices import ColorChoices
from netbox.models import OrganizationalModel, PrimaryModel
+from netbox.models.mixins import WeightMixin
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
from utilities.conversion import to_grams
from utilities.data import array_to_string, drange
from utilities.fields import ColorField, NaturalOrderingField
from .device_components import PowerPort
from .devices import Device, Module
-from .mixins import WeightMixin
from .power import PowerFeed
__all__ = (
'Rack',
'RackReservation',
'RackRole',
+ 'RackType',
)
+#
+# Rack Types
+#
+
+class RackBase(WeightMixin, PrimaryModel):
+ """
+ Base class for RackType & Rack. Holds
+ """
+ width = models.PositiveSmallIntegerField(
+ choices=RackWidthChoices,
+ default=RackWidthChoices.WIDTH_19IN,
+ verbose_name=_('width'),
+ help_text=_('Rail-to-rail width')
+ )
+
+ # Numbering
+ u_height = models.PositiveSmallIntegerField(
+ default=RACK_U_HEIGHT_DEFAULT,
+ verbose_name=_('height (U)'),
+ validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)],
+ help_text=_('Height in rack units')
+ )
+ starting_unit = models.PositiveSmallIntegerField(
+ default=RACK_STARTING_UNIT_DEFAULT,
+ verbose_name=_('starting unit'),
+ validators=[MinValueValidator(1)],
+ help_text=_('Starting unit for rack')
+ )
+ desc_units = models.BooleanField(
+ default=False,
+ verbose_name=_('descending units'),
+ help_text=_('Units are numbered top-to-bottom')
+ )
+
+ # Dimensions
+ outer_width = models.PositiveSmallIntegerField(
+ verbose_name=_('outer width'),
+ blank=True,
+ null=True,
+ help_text=_('Outer dimension of rack (width)')
+ )
+ outer_depth = models.PositiveSmallIntegerField(
+ verbose_name=_('outer depth'),
+ blank=True,
+ null=True,
+ help_text=_('Outer dimension of rack (depth)')
+ )
+ outer_unit = models.CharField(
+ verbose_name=_('outer unit'),
+ max_length=50,
+ choices=RackDimensionUnitChoices,
+ blank=True
+ )
+ mounting_depth = models.PositiveSmallIntegerField(
+ verbose_name=_('mounting depth'),
+ blank=True,
+ null=True,
+ help_text=(_(
+ 'Maximum depth of a mounted device, in millimeters. For four-post racks, this is the distance between the '
+ 'front and rear rails.'
+ ))
+ )
+
+ # Weight
+ # WeightMixin provides weight, weight_unit, and _abs_weight
+ max_weight = models.PositiveIntegerField(
+ verbose_name=_('max weight'),
+ blank=True,
+ null=True,
+ help_text=_('Maximum load capacity for the rack')
+ )
+ # Stores the normalized max weight (in grams) for database ordering
+ _abs_max_weight = models.PositiveBigIntegerField(
+ blank=True,
+ null=True
+ )
+
+ class Meta:
+ abstract = True
+
+
+class RackType(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.
+ """
+ form_factor = models.CharField(
+ choices=RackFormFactorChoices,
+ max_length=50,
+ verbose_name=_('form factor')
+ )
+ manufacturer = models.ForeignKey(
+ to='dcim.Manufacturer',
+ on_delete=models.PROTECT,
+ related_name='rack_types'
+ )
+ model = models.CharField(
+ verbose_name=_('model'),
+ max_length=100
+ )
+ slug = models.SlugField(
+ verbose_name=_('slug'),
+ max_length=100,
+ unique=True
+ )
+
+ clone_fields = (
+ 'manufacturer', 'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
+ 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
+ )
+ prerequisite_models = (
+ 'dcim.Manufacturer',
+ )
+
+ class Meta:
+ ordering = ('manufacturer', 'model')
+ constraints = (
+ models.UniqueConstraint(
+ fields=('manufacturer', 'model'),
+ name='%(app_label)s_%(class)s_unique_manufacturer_model'
+ ),
+ models.UniqueConstraint(
+ fields=('manufacturer', 'slug'),
+ name='%(app_label)s_%(class)s_unique_manufacturer_slug'
+ ),
+ )
+ verbose_name = _('rack type')
+ verbose_name_plural = _('rack types')
+
+ def __str__(self):
+ return self.model
+
+ @property
+ def full_name(self):
+ return f"{self.manufacturer} {self.model}"
+
+ def clean(self):
+ super().clean()
+
+ # Validate outer dimensions and unit
+ if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
+ raise ValidationError(_("Must specify a unit when setting an outer width/depth"))
+
+ # Validate max_weight and weight_unit
+ if self.max_weight and not self.weight_unit:
+ raise ValidationError(_("Must specify a unit when setting a maximum weight"))
+
+ def save(self, *args, **kwargs):
+ # Store the given max weight (if any) in grams for use in database ordering
+ if self.max_weight and self.weight_unit:
+ self._abs_max_weight = to_grams(self.max_weight, self.weight_unit)
+ else:
+ self._abs_max_weight = None
+
+ # Clear unit if outer width & depth are not set
+ if self.outer_width is None and self.outer_depth is None:
+ self.outer_unit = ''
+
+ super().save(*args, **kwargs)
+
+ # Update all Racks associated with this RackType
+ for rack in self.racks.all():
+ rack.snapshot()
+ rack.copy_racktype_attrs()
+ rack.save()
+
+ @property
+ def units(self):
+ """
+ Return a list of unit numbers, top to bottom.
+ """
+ if self.desc_units:
+ return drange(decimal.Decimal(self.starting_unit), self.u_height + self.starting_unit, 0.5)
+ return drange(self.u_height + decimal.Decimal(0.5) + self.starting_unit - 1, 0.5 + self.starting_unit - 1, -0.5)
+
+
#
# Racks
#
@@ -50,15 +226,31 @@ class RackRole(OrganizationalModel):
verbose_name = _('rack role')
verbose_name_plural = _('rack roles')
- def get_absolute_url(self):
- return reverse('dcim:rackrole', args=[self.pk])
-
-class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
+class Rack(ContactsMixin, 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.
"""
+ # Fields which cannot be set locally if a RackType is assigned
+ RACKTYPE_FIELDS = (
+ 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth',
+ 'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'max_weight',
+ )
+
+ form_factor = models.CharField(
+ choices=RackFormFactorChoices,
+ max_length=50,
+ blank=True,
+ verbose_name=_('form factor')
+ )
+ rack_type = models.ForeignKey(
+ to='dcim.RackType',
+ on_delete=models.PROTECT,
+ related_name='racks',
+ blank=True,
+ null=True,
+ )
name = models.CharField(
verbose_name=_('name'),
max_length=100
@@ -121,72 +313,11 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
verbose_name=_('asset tag'),
help_text=_('A unique tag used to identify this rack')
)
- type = models.CharField(
- choices=RackTypeChoices,
+ airflow = models.CharField(
+ verbose_name=_('airflow'),
max_length=50,
- blank=True,
- verbose_name=_('type')
- )
- width = models.PositiveSmallIntegerField(
- choices=RackWidthChoices,
- default=RackWidthChoices.WIDTH_19IN,
- verbose_name=_('width'),
- help_text=_('Rail-to-rail width')
- )
- u_height = models.PositiveSmallIntegerField(
- default=RACK_U_HEIGHT_DEFAULT,
- verbose_name=_('height (U)'),
- validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)],
- help_text=_('Height in rack units')
- )
- starting_unit = models.PositiveSmallIntegerField(
- default=RACK_STARTING_UNIT_DEFAULT,
- verbose_name=_('starting unit'),
- validators=[MinValueValidator(1),],
- help_text=_('Starting unit for rack')
- )
- desc_units = models.BooleanField(
- default=False,
- verbose_name=_('descending units'),
- help_text=_('Units are numbered top-to-bottom')
- )
- outer_width = models.PositiveSmallIntegerField(
- verbose_name=_('outer width'),
- blank=True,
- null=True,
- help_text=_('Outer dimension of rack (width)')
- )
- outer_depth = models.PositiveSmallIntegerField(
- verbose_name=_('outer depth'),
- blank=True,
- null=True,
- help_text=_('Outer dimension of rack (depth)')
- )
- outer_unit = models.CharField(
- verbose_name=_('outer unit'),
- max_length=50,
- choices=RackDimensionUnitChoices,
- blank=True,
- )
- max_weight = models.PositiveIntegerField(
- verbose_name=_('max weight'),
- blank=True,
- null=True,
- help_text=_('Maximum load capacity for the rack')
- )
- # Stores the normalized max weight (in grams) for database ordering
- _abs_max_weight = models.PositiveBigIntegerField(
- blank=True,
- null=True
- )
- mounting_depth = models.PositiveSmallIntegerField(
- verbose_name=_('mounting depth'),
- blank=True,
- null=True,
- help_text=(
- _('Maximum depth of a mounted device, in millimeters. For four-post racks, this is the '
- 'distance between the front and rear rails.')
- )
+ choices=RackAirflowChoices,
+ blank=True
)
# Generic relations
@@ -198,8 +329,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
)
clone_fields = (
- 'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
- 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
+ 'site', 'location', 'tenant', 'status', 'role', 'form_factor', 'width', 'airflow', 'u_height', 'desc_units',
+ 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
)
prerequisite_models = (
'dcim.Site',
@@ -226,9 +357,6 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
return f'{self.name} ({self.facility_id})'
return self.name
- def get_absolute_url(self):
- return reverse('dcim:rack', args=[self.pk])
-
def clean(self):
super().clean()
@@ -244,7 +372,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
if self.max_weight and not self.weight_unit:
raise ValidationError(_("Must specify a unit when setting a maximum weight"))
- if self.pk:
+ if not self._state.adding:
mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position')
# Validate that Rack is tall enough to house the highest mounted Device
@@ -271,6 +399,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
})
def save(self, *args, **kwargs):
+ self.copy_racktype_attrs()
# Store the given max weight (if any) in grams for use in database ordering
if self.max_weight and self.weight_unit:
@@ -284,6 +413,14 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
super().save(*args, **kwargs)
+ def copy_racktype_attrs(self):
+ """
+ Copy physical attributes from the assigned RackType (if any).
+ """
+ if self.rack_type:
+ for field_name in self.RACKTYPE_FIELDS:
+ setattr(self, field_name, getattr(self.rack_type, field_name))
+
@property
def units(self):
"""
@@ -321,7 +458,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
}
# Add devices to rack units list
- if self.pk:
+ if not self._state.adding:
# Retrieve all devices installed within the rack
devices = Device.objects.prefetch_related(
@@ -552,9 +689,6 @@ class RackReservation(PrimaryModel):
def __str__(self):
return "Reservation for rack {}".format(self.rack)
- def get_absolute_url(self):
- return reverse('dcim:rackreservation', args=[self.pk])
-
def clean(self):
super().clean()
diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py
index c1da807adf6..37f59045d5e 100644
--- a/netbox/dcim/models/sites.py
+++ b/netbox/dcim/models/sites.py
@@ -1,7 +1,6 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
-from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneField
@@ -62,9 +61,6 @@ class Region(ContactsMixin, NestedGroupModel):
verbose_name = _('region')
verbose_name_plural = _('regions')
- def get_absolute_url(self):
- return reverse('dcim:region', args=[self.pk])
-
def get_site_count(self):
return Site.objects.filter(
Q(region=self) |
@@ -115,9 +111,6 @@ class SiteGroup(ContactsMixin, NestedGroupModel):
verbose_name = _('site group')
verbose_name_plural = _('site groups')
- def get_absolute_url(self):
- return reverse('dcim:sitegroup', args=[self.pk])
-
def get_site_count(self):
return Site.objects.filter(
Q(group=self) |
@@ -241,9 +234,6 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
def __str__(self):
return self.name
- def get_absolute_url(self):
- return reverse('dcim:site', args=[self.pk])
-
def get_status_color(self):
return SiteStatusChoices.colors.get(self.status)
@@ -322,9 +312,6 @@ class Location(ContactsMixin, ImageAttachmentsMixin, NestedGroupModel):
verbose_name = _('location')
verbose_name_plural = _('locations')
- def get_absolute_url(self):
- return reverse('dcim:location', args=[self.pk])
-
def get_status_color(self):
return LocationStatusChoices.colors.get(self.status)
diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py
index b349bcac063..45431cb05aa 100644
--- a/netbox/dcim/search.py
+++ b/netbox/dcim/search.py
@@ -242,6 +242,17 @@ class PowerPortIndex(SearchIndex):
display_attrs = ('device', 'label', 'type', 'description')
+@register_search
+class RackTypeIndex(SearchIndex):
+ model = models.RackType
+ fields = (
+ ('model', 100),
+ ('description', 500),
+ ('comments', 5000),
+ )
+ display_attrs = ('model', 'description')
+
+
@register_search
class RackIndex(SearchIndex):
model = models.Rack
diff --git a/netbox/dcim/svg/cables.py b/netbox/dcim/svg/cables.py
index 959414d7549..4e0f7aea638 100644
--- a/netbox/dcim/svg/cables.py
+++ b/netbox/dcim/svg/cables.py
@@ -162,6 +162,9 @@ class CableTraceSVG:
location_label += f' / {instance.location}'
if instance.rack:
location_label += f' / {instance.rack}'
+ if instance.position:
+ location_label += f' / {instance.get_face_display()}'
+ location_label += f' / U{instance.position}'
labels.append(location_label)
elif instance._meta.model_name == 'circuit':
labels[0] = f'Circuit {instance}'
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 7fa307bc80b..b39a2b87fc2 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -63,7 +63,10 @@ class DeviceRoleTable(NetBoxTable):
verbose_name=_('VMs')
)
color = columns.ColorColumn()
- vm_role = columns.BooleanColumn()
+ vm_role = columns.BooleanColumn(
+ verbose_name=_('VM role'),
+ false_mark=None
+ )
config_template = tables.Column(
linkify=True
)
@@ -287,6 +290,11 @@ class DeviceComponentTable(NetBoxTable):
linkify=True,
order_by=('_name',)
)
+ device_status = columns.ChoiceFieldColumn(
+ accessor=tables.A('device__status'),
+ verbose_name=_('Device Status'),
+ color=lambda x: x.device.get_status_color(),
+ )
class Meta(NetBoxTable.Meta):
order_by = ('device', 'name')
@@ -310,6 +318,9 @@ class ModularDeviceComponentTable(DeviceComponentTable):
verbose_name=_('Inventory Items'),
)
+ class Meta(NetBoxTable.Meta):
+ pass
+
class CableTerminationTable(NetBoxTable):
cable = tables.Column(
@@ -329,6 +340,7 @@ class CableTerminationTable(NetBoxTable):
)
mark_connected = columns.BooleanColumn(
verbose_name=_('Mark Connected'),
+ false_mark=None
)
class Meta:
@@ -500,6 +512,7 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
verbose_name=_('Power Port'),
linkify=True
)
+ color = columns.ColorColumn()
tags = columns.TagColumn(
url_name='dcim:poweroutlet_list'
)
@@ -508,10 +521,10 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
model = models.PowerOutlet
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port',
- 'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'inventory_items',
+ 'color', 'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'inventory_items',
'tags', 'created', 'last_updated',
)
- default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description')
+ default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description')
class DevicePowerOutletTable(PowerOutletTable):
@@ -528,11 +541,11 @@ class DevicePowerOutletTable(PowerOutletTable):
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
model = models.PowerOutlet
fields = (
- 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'power_port', 'feed_leg', 'description',
- 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
+ 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'power_port', 'feed_leg',
+ 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
)
default_columns = (
- 'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection',
+ 'pk', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description', 'cable', 'connection',
)
@@ -576,6 +589,9 @@ class BaseInterfaceTable(NetBoxTable):
def value_ip_addresses(self, value):
return ",".join([str(obj.address) for obj in value.all()])
+ def value_tagged_vlans(self, value):
+ return ",".join([str(obj) for obj in value.all()])
+
class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
device = tables.Column(
@@ -586,7 +602,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
}
)
mgmt_only = columns.BooleanColumn(
- verbose_name=_('Management Only')
+ verbose_name=_('Management Only'),
+ false_mark=None
)
speed_formatted = columns.TemplateColumn(
template_code='{% load helpers %}{{ value|humanize_speed }}',
@@ -671,7 +688,8 @@ class DeviceInterfaceTable(InterfaceTable):
'data-virtual': lambda record: "true" if record.is_virtual else "false",
'data-mark-connected': lambda record: "true" if record.mark_connected else "false",
'data-cable-status': lambda record: record.cable.status if record.cable else "",
- 'data-type': lambda record: record.type
+ 'data-type': lambda record: record.type,
+ 'data-connected': lambda record: "connected" if record.mark_connected or record.cable else "disconnected"
}
@@ -839,7 +857,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
default_columns = ('pk', 'name', 'label', 'status', 'installed_device', 'description')
-class ModuleBayTable(DeviceComponentTable):
+class ModuleBayTable(ModularDeviceComponentTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
@@ -847,6 +865,10 @@ class ModuleBayTable(DeviceComponentTable):
'args': [Accessor('device_id')],
}
)
+ parent = tables.Column(
+ linkify=True,
+ verbose_name=_('Parent'),
+ )
installed_module = tables.Column(
linkify=True,
verbose_name=_('Installed Module')
@@ -868,25 +890,38 @@ class ModuleBayTable(DeviceComponentTable):
verbose_name=_('Module Status')
)
- class Meta(DeviceComponentTable.Meta):
+ class Meta(ModularDeviceComponentTable.Meta):
model = models.ModuleBay
fields = (
- 'pk', 'id', 'name', 'device', 'label', 'position', 'installed_module', 'module_status', 'module_serial',
- 'module_asset_tag', 'description', 'tags',
+ 'pk', 'id', 'name', 'device', 'parent', 'label', 'position', 'installed_module', 'module_status',
+ 'module_serial', 'module_asset_tag', 'description', 'tags',
)
- default_columns = ('pk', 'name', 'device', 'label', 'installed_module', 'module_status', 'description')
+ default_columns = (
+ 'pk', 'name', 'device', 'parent', 'label', 'installed_module', 'module_status', 'description',
+ )
+
+ def render_parent_bay(self, value):
+ return value.name if value else ''
+
+ def render_installed_module(self, value):
+ return value.module_type if value else ''
class DeviceModuleBayTable(ModuleBayTable):
+ name = columns.MPTTColumn(
+ verbose_name=_('Name'),
+ linkify=True,
+ order_by=Accessor('_name')
+ )
actions = columns.ActionsColumn(
extra_buttons=MODULEBAY_BUTTONS
)
- class Meta(DeviceComponentTable.Meta):
+ class Meta(ModuleBayTable.Meta):
model = models.ModuleBay
fields = (
- 'pk', 'id', 'name', 'label', 'position', 'installed_module', 'module_status', 'module_serial', 'module_asset_tag',
- 'description', 'tags', 'actions',
+ 'pk', 'id', 'parent', 'name', 'label', 'position', 'installed_module', 'module_status', 'module_serial',
+ 'module_asset_tag', 'description', 'tags', 'actions',
)
default_columns = ('pk', 'name', 'label', 'installed_module', 'module_status', 'description')
@@ -913,6 +948,10 @@ class InventoryItemTable(DeviceComponentTable):
)
discovered = columns.BooleanColumn(
verbose_name=_('Discovered'),
+ false_mark=None
+ )
+ status = columns.ChoiceFieldColumn(
+ verbose_name=_('Status'),
)
parent = tables.Column(
linkify=True,
@@ -926,11 +965,11 @@ class InventoryItemTable(DeviceComponentTable):
class Meta(NetBoxTable.Meta):
model = models.InventoryItem
fields = (
- 'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
+ 'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial',
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
)
default_columns = (
- 'pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
+ 'pk', 'name', 'device', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
)
@@ -946,11 +985,11 @@ class DeviceInventoryItemTable(InventoryItemTable):
class Meta(NetBoxTable.Meta):
model = models.InventoryItem
fields = (
- 'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
+ 'pk', 'id', 'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
'description', 'discovered', 'tags', 'actions',
)
default_columns = (
- 'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
+ 'pk', 'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
)
@@ -1020,7 +1059,7 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
)
device = tables.TemplateColumn(
verbose_name=_('Device'),
- order_by=('_name',),
+ order_by=('device___name',),
template_code=DEVICE_LINK,
linkify=True
)
diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py
index fad238c6e53..e8a4e35f114 100644
--- a/netbox/dcim/tables/devicetypes.py
+++ b/netbox/dcim/tables/devicetypes.py
@@ -1,6 +1,5 @@
-from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
from dcim import models
from netbox.tables import NetBoxTable, columns
@@ -86,7 +85,8 @@ class DeviceTypeTable(NetBoxTable):
linkify=True
)
is_full_depth = columns.BooleanColumn(
- verbose_name=_('Full Depth')
+ verbose_name=_('Full Depth'),
+ false_mark=None
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
@@ -98,7 +98,10 @@ class DeviceTypeTable(NetBoxTable):
verbose_name=_('U Height'),
template_code='{{ value|floatformat }}'
)
- exclude_from_utilization = columns.BooleanColumn()
+ exclude_from_utilization = columns.BooleanColumn(
+ verbose_name=_('Exclude from utilization'),
+ false_mark=None
+ )
weight = columns.TemplateColumn(
verbose_name=_('Weight'),
template_code=WEIGHT,
@@ -221,7 +224,8 @@ class InterfaceTemplateTable(ComponentTemplateTable):
verbose_name=_('Enabled'),
)
mgmt_only = columns.BooleanColumn(
- verbose_name=_('Management Only')
+ verbose_name=_('Management Only'),
+ false_mark=None
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
diff --git a/netbox/dcim/tables/modules.py b/netbox/dcim/tables/modules.py
index 0cd9e438ee9..5b06e08b28b 100644
--- a/netbox/dcim/tables/modules.py
+++ b/netbox/dcim/tables/modules.py
@@ -40,7 +40,7 @@ class ModuleTypeTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = ModuleType
fields = (
- 'pk', 'id', 'model', 'manufacturer', 'part_number', 'weight', 'description', 'comments', 'tags',
+ 'pk', 'id', 'model', 'manufacturer', 'part_number', 'airflow', 'weight', 'description', 'comments', 'tags',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number',
diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py
index 22ca3da90d3..a6b704161af 100644
--- a/netbox/dcim/tables/racks.py
+++ b/netbox/dcim/tables/racks.py
@@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from django_tables2.utils import Accessor
-from dcim.models import Rack, RackReservation, RackRole
+from dcim.models import Rack, RackReservation, RackRole, RackType
from netbox.tables import NetBoxTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from .template_code import WEIGHT
@@ -11,6 +11,7 @@ __all__ = (
'RackTable',
'RackReservationTable',
'RackRoleTable',
+ 'RackTypeTable',
)
@@ -44,6 +45,65 @@ class RackRoleTable(NetBoxTable):
default_columns = ('pk', 'name', 'rack_count', 'color', 'description')
+#
+# Rack Types
+#
+
+class RackTypeTable(NetBoxTable):
+ model = tables.Column(
+ verbose_name=_('Model'),
+ linkify=True
+ )
+ manufacturer = tables.Column(
+ verbose_name=_('Manufacturer'),
+ linkify=True
+ )
+ u_height = tables.TemplateColumn(
+ template_code="{{ value }}U",
+ verbose_name=_('Height')
+ )
+ outer_width = tables.TemplateColumn(
+ template_code="{{ record.outer_width }} {{ record.outer_unit }}",
+ verbose_name=_('Outer Width')
+ )
+ outer_depth = tables.TemplateColumn(
+ template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
+ verbose_name=_('Outer Depth')
+ )
+ weight = columns.TemplateColumn(
+ verbose_name=_('Weight'),
+ template_code=WEIGHT,
+ order_by=('_abs_weight', 'weight_unit')
+ )
+ max_weight = columns.TemplateColumn(
+ verbose_name=_('Max Weight'),
+ template_code=WEIGHT,
+ order_by=('_abs_max_weight', 'weight_unit')
+ )
+ comments = columns.MarkdownColumn(
+ verbose_name=_('Comments'),
+ )
+ instance_count = columns.LinkedCountColumn(
+ viewname='dcim:rack_list',
+ url_params={'rack_type_id': 'pk'},
+ verbose_name=_('Instances')
+ )
+ tags = columns.TagColumn(
+ url_name='dcim:rack_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = RackType
+ fields = (
+ 'pk', 'id', 'model', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
+ 'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'description', 'comments',
+ 'instance_count', 'tags', 'created', 'last_updated',
+ )
+ default_columns = (
+ 'pk', 'model', 'manufacturer', 'type', 'u_height', 'description', 'instance_count',
+ )
+
+
#
# Racks
#
@@ -68,6 +128,15 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
role = columns.ColoredLabelColumn(
verbose_name=_('Role'),
)
+ manufacturer = tables.Column(
+ verbose_name=_('Manufacturer'),
+ accessor=Accessor('rack_type__manufacturer'),
+ linkify=True
+ )
+ rack_type = tables.Column(
+ linkify=True,
+ verbose_name=_('Type')
+ )
u_height = tables.TemplateColumn(
template_code="{{ value }}U",
verbose_name=_('Height')
@@ -113,14 +182,14 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Rack
fields = (
- 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial',
- 'asset_tag', 'type', 'u_height', 'starting_unit', 'width', 'outer_width', 'outer_depth', 'mounting_depth',
- 'weight', 'max_weight', 'comments', 'device_count', 'get_utilization', 'get_power_utilization',
- 'description', 'contacts', 'tags', 'created', 'last_updated',
+ 'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role',
+ 'rack_type', 'serial', 'asset_tag', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
+ 'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'comments', 'device_count',
+ 'get_utilization', 'get_power_utilization', 'description', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
- 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
- 'get_utilization',
+ 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'rack_type', 'u_height',
+ 'device_count', 'get_utilization',
)
diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py
index e179ec43a87..77844f08622 100644
--- a/netbox/dcim/tables/sites.py
+++ b/netbox/dcim/tables/sites.py
@@ -99,6 +99,11 @@ class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
url_params={'site_id': 'pk'},
verbose_name=_('ASN Count')
)
+ device_count = columns.LinkedCountColumn(
+ viewname='dcim:device_list',
+ url_params={'site_id': 'pk'},
+ verbose_name=_('Devices')
+ )
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index d3134656ce6..96ab803e6a7 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -56,9 +56,13 @@ INTERFACE_FHRPGROUPS = """
INTERFACE_TAGGED_VLANS = """
{% if record.mode == 'tagged' %}
+ {% if value.count > 3 %}
+ {{ value.count }} VLANs
+ {% else %}
{% for vlan in value.all %}
{{ vlan }}
{% endfor %}
+ {% endif %}
{% elif record.mode == 'tagged-all' %}
All
{% endif %}
diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py
index 52b850b244c..1b460cd591c 100644
--- a/netbox/dcim/tests/test_api.py
+++ b/netbox/dcim/tests/test_api.py
@@ -1,4 +1,3 @@
-from django.contrib.auth import get_user_model
from django.test import override_settings
from django.urls import reverse
from django.utils.translation import gettext as _
@@ -11,15 +10,13 @@ from extras.models import ConfigTemplate
from ipam.models import ASN, RIR, VLAN, VRF
from netbox.api.serializers import GenericObjectSerializer
from tenancy.models import Tenant
+from users.models import User
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
from virtualization.models import Cluster, ClusterType
from wireless.choices import WirelessChannelChoices
from wireless.models import WirelessLAN
-User = get_user_model()
-
-
class AppTest(APITestCase):
def test_root(self):
@@ -195,6 +192,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
+ user_permissions = ('dcim.view_site',)
@classmethod
def setUpTestData(cls):
@@ -274,12 +272,58 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase):
RackRole.objects.bulk_create(rack_roles)
+class RackTypeTest(APIViewTestCases.APIViewTestCase):
+ model = RackType
+ brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'slug', 'url']
+ bulk_update_data = {
+ 'description': 'new description',
+ }
+ user_permissions = ('dcim.view_manufacturer',)
+
+ @classmethod
+ def setUpTestData(cls):
+ manufacturers = (
+ Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
+ Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
+ )
+ Manufacturer.objects.bulk_create(manufacturers)
+
+ rack_types = (
+ RackType(manufacturer=manufacturers[0], model='Rack Type 1', slug='rack-type-1', form_factor=RackFormFactorChoices.TYPE_CABINET,),
+ RackType(manufacturer=manufacturers[0], model='Rack Type 2', slug='rack-type-2', form_factor=RackFormFactorChoices.TYPE_CABINET,),
+ RackType(manufacturer=manufacturers[0], model='Rack Type 3', slug='rack-type-3', form_factor=RackFormFactorChoices.TYPE_CABINET,),
+ )
+ RackType.objects.bulk_create(rack_types)
+
+ cls.create_data = [
+ {
+ 'manufacturer': manufacturers[1].pk,
+ 'model': 'Rack Type 4',
+ 'slug': 'rack-type-4',
+ 'form_factor': RackFormFactorChoices.TYPE_CABINET,
+ },
+ {
+ 'manufacturer': manufacturers[1].pk,
+ 'model': 'Rack Type 5',
+ 'slug': 'rack-type-5',
+ 'form_factor': RackFormFactorChoices.TYPE_CABINET,
+ },
+ {
+ 'manufacturer': manufacturers[1].pk,
+ 'model': 'Rack Type 6',
+ 'slug': 'rack-type-6',
+ 'form_factor': RackFormFactorChoices.TYPE_CABINET,
+ },
+ ]
+
+
class RackTest(APIViewTestCases.APIViewTestCase):
model = Rack
brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'url']
bulk_update_data = {
'status': 'planned',
}
+ user_permissions = ('dcim.view_site', )
@classmethod
def setUpTestData(cls):
@@ -368,6 +412,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
+ user_permissions = ('dcim.view_rack', 'users.view_user')
@classmethod
def setUpTestData(cls):
@@ -447,6 +492,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'part_number': 'ABC123',
}
+ user_permissions = ('dcim.view_manufacturer', )
@classmethod
def setUpTestData(cls):
@@ -492,6 +538,7 @@ class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'part_number': 'ABC123',
}
+ user_permissions = ('dcim.view_manufacturer', )
@classmethod
def setUpTestData(cls):
@@ -663,6 +710,7 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
+ user_permissions = ('dcim.view_devicetype', )
@classmethod
def setUpTestData(cls):
@@ -768,6 +816,7 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
+ user_permissions = ('dcim.view_rearporttemplate', )
@classmethod
def setUpTestData(cls):
@@ -905,6 +954,7 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
+ user_permissions = ('dcim.view_devicetype', )
@classmethod
def setUpTestData(cls):
@@ -945,6 +995,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
+ user_permissions = ('dcim.view_devicetype', )
@classmethod
def setUpTestData(cls):
@@ -985,6 +1036,7 @@ class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
+ user_permissions = ('dcim.view_devicetype', 'dcim.view_manufacturer',)
@classmethod
def setUpTestData(cls):
@@ -1103,6 +1155,10 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'status': 'failed',
}
+ user_permissions = (
+ 'dcim.view_site', 'dcim.view_rack', 'dcim.view_location', 'dcim.view_devicerole', 'dcim.view_devicetype',
+ 'extras.view_configtemplate',
+ )
@classmethod
def setUpTestData(cls):
@@ -1293,6 +1349,7 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'serial': '1234ABCD',
}
+ user_permissions = ('dcim.view_modulebay', 'dcim.view_moduletype', 'dcim.view_device')
@classmethod
def setUpTestData(cls):
@@ -1314,7 +1371,8 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
ModuleBay(device=device, name='Module Bay 5'),
ModuleBay(device=device, name='Module Bay 6'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
modules = (
Module(device=device, module_bay=module_bays[0], module_type=module_types[0]),
@@ -1358,6 +1416,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
'description': 'New description',
}
peer_termination_type = ConsoleServerPort
+ user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@@ -1400,6 +1459,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
'description': 'New description',
}
peer_termination_type = ConsolePort
+ user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@@ -1442,6 +1502,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
'description': 'New description',
}
peer_termination_type = PowerOutlet
+ user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@@ -1481,6 +1542,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
'description': 'New description',
}
peer_termination_type = PowerPort
+ user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@@ -1529,6 +1591,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
'description': 'New description',
}
peer_termination_type = Interface
+ user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@@ -1663,6 +1726,7 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
'description': 'New description',
}
peer_termination_type = Interface
+ user_permissions = ('dcim.view_device', 'dcim.view_rearport')
@classmethod
def setUpTestData(cls):
@@ -1721,6 +1785,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
'description': 'New description',
}
peer_termination_type = Interface
+ user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@@ -1762,6 +1827,7 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
+ user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@@ -1772,12 +1838,13 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase):
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
device = Device.objects.create(device_type=device_type, role=role, name='Device 1', site=site)
- device_bays = (
+ module_bays = (
ModuleBay(device=device, name='Device Bay 1'),
ModuleBay(device=device, name='Device Bay 2'),
ModuleBay(device=device, name='Device Bay 3'),
)
- ModuleBay.objects.bulk_create(device_bays)
+ for module_bay in module_bays:
+ module_bay.save()
cls.create_data = [
{
@@ -1801,6 +1868,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
+ user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@@ -1864,6 +1932,7 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
+ user_permissions = ('dcim.view_device', 'dcim.view_manufacturer')
@classmethod
def setUpTestData(cls):
@@ -2066,12 +2135,12 @@ class ConnectedDeviceTest(APITestCase):
def test_get_connected_device(self):
url = reverse('dcim-api:connected-device-list')
- url_params = f'?peer_device=TestDevice1&peer_interface=eth0'
+ url_params = '?peer_device=TestDevice1&peer_interface=eth0'
response = self.client.get(url + url_params, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['name'], 'TestDevice2')
- url_params = f'?peer_device=TestDevice1&peer_interface=eth1'
+ url_params = '?peer_device=TestDevice1&peer_interface=eth1'
response = self.client.get(url + url_params, **self.header)
self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
@@ -2160,6 +2229,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
class PowerPanelTest(APIViewTestCases.APIViewTestCase):
model = PowerPanel
brief_fields = ['description', 'display', 'id', 'name', 'powerfeed_count', 'url']
+ user_permissions = ('dcim.view_site', )
@classmethod
def setUpTestData(cls):
@@ -2212,6 +2282,7 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'status': 'planned',
}
+ user_permissions = ('dcim.view_powerpanel', )
@classmethod
def setUpTestData(cls):
diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py
index cd7b0e6d79a..f7c337bdf78 100644
--- a/netbox/dcim/tests/test_cablepaths.py
+++ b/netbox/dcim/tests/test_cablepaths.py
@@ -2060,6 +2060,49 @@ class CablePathTestCase(TestCase):
# Test SVG generation
CableTraceSVG(interface1).render()
+ def test_222_single_path_via_multiple_singleposition_rear_ports(self):
+ """
+ [IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
+ [FP2] [RP2]
+ """
+ interface1 = Interface.objects.create(device=self.device, name='Interface 1')
+ interface2 = Interface.objects.create(device=self.device, name='Interface 2')
+ rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
+ rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
+ frontport1 = FrontPort.objects.create(
+ device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1
+ )
+ frontport2 = FrontPort.objects.create(
+ device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
+ )
+
+ cable1 = Cable(
+ a_terminations=[interface1],
+ b_terminations=[frontport1, frontport2]
+ )
+ cable1.save()
+ self.assertEqual(CablePath.objects.count(), 1)
+
+ cable2 = Cable(
+ a_terminations=[rearport1, rearport2],
+ b_terminations=[interface2]
+ )
+ cable2.save()
+ self.assertEqual(CablePath.objects.count(), 2)
+
+ self.assertPathExists(
+ (interface1, cable1, (frontport1, frontport2), (rearport1, rearport2), cable2, interface2),
+ is_complete=True
+ )
+ self.assertPathExists(
+ (interface2, cable2, (rearport1, rearport2), (frontport1, frontport2), cable1, interface1),
+ is_complete=True
+ )
+
+ # Test SVG generation both directions
+ CableTraceSVG(interface1).render()
+ CableTraceSVG(interface2).render()
+
def test_301_create_path_via_existing_cable(self):
"""
[IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index df0dc7c7e4b..ae738b57f1f 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -1,4 +1,3 @@
-from django.contrib.auth import get_user_model
from django.test import TestCase
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
@@ -6,14 +5,13 @@ from dcim.choices import *
from dcim.filtersets import *
from dcim.models import *
from ipam.models import ASN, IPAddress, RIR, VRF
-from netbox.choices import ColorChoices
+from netbox.choices import ColorChoices, WeightUnitChoices
from tenancy.models import Tenant, TenantGroup
+from users.models import User
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
-from virtualization.models import Cluster, ClusterType
+from virtualization.models import Cluster, ClusterType, ClusterGroup
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
-User = get_user_model()
-
class DeviceComponentFilterSetTests:
@@ -32,11 +30,15 @@ class DeviceComponentFilterSetTests:
params = {'device_type': [device_types[0].model, device_types[1].model]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- def test_role(self):
+ def test_device_role(self):
role = DeviceRole.objects.all()[:2]
- params = {'role_id': [role[0].pk, role[1].pk]}
+ params = {'device_role_id': [role[0].pk, role[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- params = {'role': [role[0].slug, role[1].slug]}
+ params = {'device_role': [role[0].slug, role[1].slug]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_device_status(self):
+ params = {'device_status': ['active', 'planned']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -468,6 +470,152 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = RackType.objects.all()
+ filterset = RackTypeFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ manufacturers = (
+ Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
+ Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
+ Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
+ )
+ Manufacturer.objects.bulk_create(manufacturers)
+
+ racks = (
+ RackType(
+ manufacturer=manufacturers[0],
+ model='RackType 1',
+ slug='rack-type-1',
+ form_factor=RackFormFactorChoices.TYPE_2POST,
+ width=RackWidthChoices.WIDTH_19IN,
+ u_height=42,
+ starting_unit=1,
+ desc_units=False,
+ outer_width=100,
+ outer_depth=100,
+ outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
+ mounting_depth=100,
+ weight=10,
+ max_weight=1000,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar1',
+ ),
+ RackType(
+ manufacturer=manufacturers[1],
+ model='RackType 2',
+ slug='rack-type-2',
+ form_factor=RackFormFactorChoices.TYPE_4POST,
+ width=RackWidthChoices.WIDTH_21IN,
+ u_height=43,
+ starting_unit=2,
+ desc_units=False,
+ outer_width=200,
+ outer_depth=200,
+ outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
+ mounting_depth=200,
+ weight=20,
+ max_weight=2000,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar2',
+ ),
+ RackType(
+ manufacturer=manufacturers[2],
+ model='RackType 3',
+ slug='rack-type-3',
+ form_factor=RackFormFactorChoices.TYPE_CABINET,
+ width=RackWidthChoices.WIDTH_23IN,
+ u_height=44,
+ starting_unit=3,
+ desc_units=True,
+ outer_width=300,
+ outer_depth=300,
+ outer_unit=RackDimensionUnitChoices.UNIT_INCH,
+ mounting_depth=300,
+ weight=30,
+ max_weight=3000,
+ weight_unit=WeightUnitChoices.UNIT_KILOGRAM,
+ description='foobar3'
+ ),
+ )
+ RackType.objects.bulk_create(racks)
+
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_manufacturer(self):
+ manufacturers = Manufacturer.objects.all()[:2]
+ params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_model(self):
+ params = {'model': ['RackType 1', 'RackType 2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_slug(self):
+ params = {'slug': ['rack-type-1', 'rack-type-2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_form_factor(self):
+ params = {'form_factor': [RackFormFactorChoices.TYPE_2POST, RackFormFactorChoices.TYPE_4POST]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_width(self):
+ params = {'width': [RackWidthChoices.WIDTH_19IN, RackWidthChoices.WIDTH_21IN]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_u_height(self):
+ params = {'u_height': [42, 43]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_starting_unit(self):
+ params = {'starting_unit': [1, 2]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_desc_units(self):
+ params = {'desc_units': 'true'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ params = {'desc_units': 'false'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_outer_width(self):
+ params = {'outer_width': [100, 200]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_outer_depth(self):
+ params = {'outer_depth': [100, 200]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_outer_unit(self):
+ self.assertEqual(RackType.objects.filter(outer_unit__isnull=False).count(), 3)
+ params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_mounting_depth(self):
+ params = {'mounting_depth': [100, 200]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_weight(self):
+ params = {'weight': [10, 20]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_max_weight(self):
+ params = {'max_weight': [1000, 2000]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_weight_unit(self):
+ params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Rack.objects.all()
filterset = RackFilterSet
@@ -507,6 +655,53 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
for location in locations:
location.save()
+ manufacturers = (
+ Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
+ Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
+ Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
+ )
+ Manufacturer.objects.bulk_create(manufacturers)
+
+ rack_types = (
+ RackType(
+ manufacturer=manufacturers[0],
+ model='RackType 1',
+ slug='rack-type-1',
+ form_factor=RackFormFactorChoices.TYPE_2POST,
+ width=RackWidthChoices.WIDTH_19IN,
+ u_height=42,
+ starting_unit=1,
+ desc_units=False,
+ outer_width=100,
+ outer_depth=100,
+ outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
+ mounting_depth=100,
+ weight=10,
+ max_weight=1000,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar1'
+ ),
+ RackType(
+ manufacturer=manufacturers[1],
+ model='RackType 2',
+ slug='rack-type-2',
+ form_factor=RackFormFactorChoices.TYPE_4POST,
+ width=RackWidthChoices.WIDTH_21IN,
+ u_height=43,
+ starting_unit=2,
+ desc_units=False,
+ outer_width=200,
+ outer_depth=200,
+ outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
+ mounting_depth=200,
+ weight=20,
+ max_weight=2000,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar2'
+ ),
+ )
+ RackType.objects.bulk_create(rack_types)
+
rack_roles = (
RackRole(name='Rack Role 1', slug='rack-role-1'),
RackRole(name='Rack Role 2', slug='rack-role-2'),
@@ -540,7 +735,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
role=rack_roles[0],
serial='ABC',
asset_tag='1001',
- type=RackTypeChoices.TYPE_2POST,
+ form_factor=RackFormFactorChoices.TYPE_2POST,
width=RackWidthChoices.WIDTH_19IN,
u_height=42,
desc_units=False,
@@ -550,7 +745,8 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
weight=10,
max_weight=1000,
weight_unit=WeightUnitChoices.UNIT_POUND,
- description='foobar1'
+ description='foobar1',
+ airflow=RackAirflowChoices.FRONT_TO_REAR
),
Rack(
name='Rack 2',
@@ -562,7 +758,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
role=rack_roles[1],
serial='DEF',
asset_tag='1002',
- type=RackTypeChoices.TYPE_4POST,
+ form_factor=RackFormFactorChoices.TYPE_4POST,
width=RackWidthChoices.WIDTH_21IN,
u_height=43,
desc_units=False,
@@ -572,7 +768,8 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
weight=20,
max_weight=2000,
weight_unit=WeightUnitChoices.UNIT_POUND,
- description='foobar2'
+ description='foobar2',
+ airflow=RackAirflowChoices.REAR_TO_FRONT
),
Rack(
name='Rack 3',
@@ -584,7 +781,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
role=rack_roles[2],
serial='GHI',
asset_tag='1003',
- type=RackTypeChoices.TYPE_CABINET,
+ form_factor=RackFormFactorChoices.TYPE_CABINET,
width=RackWidthChoices.WIDTH_23IN,
u_height=44,
desc_units=True,
@@ -596,6 +793,28 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
weight_unit=WeightUnitChoices.UNIT_KILOGRAM,
description='foobar3'
),
+ Rack(
+ name='Rack 4',
+ facility_id='rack-4',
+ site=sites[2],
+ location=locations[2],
+ tenant=tenants[2],
+ status=RackStatusChoices.STATUS_PLANNED,
+ role=rack_roles[2],
+ rack_type=rack_types[0],
+ description='foobar4'
+ ),
+ Rack(
+ name='Rack 5',
+ facility_id='rack-5',
+ site=sites[2],
+ location=locations[2],
+ tenant=tenants[2],
+ status=RackStatusChoices.STATUS_PLANNED,
+ role=rack_roles[2],
+ rack_type=rack_types[1],
+ description='foobar5'
+ ),
)
Rack.objects.bulk_create(racks)
@@ -619,21 +838,21 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- def test_type(self):
- params = {'type': [RackTypeChoices.TYPE_2POST, RackTypeChoices.TYPE_4POST]}
+ def test_form_factor(self):
+ params = {'form_factor': [RackFormFactorChoices.TYPE_2POST, RackFormFactorChoices.TYPE_4POST]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_width(self):
params = {'width': [RackWidthChoices.WIDTH_19IN, RackWidthChoices.WIDTH_21IN]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_u_height(self):
params = {'u_height': [42, 43]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_starting_unit(self):
params = {'starting_unit': [1]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
params = {'starting_unit': [2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
@@ -641,7 +860,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'desc_units': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'desc_units': 'false'}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_outer_width(self):
params = {'outer_width': [100, 200]}
@@ -652,7 +871,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_outer_unit(self):
- self.assertEqual(Rack.objects.filter(outer_unit__isnull=False).count(), 3)
+ self.assertEqual(Rack.objects.filter(outer_unit__isnull=False).count(), 5)
params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -686,7 +905,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_status(self):
params = {'status': [RackStatusChoices.STATUS_ACTIVE, RackStatusChoices.STATUS_PLANNED]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_role(self):
roles = RackRole.objects.all()[:2]
@@ -727,6 +946,24 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_manufacturer(self):
+ manufacturers = Manufacturer.objects.all()[:2]
+ params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_rack_type(self):
+ rack_types = RackType.objects.all()[:2]
+ params = {'rack_type_id': [rack_types[0].pk, rack_types[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'rack_type': [rack_types[0].slug, rack_types[1].slug]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_airflow(self):
+ params = {'airflow': RackAirflowChoices.FRONT_TO_REAR}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = RackReservation.objects.all()
@@ -1149,7 +1386,8 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
part_number='Part Number 1',
weight=10,
weight_unit=WeightUnitChoices.UNIT_POUND,
- description='foobar1'
+ description='foobar1',
+ airflow=ModuleAirflowChoices.FRONT_TO_REAR
),
ModuleType(
manufacturer=manufacturers[1],
@@ -1157,7 +1395,8 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
part_number='Part Number 2',
weight=20,
weight_unit=WeightUnitChoices.UNIT_POUND,
- description='foobar2'
+ description='foobar2',
+ airflow=ModuleAirflowChoices.REAR_TO_FRONT
),
ModuleType(
manufacturer=manufacturers[2],
@@ -1268,6 +1507,10 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_airflow(self):
+ params = {'airflow': RackAirflowChoices.FRONT_TO_REAR}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
class ConsolePortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = ConsolePortTemplate.objects.all()
@@ -1644,16 +1887,27 @@ class ModuleBayTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests,
)
DeviceType.objects.bulk_create(device_types)
+ module_types = (
+ ModuleType(manufacturer=manufacturer, model='Module Type 1'),
+ ModuleType(manufacturer=manufacturer, model='Module Type 2'),
+ )
+ ModuleType.objects.bulk_create(module_types)
+
ModuleBayTemplate.objects.bulk_create((
ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1', description='foobar1'),
- ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2', description='foobar2'),
- ModuleBayTemplate(device_type=device_types[2], name='Module Bay 3', description='foobar3'),
+ ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2', description='foobar2', module_type=module_types[0]),
+ ModuleBayTemplate(device_type=device_types[2], name='Module Bay 3', description='foobar3', module_type=module_types[1]),
))
def test_name(self):
params = {'name': ['Module Bay 1', 'Module Bay 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_module_type(self):
+ module_types = ModuleType.objects.all()[:2]
+ params = {'module_type_id': [module_types[0].pk, module_types[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class DeviceBayTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = DeviceBayTemplate.objects.all()
@@ -1959,10 +2213,16 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
Rack.objects.bulk_create(racks)
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
+ cluster_groups = (
+ ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
+ ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'),
+ ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'),
+ )
+ ClusterGroup.objects.bulk_create(cluster_groups)
clusters = (
- Cluster(name='Cluster 1', type=cluster_type),
- Cluster(name='Cluster 2', type=cluster_type),
- Cluster(name='Cluster 3', type=cluster_type),
+ Cluster(name='Cluster 1', type=cluster_type, group=cluster_groups[0]),
+ Cluster(name='Cluster 2', type=cluster_type, group=cluster_groups[1]),
+ Cluster(name='Cluster 3', type=cluster_type, group=cluster_groups[2]),
)
Cluster.objects.bulk_create(clusters)
@@ -2076,10 +2336,8 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]),
FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
))
- ModuleBay.objects.bulk_create((
- ModuleBay(device=devices[0], name='Module Bay 1'),
- ModuleBay(device=devices[1], name='Module Bay 2'),
- ))
+ ModuleBay.objects.create(device=devices[0], name='Module Bay 1')
+ ModuleBay.objects.create(device=devices[1], name='Module Bay 2')
DeviceBay.objects.bulk_create((
DeviceBay(device=devices[0], name='Device Bay 1'),
DeviceBay(device=devices[1], name='Device Bay 2'),
@@ -2213,6 +2471,13 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'cluster_id': [clusters[0].pk, clusters[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_cluster_group(self):
+ cluster_groups = ClusterGroup.objects.all()[:2]
+ params = {'cluster_group_id': [cluster_groups[0].pk, cluster_groups[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'cluster_group': [cluster_groups[0].slug, cluster_groups[1].slug]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_model(self):
params = {'model': ['model-1', 'model-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -2384,7 +2649,8 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
ModuleBay(device=devices[2], name='Module Bay 2'),
ModuleBay(device=devices[2], name='Module Bay 3'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
modules = (
Module(
@@ -2575,10 +2841,10 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
Rack.objects.bulk_create(racks)
devices = (
- Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
- Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
- Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
- Device(name=None, device_type=device_types[0], role=roles[0], site=sites[3]), # For cable connections
+ Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
+ Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
+ Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
+ Device(name=None, device_type=device_types[0], role=roles[0], site=sites[3], status='offline'), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -2587,7 +2853,8 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
ModuleBay(device=devices[1], name='Module Bay 2'),
ModuleBay(device=devices[2], name='Module Bay 3'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
modules = (
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
@@ -2755,10 +3022,10 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
Rack.objects.bulk_create(racks)
devices = (
- Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
- Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
- Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
- Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
+ Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
+ Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
+ Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
+ Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -2767,7 +3034,8 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
ModuleBay(device=devices[1], name='Module Bay 2'),
ModuleBay(device=devices[2], name='Module Bay 3'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
modules = (
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
@@ -2935,10 +3203,10 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
Rack.objects.bulk_create(racks)
devices = (
- Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
- Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
- Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
- Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
+ Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
+ Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
+ Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
+ Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -2947,7 +3215,8 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
ModuleBay(device=devices[1], name='Module Bay 2'),
ModuleBay(device=devices[2], name='Module Bay 3'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
modules = (
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
@@ -3123,10 +3392,10 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
Rack.objects.bulk_create(racks)
devices = (
- Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
- Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
- Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
- Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
+ Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
+ Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
+ Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
+ Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -3135,7 +3404,8 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
ModuleBay(device=devices[1], name='Module Bay 2'),
ModuleBay(device=devices[2], name='Module Bay 3'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
modules = (
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
@@ -3151,9 +3421,9 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
PowerPort.objects.bulk_create(power_ports)
power_outlets = (
- PowerOutlet(device=devices[0], module=modules[0], name='Power Outlet 1', label='A', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First'),
- PowerOutlet(device=devices[1], module=modules[1], name='Power Outlet 2', label='B', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'),
- PowerOutlet(device=devices[2], module=modules[2], name='Power Outlet 3', label='C', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'),
+ PowerOutlet(device=devices[0], module=modules[0], name='Power Outlet 1', label='A', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First', color='ff0000'),
+ PowerOutlet(device=devices[1], module=modules[1], name='Power Outlet 2', label='B', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second', color='00ff00'),
+ PowerOutlet(device=devices[2], module=modules[2], name='Power Outlet 3', label='C', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third', color='0000ff'),
)
PowerOutlet.objects.bulk_create(power_outlets)
@@ -3174,6 +3444,10 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_color(self):
+ params = {'color': ['ff0000', '00ff00']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_feed_leg(self):
params = {'feed_leg': [PowerOutletFeedLegChoices.FEED_LEG_A, PowerOutletFeedLegChoices.FEED_LEG_B]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -3321,7 +3595,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
rack=racks[0],
virtual_chassis=virtual_chassis,
vc_position=1,
- vc_priority=1
+ vc_priority=1,
+ status='active',
),
Device(
name='Device 1B',
@@ -3332,7 +3607,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
rack=racks[2],
virtual_chassis=virtual_chassis,
vc_position=2,
- vc_priority=1
+ vc_priority=1,
+ status='planned',
),
Device(
name='Device 2',
@@ -3340,7 +3616,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
role=roles[1],
site=sites[1],
location=locations[1],
- rack=racks[1]
+ rack=racks[1],
+ status='offline',
),
Device(
name='Device 3',
@@ -3348,14 +3625,16 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
role=roles[2],
site=sites[2],
location=locations[2],
- rack=racks[2]
+ rack=racks[2],
+ status='offline',
),
# For cable connections
Device(
name=None,
device_type=device_types[2],
role=roles[2],
- site=sites[3]
+ site=sites[3],
+ status='offline',
),
)
Device.objects.bulk_create(devices)
@@ -3366,7 +3645,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
ModuleBay(device=devices[2], name='Module Bay 3'),
ModuleBay(device=devices[3], name='Module Bay 4'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
modules = (
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
@@ -3801,10 +4081,10 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
Rack.objects.bulk_create(racks)
devices = (
- Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
- Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
- Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
- Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
+ Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
+ Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
+ Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
+ Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -3813,7 +4093,8 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
ModuleBay(device=devices[1], name='Module Bay 2'),
ModuleBay(device=devices[2], name='Module Bay 3'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
modules = (
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
@@ -3990,10 +4271,10 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
Rack.objects.bulk_create(racks)
devices = (
- Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
- Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
- Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
- Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
+ Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
+ Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
+ Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
+ Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
)
Device.objects.bulk_create(devices)
@@ -4002,7 +4283,8 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
ModuleBay(device=devices[1], name='Module Bay 2'),
ModuleBay(device=devices[2], name='Module Bay 3'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
modules = (
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
@@ -4171,9 +4453,9 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
Rack.objects.bulk_create(racks)
devices = (
- Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
- Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
- Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
+ Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
+ Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
+ Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
)
Device.objects.bulk_create(devices)
@@ -4181,8 +4463,22 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
ModuleBay(device=devices[0], name='Module Bay 1', label='A', description='First'),
ModuleBay(device=devices[1], name='Module Bay 2', label='B', description='Second'),
ModuleBay(device=devices[2], name='Module Bay 3', label='C', description='Third'),
+ ModuleBay(device=devices[2], name='Module Bay 4', label='D', description='Fourth'),
+ ModuleBay(device=devices[2], name='Module Bay 5', label='E', description='Fifth'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
+
+ module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
+ modules = (
+ Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
+ Module(device=devices[1], module_bay=module_bays[1], module_type=module_type),
+ )
+ Module.objects.bulk_create(modules)
+ module_bays[3].module = modules[0]
+ module_bays[3].save()
+ module_bays[4].module = modules[1]
+ module_bays[4].save()
def test_name(self):
params = {'name': ['Module Bay 1', 'Module Bay 2']}
@@ -4238,6 +4534,11 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
params = {'device': [devices[0].name, devices[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_module(self):
+ modules = Module.objects.all()[:2]
+ params = {'module_id': [modules[0].pk, modules[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = DeviceBay.objects.all()
@@ -4300,9 +4601,9 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
Rack.objects.bulk_create(racks)
devices = (
- Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
- Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
- Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
+ Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
+ Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
+ Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
)
Device.objects.bulk_create(devices)
@@ -4454,9 +4755,9 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
)
inventory_items = (
- InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First', component=components[0]),
- InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second', component=components[1]),
- InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third', component=components[2]),
+ InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, status=ModuleStatusChoices.STATUS_ACTIVE, description='First', component=components[0]),
+ InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, status=ModuleStatusChoices.STATUS_PLANNED, description='Second', component=components[1]),
+ InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, status=ModuleStatusChoices.STATUS_FAILED, description='Third', component=components[2]),
)
for i in inventory_items:
i.save()
@@ -4534,11 +4835,11 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'device_type': [device_types[0].model, device_types[1].model]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
- def test_role(self):
+ def test_device_role(self):
role = DeviceRole.objects.all()[:2]
- params = {'role_id': [role[0].pk, role[1].pk]}
+ params = {'device_role_id': [role[0].pk, role[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
- params = {'role': [role[0].slug, role[1].slug]}
+ params = {'device_role': [role[0].slug, role[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_device(self):
@@ -4577,6 +4878,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'component_type': 'dcim.interface'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ def test_status(self):
+ params = {'status': [InventoryItemStatusChoices.STATUS_PLANNED, InventoryItemStatusChoices.STATUS_FAILED]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = InventoryItemRole.objects.all()
@@ -4943,6 +5248,10 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_type(self):
params = {'type': [CableTypeChoices.TYPE_CAT3, CableTypeChoices.TYPE_CAT5E]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ params = {'type__empty': 'true'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
+ params = {'type__empty': 'false'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_status(self):
params = {'status': [LinkStatusChoices.STATUS_CONNECTED]}
diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py
index 9056a66c07f..c11badbdd43 100644
--- a/netbox/dcim/tests/test_models.py
+++ b/netbox/dcim/tests/test_models.py
@@ -6,6 +6,7 @@ from core.models import ObjectType
from dcim.choices import *
from dcim.models import *
from extras.models import CustomField
+from netbox.choices import WeightUnitChoices
from tenancy.models import Tenant
from utilities.data import drange
from virtualization.models import Cluster, ClusterType
@@ -74,6 +75,61 @@ class LocationTestCase(TestCase):
self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b)
+class RackTypeTestCase(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+
+ RackType.objects.create(
+ manufacturer=manufacturer,
+ model='RackType 1',
+ slug='rack-type-1',
+ width=11,
+ u_height=22,
+ starting_unit=3,
+ desc_units=True,
+ outer_width=444,
+ outer_depth=5,
+ outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
+ weight=66,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ max_weight=7777,
+ mounting_depth=8,
+ )
+
+ def test_rack_creation(self):
+ rack_type = RackType.objects.first()
+ sites = (
+ Site(name='Site 1', slug='site-1'),
+ )
+ Site.objects.bulk_create(sites)
+ locations = (
+ Location(name='Location 1', slug='location-1', site=sites[0]),
+ )
+ for location in locations:
+ location.save()
+
+ rack = Rack.objects.create(
+ name='Rack 1',
+ facility_id='A101',
+ site=sites[0],
+ location=locations[0],
+ rack_type=rack_type
+ )
+ self.assertEqual(rack.width, rack_type.width)
+ self.assertEqual(rack.u_height, rack_type.u_height)
+ self.assertEqual(rack.starting_unit, rack_type.starting_unit)
+ self.assertEqual(rack.desc_units, rack_type.desc_units)
+ self.assertEqual(rack.outer_width, rack_type.outer_width)
+ self.assertEqual(rack.outer_depth, rack_type.outer_depth)
+ self.assertEqual(rack.outer_unit, rack_type.outer_unit)
+ self.assertEqual(rack.weight, rack_type.weight)
+ self.assertEqual(rack.weight_unit, rack_type.weight_unit)
+ self.assertEqual(rack.max_weight, rack_type.max_weight)
+ self.assertEqual(rack.mounting_depth, rack_type.mounting_depth)
+
+
class RackTestCase(TestCase):
@classmethod
@@ -565,6 +621,96 @@ class DeviceTestCase(TestCase):
Device(name='device1', site=sites[0], device_type=device_type, role=device_role, cluster=clusters[1]).full_clean()
+class ModuleBayTestCase(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ site = Site.objects.create(name='Test Site 1', slug='test-site-1')
+ manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
+ device_type = DeviceType.objects.create(
+ manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
+ )
+ device_role = DeviceRole.objects.create(name='Test Role 1', slug='test-role-1')
+
+ # Create a CustomField with a default value & assign it to all component models
+ location = Location.objects.create(name='Location 1', slug='location-1', site=site)
+ rack = Rack.objects.create(name='Rack 1', site=site)
+ device = Device.objects.create(name='Device 1', device_type=device_type, role=device_role, site=site, location=location, rack=rack)
+
+ module_bays = (
+ ModuleBay(device=device, name='Module Bay 1', label='A', description='First'),
+ ModuleBay(device=device, name='Module Bay 2', label='B', description='Second'),
+ ModuleBay(device=device, name='Module Bay 3', label='C', description='Third'),
+ )
+ for module_bay in module_bays:
+ module_bay.save()
+
+ manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+ module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
+ modules = (
+ Module(device=device, module_bay=module_bays[0], module_type=module_type),
+ Module(device=device, module_bay=module_bays[1], module_type=module_type),
+ Module(device=device, module_bay=module_bays[2], module_type=module_type),
+ )
+ # M3 -> MB3 -> M2 -> MB2 -> M1 -> MB1
+ Module.objects.bulk_create(modules)
+ module_bays[1].module = modules[0]
+ module_bays[1].clean()
+ module_bays[1].save()
+ module_bays[2].module = modules[1]
+ module_bays[2].clean()
+ module_bays[2].save()
+
+ def test_module_bay_recursion(self):
+ module_bay_1 = ModuleBay.objects.get(name='Module Bay 1')
+ module_bay_3 = ModuleBay.objects.get(name='Module Bay 3')
+ module_1 = Module.objects.get(module_bay=module_bay_1)
+ module_3 = Module.objects.get(module_bay=module_bay_3)
+
+ # Confirm error if ModuleBay recurses
+ with self.assertRaises(ValidationError):
+ module_bay_1.module = module_3
+ module_bay_1.clean()
+ module_bay_1.save()
+
+ # Confirm error if Module recurses
+ with self.assertRaises(ValidationError):
+ module_1.module_bay = module_bay_3
+ module_1.clean()
+ module_1.save()
+
+ def test_single_module_token(self):
+ device_type = DeviceType.objects.first()
+ device_role = DeviceRole.objects.first()
+ site = Site.objects.first()
+ location = Location.objects.first()
+ rack = Rack.objects.first()
+
+ # Create DeviceType components
+ ConsolePortTemplate.objects.create(
+ device_type=device_type,
+ name='{module}',
+ label='{module}',
+ )
+ ModuleBayTemplate.objects.create(
+ device_type=device_type,
+ name='Module Bay 1'
+ )
+
+ device = Device.objects.create(
+ name='Device 2',
+ device_type=device_type,
+ role=device_role,
+ site=site,
+ location=location,
+ rack=rack
+ )
+ device.consoleports.first()
+
+ def test_nested_module_token(self):
+ pass
+
+
class CableTestCase(TestCase):
@classmethod
@@ -584,39 +730,41 @@ class CableTestCase(TestCase):
device2 = Device.objects.create(
device_type=devicetype, role=role, name='TestDevice2', site=site
)
- interface1 = Interface.objects.create(device=device1, name='eth0')
- interface2 = Interface.objects.create(device=device2, name='eth0')
- interface3 = Interface.objects.create(device=device2, name='eth1')
- Cable(a_terminations=[interface1], b_terminations=[interface2]).save()
+ interfaces = (
+ Interface(device=device1, name='eth0'),
+ Interface(device=device2, name='eth0'),
+ Interface(device=device2, name='eth1'),
+ )
+ Interface.objects.bulk_create(interfaces)
+ Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[1]]).save()
+ PowerPort.objects.create(device=device2, name='psu1')
- power_port1 = PowerPort.objects.create(device=device2, name='psu1')
- patch_pannel = Device.objects.create(
+ patch_panel = Device.objects.create(
device_type=devicetype, role=role, name='TestPatchPanel', site=site
)
- rear_port1 = RearPort.objects.create(device=patch_pannel, name='RP1', type='8p8c')
- front_port1 = FrontPort.objects.create(
- device=patch_pannel, name='FP1', type='8p8c', rear_port=rear_port1, rear_port_position=1
+ rear_ports = (
+ RearPort(device=patch_panel, name='RP1', type='8p8c'),
+ RearPort(device=patch_panel, name='RP2', type='8p8c', positions=2),
+ RearPort(device=patch_panel, name='RP3', type='8p8c', positions=3),
+ RearPort(device=patch_panel, name='RP4', type='8p8c', positions=3),
)
- rear_port2 = RearPort.objects.create(device=patch_pannel, name='RP2', type='8p8c', positions=2)
- front_port2 = FrontPort.objects.create(
- device=patch_pannel, name='FP2', type='8p8c', rear_port=rear_port2, rear_port_position=1
- )
- rear_port3 = RearPort.objects.create(device=patch_pannel, name='RP3', type='8p8c', positions=3)
- front_port3 = FrontPort.objects.create(
- device=patch_pannel, name='FP3', type='8p8c', rear_port=rear_port3, rear_port_position=1
- )
- rear_port4 = RearPort.objects.create(device=patch_pannel, name='RP4', type='8p8c', positions=3)
- front_port4 = FrontPort.objects.create(
- device=patch_pannel, name='FP4', type='8p8c', rear_port=rear_port4, rear_port_position=1
+ RearPort.objects.bulk_create(rear_ports)
+ front_ports = (
+ FrontPort(device=patch_panel, name='FP1', type='8p8c', rear_port=rear_ports[0], rear_port_position=1),
+ FrontPort(device=patch_panel, name='FP2', type='8p8c', rear_port=rear_ports[1], rear_port_position=1),
+ FrontPort(device=patch_panel, name='FP3', type='8p8c', rear_port=rear_ports[2], rear_port_position=1),
+ FrontPort(device=patch_panel, name='FP4', type='8p8c', rear_port=rear_ports[3], rear_port_position=1),
)
+ FrontPort.objects.bulk_create(front_ports)
+
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider)
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuit1 = Circuit.objects.create(provider=provider, type=circuittype, cid='1')
circuit2 = Circuit.objects.create(provider=provider, type=circuittype, cid='2')
- circuittermination1 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A')
- circuittermination2 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z')
- circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A')
+ CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A')
+ CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z')
+ CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A')
def test_cable_creation(self):
"""
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index ec85fc1d5f8..ca0db5588fb 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -2,7 +2,6 @@ from decimal import Decimal
from zoneinfo import ZoneInfo
import yaml
-from django.contrib.auth import get_user_model
from django.test import override_settings
from django.urls import reverse
from netaddr import EUI
@@ -11,13 +10,12 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from ipam.models import ASN, RIR, VLAN, VRF
-from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
+from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices
from tenancy.models import Tenant
+from users.models import User
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
from wireless.models import WirelessLAN
-User = get_user_model()
-
class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = Region
@@ -336,6 +334,76 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
+class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = RackType
+
+ @classmethod
+ def setUpTestData(cls):
+ manufacturers = (
+ Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
+ Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
+ )
+ Manufacturer.objects.bulk_create(manufacturers)
+
+ rack_types = (
+ RackType(manufacturer=manufacturers[0], model='RackType 1', slug='rack-type-1', form_factor=RackFormFactorChoices.TYPE_CABINET,),
+ RackType(manufacturer=manufacturers[0], model='RackType 2', slug='rack-type-2', form_factor=RackFormFactorChoices.TYPE_CABINET,),
+ RackType(manufacturer=manufacturers[0], model='RackType 3', slug='rack-type-3', form_factor=RackFormFactorChoices.TYPE_CABINET,),
+ )
+ RackType.objects.bulk_create(rack_types)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'manufacturer': manufacturers[1].pk,
+ 'model': 'RackType X',
+ 'slug': 'rack-type-x',
+ 'type': RackFormFactorChoices.TYPE_CABINET,
+ 'width': RackWidthChoices.WIDTH_19IN,
+ 'u_height': 48,
+ 'desc_units': False,
+ 'outer_width': 500,
+ 'outer_depth': 500,
+ 'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER,
+ 'starting_unit': 1,
+ 'weight': 100,
+ 'max_weight': 2000,
+ 'weight_unit': WeightUnitChoices.UNIT_POUND,
+ 'form_factor': RackFormFactorChoices.TYPE_CABINET,
+ 'comments': 'Some comments',
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "manufacturer,model,slug,width,u_height,weight,max_weight,weight_unit",
+ "Manufacturer 1,RackType 4,rack-type-4,19,42,100,2000,kg",
+ "Manufacturer 1,RackType 5,rack-type-5,19,42,100,2000,kg",
+ "Manufacturer 1,RackType 6,rack-type-6,19,42,100,2000,kg",
+ )
+
+ cls.csv_update_data = (
+ "id,model",
+ f"{rack_types[0].pk},RackType 7",
+ f"{rack_types[1].pk},RackType 8",
+ f"{rack_types[2].pk},RackType 9",
+ )
+
+ cls.bulk_edit_data = {
+ 'manufacturer': manufacturers[1].pk,
+ 'type': RackFormFactorChoices.TYPE_4POST,
+ 'width': RackWidthChoices.WIDTH_23IN,
+ 'u_height': 49,
+ 'desc_units': True,
+ 'outer_width': 30,
+ 'outer_depth': 30,
+ 'outer_unit': RackDimensionUnitChoices.UNIT_INCH,
+ 'weight': 200,
+ 'max_weight': 4000,
+ 'weight_unit': WeightUnitChoices.UNIT_POUND,
+ 'comments': 'New comments',
+ }
+
+
class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Rack
@@ -380,7 +448,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'role': rackroles[1].pk,
'serial': '123456',
'asset_tag': 'ABCDEF',
- 'type': RackTypeChoices.TYPE_CABINET,
+ 'form_factor': RackFormFactorChoices.TYPE_CABINET,
'width': RackWidthChoices.WIDTH_19IN,
'u_height': 48,
'desc_units': False,
@@ -416,7 +484,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'status': RackStatusChoices.STATUS_DEPRECATED,
'role': rackroles[1].pk,
'serial': '654321',
- 'type': RackTypeChoices.TYPE_4POST,
+ 'form_factor': RackFormFactorChoices.TYPE_4POST,
'width': RackWidthChoices.WIDTH_23IN,
'u_height': 49,
'desc_units': True,
@@ -1832,12 +1900,9 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_device_modulebays(self):
device = Device.objects.first()
- device_bays = (
- ModuleBay(device=device, name='Module Bay 1'),
- ModuleBay(device=device, name='Module Bay 2'),
- ModuleBay(device=device, name='Module Bay 3'),
- )
- ModuleBay.objects.bulk_create(device_bays)
+ ModuleBay.objects.create(device=device, name='Module Bay 1')
+ ModuleBay.objects.create(device=device, name='Module Bay 2')
+ ModuleBay.objects.create(device=device, name='Module Bay 3')
url = reverse('dcim:device_modulebays', kwargs={'pk': device.pk})
self.assertHttpStatus(self.client.get(url), 200)
@@ -1913,7 +1978,8 @@ class ModuleTestCase(
ModuleBay(device=devices[1], name='Module Bay 4'),
ModuleBay(device=devices[1], name='Module Bay 5'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
modules = (
Module(device=devices[0], module_bay=module_bays[0], module_type=module_types[0]),
@@ -2505,7 +2571,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
}
cls.csv_data = (
- f"device,name,type,vrf.pk,poe_mode,poe_type",
+ "device,name,type,vrf.pk,poe_mode,poe_type",
f"Device 1,Interface 4,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
f"Device 1,Interface 5,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
f"Device 1,Interface 6,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af",
@@ -2715,7 +2781,8 @@ class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
ModuleBay(device=device, name='Module Bay 2'),
ModuleBay(device=device, name='Module Bay 3'),
)
- ModuleBay.objects.bulk_create(module_bays)
+ for module_bay in module_bays:
+ module_bay.save()
tags = create_tags('Alpha', 'Bravo', 'Charlie')
@@ -2836,6 +2903,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
'part_id': '123456',
'serial': '123ABC',
'asset_tag': 'ABC123',
+ 'status': InventoryItemStatusChoices.STATUS_ACTIVE,
'description': 'An inventory item',
'tags': [t.pk for t in tags],
}
@@ -2849,6 +2917,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
'discovered': False,
'part_id': '123456',
'serial': '123ABC',
+ 'status': InventoryItemStatusChoices.STATUS_ACTIVE,
'description': 'An inventory item',
'tags': [t.pk for t in tags],
}
@@ -2860,10 +2929,10 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
}
cls.csv_data = (
- "device,name,parent",
- "Device 1,Inventory Item 4,Inventory Item 1",
- "Device 1,Inventory Item 5,Inventory Item 2",
- "Device 1,Inventory Item 6,Inventory Item 3",
+ "device,name,parent,status",
+ "Device 1,Inventory Item 4,Inventory Item 1,active",
+ "Device 1,Inventory Item 5,Inventory Item 2,planned",
+ "Device 1,Inventory Item 6,Inventory Item 3,failed",
)
cls.csv_update_data = (
diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py
index c71a0aff136..627136bf96c 100644
--- a/netbox/dcim/urls.py
+++ b/netbox/dcim/urls.py
@@ -63,6 +63,14 @@ urlpatterns = [
path('racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
path('racks//', include(get_model_urls('dcim', 'rack'))),
+ # Rack Types
+ path('rack-types/', views.RackTypeListView.as_view(), name='racktype_list'),
+ path('rack-types/add/', views.RackTypeEditView.as_view(), name='racktype_add'),
+ path('rack-types/import/', views.RackTypeBulkImportView.as_view(), name='racktype_import'),
+ path('rack-types/edit/', views.RackTypeBulkEditView.as_view(), name='racktype_bulk_edit'),
+ path('rack-types/delete/', views.RackTypeBulkDeleteView.as_view(), name='racktype_bulk_delete'),
+ path('rack-types//', include(get_model_urls('dcim', 'racktype'))),
+
# Manufacturers
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
path('manufacturers/add/', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
diff --git a/netbox/dcim/utils.py b/netbox/dcim/utils.py
index eadd2da9694..4d42284908c 100644
--- a/netbox/dcim/utils.py
+++ b/netbox/dcim/utils.py
@@ -1,5 +1,3 @@
-import itertools
-
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 87f351e4d07..98665a7a08d 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -31,6 +31,7 @@ from utilities.views import (
GetRelatedModelsMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
)
from virtualization.filtersets import VirtualMachineFilterSet
+from virtualization.forms import VirtualMachineFilterForm
from virtualization.models import VirtualMachine
from virtualization.tables import VirtualMachineTable
from . import filtersets, forms, tables
@@ -379,7 +380,9 @@ class SiteGroupContactsView(ObjectContactsView):
#
class SiteListView(generic.ObjectListView):
- queryset = Site.objects.all()
+ queryset = Site.objects.annotate(
+ device_count=count_related(Device, 'site')
+ )
filterset = filtersets.SiteFilterSet
filterset_form = forms.SiteFilterForm
table = tables.SiteTable
@@ -578,6 +581,58 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
table = tables.RackRoleTable
+#
+# RackTypes
+#
+
+class RackTypeListView(generic.ObjectListView):
+ queryset = RackType.objects.annotate(
+ instance_count=count_related(Rack, 'rack_type')
+ )
+ filterset = filtersets.RackTypeFilterSet
+ filterset_form = forms.RackTypeFilterForm
+ table = tables.RackTypeTable
+
+
+@register_model_view(RackType)
+class RackTypeView(GetRelatedModelsMixin, generic.ObjectView):
+ queryset = RackType.objects.all()
+
+ def get_extra_context(self, request, instance):
+ return {
+ 'related_models': self.get_related_models(request, instance),
+ }
+
+
+@register_model_view(RackType, 'edit')
+class RackTypeEditView(generic.ObjectEditView):
+ queryset = RackType.objects.all()
+ form = forms.RackTypeForm
+
+
+@register_model_view(RackType, 'delete')
+class RackTypeDeleteView(generic.ObjectDeleteView):
+ queryset = RackType.objects.all()
+
+
+class RackTypeBulkImportView(generic.BulkImportView):
+ queryset = RackType.objects.all()
+ model_form = forms.RackTypeImportForm
+
+
+class RackTypeBulkEditView(generic.BulkEditView):
+ queryset = RackType.objects.all()
+ filterset = filtersets.RackTypeFilterSet
+ table = tables.RackTypeTable
+ form = forms.RackTypeBulkEditForm
+
+
+class RackTypeBulkDeleteView(generic.BulkDeleteView):
+ queryset = RackType.objects.all()
+ filterset = filtersets.RackTypeFilterSet
+ table = tables.RackTypeTable
+
+
#
# Racks
#
@@ -679,6 +734,7 @@ class RackRackReservationsView(generic.ObjectChildrenView):
child_model = RackReservation
table = tables.RackReservationTable
filterset = filtersets.RackReservationFilterSet
+ filterset_form = forms.RackReservationFilterForm
template_name = 'dcim/rack/reservations.html'
tab = ViewTab(
label=_('Reservations'),
@@ -697,6 +753,7 @@ class RackNonRackedView(generic.ObjectChildrenView):
child_model = Device
table = tables.DeviceTable
filterset = filtersets.DeviceFilterSet
+ filterset_form = forms.DeviceFilterForm
template_name = 'dcim/rack/non_racked_devices.html'
tab = ViewTab(
label=_('Non-Racked Devices'),
@@ -1259,6 +1316,21 @@ class ModuleTypeRearPortsView(ModuleTypeComponentsView):
)
+@register_model_view(ModuleType, 'modulebays', path='module-bays')
+class ModuleTypeModuleBaysView(ModuleTypeComponentsView):
+ child_model = ModuleBayTemplate
+ table = tables.ModuleBayTemplateTable
+ filterset = filtersets.ModuleBayTemplateFilterSet
+ viewname = 'dcim:moduletype_modulebays'
+ tab = ViewTab(
+ label=_('Module Bays'),
+ badge=lambda obj: obj.modulebaytemplates.count(),
+ permission='dcim.view_modulebaytemplate',
+ weight=570,
+ hide_if_empty=True
+ )
+
+
class ModuleTypeImportView(generic.BulkImportView):
additional_permissions = [
'dcim.add_moduletype',
@@ -1835,6 +1907,7 @@ class DeviceConsolePortsView(DeviceComponentsView):
child_model = ConsolePort
table = tables.DeviceConsolePortTable
filterset = filtersets.ConsolePortFilterSet
+ filterset_form = forms.ConsolePortFilterForm
template_name = 'dcim/device/consoleports.html',
tab = ViewTab(
label=_('Console Ports'),
@@ -1850,6 +1923,7 @@ class DeviceConsoleServerPortsView(DeviceComponentsView):
child_model = ConsoleServerPort
table = tables.DeviceConsoleServerPortTable
filterset = filtersets.ConsoleServerPortFilterSet
+ filterset_form = forms.ConsoleServerPortFilterForm
template_name = 'dcim/device/consoleserverports.html'
tab = ViewTab(
label=_('Console Server Ports'),
@@ -1865,6 +1939,7 @@ class DevicePowerPortsView(DeviceComponentsView):
child_model = PowerPort
table = tables.DevicePowerPortTable
filterset = filtersets.PowerPortFilterSet
+ filterset_form = forms.PowerPortFilterForm
template_name = 'dcim/device/powerports.html'
tab = ViewTab(
label=_('Power Ports'),
@@ -1880,6 +1955,7 @@ class DevicePowerOutletsView(DeviceComponentsView):
child_model = PowerOutlet
table = tables.DevicePowerOutletTable
filterset = filtersets.PowerOutletFilterSet
+ filterset_form = forms.PowerOutletFilterForm
template_name = 'dcim/device/poweroutlets.html'
tab = ViewTab(
label=_('Power Outlets'),
@@ -1895,6 +1971,7 @@ class DeviceInterfacesView(DeviceComponentsView):
child_model = Interface
table = tables.DeviceInterfaceTable
filterset = filtersets.InterfaceFilterSet
+ filterset_form = forms.InterfaceFilterForm
template_name = 'dcim/device/interfaces.html'
tab = ViewTab(
label=_('Interfaces'),
@@ -1916,6 +1993,7 @@ class DeviceFrontPortsView(DeviceComponentsView):
child_model = FrontPort
table = tables.DeviceFrontPortTable
filterset = filtersets.FrontPortFilterSet
+ filterset_form = forms.FrontPortFilterForm
template_name = 'dcim/device/frontports.html'
tab = ViewTab(
label=_('Front Ports'),
@@ -1931,6 +2009,7 @@ class DeviceRearPortsView(DeviceComponentsView):
child_model = RearPort
table = tables.DeviceRearPortTable
filterset = filtersets.RearPortFilterSet
+ filterset_form = forms.RearPortFilterForm
template_name = 'dcim/device/rearports.html'
tab = ViewTab(
label=_('Rear Ports'),
@@ -1946,6 +2025,7 @@ class DeviceModuleBaysView(DeviceComponentsView):
child_model = ModuleBay
table = tables.DeviceModuleBayTable
filterset = filtersets.ModuleBayFilterSet
+ filterset_form = forms.ModuleBayFilterForm
template_name = 'dcim/device/modulebays.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
@@ -1965,6 +2045,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
child_model = DeviceBay
table = tables.DeviceDeviceBayTable
filterset = filtersets.DeviceBayFilterSet
+ filterset_form = forms.DeviceBayFilterForm
template_name = 'dcim/device/devicebays.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
@@ -1984,6 +2065,7 @@ class DeviceInventoryView(DeviceComponentsView):
child_model = InventoryItem
table = tables.DeviceInventoryItemTable
filterset = filtersets.InventoryItemFilterSet
+ filterset_form = forms.InventoryItemFilterForm
template_name = 'dcim/device/inventory.html'
actions = {
**DEFAULT_ACTION_PERMISSIONS,
@@ -2046,7 +2128,7 @@ class DeviceRenderConfigView(generic.ObjectView):
try:
rendered_config = config_template.render(context=context_data)
except TemplateError as e:
- messages.error(request, f"An error occurred while rendering the template: {e}")
+ messages.error(request, _("An error occurred while rendering the template: {error}").format(error=e))
rendered_config = traceback.format_exc()
return {
@@ -2062,6 +2144,7 @@ class DeviceVirtualMachinesView(generic.ObjectChildrenView):
child_model = VirtualMachine
table = VirtualMachineTable
filterset = VirtualMachineFilterSet
+ filterset_form = VirtualMachineFilterForm
tab = ViewTab(
label=_('Virtual Machines'),
badge=lambda obj: VirtualMachine.objects.filter(cluster=obj.cluster, device=obj).count(),
@@ -2809,7 +2892,13 @@ class DeviceBayPopulateView(generic.ObjectEditView):
device_bay.snapshot()
device_bay.installed_device = form.cleaned_data['installed_device']
device_bay.save()
- messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
+ messages.success(
+ request,
+ _("Installed device {device} in bay {device_bay}.").format(
+ device=device_bay.installed_device,
+ device_bay=device_bay
+ )
+ )
return_url = self.get_return_url(request)
return redirect(return_url)
@@ -2844,7 +2933,13 @@ class DeviceBayDepopulateView(generic.ObjectEditView):
removed_device = device_bay.installed_device
device_bay.installed_device = None
device_bay.save()
- messages.success(request, f"{removed_device} has been removed from {device_bay}.")
+ messages.success(
+ request,
+ _("Removed device {device} from bay {device_bay}.").format(
+ device=removed_device,
+ device_bay=device_bay
+ )
+ )
return_url = self.get_return_url(request, device_bay.device)
return redirect(return_url)
@@ -2944,6 +3039,7 @@ class InventoryItemChildrenView(generic.ObjectChildrenView):
child_model = InventoryItem
table = tables.InventoryItemTable
filterset = filtersets.InventoryItemFilterSet
+ filterset_form = forms.InventoryItemFilterForm
tab = ViewTab(
label=_('Children'),
badge=lambda obj: obj.child_items.count(),
@@ -3157,10 +3253,10 @@ class CableEditView(generic.ObjectEditView):
doesn't currently provide a hook for dynamic class resolution.
"""
a_terminations_type = CABLE_TERMINATION_TYPES.get(
- request.GET.get('a_terminations_type') or request.POST.get('a_terminations_type')
+ request.POST.get('a_terminations_type') or request.GET.get('a_terminations_type')
)
b_terminations_type = CABLE_TERMINATION_TYPES.get(
- request.GET.get('b_terminations_type') or request.POST.get('b_terminations_type')
+ request.POST.get('b_terminations_type') or request.GET.get('b_terminations_type')
)
if obj.pk:
@@ -3411,7 +3507,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
membership_form.save()
messages.success(request, mark_safe(
- f'Added member {escape(device)} '
+ _('Added member {device} ').format(url=device.get_absolute_url(), device=escape(device))
))
if '_addanother' in request.POST:
@@ -3456,7 +3552,10 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
# Protect master device from being removed
virtual_chassis = VirtualChassis.objects.filter(master=device).first()
if virtual_chassis is not None:
- messages.error(request, f'Unable to remove master device {device} from the virtual chassis.')
+ messages.error(
+ request,
+ _('Unable to remove master device {device} from the virtual chassis.').format(device=device)
+ )
return redirect(device.get_absolute_url())
if form.is_valid():
@@ -3468,7 +3567,10 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
device.vc_priority = None
device.save()
- msg = 'Removed {} from virtual chassis {}'.format(device, device.virtual_chassis)
+ msg = _('Removed {device} from virtual chassis {chassis}').format(
+ device=device,
+ chassis=device.virtual_chassis
+ )
messages.success(request, msg)
return redirect(self.get_return_url(request, device))
diff --git a/netbox/extras/api/nested_serializers.py b/netbox/extras/api/nested_serializers.py
index 3904493c08a..235cdd6d61d 100644
--- a/netbox/extras/api/nested_serializers.py
+++ b/netbox/extras/api/nested_serializers.py
@@ -1,3 +1,5 @@
+import warnings
+
from rest_framework import serializers
from extras import models
@@ -20,6 +22,12 @@ __all__ = [
'NestedWebhookSerializer',
]
+# TODO: Remove in v4.2
+warnings.warn(
+ "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+ DeprecationWarning
+)
+
class NestedEventRuleSerializer(WritableNestedSerializer):
diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py
index ddd13815a98..5e799b504e3 100644
--- a/netbox/extras/api/serializers.py
+++ b/netbox/extras/api/serializers.py
@@ -7,9 +7,9 @@ from .serializers_.dashboard import *
from .serializers_.events import *
from .serializers_.exporttemplates import *
from .serializers_.journaling import *
+from .serializers_.notifications import *
from .serializers_.configcontexts import *
from .serializers_.configtemplates import *
from .serializers_.savedfilters import *
from .serializers_.scripts import *
from .serializers_.tags import *
-from .nested_serializers import *
diff --git a/netbox/extras/api/serializers_/attachments.py b/netbox/extras/api/serializers_/attachments.py
index 0c6ce1f3ad1..fe0964eae4d 100644
--- a/netbox/extras/api/serializers_/attachments.py
+++ b/netbox/extras/api/serializers_/attachments.py
@@ -18,6 +18,8 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
queryset=ObjectType.objects.all()
)
parent = serializers.SerializerMethodField(read_only=True)
+ image_width = serializers.IntegerField(read_only=True)
+ image_height = serializers.IntegerField(read_only=True)
class Meta:
model = ImageAttachment
diff --git a/netbox/extras/api/serializers_/configtemplates.py b/netbox/extras/api/serializers_/configtemplates.py
index 30d2fb46856..c4a683c742f 100644
--- a/netbox/extras/api/serializers_/configtemplates.py
+++ b/netbox/extras/api/serializers_/configtemplates.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
from extras.models import ConfigTemplate
from netbox.api.serializers import ValidatedModelSerializer
diff --git a/netbox/extras/api/serializers_/customfields.py b/netbox/extras/api/serializers_/customfields.py
index 9675cb173cb..a65fafc4e91 100644
--- a/netbox/extras/api/serializers_/customfields.py
+++ b/netbox/extras/api/serializers_/customfields.py
@@ -61,9 +61,10 @@ class CustomFieldSerializer(ValidatedModelSerializer):
model = CustomField
fields = [
'id', 'url', 'display_url', 'display', 'object_types', 'type', 'related_object_type', 'data_type',
- 'name', 'label', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible',
- 'ui_editable', 'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum',
- 'validation_regex', 'validation_unique', 'choice_set', 'comments', 'created', 'last_updated',
+ 'name', 'label', 'group_name', 'description', 'required', 'unique', 'search_weight', 'filter_logic',
+ 'ui_visible', 'ui_editable', 'is_cloneable', 'default', 'related_object_filter', 'weight',
+ 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'comments', 'created',
+ 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
diff --git a/netbox/extras/api/serializers_/customlinks.py b/netbox/extras/api/serializers_/customlinks.py
index 6391e047139..8cc4f5f77f5 100644
--- a/netbox/extras/api/serializers_/customlinks.py
+++ b/netbox/extras/api/serializers_/customlinks.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from core.models import ObjectType
from extras.models import CustomLink
from netbox.api.fields import ContentTypeField
diff --git a/netbox/extras/api/serializers_/events.py b/netbox/extras/api/serializers_/events.py
index 6af30e70742..926259cf3a8 100644
--- a/netbox/extras/api/serializers_/events.py
+++ b/netbox/extras/api/serializers_/events.py
@@ -34,9 +34,9 @@ class EventRuleSerializer(NetBoxModelSerializer):
class Meta:
model = EventRule
fields = [
- 'id', 'url', 'display_url', 'display', 'object_types', 'name', 'type_create', 'type_update', 'type_delete',
- 'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type',
- 'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated',
+ 'id', 'url', 'display_url', 'display', 'object_types', 'name', 'enabled', 'event_types', 'conditions',
+ 'action_type', 'action_object_type', 'action_object_id', 'action_object', 'description', 'custom_fields',
+ 'tags', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
diff --git a/netbox/extras/api/serializers_/exporttemplates.py b/netbox/extras/api/serializers_/exporttemplates.py
index faef9bb9ef2..11f502a02b9 100644
--- a/netbox/extras/api/serializers_/exporttemplates.py
+++ b/netbox/extras/api/serializers_/exporttemplates.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
from core.models import ObjectType
from extras.models import ExportTemplate
diff --git a/netbox/extras/api/serializers_/journaling.py b/netbox/extras/api/serializers_/journaling.py
index 4afd3e70acc..cba56fc32fc 100644
--- a/netbox/extras/api/serializers_/journaling.py
+++ b/netbox/extras/api/serializers_/journaling.py
@@ -1,4 +1,3 @@
-from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
@@ -8,6 +7,7 @@ from extras.choices import *
from extras.models import JournalEntry
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import NetBoxModelSerializer
+from users.models import User
from utilities.api import get_serializer_for_model
__all__ = (
@@ -22,7 +22,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
assigned_object = serializers.SerializerMethodField(read_only=True)
created_by = serializers.PrimaryKeyRelatedField(
allow_null=True,
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
required=False,
default=serializers.CurrentUserDefault()
)
diff --git a/netbox/extras/api/serializers_/notifications.py b/netbox/extras/api/serializers_/notifications.py
new file mode 100644
index 00000000000..62e1a8d635e
--- /dev/null
+++ b/netbox/extras/api/serializers_/notifications.py
@@ -0,0 +1,82 @@
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from core.models import ObjectType
+from extras.models import Notification, NotificationGroup, Subscription
+from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
+from netbox.api.serializers import ValidatedModelSerializer
+from users.api.serializers_.users import GroupSerializer, UserSerializer
+from users.models import Group, User
+from utilities.api import get_serializer_for_model
+
+__all__ = (
+ 'NotificationSerializer',
+ 'NotificationGroupSerializer',
+ 'SubscriptionSerializer',
+)
+
+
+class NotificationSerializer(ValidatedModelSerializer):
+ object_type = ContentTypeField(
+ queryset=ObjectType.objects.with_feature('notifications'),
+ )
+ object = serializers.SerializerMethodField(read_only=True)
+ user = UserSerializer(nested=True)
+
+ class Meta:
+ model = Notification
+ fields = [
+ 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created', 'read', 'event_type',
+ ]
+ brief_fields = ('id', 'url', 'display', 'object_type', 'object_id', 'user', 'read', 'event_type')
+
+ @extend_schema_field(serializers.JSONField(allow_null=True))
+ def get_object(self, instance):
+ serializer = get_serializer_for_model(instance.object)
+ context = {'request': self.context['request']}
+ return serializer(instance.object, nested=True, context=context).data
+
+
+class NotificationGroupSerializer(ValidatedModelSerializer):
+ groups = SerializedPKRelatedField(
+ queryset=Group.objects.all(),
+ serializer=GroupSerializer,
+ nested=True,
+ required=False,
+ many=True
+ )
+ users = SerializedPKRelatedField(
+ queryset=User.objects.all(),
+ serializer=UserSerializer,
+ nested=True,
+ required=False,
+ many=True
+ )
+
+ class Meta:
+ model = NotificationGroup
+ fields = [
+ 'id', 'url', 'display', 'display_url', 'name', 'description', 'groups', 'users',
+ ]
+ brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+
+class SubscriptionSerializer(ValidatedModelSerializer):
+ object_type = ContentTypeField(
+ queryset=ObjectType.objects.with_feature('notifications'),
+ )
+ object = serializers.SerializerMethodField(read_only=True)
+ user = UserSerializer(nested=True)
+
+ class Meta:
+ model = Subscription
+ fields = [
+ 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created',
+ ]
+ brief_fields = ('id', 'url', 'display', 'object_type', 'object_id', 'user')
+
+ @extend_schema_field(serializers.JSONField(allow_null=True))
+ def get_object(self, instance):
+ serializer = get_serializer_for_model(instance.object)
+ context = {'request': self.context['request']}
+ return serializer(instance.object, nested=True, context=context).data
diff --git a/netbox/extras/api/serializers_/savedfilters.py b/netbox/extras/api/serializers_/savedfilters.py
index 1403037971f..fb0744e59f0 100644
--- a/netbox/extras/api/serializers_/savedfilters.py
+++ b/netbox/extras/api/serializers_/savedfilters.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from core.models import ObjectType
from extras.models import SavedFilter
from netbox.api.fields import ContentTypeField
diff --git a/netbox/extras/api/serializers_/scripts.py b/netbox/extras/api/serializers_/scripts.py
index f35b1ea0fc1..897ccf96641 100644
--- a/netbox/extras/api/serializers_/scripts.py
+++ b/netbox/extras/api/serializers_/scripts.py
@@ -38,7 +38,7 @@ class ScriptSerializer(ValidatedModelSerializer):
def get_display(self, obj):
return f'{obj.name} ({obj.module})'
- @extend_schema_field(serializers.CharField())
+ @extend_schema_field(serializers.CharField(allow_null=True))
def get_description(self, obj):
if obj.python_class:
return obj.python_class().description
diff --git a/netbox/extras/api/serializers_/tags.py b/netbox/extras/api/serializers_/tags.py
index 946ed3c8a85..e4e62845a83 100644
--- a/netbox/extras/api/serializers_/tags.py
+++ b/netbox/extras/api/serializers_/tags.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from core.models import ObjectType
from extras.models import Tag
from netbox.api.fields import ContentTypeField, RelatedObjectCountField
diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py
index bc68103b707..bbcb8f0ef2b 100644
--- a/netbox/extras/api/urls.py
+++ b/netbox/extras/api/urls.py
@@ -15,6 +15,9 @@ router.register('custom-links', views.CustomLinkViewSet)
router.register('export-templates', views.ExportTemplateViewSet)
router.register('saved-filters', views.SavedFilterViewSet)
router.register('bookmarks', views.BookmarkViewSet)
+router.register('notifications', views.NotificationViewSet)
+router.register('notification-groups', views.NotificationGroupViewSet)
+router.register('subscriptions', views.SubscriptionViewSet)
router.register('tags', views.TagViewSet)
router.register('image-attachments', views.ImageAttachmentViewSet)
router.register('journal-entries', views.JournalEntryViewSet)
diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py
index 34565384b47..e4c3c7f3e0e 100644
--- a/netbox/extras/api/views.py
+++ b/netbox/extras/api/views.py
@@ -1,6 +1,7 @@
from django.http import Http404
from django.shortcuts import get_object_or_404
from django_rq.queues import get_connection
+from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
@@ -11,10 +12,10 @@ from rest_framework.routers import APIRootView
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rq import Worker
-from core.models import Job, ObjectType
+from core.models import ObjectType
from extras import filtersets
+from extras.jobs import ScriptJob
from extras.models import *
-from extras.scripts import run_script
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.features import SyncedDataMixin
from netbox.api.metadata import ContentTypeMetadata
@@ -140,6 +141,27 @@ class BookmarkViewSet(NetBoxModelViewSet):
filterset_class = filtersets.BookmarkFilterSet
+#
+# Notifications & subscriptions
+#
+
+class NotificationViewSet(NetBoxModelViewSet):
+ metadata_class = ContentTypeMetadata
+ queryset = Notification.objects.all()
+ serializer_class = serializers.NotificationSerializer
+
+
+class NotificationGroupViewSet(NetBoxModelViewSet):
+ queryset = NotificationGroup.objects.all()
+ serializer_class = serializers.NotificationGroupSerializer
+
+
+class SubscriptionViewSet(NetBoxModelViewSet):
+ metadata_class = ContentTypeMetadata
+ queryset = Subscription.objects.all()
+ serializer_class = serializers.SubscriptionSerializer
+
+
#
# Tags
#
@@ -207,9 +229,13 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo
# Scripts
#
+@extend_schema_view(
+ update=extend_schema(request=serializers.ScriptInputSerializer),
+ partial_update=extend_schema(request=serializers.ScriptInputSerializer),
+)
class ScriptViewSet(ModelViewSet):
permission_classes = [IsAuthenticatedOrLoginNotRequired]
- queryset = Script.objects.prefetch_related('jobs')
+ queryset = Script.objects.all()
serializer_class = serializers.ScriptSerializer
filterset_class = filtersets.ScriptFilterSet
@@ -252,10 +278,8 @@ class ScriptViewSet(ModelViewSet):
raise RQWorkerNotRunningException()
if input_serializer.is_valid():
- Job.enqueue(
- run_script,
+ ScriptJob.enqueue(
instance=script,
- name=script.python_class.class_name,
user=request.user,
data=input_serializer.data['data'],
request=copy_safe_request(request),
diff --git a/netbox/extras/apps.py b/netbox/extras/apps.py
index c565988bcf0..21232f95fdd 100644
--- a/netbox/extras/apps.py
+++ b/netbox/extras/apps.py
@@ -6,7 +6,7 @@ class ExtrasConfig(AppConfig):
def ready(self):
from netbox.models.features import register_models
- from . import dashboard, lookups, search, signals
+ from . import dashboard, lookups, search, signals # noqa: F401
# Register models
register_models(*self.get_models())
diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py
index 8959ba0abf1..4525d86891c 100644
--- a/netbox/extras/choices.py
+++ b/netbox/extras/choices.py
@@ -156,16 +156,16 @@ class LogLevelChoices(ChoiceSet):
LOG_DEBUG = 'debug'
LOG_DEFAULT = 'default'
- LOG_SUCCESS = 'success'
LOG_INFO = 'info'
+ LOG_SUCCESS = 'success'
LOG_WARNING = 'warning'
LOG_FAILURE = 'failure'
CHOICES = (
(LOG_DEBUG, _('Debug'), 'teal'),
(LOG_DEFAULT, _('Default'), 'gray'),
- (LOG_SUCCESS, _('Success'), 'green'),
(LOG_INFO, _('Info'), 'cyan'),
+ (LOG_SUCCESS, _('Success'), 'green'),
(LOG_WARNING, _('Warning'), 'yellow'),
(LOG_FAILURE, _('Failure'), 'red'),
)
@@ -173,8 +173,8 @@ class LogLevelChoices(ChoiceSet):
SYSTEM_LEVELS = {
LOG_DEBUG: logging.DEBUG,
LOG_DEFAULT: logging.INFO,
- LOG_SUCCESS: logging.INFO,
LOG_INFO: logging.INFO,
+ LOG_SUCCESS: logging.INFO,
LOG_WARNING: logging.WARNING,
LOG_FAILURE: logging.ERROR,
}
@@ -191,35 +191,6 @@ class DurationChoices(ChoiceSet):
)
-#
-# Job results
-#
-
-class JobResultStatusChoices(ChoiceSet):
-
- STATUS_PENDING = 'pending'
- STATUS_SCHEDULED = 'scheduled'
- STATUS_RUNNING = 'running'
- STATUS_COMPLETED = 'completed'
- STATUS_ERRORED = 'errored'
- STATUS_FAILED = 'failed'
-
- CHOICES = (
- (STATUS_PENDING, _('Pending'), 'cyan'),
- (STATUS_SCHEDULED, _('Scheduled'), 'gray'),
- (STATUS_RUNNING, _('Running'), 'blue'),
- (STATUS_COMPLETED, _('Completed'), 'green'),
- (STATUS_ERRORED, _('Errored'), 'red'),
- (STATUS_FAILED, _('Failed'), 'red'),
- )
-
- TERMINAL_STATE_CHOICES = (
- STATUS_COMPLETED,
- STATUS_ERRORED,
- STATUS_FAILED,
- )
-
-
#
# Webhooks
#
@@ -302,8 +273,10 @@ class EventRuleActionChoices(ChoiceSet):
WEBHOOK = 'webhook'
SCRIPT = 'script'
+ NOTIFICATION = 'notification'
CHOICES = (
(WEBHOOK, _('Webhook')),
(SCRIPT, _('Script')),
+ (NOTIFICATION, _('Notification')),
)
diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py
index 7162299e737..3bfe3b21b54 100644
--- a/netbox/extras/constants.py
+++ b/netbox/extras/constants.py
@@ -1,12 +1,6 @@
+from core.events import *
from extras.choices import LogLevelChoices
-# Events
-EVENT_CREATE = 'create'
-EVENT_UPDATE = 'update'
-EVENT_DELETE = 'delete'
-EVENT_JOB_START = 'job_start'
-EVENT_JOB_END = 'job_end'
-
# Custom fields
CUSTOMFIELD_EMPTY_VALUES = (None, '', [])
@@ -14,11 +8,14 @@ CUSTOMFIELD_EMPTY_VALUES = (None, '', [])
HTTP_CONTENT_TYPE_JSON = 'application/json'
WEBHOOK_EVENT_TYPES = {
- EVENT_CREATE: 'created',
- EVENT_UPDATE: 'updated',
- EVENT_DELETE: 'deleted',
- EVENT_JOB_START: 'job_started',
- EVENT_JOB_END: 'job_ended',
+ # Map registered event types to public webhook "event" equivalents
+ OBJECT_CREATED: 'created',
+ OBJECT_UPDATED: 'updated',
+ OBJECT_DELETED: 'deleted',
+ JOB_STARTED: 'job_started',
+ JOB_COMPLETED: 'job_ended',
+ JOB_FAILED: 'job_ended',
+ JOB_ERRORED: 'job_ended',
}
# Dashboard
@@ -139,10 +136,10 @@ DEFAULT_DASHBOARD = [
]
LOG_LEVEL_RANK = {
- LogLevelChoices.LOG_DEFAULT: 0,
- LogLevelChoices.LOG_DEBUG: 1,
- LogLevelChoices.LOG_SUCCESS: 2,
- LogLevelChoices.LOG_INFO: 3,
+ LogLevelChoices.LOG_DEBUG: 0,
+ LogLevelChoices.LOG_DEFAULT: 1,
+ LogLevelChoices.LOG_INFO: 2,
+ LogLevelChoices.LOG_SUCCESS: 3,
LogLevelChoices.LOG_WARNING: 4,
LogLevelChoices.LOG_FAILURE: 5,
}
diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py
index 5327421ca1d..c56e4cd7df7 100644
--- a/netbox/extras/dashboard/widgets.py
+++ b/netbox/extras/dashboard/widgets.py
@@ -15,7 +15,6 @@ from django.utils.translation import gettext as _
from core.models import ObjectType
from extras.choices import BookmarkOrderingChoices
-from netbox.choices import ButtonColorChoices
from utilities.object_types import object_type_identifier, object_type_name
from utilities.permissions import get_permission_for_model
from utilities.querydict import dict_to_querydict
@@ -131,22 +130,6 @@ class DashboardWidget:
def name(self):
return f'{self.__class__.__module__.split(".")[0]}.{self.__class__.__name__}'
- @property
- def fg_color(self):
- """
- Return the appropriate foreground (text) color for the widget's color.
- """
- if self.color in (
- ButtonColorChoices.CYAN,
- ButtonColorChoices.GRAY,
- ButtonColorChoices.GREY,
- ButtonColorChoices.TEAL,
- ButtonColorChoices.WHITE,
- ButtonColorChoices.YELLOW,
- ):
- return ButtonColorChoices.BLACK
- return ButtonColorChoices.WHITE
-
@property
def form_data(self):
return {
@@ -199,10 +182,13 @@ class ObjectCountsWidget(DashboardWidget):
for model in get_models_from_content_types(self.config['models']):
permission = get_permission_for_model(model, 'view')
if request.user.has_perm(permission):
- url = reverse(get_viewname(model, 'list'))
+ try:
+ url = reverse(get_viewname(model, 'list'))
+ except NoReverseMatch:
+ url = None
qs = model.objects.restrict(request.user, 'view')
# Apply any specified filters
- if filters := self.config.get('filters'):
+ if url and (filters := self.config.get('filters')):
params = dict_to_querydict(filters)
filterset = getattr(resolve(url).func.view_class, 'filterset', None)
qs = filterset(params, qs).qs
@@ -251,6 +237,10 @@ class ObjectListWidget(DashboardWidget):
def render(self, request):
app_label, model_name = self.config['model'].split('.')
model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class()
+ if not model:
+ logger.debug(f"Dashboard Widget model_class not found: {app_label}:{model_name}")
+ return
+
viewname = get_viewname(model, action='list')
# Evaluate user's permission. Note that this controls only whether the HTMX element is
@@ -381,17 +371,17 @@ class BookmarksWidget(DashboardWidget):
if request.user.is_anonymous:
bookmarks = list()
else:
- user_bookmarks = Bookmark.objects.filter(user=request.user)
- if self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_AZ:
- bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower())
- elif self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_ZA:
- bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower(), reverse=True)
- else:
- bookmarks = user_bookmarks.order_by(self.config['order_by'])
+ bookmarks = Bookmark.objects.filter(user=request.user)
if object_types := self.config.get('object_types'):
models = get_models_from_content_types(object_types)
content_types = ObjectType.objects.get_for_models(*models).values()
bookmarks = bookmarks.filter(object_type__in=content_types)
+ if self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_AZ:
+ bookmarks = sorted(bookmarks, key=lambda bookmark: bookmark.__str__().lower())
+ elif self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_ZA:
+ bookmarks = sorted(bookmarks, key=lambda bookmark: bookmark.__str__().lower(), reverse=True)
+ else:
+ bookmarks = bookmarks.order_by(self.config['order_by'])
if max_items := self.config.get('max_items'):
bookmarks = bookmarks[:max_items]
diff --git a/netbox/extras/events.py b/netbox/extras/events.py
index dae3f29cf4d..f13a3b48fdb 100644
--- a/netbox/extras/events.py
+++ b/netbox/extras/events.py
@@ -1,18 +1,18 @@
import logging
+from collections import defaultdict
from django.conf import settings
-from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.utils.module_loading import import_string
from django.utils.translation import gettext as _
from django_rq import get_queue
-from core.choices import ObjectChangeActionChoices
-from core.models import Job
+from core.events import *
from netbox.config import get_config
from netbox.constants import RQ_QUEUE_DEFAULT
from netbox.registry import registry
+from users.models import User
from utilities.api import get_serializer_for_model
from utilities.rqworker import get_rq_retry
from utilities.serialization import serialize_object
@@ -35,12 +35,12 @@ def serialize_for_event(instance):
return serializer.data
-def get_snapshots(instance, action):
+def get_snapshots(instance, event_type):
snapshots = {
'prechange': getattr(instance, '_prechange_snapshot', None),
'postchange': None,
}
- if action != ObjectChangeActionChoices.ACTION_DELETE:
+ 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()
@@ -50,7 +50,7 @@ def get_snapshots(instance, action):
return snapshots
-def enqueue_object(queue, instance, user, request_id, action):
+def enqueue_event(queue, instance, user, request_id, event_type):
"""
Enqueue a serialized representation of a created/updated/deleted object for the processing of
events once the request has completed.
@@ -65,24 +65,24 @@ def enqueue_object(queue, instance, user, request_id, action):
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, action)['postchange']
+ 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] = {
- 'content_type': ContentType.objects.get_for_model(instance),
+ 'object_type': ContentType.objects.get_for_model(instance),
'object_id': instance.pk,
- 'event': action,
+ 'event_type': event_type,
'data': serialize_for_event(instance),
- 'snapshots': get_snapshots(instance, action),
+ 'snapshots': get_snapshots(instance, event_type),
'username': user.username,
'request_id': request_id
}
-def process_event_rules(event_rules, model_name, event, data, username=None, snapshots=None, request_id=None):
- if username:
- user = get_user_model().objects.get(username=username)
- else:
- user = None
+def process_event_rules(event_rules, object_type, event_type, data, username=None, snapshots=None, request_id=None):
+ user = User.objects.get(username=username) if username else None
for event_rule in event_rules:
@@ -100,8 +100,8 @@ def process_event_rules(event_rules, model_name, event, data, username=None, sna
# Compile the task parameters
params = {
"event_rule": event_rule,
- "model_name": model_name,
- "event": event,
+ "model_name": object_type.model,
+ "event_type": event_type,
"data": data,
"snapshots": snapshots,
"timestamp": timezone.now().isoformat(),
@@ -125,14 +125,24 @@ def process_event_rules(event_rules, model_name, event, data, username=None, sna
script = event_rule.action_object.python_class()
# Enqueue a Job to record the script's execution
- Job.enqueue(
- "extras.scripts.run_script",
+ from extras.jobs import ScriptJob
+ ScriptJob.enqueue(
instance=event_rule.action_object,
name=script.name,
user=user,
data=data
)
+ # Notification groups
+ elif event_rule.action_type == EventRuleActionChoices.NOTIFICATION:
+ # Bulk-create notifications for all members of the notification group
+ event_rule.action_object.notify(
+ object_type=object_type,
+ object_id=data['id'],
+ object_repr=data.get('display'),
+ event_type=event_type
+ )
+
else:
raise ValueError(_("Unknown action type for an event rule: {action_type}").format(
action_type=event_rule.action_type
@@ -143,32 +153,29 @@ def process_event_queue(events):
"""
Flush a list of object representation to RQ for EventRule processing.
"""
- events_cache = {
- 'type_create': {},
- 'type_update': {},
- 'type_delete': {},
- }
+ events_cache = defaultdict(dict)
- for data in events:
- action_flag = {
- ObjectChangeActionChoices.ACTION_CREATE: 'type_create',
- ObjectChangeActionChoices.ACTION_UPDATE: 'type_update',
- ObjectChangeActionChoices.ACTION_DELETE: 'type_delete',
- }[data['event']]
- content_type = data['content_type']
+ for event in events:
+ event_type = event['event_type']
+ object_type = event['object_type']
# Cache applicable Event Rules
- if content_type not in events_cache[action_flag]:
- events_cache[action_flag][content_type] = EventRule.objects.filter(
- **{action_flag: True},
- object_types=content_type,
+ if object_type not in events_cache[event_type]:
+ events_cache[event_type][object_type] = EventRule.objects.filter(
+ event_types__contains=[event['event_type']],
+ object_types=object_type,
enabled=True
)
- event_rules = events_cache[action_flag][content_type]
+ event_rules = events_cache[event_type][object_type]
process_event_rules(
- event_rules, content_type.model, data['event'], data['data'], data['username'],
- snapshots=data['snapshots'], request_id=data['request_id']
+ event_rules=event_rules,
+ object_type=object_type,
+ event_type=event['event_type'],
+ data=event['data'],
+ username=event['username'],
+ snapshots=event['snapshots'],
+ request_id=event['request_id']
)
diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py
index bf0275c2df6..4f40ce50017 100644
--- a/netbox/extras/filtersets.py
+++ b/netbox/extras/filtersets.py
@@ -1,5 +1,4 @@
import django_filters
-from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext as _
@@ -8,6 +7,7 @@ from core.models import DataSource, ObjectType
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
from tenancy.models import Tenant, TenantGroup
+from users.models import Group, User
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
from virtualization.models import Cluster, ClusterGroup, ClusterType
from .choices import *
@@ -26,6 +26,7 @@ __all__ = (
'ImageAttachmentFilterSet',
'JournalEntryFilterSet',
'LocalConfigContextFilterSet',
+ 'NotificationGroupFilterSet',
'ObjectTypeFilterSet',
'SavedFilterFilterSet',
'ScriptFilterSet',
@@ -97,6 +98,9 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
object_type = ContentTypeFilter(
field_name='object_types'
)
+ event_type = MultiValueCharFilter(
+ method='filter_event_type'
+ )
action_type = django_filters.MultipleChoiceFilter(
choices=EventRuleActionChoices
)
@@ -106,8 +110,7 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
class Meta:
model = EventRule
fields = (
- 'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled',
- 'action_type', 'description',
+ 'id', 'name', 'enabled', 'action_type', 'description',
)
def search(self, queryset, name, value):
@@ -119,6 +122,9 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
Q(comments__icontains=value)
)
+ def filter_event_type(self, queryset, name, value):
+ return queryset.filter(event_types__overlap=value)
+
class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter(
@@ -152,9 +158,9 @@ class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
class Meta:
model = CustomField
fields = (
- 'id', 'name', 'label', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
+ 'id', 'name', 'label', 'group_name', 'required', 'unique', 'search_weight', 'filter_logic', 'ui_visible',
'ui_editable', 'weight', 'is_cloneable', 'description', 'validation_minimum', 'validation_maximum',
- 'validation_regex', 'validation_unique',
+ 'validation_regex',
)
def search(self, queryset, name, value):
@@ -277,12 +283,12 @@ class SavedFilterFilterSet(ChangeLoggedModelFilterSet):
field_name='object_types'
)
user_id = django_filters.ModelMultipleChoiceFilter(
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
label=_('User (ID)'),
)
user = django_filters.ModelMultipleChoiceFilter(
field_name='user__username',
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
to_field_name='username',
label=_('User (name)'),
)
@@ -321,12 +327,12 @@ class BookmarkFilterSet(BaseFilterSet):
object_type_id = MultiValueNumberFilter()
object_type = ContentTypeFilter()
user_id = django_filters.ModelMultipleChoiceFilter(
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
label=_('User (ID)'),
)
user = django_filters.ModelMultipleChoiceFilter(
field_name='user__username',
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
to_field_name='username',
label=_('User (name)'),
)
@@ -336,6 +342,49 @@ class BookmarkFilterSet(BaseFilterSet):
fields = ('id', 'object_id')
+class NotificationGroupFilterSet(ChangeLoggedModelFilterSet):
+ q = django_filters.CharFilter(
+ method='search',
+ label=_('Search'),
+ )
+ user_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='users',
+ queryset=User.objects.all(),
+ label=_('User (ID)'),
+ )
+ user = django_filters.ModelMultipleChoiceFilter(
+ field_name='users__username',
+ queryset=User.objects.all(),
+ to_field_name='username',
+ label=_('User (name)'),
+ )
+ group_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='groups',
+ queryset=Group.objects.all(),
+ label=_('Group (ID)'),
+ )
+ group = django_filters.ModelMultipleChoiceFilter(
+ field_name='groups__name',
+ queryset=Group.objects.all(),
+ to_field_name='name',
+ label=_('Group (name)'),
+ )
+
+ class Meta:
+ model = NotificationGroup
+ fields = (
+ 'id', 'name', 'description',
+ )
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(name__icontains=value) |
+ Q(description__icontains=value)
+ )
+
+
class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter(
method='search',
@@ -360,12 +409,12 @@ class JournalEntryFilterSet(NetBoxModelFilterSet):
queryset=ContentType.objects.all()
)
created_by_id = django_filters.ModelMultipleChoiceFilter(
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
label=_('User (ID)'),
)
created_by = django_filters.ModelMultipleChoiceFilter(
field_name='created_by__username',
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
to_field_name='username',
label=_('User (name)'),
)
diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py
index acb564b308b..30d06683b83 100644
--- a/netbox/extras/forms/bulk_edit.py
+++ b/netbox/extras/forms/bulk_edit.py
@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
from extras.choices import *
from extras.models import *
+from netbox.events import get_event_type_choices
from netbox.forms import NetBoxModelBulkEditForm
from utilities.forms import BulkEditForm, add_blank_choice
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField
@@ -18,6 +19,7 @@ __all__ = (
'EventRuleBulkEditForm',
'ExportTemplateBulkEditForm',
'JournalEntryBulkEditForm',
+ 'NotificationGroupBulkEditForm',
'SavedFilterBulkEditForm',
'TagBulkEditForm',
'WebhookBulkEditForm',
@@ -42,6 +44,11 @@ class CustomFieldBulkEditForm(BulkEditForm):
required=False,
widget=BulkEditNullBooleanSelect()
)
+ unique = forms.NullBooleanField(
+ label=_('Must be unique'),
+ required=False,
+ widget=BulkEditNullBooleanSelect()
+ )
weight = forms.IntegerField(
label=_('Weight'),
required=False
@@ -77,19 +84,12 @@ class CustomFieldBulkEditForm(BulkEditForm):
label=_('Validation regex'),
required=False
)
- validation_unique = forms.NullBooleanField(
- label=_('Must be unique'),
- required=False,
- widget=BulkEditNullBooleanSelect()
- )
comments = CommentField()
fieldsets = (
- FieldSet('group_name', 'description', 'weight', 'choice_set', name=_('Attributes')),
+ FieldSet('group_name', 'description', 'weight', 'required', 'unique', 'choice_set', name=_('Attributes')),
FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
- FieldSet(
- 'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
- ),
+ FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
)
nullable_fields = ('group_name', 'description', 'choice_set')
@@ -247,33 +247,18 @@ class EventRuleBulkEditForm(NetBoxModelBulkEditForm):
required=False,
widget=BulkEditNullBooleanSelect()
)
- type_create = forms.NullBooleanField(
- label=_('On create'),
+ event_types = forms.MultipleChoiceField(
+ choices=get_event_type_choices(),
required=False,
- widget=BulkEditNullBooleanSelect()
+ label=_('Event types')
)
- type_update = forms.NullBooleanField(
- label=_('On update'),
- required=False,
- widget=BulkEditNullBooleanSelect()
- )
- type_delete = forms.NullBooleanField(
- label=_('On delete'),
- required=False,
- widget=BulkEditNullBooleanSelect()
- )
- type_job_start = forms.NullBooleanField(
- label=_('On job start'),
- required=False,
- widget=BulkEditNullBooleanSelect()
- )
- type_job_end = forms.NullBooleanField(
- label=_('On job end'),
- required=False,
- widget=BulkEditNullBooleanSelect()
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
)
- nullable_fields = ('description', 'conditions',)
+ nullable_fields = ('description', 'conditions')
class TagBulkEditForm(BulkEditForm):
@@ -343,3 +328,17 @@ class JournalEntryBulkEditForm(BulkEditForm):
required=False
)
comments = CommentField()
+
+
+class NotificationGroupBulkEditForm(BulkEditForm):
+ pk = forms.ModelMultipleChoiceField(
+ queryset=NotificationGroup.objects.all(),
+ widget=forms.MultipleHiddenInput
+ )
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
+ )
+
+ nullable_fields = ('description',)
diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py
index cf022ba0e0c..258df826435 100644
--- a/netbox/extras/forms/bulk_import.py
+++ b/netbox/extras/forms/bulk_import.py
@@ -3,16 +3,18 @@ import re
from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.core.exceptions import ObjectDoesNotExist
-from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from core.models import ObjectType
from extras.choices import *
from extras.models import *
+from netbox.events import get_event_type_choices
from netbox.forms import NetBoxModelImportForm
+from users.models import Group, User
from utilities.forms import CSVModelForm
from utilities.forms.fields import (
- CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVMultipleContentTypeField, SlugField,
+ CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField,
+ CSVMultipleContentTypeField, SlugField,
)
__all__ = (
@@ -23,6 +25,7 @@ __all__ = (
'EventRuleImportForm',
'ExportTemplateImportForm',
'JournalEntryImportForm',
+ 'NotificationGroupImportForm',
'SavedFilterImportForm',
'TagImportForm',
'WebhookImportForm',
@@ -69,10 +72,9 @@ class CustomFieldImportForm(CSVModelForm):
class Meta:
model = CustomField
fields = (
- 'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description',
- 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
- 'validation_maximum', 'validation_regex', 'validation_unique', 'ui_visible', 'ui_editable', 'is_cloneable',
- 'comments',
+ 'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'unique',
+ 'description', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
+ 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'comments',
)
@@ -185,6 +187,11 @@ class EventRuleImportForm(NetBoxModelImportForm):
queryset=ObjectType.objects.with_feature('event_rules'),
help_text=_("One or more assigned object types")
)
+ event_types = CSVMultipleChoiceField(
+ choices=get_event_type_choices(),
+ label=_('Event types'),
+ help_text=_('The event type(s) which will trigger this rule')
+ )
action_object = forms.CharField(
label=_('Action object'),
required=True,
@@ -194,8 +201,8 @@ class EventRuleImportForm(NetBoxModelImportForm):
class Meta:
model = EventRule
fields = (
- 'name', 'description', 'enabled', 'conditions', 'object_types', 'type_create', 'type_update',
- 'type_delete', 'type_job_start', 'type_job_end', 'action_type', 'action_object', 'comments', 'tags'
+ 'name', 'description', 'enabled', 'conditions', 'object_types', 'event_types', 'action_type',
+ 'comments', 'tags'
)
def clean(self):
@@ -229,9 +236,6 @@ class TagImportForm(CSVModelForm):
class Meta:
model = Tag
fields = ('name', 'slug', 'color', 'description')
- help_texts = {
- 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'),
- }
class JournalEntryImportForm(NetBoxModelImportForm):
@@ -250,3 +254,24 @@ class JournalEntryImportForm(NetBoxModelImportForm):
fields = (
'assigned_object_type', 'assigned_object_id', 'created_by', 'kind', 'comments', 'tags'
)
+
+
+class NotificationGroupImportForm(CSVModelForm):
+ users = CSVModelMultipleChoiceField(
+ label=_('Users'),
+ queryset=User.objects.all(),
+ required=False,
+ to_field_name='username',
+ help_text=_('User names separated by commas, encased with double quotes')
+ )
+ groups = CSVModelMultipleChoiceField(
+ label=_('Groups'),
+ queryset=Group.objects.all(),
+ required=False,
+ to_field_name='name',
+ help_text=_('Group names separated by commas, encased with double quotes')
+ )
+
+ class Meta:
+ model = NotificationGroup
+ fields = ('name', 'description', 'users', 'groups')
diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py
index e29fd549dc5..05dcf96c476 100644
--- a/netbox/extras/forms/filtersets.py
+++ b/netbox/extras/forms/filtersets.py
@@ -1,14 +1,15 @@
from django import forms
-from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from core.models import ObjectType, DataFile, DataSource
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import *
from extras.models import *
+from netbox.events import get_event_type_choices
from netbox.forms.base import NetBoxModelFilterSetForm
from netbox.forms.mixins import SavedFiltersMixin
from tenancy.models import Tenant, TenantGroup
+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, DynamicModelMultipleChoiceField, TagFilterField,
@@ -28,6 +29,7 @@ __all__ = (
'ImageAttachmentFilterForm',
'JournalEntryFilterForm',
'LocalConfigContextFilterForm',
+ 'NotificationGroupFilterForm',
'SavedFilterFilterForm',
'TagFilterForm',
'WebhookFilterForm',
@@ -38,12 +40,11 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
FieldSet('q', 'filter_id'),
FieldSet(
- 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
- 'ui_editable', 'is_cloneable', name=_('Attributes')
- ),
- FieldSet(
- 'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
+ 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'unique', 'choice_set_id',
+ name=_('Attributes')
),
+ FieldSet('ui_visible', 'ui_editable', 'is_cloneable', name=_('Behavior')),
+ FieldSet('validation_minimum', 'validation_maximum', 'validation_regex', name=_('Validation')),
)
related_object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('custom_fields'),
@@ -70,6 +71,13 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
+ unique = forms.NullBooleanField(
+ label=_('Must be unique'),
+ required=False,
+ widget=forms.Select(
+ choices=BOOLEAN_WITH_BLANK_CHOICES
+ )
+ )
choice_set_id = DynamicModelMultipleChoiceField(
queryset=CustomFieldChoiceSet.objects.all(),
required=False,
@@ -104,13 +112,6 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
label=_('Validation regex'),
required=False
)
- validation_unique = forms.NullBooleanField(
- label=_('Must be unique'),
- required=False,
- widget=forms.Select(
- choices=BOOLEAN_WITH_BLANK_CHOICES
- )
- )
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
@@ -272,14 +273,18 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
- FieldSet('object_type_id', 'action_type', 'enabled', name=_('Attributes')),
- FieldSet('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', name=_('Events')),
+ FieldSet('object_type_id', 'event_type', 'action_type', 'enabled', name=_('Attributes')),
)
object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('event_rules'),
required=False,
label=_('Object type')
)
+ event_type = forms.MultipleChoiceField(
+ choices=get_event_type_choices,
+ required=False,
+ label=_('Event type')
+ )
action_type = forms.ChoiceField(
choices=add_blank_choice(EventRuleActionChoices),
required=False,
@@ -292,41 +297,6 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
- type_create = forms.NullBooleanField(
- required=False,
- widget=forms.Select(
- choices=BOOLEAN_WITH_BLANK_CHOICES
- ),
- label=_('Object creations')
- )
- type_update = forms.NullBooleanField(
- required=False,
- widget=forms.Select(
- choices=BOOLEAN_WITH_BLANK_CHOICES
- ),
- label=_('Object updates')
- )
- type_delete = forms.NullBooleanField(
- required=False,
- widget=forms.Select(
- choices=BOOLEAN_WITH_BLANK_CHOICES
- ),
- label=_('Object deletions')
- )
- type_job_start = forms.NullBooleanField(
- required=False,
- widget=forms.Select(
- choices=BOOLEAN_WITH_BLANK_CHOICES
- ),
- label=_('Job starts')
- )
- type_job_end = forms.NullBooleanField(
- required=False,
- widget=forms.Select(
- choices=BOOLEAN_WITH_BLANK_CHOICES
- ),
- label=_('Job terminations')
- )
class TagFilterForm(SavedFiltersMixin, FilterForm):
@@ -481,7 +451,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
widget=DateTimePicker()
)
created_by_id = DynamicModelMultipleChoiceField(
- queryset=get_user_model().objects.all(),
+ queryset=User.objects.all(),
required=False,
label=_('User')
)
@@ -496,3 +466,16 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
required=False
)
tag = TagFilterField(model)
+
+
+class NotificationGroupFilterForm(SavedFiltersMixin, FilterForm):
+ user_id = DynamicModelMultipleChoiceField(
+ queryset=User.objects.all(),
+ required=False,
+ label=_('User')
+ )
+ group_id = DynamicModelMultipleChoiceField(
+ queryset=Group.objects.all(),
+ required=False,
+ label=_('Group')
+ )
diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py
index 19823e9a4f4..a45daaf7084 100644
--- a/netbox/extras/forms/model_forms.py
+++ b/netbox/extras/forms/model_forms.py
@@ -10,8 +10,10 @@ from core.models import ObjectType
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import *
from extras.models import *
+from netbox.events import get_event_type_choices
from netbox.forms import NetBoxModelForm
from tenancy.models import Tenant, TenantGroup
+from users.models import Group, User
from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
@@ -32,7 +34,9 @@ __all__ = (
'ExportTemplateForm',
'ImageAttachmentForm',
'JournalEntryForm',
+ 'NotificationGroupForm',
'SavedFilterForm',
+ 'SubscriptionForm',
'TagForm',
'WebhookForm',
)
@@ -41,32 +45,36 @@ __all__ = (
class CustomFieldForm(forms.ModelForm):
object_types = ContentTypeMultipleChoiceField(
label=_('Object types'),
- queryset=ObjectType.objects.with_feature('custom_fields')
+ queryset=ObjectType.objects.with_feature('custom_fields'),
+ help_text=_("The type(s) of object that have this custom field")
+ )
+ default = JSONField(
+ label=_('Default value'),
+ required=False
)
related_object_type = ContentTypeChoiceField(
label=_('Related object type'),
queryset=ObjectType.objects.public(),
- required=False,
help_text=_("Type of the related object (for object/multi-object fields only)")
)
+ related_object_filter = JSONField(
+ label=_('Related object filter'),
+ required=False,
+ help_text=_('Specify query parameters as a JSON object.')
+ )
choice_set = DynamicModelChoiceField(
- queryset=CustomFieldChoiceSet.objects.all(),
- required=False
+ queryset=CustomFieldChoiceSet.objects.all()
)
comments = CommentField()
fieldsets = (
FieldSet(
- 'object_types', 'name', 'label', 'group_name', 'type', 'related_object_type', 'required', 'description',
+ 'object_types', 'name', 'label', 'group_name', 'description', 'type', 'required', 'unique', 'default',
name=_('Custom Field')
),
FieldSet(
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior')
),
- FieldSet('default', 'choice_set', name=_('Values')),
- FieldSet(
- 'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation')
- ),
)
class Meta:
@@ -83,11 +91,75 @@ class CustomFieldForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+ # Mimic HTMXSelect()
+ self.fields['type'].widget.attrs.update({
+ 'hx-get': '.',
+ 'hx-include': '#form_fields',
+ 'hx-target': '#form_fields',
+ })
+
# Disable changing the type of a CustomField as it almost universally causes errors if custom field data
# is already present.
if self.instance.pk:
self.fields['type'].disabled = True
+ field_type = get_field_value(self, 'type')
+
+ # Adjust for text fields
+ if field_type in (
+ CustomFieldTypeChoices.TYPE_TEXT,
+ CustomFieldTypeChoices.TYPE_LONGTEXT,
+ CustomFieldTypeChoices.TYPE_URL
+ ):
+ self.fieldsets = (
+ self.fieldsets[0],
+ FieldSet('validation_regex', name=_('Validation')),
+ *self.fieldsets[1:]
+ )
+ else:
+ del self.fields['validation_regex']
+
+ # Adjust for numeric fields
+ if field_type in (
+ CustomFieldTypeChoices.TYPE_INTEGER,
+ CustomFieldTypeChoices.TYPE_DECIMAL
+ ):
+ self.fieldsets = (
+ self.fieldsets[0],
+ FieldSet('validation_minimum', 'validation_maximum', name=_('Validation')),
+ *self.fieldsets[1:]
+ )
+ else:
+ del self.fields['validation_minimum']
+ del self.fields['validation_maximum']
+
+ # Adjust for object & multi-object fields
+ if field_type in (
+ CustomFieldTypeChoices.TYPE_OBJECT,
+ CustomFieldTypeChoices.TYPE_MULTIOBJECT
+ ):
+ self.fieldsets = (
+ self.fieldsets[0],
+ FieldSet('related_object_type', 'related_object_filter', name=_('Related Object')),
+ *self.fieldsets[1:]
+ )
+ else:
+ del self.fields['related_object_type']
+ del self.fields['related_object_filter']
+
+ # Adjust for selection & multi-select fields
+ if field_type in (
+ CustomFieldTypeChoices.TYPE_SELECT,
+ CustomFieldTypeChoices.TYPE_MULTISELECT
+ ):
+ self.fieldsets = (
+ self.fieldsets[0],
+ FieldSet('choice_set', name=_('Choices')),
+ *self.fieldsets[1:]
+ )
+ else:
+ del self.fields['choice_set']
+
class CustomFieldChoiceSetForm(forms.ModelForm):
extra_choices = forms.CharField(
@@ -238,6 +310,43 @@ class BookmarkForm(forms.ModelForm):
fields = ('object_type', 'object_id')
+class NotificationGroupForm(forms.ModelForm):
+ groups = DynamicModelMultipleChoiceField(
+ label=_('Groups'),
+ required=False,
+ queryset=Group.objects.all()
+ )
+ users = DynamicModelMultipleChoiceField(
+ label=_('Users'),
+ required=False,
+ queryset=User.objects.all()
+ )
+
+ class Meta:
+ model = NotificationGroup
+ fields = ('name', 'description', 'groups', 'users')
+
+ def clean(self):
+ super().clean()
+
+ # At least one User or Group must be assigned
+ if not self.cleaned_data['groups'] and not self.cleaned_data['users']:
+ raise forms.ValidationError(_("A notification group specify at least one user or group."))
+
+ return self.cleaned_data
+
+
+class SubscriptionForm(forms.ModelForm):
+ object_type = ContentTypeChoiceField(
+ label=_('Object type'),
+ queryset=ObjectType.objects.with_feature('notifications')
+ )
+
+ class Meta:
+ model = Subscription
+ fields = ('object_type', 'object_id')
+
+
class WebhookForm(NetBoxModelForm):
fieldsets = (
@@ -263,6 +372,10 @@ class EventRuleForm(NetBoxModelForm):
label=_('Object types'),
queryset=ObjectType.objects.with_feature('event_rules'),
)
+ event_types = forms.MultipleChoiceField(
+ choices=get_event_type_choices(),
+ label=_('Event types')
+ )
action_choice = forms.ChoiceField(
label=_('Action choice'),
choices=[]
@@ -279,25 +392,16 @@ class EventRuleForm(NetBoxModelForm):
fieldsets = (
FieldSet('name', 'description', 'object_types', 'enabled', 'tags', name=_('Event Rule')),
- FieldSet('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', name=_('Events')),
- FieldSet('conditions', name=_('Conditions')),
+ FieldSet('event_types', 'conditions', name=_('Triggers')),
FieldSet('action_type', 'action_choice', 'action_data', name=_('Action')),
)
class Meta:
model = EventRule
fields = (
- 'object_types', 'name', 'description', 'type_create', 'type_update', 'type_delete', 'type_job_start',
- 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', 'action_object_id',
- 'action_data', 'comments', 'tags'
+ 'object_types', 'name', 'description', 'enabled', 'event_types', 'conditions', 'action_type',
+ 'action_object_type', 'action_object_id', 'action_data', 'comments', 'tags'
)
- labels = {
- 'type_create': _('Creations'),
- 'type_update': _('Updates'),
- 'type_delete': _('Deletions'),
- 'type_job_start': _('Job executions'),
- 'type_job_end': _('Job terminations'),
- }
widgets = {
'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
'action_type': HTMXSelect(),
@@ -329,6 +433,18 @@ class EventRuleForm(NetBoxModelForm):
initial=initial
)
+ def init_notificationgroup_choice(self):
+ initial = None
+ if self.instance.action_type == EventRuleActionChoices.NOTIFICATION:
+ notificationgroup_id = get_field_value(self, 'action_object_id')
+ initial = NotificationGroup.objects.get(pk=notificationgroup_id) if notificationgroup_id else None
+ self.fields['action_choice'] = DynamicModelChoiceField(
+ label=_('Notification group'),
+ queryset=NotificationGroup.objects.all(),
+ required=True,
+ initial=initial
+ )
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['action_object_type'].required = False
@@ -341,6 +457,8 @@ class EventRuleForm(NetBoxModelForm):
self.init_webhook_choice()
elif action_type == EventRuleActionChoices.SCRIPT:
self.init_script_choice()
+ elif action_type == EventRuleActionChoices.NOTIFICATION:
+ self.init_notificationgroup_choice()
def clean(self):
super().clean()
@@ -357,6 +475,10 @@ class EventRuleForm(NetBoxModelForm):
for_concrete_model=False
)
self.cleaned_data['action_object_id'] = action_choice.id
+ # Notification
+ elif self.cleaned_data.get('action_type') == EventRuleActionChoices.NOTIFICATION:
+ self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(action_choice)
+ self.cleaned_data['action_object_id'] = action_choice.id
return self.cleaned_data
diff --git a/netbox/extras/forms/reports.py b/netbox/extras/forms/reports.py
index 358ee90e345..95692b3f625 100644
--- a/netbox/extras/forms/reports.py
+++ b/netbox/extras/forms/reports.py
@@ -31,7 +31,7 @@ class ReportForm(forms.Form):
super().__init__(*args, **kwargs)
# Annotate the current system time for reference
- now = local_now().strftime('%Y-%m-%d %H:%M:%S')
+ now = local_now().strftime('%Y-%m-%d %H:%M:%S %Z')
self.fields['schedule_at'].help_text += _(' (current time: {now} )').format(now=now)
# Remove scheduling fields if scheduling is disabled
diff --git a/netbox/extras/forms/scripts.py b/netbox/extras/forms/scripts.py
index ece96f5e492..331f7f01f96 100644
--- a/netbox/extras/forms/scripts.py
+++ b/netbox/extras/forms/scripts.py
@@ -37,7 +37,7 @@ class ScriptForm(forms.Form):
super().__init__(*args, **kwargs)
# Annotate the current system time for reference
- now = local_now().strftime('%Y-%m-%d %H:%M:%S')
+ now = local_now().strftime('%Y-%m-%d %H:%M:%S %Z')
self.fields['_schedule_at'].help_text += _(' (current time: {now} )').format(now=now)
# Remove scheduling fields if scheduling is disabled
diff --git a/netbox/extras/graphql/filters.py b/netbox/extras/graphql/filters.py
index 7451eef8a9f..ff2e6a0f177 100644
--- a/netbox/extras/graphql/filters.py
+++ b/netbox/extras/graphql/filters.py
@@ -13,6 +13,7 @@ __all__ = (
'ExportTemplateFilter',
'ImageAttachmentFilter',
'JournalEntryFilter',
+ 'NotificationGroupFilter',
'SavedFilterFilter',
'TagFilter',
'WebhookFilter',
@@ -67,6 +68,12 @@ class JournalEntryFilter(BaseFilterMixin):
pass
+@strawberry_django.filter(models.NotificationGroup, lookups=True)
+@autotype_decorator(filtersets.NotificationGroupFilterSet)
+class NotificationGroupFilter(BaseFilterMixin):
+ pass
+
+
@strawberry_django.filter(models.SavedFilter, lookups=True)
@autotype_decorator(filtersets.SavedFilterFilterSet)
class SavedFilterFilter(BaseFilterMixin):
diff --git a/netbox/extras/graphql/schema.py b/netbox/extras/graphql/schema.py
index f7828503566..7d2d11bf169 100644
--- a/netbox/extras/graphql/schema.py
+++ b/netbox/extras/graphql/schema.py
@@ -3,68 +3,52 @@ from typing import List
import strawberry
import strawberry_django
-from extras import models
from .types import *
-@strawberry.type
+@strawberry.type(name="Query")
class ExtrasQuery:
- @strawberry.field
- def config_context(self, id: int) -> ConfigContextType:
- return models.ConfigContext.objects.get(pk=id)
+ config_context: ConfigContextType = strawberry_django.field()
config_context_list: List[ConfigContextType] = strawberry_django.field()
- @strawberry.field
- def config_template(self, id: int) -> ConfigTemplateType:
- return models.ConfigTemplate.objects.get(pk=id)
+ config_template: ConfigTemplateType = strawberry_django.field()
config_template_list: List[ConfigTemplateType] = strawberry_django.field()
- @strawberry.field
- def custom_field(self, id: int) -> CustomFieldType:
- return models.CustomField.objects.get(pk=id)
+ custom_field: CustomFieldType = strawberry_django.field()
custom_field_list: List[CustomFieldType] = strawberry_django.field()
- @strawberry.field
- def custom_field_choice_set(self, id: int) -> CustomFieldChoiceSetType:
- return models.CustomFieldChoiceSet.objects.get(pk=id)
+ custom_field_choice_set: CustomFieldChoiceSetType = strawberry_django.field()
custom_field_choice_set_list: List[CustomFieldChoiceSetType] = strawberry_django.field()
- @strawberry.field
- def custom_link(self, id: int) -> CustomLinkType:
- return models.CustomLink.objects.get(pk=id)
+ custom_link: CustomLinkType = strawberry_django.field()
custom_link_list: List[CustomLinkType] = strawberry_django.field()
- @strawberry.field
- def export_template(self, id: int) -> ExportTemplateType:
- return models.ExportTemplate.objects.get(pk=id)
+ export_template: ExportTemplateType = strawberry_django.field()
export_template_list: List[ExportTemplateType] = strawberry_django.field()
- @strawberry.field
- def image_attachment(self, id: int) -> ImageAttachmentType:
- return models.ImageAttachment.objects.get(pk=id)
+ image_attachment: ImageAttachmentType = strawberry_django.field()
image_attachment_list: List[ImageAttachmentType] = strawberry_django.field()
- @strawberry.field
- def saved_filter(self, id: int) -> SavedFilterType:
- return models.SavedFilter.objects.get(pk=id)
+ saved_filter: SavedFilterType = strawberry_django.field()
saved_filter_list: List[SavedFilterType] = strawberry_django.field()
- @strawberry.field
- def journal_entry(self, id: int) -> JournalEntryType:
- return models.JournalEntry.objects.get(pk=id)
+ journal_entry: JournalEntryType = strawberry_django.field()
journal_entry_list: List[JournalEntryType] = strawberry_django.field()
- @strawberry.field
- def tag(self, id: int) -> TagType:
- return models.Tag.objects.get(pk=id)
+ notification: NotificationType = strawberry_django.field()
+ notification_list: List[NotificationType] = strawberry_django.field()
+
+ notification_group: NotificationGroupType = strawberry_django.field()
+ notification_group_list: List[NotificationGroupType] = strawberry_django.field()
+
+ subscription: SubscriptionType = strawberry_django.field()
+ subscription_list: List[SubscriptionType] = strawberry_django.field()
+
+ tag: TagType = strawberry_django.field()
tag_list: List[TagType] = strawberry_django.field()
- @strawberry.field
- def webhook(self, id: int) -> WebhookType:
- return models.Webhook.objects.get(pk=id)
+ webhook: WebhookType = strawberry_django.field()
webhook_list: List[WebhookType] = strawberry_django.field()
- @strawberry.field
- def event_rule(self, id: int) -> EventRuleType:
- return models.EventRule.objects.get(pk=id)
+ event_rule: EventRuleType = strawberry_django.field()
event_rule_list: List[EventRuleType] = strawberry_django.field()
diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py
index 1f3bfcdb913..a53c7bed3e7 100644
--- a/netbox/extras/graphql/types.py
+++ b/netbox/extras/graphql/types.py
@@ -18,7 +18,10 @@ __all__ = (
'ExportTemplateType',
'ImageAttachmentType',
'JournalEntryType',
+ 'NotificationGroupType',
+ 'NotificationType',
'SavedFilterType',
+ 'SubscriptionType',
'TagType',
'WebhookType',
)
@@ -81,7 +84,7 @@ class CustomFieldType(ObjectType):
class CustomFieldChoiceSetType(ObjectType):
choices_for: List[Annotated["CustomFieldType", strawberry.lazy('extras.graphql.types')]]
- extra_choices: List[str] | None
+ extra_choices: List[List[str]] | None
@strawberry_django.type(
@@ -122,6 +125,23 @@ class JournalEntryType(CustomFieldsMixin, TagsMixin, ObjectType):
created_by: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
+@strawberry_django.type(
+ models.Notification,
+ # filters=NotificationFilter
+)
+class NotificationType(ObjectType):
+ user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
+
+
+@strawberry_django.type(
+ models.NotificationGroup,
+ filters=NotificationGroupFilter
+)
+class NotificationGroupType(ObjectType):
+ users: List[Annotated["UserType", strawberry.lazy('users.graphql.types')]]
+ groups: List[Annotated["GroupType", strawberry.lazy('users.graphql.types')]]
+
+
@strawberry_django.type(
models.SavedFilter,
exclude=['content_types',],
@@ -131,6 +151,14 @@ class SavedFilterType(ObjectType):
user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
+@strawberry_django.type(
+ models.Subscription,
+ # filters=NotificationFilter
+)
+class SubscriptionType(ObjectType):
+ user: Annotated["UserType", strawberry.lazy('users.graphql.types')] | None
+
+
@strawberry_django.type(
models.Tag,
exclude=['extras_taggeditem_items', ],
diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py
new file mode 100644
index 00000000000..64a7d6a69b4
--- /dev/null
+++ b/netbox/extras/jobs.py
@@ -0,0 +1,107 @@
+import logging
+import traceback
+from contextlib import nullcontext
+
+from django.db import transaction
+from django.utils.translation import gettext as _
+
+from core.signals import clear_events
+from extras.models import Script as ScriptModel
+from netbox.context_managers import event_tracking
+from netbox.jobs import JobRunner
+from utilities.exceptions import AbortScript, AbortTransaction
+from .utils import is_report
+
+
+class ScriptJob(JobRunner):
+ """
+ Script execution job.
+
+ A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
+ exists outside the Script class to ensure it cannot be overridden by a script author.
+ """
+
+ class Meta:
+ # An explicit job name is not set because it doesn't make sense in this context. Currently, there's no scenario
+ # where jobs other than this one are used. Therefore, it is hidden, resulting in a cleaner job table overview.
+ name = ''
+
+ def run_script(self, script, request, data, commit):
+ """
+ Core script execution task. We capture this within a method to allow for conditionally wrapping it with the
+ event_tracking context manager (which is bypassed if commit == False).
+
+ Args:
+ request: The WSGI request associated with this execution (if any)
+ data: A dictionary of data to be passed to the script upon execution
+ commit: Passed through to Script.run()
+ """
+ logger = logging.getLogger(f"netbox.scripts.{script.full_name}")
+ logger.info(f"Running script (commit={commit})")
+
+ try:
+ try:
+ with transaction.atomic():
+ script.output = script.run(data, commit)
+ if not commit:
+ raise AbortTransaction()
+ except AbortTransaction:
+ script.log_info(message=_("Database changes have been reverted automatically."))
+ if script.failed:
+ logger.warning("Script failed")
+ raise
+
+ except Exception as e:
+ if type(e) is AbortScript:
+ msg = _("Script aborted with error: ") + str(e)
+ if is_report(type(script)):
+ script.log_failure(message=msg)
+ else:
+ script.log_failure(msg)
+ logger.error(f"Script aborted with error: {e}")
+
+ else:
+ stacktrace = traceback.format_exc()
+ script.log_failure(
+ message=_("An exception occurred: ") + f"`{type(e).__name__}: {e}`\n```\n{stacktrace}\n```"
+ )
+ logger.error(f"Exception raised during script execution: {e}")
+
+ if type(e) is not AbortTransaction:
+ script.log_info(message=_("Database changes have been reverted due to error."))
+
+ # Clear all pending events. Job termination (including setting the status) is handled by the job framework.
+ if request:
+ clear_events.send(request)
+ raise
+
+ # Update the job data regardless of the execution status of the job. Successes should be reported as well as
+ # failures.
+ finally:
+ self.job.data = script.get_job_data()
+
+ def run(self, data, request=None, commit=True, **kwargs):
+ """
+ Run the script.
+
+ Args:
+ job: The Job associated with this execution
+ data: A dictionary of data to be passed to the script upon execution
+ request: The WSGI request associated with this execution (if any)
+ commit: Passed through to Script.run()
+ """
+ script = ScriptModel.objects.get(pk=self.job.object_id).python_class()
+
+ # Add files to form data
+ if request:
+ files = request.FILES
+ for field_name, fileobj in files.items():
+ data[field_name] = fileobj
+
+ # Add the current request as a property of the script
+ script.request = request
+
+ # Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
+ # change logging, event rules, etc.
+ with event_tracking(request) if commit else nullcontext():
+ self.run_script(script, request, data, commit)
diff --git a/netbox/extras/lookups.py b/netbox/extras/lookups.py
index a8d89c943cf..c496cce78b9 100644
--- a/netbox/extras/lookups.py
+++ b/netbox/extras/lookups.py
@@ -1,4 +1,5 @@
-from django.db.models import CharField, TextField, Lookup
+from django.db.models import CharField, Lookup
+
from .fields import CachedValueField
diff --git a/netbox/extras/management/commands/housekeeping.py b/netbox/extras/management/commands/housekeeping.py
index 2ba0cbd7204..ade486fc024 100644
--- a/netbox/extras/management/commands/housekeeping.py
+++ b/netbox/extras/management/commands/housekeeping.py
@@ -93,7 +93,10 @@ class Command(BaseCommand):
# Check for new releases (if enabled)
if options['verbosity']:
self.stdout.write("[*] Checking for latest release")
- if settings.RELEASE_CHECK_URL:
+ if settings.ISOLATED_DEPLOYMENT:
+ if options['verbosity']:
+ self.stdout.write("\tSkipping: ISOLATED_DEPLOYMENT is enabled")
+ elif settings.RELEASE_CHECK_URL:
headers = {
'Accept': 'application/vnd.github.v3+json',
}
@@ -126,7 +129,7 @@ class Command(BaseCommand):
self.stdout.write(f"\tRequest error: {exc}", self.style.ERROR)
else:
if options['verbosity']:
- self.stdout.write(f"\tSkipping: RELEASE_CHECK_URL not set")
+ self.stdout.write("\tSkipping: RELEASE_CHECK_URL not set")
if options['verbosity']:
self.stdout.write("Finished.", self.style.SUCCESS)
diff --git a/netbox/extras/management/commands/reindex.py b/netbox/extras/management/commands/reindex.py
index e20fad0ceb8..21442be934d 100644
--- a/netbox/extras/management/commands/reindex.py
+++ b/netbox/extras/management/commands/reindex.py
@@ -66,11 +66,16 @@ class Command(BaseCommand):
raise CommandError(_("No indexers found!"))
self.stdout.write(f'Reindexing {len(indexers)} models.')
- # Clear all cached values for the specified models (if not being lazy)
+ # Clear cached values for the specified models (if not being lazy)
if not kwargs['lazy']:
+ if model_labels:
+ content_types = [ContentType.objects.get_for_model(model) for model in indexers.keys()]
+ else:
+ content_types = None
+
self.stdout.write('Clearing cached values... ', ending='')
self.stdout.flush()
- deleted_count = search_backend.clear()
+ deleted_count = search_backend.clear(object_types=content_types)
self.stdout.write(f'{deleted_count} entries deleted.')
# Index models
@@ -91,9 +96,9 @@ class Command(BaseCommand):
if i:
self.stdout.write(f'{i} entries cached.')
else:
- self.stdout.write(f'No objects found.')
+ self.stdout.write('No objects found.')
- msg = f'Completed.'
+ msg = 'Completed.'
if total_count := search_backend.size:
msg += f' Total entries: {total_count}'
self.stdout.write(msg, self.style.SUCCESS)
diff --git a/netbox/extras/management/commands/runscript.py b/netbox/extras/management/commands/runscript.py
index dbfbb40d90d..d5fb435ad12 100644
--- a/netbox/extras/management/commands/runscript.py
+++ b/netbox/extras/management/commands/runscript.py
@@ -1,19 +1,13 @@
import json
import logging
import sys
-import traceback
import uuid
-from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError
-from django.db import transaction
-from core.choices import JobStatusChoices
-from core.models import Job
+from extras.jobs import ScriptJob
from extras.scripts import get_module_and_script
-from extras.signals import clear_events
-from netbox.context_managers import event_tracking
-from utilities.exceptions import AbortTransaction
+from users.models import User
from utilities.request import NetBoxFakeRequest
@@ -33,46 +27,6 @@ class Command(BaseCommand):
parser.add_argument('script', help="Script to run")
def handle(self, *args, **options):
-
- def _run_script():
- """
- Core script execution task. We capture this within a subfunction to allow for conditionally wrapping it with
- the event_tracking context manager (which is bypassed if commit == False).
- """
- try:
- try:
- with transaction.atomic():
- script.output = script.run(data=data, commit=commit)
- if not commit:
- raise AbortTransaction()
- except AbortTransaction:
- script.log_info("Database changes have been reverted automatically.")
- clear_events.send(request)
- job.data = script.get_job_data()
- job.terminate()
- except Exception as e:
- stacktrace = traceback.format_exc()
- script.log_failure(
- f"An exception occurred: `{type(e).__name__}: {e}`\n```\n{stacktrace}\n```"
- )
- script.log_info("Database changes have been reverted due to error.")
- logger.error(f"Exception raised during script execution: {e}")
- clear_events.send(request)
- job.data = script.get_job_data()
- job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
-
- # Print any test method results
- for test_name, attrs in job.data['tests'].items():
- self.stdout.write(
- "\t{}: {} success, {} info, {} warning, {} failure".format(
- test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failure']
- )
- )
-
- logger.info(f"Script completed in {job.duration}")
-
- User = get_user_model()
-
# Params
script = options['script']
loglevel = options['loglevel']
@@ -84,8 +38,8 @@ class Command(BaseCommand):
data = {}
module_name, script_name = script.split('.', 1)
- module, script = get_module_and_script(module_name, script_name)
- script = script.python_class
+ module, script_obj = get_module_and_script(module_name, script_name)
+ script = script_obj.python_class
# Take user from command line if provided and exists, other
if options['user']:
@@ -97,7 +51,7 @@ class Command(BaseCommand):
user = User.objects.filter(is_superuser=True).order_by('pk')[0]
# Setup logging to Stdout
- formatter = logging.Formatter(f'[%(asctime)s][%(levelname)s] - %(message)s')
+ formatter = logging.Formatter('[%(asctime)s][%(levelname)s] - %(message)s')
stdouthandler = logging.StreamHandler(sys.stdout)
stdouthandler.setLevel(logging.DEBUG)
stdouthandler.setFormatter(formatter)
@@ -120,40 +74,29 @@ class Command(BaseCommand):
# Initialize the script form
script = script()
form = script.as_form(data, None)
-
- # Create the job
- job = Job.objects.create(
- object=module,
- name=script.class_name,
- user=User.objects.filter(is_superuser=True).order_by('pk')[0],
- job_id=uuid.uuid4()
- )
-
- request = NetBoxFakeRequest({
- 'META': {},
- 'POST': data,
- 'GET': {},
- 'FILES': {},
- 'user': user,
- 'path': '',
- 'id': job.job_id
- })
-
- if form.is_valid():
- job.status = JobStatusChoices.STATUS_RUNNING
- job.save()
-
- logger.info(f"Running script (commit={commit})")
- script.request = request
-
- # Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
- # change logging, webhooks, etc.
- with event_tracking(request):
- _run_script()
- else:
+ if not form.is_valid():
logger.error('Data is not valid:')
for field, errors in form.errors.get_json_data().items():
for error in errors:
logger.error(f'\t{field}: {error.get("message")}')
- job.status = JobStatusChoices.STATUS_ERRORED
- job.save()
+ raise CommandError()
+
+ # Execute the script.
+ job = ScriptJob.enqueue(
+ instance=script_obj,
+ user=user,
+ immediate=True,
+ data=data,
+ request=NetBoxFakeRequest({
+ 'META': {},
+ 'POST': data,
+ 'GET': {},
+ 'FILES': {},
+ 'user': user,
+ 'path': '',
+ 'id': uuid.uuid4()
+ }),
+ commit=commit,
+ )
+
+ logger.info(f"Script completed in {job.duration}")
diff --git a/netbox/extras/migrations/0002_squashed_0059.py b/netbox/extras/migrations/0002_squashed_0059.py
index 98bed255a54..a403a0e191f 100644
--- a/netbox/extras/migrations/0002_squashed_0059.py
+++ b/netbox/extras/migrations/0002_squashed_0059.py
@@ -131,10 +131,6 @@ class Migration(migrations.Migration):
name='webhook',
unique_together={('payload_url', 'type_create', 'type_update', 'type_delete')},
),
- migrations.AlterIndexTogether(
- name='taggeditem',
- index_together={('content_type', 'object_id')},
- ),
migrations.AlterUniqueTogether(
name='exporttemplate',
unique_together={('content_type', 'name')},
diff --git a/netbox/extras/migrations/0087_squashed_0098.py b/netbox/extras/migrations/0087_squashed_0098.py
index 55f276ecdbb..bbe7f79f57b 100644
--- a/netbox/extras/migrations/0087_squashed_0098.py
+++ b/netbox/extras/migrations/0087_squashed_0098.py
@@ -98,10 +98,9 @@ class Migration(migrations.Migration):
name='object_types',
field=models.ManyToManyField(blank=True, related_name='+', to='contenttypes.contenttype'),
),
- migrations.RenameIndex(
+ migrations.AddIndex(
model_name='taggeditem',
- new_name='extras_tagg_content_717743_idx',
- old_fields=('content_type', 'object_id'),
+ index=models.Index(fields=['content_type', 'object_id'], name='extras_tagg_content_717743_idx'),
),
migrations.CreateModel(
name='Bookmark',
diff --git a/netbox/extras/migrations/0116_custom_link_button_color.py b/netbox/extras/migrations/0116_custom_link_button_color.py
new file mode 100644
index 00000000000..665d73017b6
--- /dev/null
+++ b/netbox/extras/migrations/0116_custom_link_button_color.py
@@ -0,0 +1,25 @@
+from django.db import migrations, models
+
+
+def update_link_buttons(apps, schema_editor):
+ CustomLink = apps.get_model('extras', 'CustomLink')
+ CustomLink.objects.filter(button_class='outline-dark').update(button_class='default')
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0115_convert_dashboard_widgets'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='customlink',
+ name='button_class',
+ field=models.CharField(default='default', max_length=30),
+ ),
+ migrations.RunPython(
+ code=update_link_buttons,
+ reverse_code=migrations.RunPython.noop
+ ),
+ ]
diff --git a/netbox/extras/migrations/0116_move_objectchange.py b/netbox/extras/migrations/0117_move_objectchange.py
similarity index 98%
rename from netbox/extras/migrations/0116_move_objectchange.py
rename to netbox/extras/migrations/0117_move_objectchange.py
index b3833f7f89a..a69b5a7118f 100644
--- a/netbox/extras/migrations/0116_move_objectchange.py
+++ b/netbox/extras/migrations/0117_move_objectchange.py
@@ -28,7 +28,7 @@ def update_dashboard_widgets(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
- ('extras', '0115_convert_dashboard_widgets'),
+ ('extras', '0116_custom_link_button_color'),
('core', '0011_move_objectchange'),
]
diff --git a/netbox/extras/migrations/0117_customfield_uniqueness.py b/netbox/extras/migrations/0118_customfield_uniqueness.py
similarity index 76%
rename from netbox/extras/migrations/0117_customfield_uniqueness.py
rename to netbox/extras/migrations/0118_customfield_uniqueness.py
index 5d633b039d8..b7693aa24e8 100644
--- a/netbox/extras/migrations/0117_customfield_uniqueness.py
+++ b/netbox/extras/migrations/0118_customfield_uniqueness.py
@@ -4,13 +4,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('extras', '0116_move_objectchange'),
+ ('extras', '0117_move_objectchange'),
]
operations = [
migrations.AddField(
model_name='customfield',
- name='validation_unique',
+ name='unique',
field=models.BooleanField(default=False),
),
]
diff --git a/netbox/extras/migrations/0119_notifications.py b/netbox/extras/migrations/0119_notifications.py
new file mode 100644
index 00000000000..c266f3b6c96
--- /dev/null
+++ b/netbox/extras/migrations/0119_notifications.py
@@ -0,0 +1,79 @@
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('extras', '0118_customfield_uniqueness'),
+ ('users', '0009_update_group_perms'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='NotificationGroup',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('created', models.DateTimeField(auto_now_add=True, null=True)),
+ ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+ ('name', models.CharField(max_length=100, unique=True)),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ('groups', models.ManyToManyField(blank=True, related_name='notification_groups', to='users.group')),
+ ('users', models.ManyToManyField(blank=True, related_name='notification_groups', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'notification group',
+ 'verbose_name_plural': 'notification groups',
+ 'ordering': ('name',),
+ },
+ ),
+ migrations.CreateModel(
+ name='Subscription',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('object_id', models.PositiveBigIntegerField()),
+ ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'subscription',
+ 'verbose_name_plural': 'subscriptions',
+ 'ordering': ('-created', 'user'),
+ },
+ ),
+ migrations.CreateModel(
+ name='Notification',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('read', models.DateTimeField(blank=True, null=True)),
+ ('object_id', models.PositiveBigIntegerField()),
+ ('event_type', models.CharField(max_length=50)),
+ ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')),
+ ('object_repr', models.CharField(editable=False, max_length=200)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'notification',
+ 'verbose_name_plural': 'notifications',
+ 'ordering': ('-created', 'pk'),
+ 'indexes': [models.Index(fields=['object_type', 'object_id'], name='extras_noti_object__be74d5_idx')],
+ },
+ ),
+ migrations.AddConstraint(
+ model_name='notification',
+ constraint=models.UniqueConstraint(fields=('object_type', 'object_id', 'user'), name='extras_notification_unique_per_object_and_user'),
+ ),
+ migrations.AddIndex(
+ model_name='subscription',
+ index=models.Index(fields=['object_type', 'object_id'], name='extras_subs_object__37ef68_idx'),
+ ),
+ migrations.AddConstraint(
+ model_name='subscription',
+ constraint=models.UniqueConstraint(fields=('object_type', 'object_id', 'user'), name='extras_subscription_unique_per_object_and_user'),
+ ),
+ ]
diff --git a/netbox/extras/migrations/0120_eventrule_event_types.py b/netbox/extras/migrations/0120_eventrule_event_types.py
new file mode 100644
index 00000000000..f62c83e4c22
--- /dev/null
+++ b/netbox/extras/migrations/0120_eventrule_event_types.py
@@ -0,0 +1,75 @@
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+from core.events import *
+
+
+def set_event_types(apps, schema_editor):
+ EventRule = apps.get_model('extras', 'EventRule')
+ event_rules = EventRule.objects.all()
+
+ for event_rule in event_rules:
+ event_rule.event_types = []
+ if event_rule.type_create:
+ event_rule.event_types.append(OBJECT_CREATED)
+ if event_rule.type_update:
+ event_rule.event_types.append(OBJECT_UPDATED)
+ if event_rule.type_delete:
+ event_rule.event_types.append(OBJECT_DELETED)
+ if event_rule.type_job_start:
+ event_rule.event_types.append(JOB_STARTED)
+ if event_rule.type_job_end:
+ # Map type_job_end to all job termination events
+ event_rule.event_types.extend([JOB_COMPLETED, JOB_ERRORED, JOB_FAILED])
+
+ EventRule.objects.bulk_update(event_rules, ['event_types'])
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0119_notifications'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='eventrule',
+ name='event_types',
+ field=django.contrib.postgres.fields.ArrayField(
+ base_field=models.CharField(max_length=50),
+ blank=True,
+ null=True,
+ size=None
+ ),
+ ),
+ migrations.RunPython(
+ code=set_event_types,
+ reverse_code=migrations.RunPython.noop
+ ),
+ migrations.AlterField(
+ model_name='eventrule',
+ name='event_types',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), size=None),
+ preserve_default=False,
+ ),
+ migrations.RemoveField(
+ model_name='eventrule',
+ name='type_create',
+ ),
+ migrations.RemoveField(
+ model_name='eventrule',
+ name='type_delete',
+ ),
+ migrations.RemoveField(
+ model_name='eventrule',
+ name='type_job_end',
+ ),
+ migrations.RemoveField(
+ model_name='eventrule',
+ name='type_job_start',
+ ),
+ migrations.RemoveField(
+ model_name='eventrule',
+ name='type_update',
+ ),
+ ]
diff --git a/netbox/extras/migrations/0121_customfield_related_object_filter.py b/netbox/extras/migrations/0121_customfield_related_object_filter.py
new file mode 100644
index 00000000000..d6e41fd7def
--- /dev/null
+++ b/netbox/extras/migrations/0121_customfield_related_object_filter.py
@@ -0,0 +1,16 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0120_eventrule_event_types'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='customfield',
+ name='related_object_filter',
+ field=models.JSONField(blank=True, null=True),
+ ),
+ ]
diff --git a/netbox/extras/models/__init__.py b/netbox/extras/models/__init__.py
index 0413d1b91cd..e8572103428 100644
--- a/netbox/extras/models/__init__.py
+++ b/netbox/extras/models/__init__.py
@@ -2,6 +2,7 @@ from .configs import *
from .customfields import *
from .dashboard import *
from .models import *
+from .notifications import *
from .scripts import *
from .search import *
from .staging import *
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index cee69d16b1e..8b7fc0cb637 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -21,6 +21,7 @@ from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, ExportTemplatesMixin
from netbox.search import FieldTypes
from utilities import filters
+from utilities.datetime import datetime_from_timestamp
from utilities.forms.fields import (
CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicChoiceField,
DynamicModelChoiceField, DynamicModelMultipleChoiceField, DynamicMultipleChoiceField, JSONField, LaxURLField,
@@ -128,7 +129,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
required = models.BooleanField(
verbose_name=_('required'),
default=False,
- help_text=_("If true, this field is required when creating new objects or editing an existing object.")
+ help_text=_("This field is required when creating new objects or editing an existing object.")
+ )
+ unique = models.BooleanField(
+ verbose_name=_('must be unique'),
+ default=False,
+ help_text=_("The value of this field must be unique for the assigned object")
)
search_weight = models.PositiveSmallIntegerField(
verbose_name=_('search weight'),
@@ -153,6 +159,14 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
'Default value for the field (must be a JSON value). Encapsulate strings with double quotes (e.g. "Foo").'
)
)
+ related_object_filter = models.JSONField(
+ blank=True,
+ null=True,
+ help_text=_(
+ 'Filter the object selection choices using a query_params dict (must be a JSON value).'
+ 'Encapsulate strings with double quotes (e.g. "Foo").'
+ )
+ )
weight = models.PositiveSmallIntegerField(
default=100,
verbose_name=_('display weight'),
@@ -180,11 +194,6 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
'example, ^[A-Z]{3}$ will limit values to exactly three uppercase letters.'
)
)
- validation_unique = models.BooleanField(
- verbose_name=_('must be unique'),
- default=False,
- help_text=_('The value of this field must be unique for the assigned object')
- )
choice_set = models.ForeignKey(
to='CustomFieldChoiceSet',
on_delete=models.PROTECT,
@@ -220,9 +229,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
objects = CustomFieldManager()
clone_fields = (
- 'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'search_weight',
- 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
- 'validation_unique', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
+ 'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'unique',
+ 'search_weight', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum',
+ 'validation_regex', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
)
class Meta:
@@ -274,7 +283,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
"""
for ct in content_types:
model = ct.model_class()
- instances = model.objects.exclude(**{f'custom_field_data__contains': self.name})
+ instances = model.objects.exclude(**{'custom_field_data__contains': self.name})
for instance in instances:
instance.custom_field_data[self.name] = self.default
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
@@ -285,11 +294,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
no longer assigned to a model, or because it has been deleted).
"""
for ct in content_types:
- model = ct.model_class()
- instances = model.objects.filter(custom_field_data__has_key=self.name)
- for instance in instances:
- del instance.custom_field_data[self.name]
- model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
+ if model := ct.model_class():
+ instances = model.objects.filter(custom_field_data__has_key=self.name)
+ for instance in instances:
+ del instance.custom_field_data[self.name]
+ model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
def rename_object_data(self, old_name, new_name):
"""
@@ -340,9 +349,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
})
# Uniqueness can not be enforced for boolean fields
- if self.validation_unique and self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
+ if self.unique and self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
raise ValidationError({
- 'validation_unique': _("Uniqueness cannot be enforced for boolean fields")
+ 'unique': _("Uniqueness cannot be enforced for boolean fields")
})
# Choice set must be set on selection fields, and *only* on selection fields
@@ -363,15 +372,24 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
if not self.related_object_type:
raise ValidationError({
- 'object_type': _("Object fields must define an object type.")
+ 'related_object_type': _("Object fields must define an object type.")
})
elif self.related_object_type:
raise ValidationError({
- 'object_type': _(
- "{type} fields may not define an object type.")
- .format(type=self.get_type_display())
+ 'type': _("{type} fields may not define an object type.") .format(type=self.get_type_display())
})
+ # Related object filter can be set only for object-type fields, and must contain a dictionary mapping (if set)
+ if self.related_object_filter is not None:
+ if self.type not in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
+ raise ValidationError({
+ 'related_object_filter': _("A related object filter can be defined only for object fields.")
+ })
+ if type(self.related_object_filter) is not dict:
+ raise ValidationError({
+ 'related_object_filter': _("Filter must be defined as a dictionary mapping attributes to values.")
+ })
+
def serialize(self, value):
"""
Prepare a value for storage as JSON data.
@@ -501,27 +519,35 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# JSON
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
- field = JSONField(required=required, initial=json.dumps(initial) if initial else '')
+ field = JSONField(required=required, initial=json.dumps(initial) if initial else None)
# Object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
model = self.related_object_type.model_class()
field_class = CSVModelChoiceField if for_csv_import else DynamicModelChoiceField
- field = field_class(
- queryset=model.objects.all(),
- required=required,
- initial=initial
- )
+ kwargs = {
+ 'queryset': model.objects.all(),
+ 'required': required,
+ 'initial': initial,
+ }
+ if not for_csv_import:
+ kwargs['query_params'] = self.related_object_filter
+
+ field = field_class(**kwargs)
# Multiple objects
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
model = self.related_object_type.model_class()
field_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField
- field = field_class(
- queryset=model.objects.all(),
- required=required,
- initial=initial,
- )
+ kwargs = {
+ 'queryset': model.objects.all(),
+ 'required': required,
+ 'initial': initial,
+ }
+ if not for_csv_import:
+ kwargs['query_params'] = self.related_object_filter
+
+ field = field_class(**kwargs)
# Text
else:
@@ -635,7 +661,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
raise ValidationError(_("Value must be an integer."))
if self.validation_minimum is not None and value < self.validation_minimum:
raise ValidationError(
- _("Value must be at least {minimum}").format(minimum=self.validation_maximum)
+ _("Value must be at least {minimum}").format(minimum=self.validation_minimum)
)
if self.validation_maximum is not None and value > self.validation_maximum:
raise ValidationError(
@@ -672,12 +698,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Validate date & time
elif self.type == CustomFieldTypeChoices.TYPE_DATETIME:
if type(value) is not datetime:
- # Work around UTC issue for Python < 3.11; see
- # https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat
- if type(value) is str and value.endswith('Z'):
- value = f'{value[:-1]}+00:00'
try:
- datetime.fromisoformat(value)
+ datetime_from_timestamp(value)
except ValueError:
raise ValidationError(
_("Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS).")
@@ -763,6 +785,12 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
def __str__(self):
return self.name
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Cache the initial set of choices for comparison under clean()
+ self._original_extra_choices = self.__dict__.get('extra_choices')
+
def get_absolute_url(self):
return reverse('extras:customfieldchoiceset', args=[self.pk])
@@ -796,6 +824,32 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
if not self.base_choices and not self.extra_choices:
raise ValidationError(_("Must define base or extra choices."))
+ # Check whether any choices have been removed. If so, check whether any of the removed
+ # choices are still set in custom field data for any object.
+ original_choices = set([
+ c[0] for c in self._original_extra_choices
+ ]) if self._original_extra_choices else set()
+ current_choices = set([
+ c[0] for c in self.extra_choices
+ ]) if self.extra_choices else set()
+ if removed_choices := original_choices - current_choices:
+ for custom_field in self.choices_for.all():
+ for object_type in custom_field.object_types.all():
+ model = object_type.model_class()
+ for choice in removed_choices:
+ # Form the query based on the type of custom field
+ if custom_field.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
+ query_args = {f"custom_field_data__{custom_field.name}__contains": choice}
+ else:
+ query_args = {f"custom_field_data__{custom_field.name}": choice}
+ # Raise a ValidationError if there are any objects which still reference the removed choice
+ if model.objects.filter(models.Q(**query_args)).exists():
+ raise ValidationError(
+ _(
+ "Cannot remove choice {choice} as there are {model} objects which reference it."
+ ).format(choice=choice, model=object_type)
+ )
+
def save(self, *args, **kwargs):
# Sort choices if alphabetical ordering is enforced
diff --git a/netbox/extras/models/dashboard.py b/netbox/extras/models/dashboard.py
index 7b9293777b0..669785cdde8 100644
--- a/netbox/extras/models/dashboard.py
+++ b/netbox/extras/models/dashboard.py
@@ -1,4 +1,3 @@
-from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _
@@ -11,7 +10,7 @@ __all__ = (
class Dashboard(models.Model):
user = models.OneToOneField(
- to=get_user_model(),
+ to='users.User',
on_delete=models.CASCADE,
related_name='dashboard'
)
diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py
index cf439594319..d8a274c8975 100644
--- a/netbox/extras/models/models.py
+++ b/netbox/extras/models/models.py
@@ -3,6 +3,7 @@ import urllib.parse
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
+from django.contrib.postgres.fields import ArrayField
from django.core.validators import ValidationError
from django.db import models
from django.http import HttpResponse
@@ -17,6 +18,7 @@ from extras.conditions import ConditionSet
from extras.constants import *
from extras.utils import image_upload
from netbox.config import get_config
+from netbox.events import get_event_type_choices
from netbox.models import ChangeLoggedModel
from netbox.models.features import (
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin,
@@ -60,30 +62,9 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
max_length=200,
blank=True
)
- type_create = models.BooleanField(
- verbose_name=_('on create'),
- default=False,
- help_text=_("Triggers when a matching object is created.")
- )
- type_update = models.BooleanField(
- verbose_name=_('on update'),
- default=False,
- help_text=_("Triggers when a matching object is updated.")
- )
- type_delete = models.BooleanField(
- verbose_name=_('on delete'),
- default=False,
- help_text=_("Triggers when a matching object is deleted.")
- )
- type_job_start = models.BooleanField(
- verbose_name=_('on job start'),
- default=False,
- help_text=_("Triggers when a job for a matching object is started.")
- )
- type_job_end = models.BooleanField(
- verbose_name=_('on job end'),
- default=False,
- help_text=_("Triggers when a job for a matching object terminates.")
+ event_types = ArrayField(
+ base_field=models.CharField(max_length=50, choices=get_event_type_choices),
+ help_text=_("The types of event which will trigger this rule.")
)
enabled = models.BooleanField(
verbose_name=_('enabled'),
@@ -144,14 +125,6 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
def clean(self):
super().clean()
- # At least one action type must be selected
- if not any([
- self.type_create, self.type_update, self.type_delete, self.type_job_start, self.type_job_end
- ]):
- raise ValidationError(
- _("At least one event type must be selected: create, update, delete, job start, and/or job end.")
- )
-
# Validate that any conditions are in the correct format
if self.conditions:
try:
diff --git a/netbox/extras/models/notifications.py b/netbox/extras/models/notifications.py
new file mode 100644
index 00000000000..7fe03147cd6
--- /dev/null
+++ b/netbox/extras/models/notifications.py
@@ -0,0 +1,235 @@
+from functools import cached_property
+
+from django.conf import settings
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+
+from core.models import ObjectType
+from extras.querysets import NotificationQuerySet
+from netbox.models import ChangeLoggedModel
+from netbox.registry import registry
+from users.models import User
+from utilities.querysets import RestrictedQuerySet
+
+__all__ = (
+ 'Notification',
+ 'NotificationGroup',
+ 'Subscription',
+)
+
+
+def get_event_type_choices():
+ """
+ Compile a list of choices from all registered event types
+ """
+ return [
+ (name, event.text)
+ for name, event in registry['event_types'].items()
+ ]
+
+
+class Notification(models.Model):
+ """
+ A notification message for a User relating to a specific object in NetBox.
+ """
+ created = models.DateTimeField(
+ verbose_name=_('created'),
+ auto_now_add=True
+ )
+ read = models.DateTimeField(
+ verbose_name=_('read'),
+ null=True,
+ blank=True
+ )
+ user = models.ForeignKey(
+ to=settings.AUTH_USER_MODEL,
+ on_delete=models.CASCADE,
+ related_name='notifications'
+ )
+ object_type = models.ForeignKey(
+ to='contenttypes.ContentType',
+ on_delete=models.PROTECT
+ )
+ object_id = models.PositiveBigIntegerField()
+ object = GenericForeignKey(
+ ct_field='object_type',
+ fk_field='object_id'
+ )
+ object_repr = models.CharField(
+ max_length=200,
+ editable=False
+ )
+ event_type = models.CharField(
+ verbose_name=_('event'),
+ max_length=50,
+ choices=get_event_type_choices
+ )
+
+ objects = NotificationQuerySet.as_manager()
+
+ class Meta:
+ ordering = ('-created', 'pk')
+ indexes = (
+ models.Index(fields=('object_type', 'object_id')),
+ )
+ constraints = (
+ models.UniqueConstraint(
+ fields=('object_type', 'object_id', 'user'),
+ name='%(app_label)s_%(class)s_unique_per_object_and_user'
+ ),
+ )
+ verbose_name = _('notification')
+ verbose_name_plural = _('notifications')
+
+ def __str__(self):
+ return self.object_repr
+
+ def get_absolute_url(self):
+ return reverse('account:notifications')
+
+ def clean(self):
+ super().clean()
+
+ # Validate the assigned object type
+ if self.object_type not in ObjectType.objects.with_feature('notifications'):
+ raise ValidationError(
+ _("Objects of this type ({type}) do not support notifications.").format(type=self.object_type)
+ )
+
+ def save(self, *args, **kwargs):
+ # Record a string representation of the associated object
+ if self.object:
+ self.object_repr = self.get_object_repr(self.object)
+
+ super().save(*args, **kwargs)
+
+ @cached_property
+ def event(self):
+ """
+ Returns the registered Event which triggered this Notification.
+ """
+ return registry['event_types'].get(self.event_type)
+
+ @classmethod
+ def get_object_repr(cls, obj):
+ return str(obj)[:200]
+
+
+class NotificationGroup(ChangeLoggedModel):
+ """
+ A collection of users and/or groups to be informed for certain notifications.
+ """
+ name = models.CharField(
+ verbose_name=_('name'),
+ max_length=100,
+ unique=True
+ )
+ description = models.CharField(
+ verbose_name=_('description'),
+ max_length=200,
+ blank=True
+ )
+ groups = models.ManyToManyField(
+ to='users.Group',
+ verbose_name=_('groups'),
+ blank=True,
+ related_name='notification_groups'
+ )
+ users = models.ManyToManyField(
+ to='users.User',
+ verbose_name=_('users'),
+ blank=True,
+ related_name='notification_groups'
+ )
+
+ objects = RestrictedQuerySet.as_manager()
+
+ class Meta:
+ ordering = ('name',)
+ verbose_name = _('notification group')
+ verbose_name_plural = _('notification groups')
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('extras:notificationgroup', args=[self.pk])
+
+ @cached_property
+ def members(self):
+ """
+ Return all Users who belong to this notification group.
+ """
+ return self.users.union(
+ User.objects.filter(groups__in=self.groups.all())
+ ).order_by('username')
+
+ def notify(self, **kwargs):
+ """
+ Bulk-create Notifications for all members of this group.
+ """
+ Notification.objects.bulk_create([
+ Notification(user=member, **kwargs)
+ for member in self.members
+ ])
+ notify.alters_data = True
+
+
+class Subscription(models.Model):
+ """
+ A User's subscription to a particular object, to be notified of changes.
+ """
+ created = models.DateTimeField(
+ verbose_name=_('created'),
+ auto_now_add=True
+ )
+ user = models.ForeignKey(
+ to=settings.AUTH_USER_MODEL,
+ on_delete=models.CASCADE,
+ related_name='subscriptions'
+ )
+ object_type = models.ForeignKey(
+ to='contenttypes.ContentType',
+ on_delete=models.PROTECT
+ )
+ object_id = models.PositiveBigIntegerField()
+ object = GenericForeignKey(
+ ct_field='object_type',
+ fk_field='object_id'
+ )
+
+ objects = RestrictedQuerySet.as_manager()
+
+ class Meta:
+ ordering = ('-created', 'user')
+ indexes = (
+ models.Index(fields=('object_type', 'object_id')),
+ )
+ constraints = (
+ models.UniqueConstraint(
+ fields=('object_type', 'object_id', 'user'),
+ name='%(app_label)s_%(class)s_unique_per_object_and_user'
+ ),
+ )
+ verbose_name = _('subscription')
+ verbose_name_plural = _('subscriptions')
+
+ def __str__(self):
+ if self.object:
+ return str(self.object)
+ return super().__str__()
+
+ def get_absolute_url(self):
+ return reverse('account:subscriptions')
+
+ def clean(self):
+ super().clean()
+
+ # Validate the assigned object type
+ if self.object_type not in ObjectType.objects.with_feature('notifications'):
+ raise ValidationError(
+ _("Objects of this type ({type}) do not support notifications.").format(type=self.object_type)
+ )
diff --git a/netbox/extras/models/staging.py b/netbox/extras/models/staging.py
index 7ffbde08965..68d37de7f53 100644
--- a/netbox/extras/models/staging.py
+++ b/netbox/extras/models/staging.py
@@ -1,6 +1,6 @@
import logging
+import warnings
-from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericForeignKey
from django.db import models, transaction
from django.utils.translation import gettext_lazy as _
@@ -34,7 +34,7 @@ class Branch(ChangeLoggedModel):
blank=True
)
user = models.ForeignKey(
- to=get_user_model(),
+ to='users.User',
on_delete=models.SET_NULL,
blank=True,
null=True
@@ -45,6 +45,13 @@ class Branch(ChangeLoggedModel):
verbose_name = _('branch')
verbose_name_plural = _('branches')
+ def __init__(self, *args, **kwargs):
+ warnings.warn(
+ 'The staged changes functionality has been deprecated and will be removed in a future release.',
+ DeprecationWarning
+ )
+ super().__init__(*args, **kwargs)
+
def __str__(self):
return f'{self.name} ({self.pk})'
@@ -98,6 +105,13 @@ class StagedChange(CustomValidationMixin, EventRulesMixin, models.Model):
verbose_name = _('staged change')
verbose_name_plural = _('staged changes')
+ def __init__(self, *args, **kwargs):
+ warnings.warn(
+ 'The staged changes functionality has been deprecated and will be removed in a future release.',
+ DeprecationWarning
+ )
+ super().__init__(*args, **kwargs)
+
def __str__(self):
action = self.get_action_display()
app_label, model_name = self.object_type.natural_key()
diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py
index 3ee9d73e8ba..9b3722eef07 100644
--- a/netbox/extras/querysets.py
+++ b/netbox/extras/querysets.py
@@ -5,6 +5,12 @@ from extras.models.tags import TaggedItem
from utilities.query_functions import EmptyGroupByJSONBAgg
from utilities.querysets import RestrictedQuerySet
+__all__ = (
+ 'ConfigContextModelQuerySet',
+ 'ConfigContextQuerySet',
+ 'NotificationQuerySet',
+)
+
class ConfigContextQuerySet(RestrictedQuerySet):
@@ -145,3 +151,12 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
)
return base_query
+
+
+class NotificationQuerySet(RestrictedQuerySet):
+
+ def unread(self):
+ """
+ Return only unread notifications.
+ """
+ return self.filter(read__isnull=True)
diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py
index f6cc2bad002..f2bd75a1dc0 100644
--- a/netbox/extras/scripts.py
+++ b/netbox/extras/scripts.py
@@ -2,32 +2,23 @@ import inspect
import json
import logging
import os
-import traceback
-from datetime import timedelta
import yaml
from django import forms
from django.conf import settings
from django.core.validators import RegexValidator
-from django.db import transaction
from django.utils import timezone
from django.utils.functional import classproperty
from django.utils.translation import gettext as _
-from core.choices import JobStatusChoices
-from core.models import Job
from extras.choices import LogLevelChoices
-from extras.models import ScriptModule, Script as ScriptModel
-from extras.signals import clear_events
+from extras.models import ScriptModule
from ipam.formfields import IPAddressFormField, IPNetworkFormField
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
-from netbox.context_managers import event_tracking
-from utilities.exceptions import AbortScript, AbortTransaction
from utilities.forms import add_blank_choice
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import DatePicker, DateTimePicker
from .forms import ScriptForm
-from .utils import is_report
__all__ = (
@@ -48,7 +39,6 @@ __all__ = (
'StringVar',
'TextVar',
'get_module_and_script',
- 'run_script',
)
@@ -564,7 +554,7 @@ class BaseScript:
"""
Run the report and save its results. Each test method will be executed in order.
"""
- self.logger.info(f"Running report")
+ self.logger.info("Running report")
try:
for test_name in self.tests:
@@ -613,111 +603,3 @@ def get_module_and_script(module_name, script_name):
module = ScriptModule.objects.get(file_path=f'{module_name}.py')
script = module.scripts.get(name=script_name)
return module, script
-
-
-def run_script(data, job, request=None, commit=True, **kwargs):
- """
- A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
- exists outside the Script class to ensure it cannot be overridden by a script author.
-
- Args:
- data: A dictionary of data to be passed to the script upon execution
- job: The Job associated with this execution
- request: The WSGI request associated with this execution (if any)
- commit: Passed through to Script.run()
- """
- job.start()
-
- script = ScriptModel.objects.get(pk=job.object_id).python_class()
-
- logger = logging.getLogger(f"netbox.scripts.{script.full_name}")
- logger.info(f"Running script (commit={commit})")
-
- # Add files to form data
- if request:
- files = request.FILES
- for field_name, fileobj in files.items():
- data[field_name] = fileobj
-
- # Add the current request as a property of the script
- script.request = request
-
- def set_job_data(script):
- job.data = {
- 'log': script.messages,
- 'output': script.output,
- 'tests': script.tests,
- }
-
- return job
-
- def _run_script(job):
- """
- Core script execution task. We capture this within a subfunction to allow for conditionally wrapping it with
- the event_tracking context manager (which is bypassed if commit == False).
- """
- try:
- try:
- with transaction.atomic():
- script.output = script.run(data, commit)
- if not commit:
- raise AbortTransaction()
- except AbortTransaction:
- script.log_info(message=_("Database changes have been reverted automatically."))
- if request:
- clear_events.send(request)
-
- job.data = script.get_job_data()
- if script.failed:
- logger.warning(f"Script failed")
- job.terminate(status=JobStatusChoices.STATUS_FAILED)
- else:
- job.terminate()
-
- except Exception as e:
- if type(e) is AbortScript:
- msg = _("Script aborted with error: ") + str(e)
- if is_report(type(script)):
- script.log_failure(message=msg)
- else:
- script.log_failure(msg)
-
- logger.error(f"Script aborted with error: {e}")
- else:
- stacktrace = traceback.format_exc()
- script.log_failure(
- message=_("An exception occurred: ") + f"`{type(e).__name__}: {e}`\n```\n{stacktrace}\n```"
- )
- logger.error(f"Exception raised during script execution: {e}")
- script.log_info(message=_("Database changes have been reverted due to error."))
-
- job.data = script.get_job_data()
- job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
- if request:
- clear_events.send(request)
-
- logger.info(f"Script completed in {job.duration}")
-
- # Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
- # change logging, event rules, etc.
- if commit:
- with event_tracking(request):
- _run_script(job)
- else:
- _run_script(job)
-
- # Schedule the next job if an interval has been set
- if job.interval:
- new_scheduled_time = job.scheduled + timedelta(minutes=job.interval)
- Job.enqueue(
- run_script,
- instance=job.object,
- name=job.name,
- user=job.user,
- schedule_at=new_scheduled_time,
- interval=job.interval,
- job_timeout=script.job_timeout,
- data=data,
- request=request,
- commit=commit
- )
diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py
index 53ec39cac83..10c3f73c543 100644
--- a/netbox/extras/signals.py
+++ b/netbox/extras/signals.py
@@ -1,188 +1,18 @@
-import importlib
-import logging
-
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ImproperlyConfigured, ValidationError
-from django.db.models.fields.reverse_related import ManyToManyRel
from django.db.models.signals import m2m_changed, post_save, pre_delete
-from django.dispatch import receiver, Signal
-from django.utils.translation import gettext_lazy as _
-from django_prometheus.models import model_deletes, model_inserts, model_updates
+from django.dispatch import receiver
-from core.choices import ObjectChangeActionChoices
-from core.models import ObjectChange, ObjectType
+from core.events import *
+from core.models import ObjectType
from core.signals import job_end, job_start
-from extras.constants import EVENT_JOB_END, EVENT_JOB_START
from extras.events import process_event_rules
-from extras.models import EventRule
+from extras.models import EventRule, Notification, Subscription
from netbox.config import get_config
-from netbox.context import current_request, events_queue
-from netbox.models.features import ChangeLoggingMixin
+from netbox.registry import registry
from netbox.signals import post_clean
from utilities.exceptions import AbortRequest
-from .events import enqueue_object, get_snapshots, serialize_for_event
from .models import CustomField, TaggedItem
-from .validators import CustomValidator
-
-
-def run_validators(instance, validators):
- """
- Run the provided iterable of validators for the instance.
- """
- request = current_request.get()
- for validator in validators:
-
- # Loading a validator class by dotted path
- if type(validator) is str:
- module, cls = validator.rsplit('.', 1)
- validator = getattr(importlib.import_module(module), cls)()
-
- # Constructing a new instance on the fly from a ruleset
- elif type(validator) is dict:
- validator = CustomValidator(validator)
-
- elif not issubclass(validator.__class__, CustomValidator):
- raise ImproperlyConfigured(f"Invalid value for custom validator: {validator}")
-
- validator(instance, request)
-
-
-#
-# Change logging/webhooks
-#
-
-# Define a custom signal that can be sent to clear any queued events
-clear_events = Signal()
-
-
-@receiver((post_save, m2m_changed))
-def handle_changed_object(sender, instance, **kwargs):
- """
- Fires when an object is created or updated.
- """
- m2m_changed = False
-
- if not hasattr(instance, 'to_objectchange'):
- return
-
- # Get the current request, or bail if not set
- request = current_request.get()
- if request is None:
- return
-
- # Determine the type of change being made
- if kwargs.get('created'):
- action = ObjectChangeActionChoices.ACTION_CREATE
- elif 'created' in kwargs:
- action = ObjectChangeActionChoices.ACTION_UPDATE
- elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']:
- # m2m_changed with objects added or removed
- m2m_changed = True
- action = ObjectChangeActionChoices.ACTION_UPDATE
- else:
- return
-
- # Create/update an ObjectChange record for this change
- objectchange = instance.to_objectchange(action)
- # If this is a many-to-many field change, check for a previous ObjectChange instance recorded
- # for this object by this request and update it
- if m2m_changed and (
- prev_change := ObjectChange.objects.filter(
- changed_object_type=ContentType.objects.get_for_model(instance),
- changed_object_id=instance.pk,
- request_id=request.id
- ).first()
- ):
- prev_change.postchange_data = objectchange.postchange_data
- prev_change.save()
- elif objectchange and objectchange.has_changes:
- objectchange.user = request.user
- objectchange.request_id = request.id
- objectchange.save()
-
- # Ensure that we're working with fresh M2M assignments
- if m2m_changed:
- instance.refresh_from_db()
-
- # Enqueue the object for event processing
- queue = events_queue.get()
- enqueue_object(queue, instance, request.user, request.id, action)
- events_queue.set(queue)
-
- # Increment metric counters
- if action == ObjectChangeActionChoices.ACTION_CREATE:
- model_inserts.labels(instance._meta.model_name).inc()
- elif action == ObjectChangeActionChoices.ACTION_UPDATE:
- model_updates.labels(instance._meta.model_name).inc()
-
-
-@receiver(pre_delete)
-def handle_deleted_object(sender, instance, **kwargs):
- """
- Fires when an object is deleted.
- """
- # Run any deletion protection rules for the object. Note that this must occur prior
- # 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, [])
- try:
- run_validators(instance, validators)
- except ValidationError as e:
- raise AbortRequest(
- _("Deletion is prevented by a protection rule: {message}").format(message=e)
- )
-
- # Get the current request, or bail if not set
- request = current_request.get()
- if request is None:
- return
-
- # Record an ObjectChange if applicable
- if hasattr(instance, 'to_objectchange'):
- if hasattr(instance, 'snapshot') and not getattr(instance, '_prechange_snapshot', None):
- instance.snapshot()
- objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
- objectchange.user = request.user
- objectchange.request_id = request.id
- objectchange.save()
-
- # Django does not automatically send an m2m_changed signal for the reverse direction of a
- # many-to-many relationship (see https://code.djangoproject.com/ticket/17688), so we need to
- # trigger one manually. We do this by checking for any reverse M2M relationships on the
- # instance being deleted, and explicitly call .remove() on the remote M2M field to delete
- # the association. This triggers an m2m_changed signal with the `post_remove` action type
- # for the forward direction of the relationship, ensuring that the change is recorded.
- for relation in instance._meta.related_objects:
- if type(relation) is not ManyToManyRel:
- continue
- related_model = relation.related_model
- related_field_name = relation.remote_field.name
- if not issubclass(related_model, ChangeLoggingMixin):
- # We only care about triggering the m2m_changed signal for models which support
- # change logging
- continue
- for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
- obj.snapshot() # Ensure the change record includes the "before" state
- getattr(obj, related_field_name).remove(instance)
-
- # Enqueue the object for event processing
- queue = events_queue.get()
- enqueue_object(queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
- events_queue.set(queue)
-
- # Increment metric counters
- model_deletes.labels(instance._meta.model_name).inc()
-
-
-@receiver(clear_events)
-def clear_events_queue(sender, **kwargs):
- """
- Delete any queued events (e.g. because of an aborted bulk transaction)
- """
- logger = logging.getLogger('events')
- logger.info(f"Clearing {len(events_queue.get())} queued events ({sender})")
- events_queue.set({})
+from .utils import run_validators
#
@@ -268,9 +98,19 @@ def process_job_start_event_rules(sender, **kwargs):
"""
Process event rules for jobs starting.
"""
- event_rules = EventRule.objects.filter(type_job_start=True, enabled=True, object_types=sender.object_type)
+ event_rules = EventRule.objects.filter(
+ event_types__contains=[JOB_STARTED],
+ enabled=True,
+ object_types=sender.object_type
+ )
username = sender.user.username if sender.user else None
- process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_START, sender.data, username)
+ process_event_rules(
+ event_rules=event_rules,
+ object_type=sender.object_type,
+ event_type=JOB_STARTED,
+ data=sender.data,
+ username=username
+ )
@receiver(job_end)
@@ -278,6 +118,57 @@ def process_job_end_event_rules(sender, **kwargs):
"""
Process event rules for jobs terminating.
"""
- event_rules = EventRule.objects.filter(type_job_end=True, enabled=True, object_types=sender.object_type)
+ event_rules = EventRule.objects.filter(
+ event_types__contains=[JOB_COMPLETED],
+ enabled=True,
+ object_types=sender.object_type
+ )
username = sender.user.username if sender.user else None
- process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_END, sender.data, username)
+ process_event_rules(
+ event_rules=event_rules,
+ object_type=sender.object_type,
+ event_type=JOB_COMPLETED,
+ data=sender.data,
+ username=username
+ )
+
+
+#
+# Notifications
+#
+
+@receiver((post_save, pre_delete))
+def notify_object_changed(sender, instance, **kwargs):
+ # Skip for newly-created objects
+ if kwargs.get('created'):
+ return
+
+ # Determine event type
+ if 'created' in kwargs:
+ event_type = OBJECT_UPDATED
+ else:
+ event_type = OBJECT_DELETED
+
+ # Skip unsupported object types
+ ct = ContentType.objects.get_for_model(instance)
+ if ct.model not in registry['model_features']['notifications'].get(ct.app_label, []):
+ return
+
+ # Find all subscribed Users
+ subscribed_users = Subscription.objects.filter(object_type=ct, object_id=instance.pk).values_list('user', flat=True)
+ if not subscribed_users:
+ return
+
+ # Delete any existing Notifications for the object
+ Notification.objects.filter(object_type=ct, object_id=instance.pk, user__in=subscribed_users).delete()
+
+ # Create Notifications for Subscribers
+ Notification.objects.bulk_create([
+ Notification(
+ user_id=user,
+ object=instance,
+ object_repr=Notification.get_object_repr(instance),
+ event_type=event_type
+ )
+ for user in subscribed_users
+ ])
diff --git a/netbox/extras/tables/columns.py b/netbox/extras/tables/columns.py
new file mode 100644
index 00000000000..9b6aadcbf53
--- /dev/null
+++ b/netbox/extras/tables/columns.py
@@ -0,0 +1,13 @@
+from django.utils.translation import gettext as _
+
+from netbox.tables.columns import ActionsColumn, ActionsItem
+
+__all__ = (
+ 'NotificationActionsColumn',
+)
+
+
+class NotificationActionsColumn(ActionsColumn):
+ actions = {
+ 'dismiss': ActionsItem(_('Dismiss'), 'trash-can-outline', 'delete', 'danger'),
+ }
diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py
index d919ff1d5ba..e538c488ec0 100644
--- a/netbox/extras/tables/tables.py
+++ b/netbox/extras/tables/tables.py
@@ -6,7 +6,9 @@ from django.utils.translation import gettext_lazy as _
from extras.models import *
from netbox.constants import EMPTY_TABLE_TEXT
+from netbox.events import get_event_text
from netbox.tables import BaseTable, NetBoxTable, columns
+from .columns import NotificationActionsColumn
__all__ = (
'BookmarkTable',
@@ -19,21 +21,36 @@ __all__ = (
'ExportTemplateTable',
'ImageAttachmentTable',
'JournalEntryTable',
+ 'NotificationGroupTable',
+ 'NotificationTable',
'SavedFilterTable',
'ReportResultsTable',
'ScriptResultsTable',
+ 'SubscriptionTable',
'TaggedItemTable',
'TagTable',
'WebhookTable',
)
-IMAGEATTACHMENT_IMAGE = '''
+IMAGEATTACHMENT_IMAGE = """
{% if record.image %}
{{ record }}
{% else %}
—
{% endif %}
-'''
+"""
+
+NOTIFICATION_ICON = """
+
+"""
+
+NOTIFICATION_LINK = """
+{% if not record.event.destructive %}
+ {{ record.object_repr }}
+{% else %}
+ {{ record.object_repr }}
+{% endif %}
+"""
class CustomFieldTable(NetBoxTable):
@@ -45,7 +62,12 @@ class CustomFieldTable(NetBoxTable):
verbose_name=_('Object Types')
)
required = columns.BooleanColumn(
- verbose_name=_('Required')
+ verbose_name=_('Required'),
+ false_mark=None
+ )
+ unique = columns.BooleanColumn(
+ verbose_name=_('Validate Uniqueness'),
+ false_mark=None
)
ui_visible = columns.ChoiceFieldColumn(
verbose_name=_('Visible')
@@ -70,6 +92,7 @@ class CustomFieldTable(NetBoxTable):
)
is_cloneable = columns.BooleanColumn(
verbose_name=_('Is Cloneable'),
+ false_mark=None
)
validation_minimum = tables.Column(
verbose_name=_('Minimum Value'),
@@ -80,19 +103,18 @@ class CustomFieldTable(NetBoxTable):
validation_regex = tables.Column(
verbose_name=_('Validation Regex'),
)
- validation_unique = columns.BooleanColumn(
- verbose_name=_('Validate Uniqueness'),
- )
class Meta(NetBoxTable.Meta):
model = CustomField
fields = (
'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required',
- 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
- 'weight', 'choice_set', 'choices', 'validation_minimum', 'validation_maximum', 'validation_regex',
- 'validation_unique', 'comments', 'created', 'last_updated',
+ 'unique', 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable',
+ 'is_cloneable', 'weight', 'choice_set', 'choices', 'validation_minimum', 'validation_maximum',
+ 'validation_regex', 'comments', 'created', 'last_updated',
+ )
+ default_columns = (
+ 'pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'unique', 'description',
)
- default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description')
class CustomFieldChoiceSetTable(NetBoxTable):
@@ -116,6 +138,7 @@ class CustomFieldChoiceSetTable(NetBoxTable):
)
order_alphabetically = columns.BooleanColumn(
verbose_name=_('Order Alphabetically'),
+ false_mark=None
)
class Meta(NetBoxTable.Meta):
@@ -140,6 +163,7 @@ class CustomLinkTable(NetBoxTable):
)
new_window = columns.BooleanColumn(
verbose_name=_('New Window'),
+ false_mark=None
)
class Meta(NetBoxTable.Meta):
@@ -161,6 +185,7 @@ class ExportTemplateTable(NetBoxTable):
)
as_attachment = columns.BooleanColumn(
verbose_name=_('As Attachment'),
+ false_mark=None
)
data_source = tables.Column(
verbose_name=_('Data Source'),
@@ -229,6 +254,7 @@ class SavedFilterTable(NetBoxTable):
)
shared = columns.BooleanColumn(
verbose_name=_('Shared'),
+ false_mark=None
)
def value_parameters(self, value):
@@ -263,6 +289,90 @@ class BookmarkTable(NetBoxTable):
default_columns = ('object', 'object_type', 'created')
+class SubscriptionTable(NetBoxTable):
+ object_type = columns.ContentTypeColumn(
+ verbose_name=_('Object Type'),
+ )
+ object = tables.Column(
+ verbose_name=_('Object'),
+ linkify=True,
+ orderable=False
+ )
+ user = tables.Column(
+ verbose_name=_('User'),
+ linkify=True
+ )
+ actions = columns.ActionsColumn(
+ actions=('delete',)
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = Subscription
+ fields = ('pk', 'object', 'object_type', 'created', 'user')
+ default_columns = ('object', 'object_type', 'created')
+
+
+class NotificationTable(NetBoxTable):
+ icon = columns.TemplateColumn(
+ template_code=NOTIFICATION_ICON,
+ accessor=tables.A('event'),
+ attrs={
+ 'td': {'class': 'w-1'},
+ 'th': {'class': 'w-1'},
+ },
+ verbose_name=''
+ )
+ object_type = columns.ContentTypeColumn(
+ verbose_name=_('Object Type'),
+ )
+ object = columns.TemplateColumn(
+ verbose_name=_('Object'),
+ template_code=NOTIFICATION_LINK,
+ orderable=False
+ )
+ created = columns.DateTimeColumn(
+ timespec='minutes',
+ verbose_name=_('Created'),
+ )
+ read = columns.DateTimeColumn(
+ timespec='minutes',
+ verbose_name=_('Read'),
+ )
+ user = tables.Column(
+ verbose_name=_('User'),
+ linkify=True
+ )
+ actions = NotificationActionsColumn(
+ actions=('dismiss',)
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = Notification
+ fields = ('pk', 'icon', 'object', 'object_type', 'event_type', 'created', 'read', 'user')
+ default_columns = ('icon', 'object', 'object_type', 'event_type', 'created')
+ row_attrs = {
+ 'data-read': lambda record: bool(record.read),
+ }
+
+
+class NotificationGroupTable(NetBoxTable):
+ name = tables.Column(
+ linkify=True,
+ verbose_name=_('Name')
+ )
+ users = columns.ManyToManyColumn(
+ linkify_item=True
+ )
+ groups = columns.ManyToManyColumn(
+ linkify_item=True
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = NotificationGroup
+ fields = ('pk', 'name', 'description', 'groups', 'users')
+ default_columns = ('name', 'description', 'groups', 'users')
+
+
class WebhookTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
@@ -304,20 +414,10 @@ class EventRuleTable(NetBoxTable):
enabled = columns.BooleanColumn(
verbose_name=_('Enabled'),
)
- type_create = columns.BooleanColumn(
- verbose_name=_('Create')
- )
- type_update = columns.BooleanColumn(
- verbose_name=_('Update')
- )
- type_delete = columns.BooleanColumn(
- verbose_name=_('Delete')
- )
- type_job_start = columns.BooleanColumn(
- verbose_name=_('Job Start')
- )
- type_job_end = columns.BooleanColumn(
- verbose_name=_('Job End')
+ event_types = columns.ArrayColumn(
+ verbose_name=_('Event Types'),
+ func=get_event_text,
+ orderable=False
)
tags = columns.TagColumn(
url_name='extras:webhook_list'
@@ -327,12 +427,10 @@ class EventRuleTable(NetBoxTable):
model = EventRule
fields = (
'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'action_object', 'object_types',
- 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'tags', 'created',
- 'last_updated',
+ 'event_types', 'tags', 'created', 'last_updated',
)
default_columns = (
- 'pk', 'name', 'enabled', 'action_type', 'action_object', 'object_types', 'type_create', 'type_update',
- 'type_delete', 'type_job_start', 'type_job_end',
+ 'pk', 'name', 'enabled', 'action_type', 'action_object', 'object_types', 'event_types',
)
diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py
index dd28a816010..4aeaaa6b15a 100644
--- a/netbox/extras/templatetags/custom_links.py
+++ b/netbox/extras/templatetags/custom_links.py
@@ -4,6 +4,7 @@ from django.utils.safestring import mark_safe
from core.models import ObjectType
from extras.models import CustomLink
+from netbox.choices import ButtonColorChoices
register = template.Library()
@@ -59,10 +60,11 @@ def custom_links(context, obj):
# Add non-grouped links
else:
+ button_class = 'outline-secondary' if cl.button_class == ButtonColorChoices.DEFAULT else cl.button_class
try:
if rendered := cl.render(link_context):
template_code += LINK_BUTTON.format(
- rendered['link'], rendered['link_target'], cl.button_class, rendered['text']
+ rendered['link'], rendered['link_target'], button_class, rendered['text']
)
except Exception as e:
template_code += f'' \
diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py
index 5d243ae1a9d..63baf44d30e 100644
--- a/netbox/extras/tests/test_api.py
+++ b/netbox/extras/tests/test_api.py
@@ -1,21 +1,20 @@
import datetime
-from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.timezone import make_aware
from rest_framework import status
from core.choices import ManagedFileRootPathChoices
+from core.events import *
from core.models import ObjectType
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
from extras.choices import *
from extras.models import *
from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar
+from users.models import Group, User
from utilities.testing import APITestCase, APIViewTestCases
-User = get_user_model()
-
class AppTest(APITestCase):
@@ -113,9 +112,9 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
Webhook.objects.bulk_create(webhooks)
event_rules = (
- EventRule(name='EventRule 1', type_create=True, action_object=webhooks[0]),
- EventRule(name='EventRule 2', type_create=True, action_object=webhooks[1]),
- EventRule(name='EventRule 3', type_create=True, action_object=webhooks[2]),
+ EventRule(name='EventRule 1', event_types=[OBJECT_CREATED], action_object=webhooks[0]),
+ EventRule(name='EventRule 2', event_types=[OBJECT_CREATED], action_object=webhooks[1]),
+ EventRule(name='EventRule 3', event_types=[OBJECT_CREATED], action_object=webhooks[2]),
)
EventRule.objects.bulk_create(event_rules)
@@ -123,7 +122,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
{
'name': 'EventRule 4',
'object_types': ['dcim.device', 'dcim.devicetype'],
- 'type_create': True,
+ 'event_types': [OBJECT_CREATED],
'action_type': EventRuleActionChoices.WEBHOOK,
'action_object_type': 'extras.webhook',
'action_object_id': webhooks[3].pk,
@@ -131,7 +130,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
{
'name': 'EventRule 5',
'object_types': ['dcim.device', 'dcim.devicetype'],
- 'type_create': True,
+ 'event_types': [OBJECT_CREATED],
'action_type': EventRuleActionChoices.WEBHOOK,
'action_object_type': 'extras.webhook',
'action_object_id': webhooks[4].pk,
@@ -139,7 +138,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
{
'name': 'EventRule 6',
'object_types': ['dcim.device', 'dcim.devicetype'],
- 'type_create': True,
+ 'event_types': [OBJECT_CREATED],
'action_type': EventRuleActionChoices.WEBHOOK,
'action_object_type': 'extras.webhook',
'action_object_id': webhooks[5].pk,
@@ -244,9 +243,18 @@ class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):
@classmethod
def setUpTestData(cls):
choice_sets = (
- CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['1A', '1B', '1C', '1D', '1E']),
- CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['2A', '2B', '2C', '2D', '2E']),
- CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['3A', '3B', '3C', '3D', '3E']),
+ CustomFieldChoiceSet(
+ name='Choice Set 1',
+ extra_choices=[['1A', '1A'], ['1B', '1B'], ['1C', '1C'], ['1D', '1D'], ['1E', '1E']],
+ ),
+ CustomFieldChoiceSet(
+ name='Choice Set 2',
+ extra_choices=[['2A', '2A'], ['2B', '2B'], ['2C', '2C'], ['2D', '2D'], ['2E', '2E']],
+ ),
+ CustomFieldChoiceSet(
+ name='Choice Set 3',
+ extra_choices=[['3A', '3A'], ['3B', '3B'], ['3C', '3C'], ['3D', '3D'], ['3E', '3E']],
+ ),
)
CustomFieldChoiceSet.objects.bulk_create(choice_sets)
@@ -784,7 +792,6 @@ class ScriptTest(APITestCase):
super().setUp()
# Monkey-patch the Script model to return our TestScriptClass above
- from extras.api.views import ScriptViewSet
Script.python_class = self.python_class
def test_get_script(self):
@@ -890,3 +897,196 @@ class ObjectTypeTest(APITestCase):
url = reverse('extras-api:objecttype-detail', kwargs={'pk': object_type.pk})
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK)
+
+
+class SubscriptionTest(APIViewTestCases.APIViewTestCase):
+ model = Subscription
+ brief_fields = ['display', 'id', 'object_id', 'object_type', 'url', 'user']
+
+ @classmethod
+ def setUpTestData(cls):
+ users = (
+ User(username='User 1'),
+ User(username='User 2'),
+ User(username='User 3'),
+ User(username='User 4'),
+ )
+ User.objects.bulk_create(users)
+ 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)
+
+ subscriptions = (
+ Subscription(
+ object=sites[0],
+ user=users[0],
+ ),
+ Subscription(
+ object=sites[1],
+ user=users[1],
+ ),
+ Subscription(
+ object=sites[2],
+ user=users[2],
+ ),
+ )
+ Subscription.objects.bulk_create(subscriptions)
+
+ cls.create_data = [
+ {
+ 'object_type': 'dcim.site',
+ 'object_id': sites[0].pk,
+ 'user': users[3].pk,
+ },
+ {
+ 'object_type': 'dcim.site',
+ 'object_id': sites[1].pk,
+ 'user': users[3].pk,
+ },
+ {
+ 'object_type': 'dcim.site',
+ 'object_id': sites[2].pk,
+ 'user': users[3].pk,
+ },
+ ]
+
+
+class NotificationGroupTest(APIViewTestCases.APIViewTestCase):
+ model = NotificationGroup
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
+ create_data = [
+ {
+ 'object_types': ['dcim.site'],
+ 'name': 'Custom Link 4',
+ 'enabled': True,
+ 'link_text': 'Link 4',
+ 'link_url': 'http://example.com/?4',
+ },
+ {
+ 'object_types': ['dcim.site'],
+ 'name': 'Custom Link 5',
+ 'enabled': True,
+ 'link_text': 'Link 5',
+ 'link_url': 'http://example.com/?5',
+ },
+ {
+ 'object_types': ['dcim.site'],
+ 'name': 'Custom Link 6',
+ 'enabled': False,
+ 'link_text': 'Link 6',
+ 'link_url': 'http://example.com/?6',
+ },
+ ]
+ bulk_update_data = {
+ 'description': 'New description',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+ users = (
+ User(username='User 1'),
+ User(username='User 2'),
+ User(username='User 3'),
+ )
+ User.objects.bulk_create(users)
+ groups = (
+ Group(name='Group 1'),
+ Group(name='Group 2'),
+ Group(name='Group 3'),
+ )
+ Group.objects.bulk_create(groups)
+
+ notification_groups = (
+ NotificationGroup(name='Notification Group 1'),
+ NotificationGroup(name='Notification Group 2'),
+ NotificationGroup(name='Notification Group 3'),
+ )
+ NotificationGroup.objects.bulk_create(notification_groups)
+ for i, notification_group in enumerate(notification_groups):
+ notification_group.users.add(users[i])
+ notification_group.groups.add(groups[i])
+
+ cls.create_data = [
+ {
+ 'name': 'Notification Group 4',
+ 'description': 'Foo',
+ 'users': [users[0].pk],
+ 'groups': [groups[0].pk],
+ },
+ {
+ 'name': 'Notification Group 5',
+ 'description': 'Bar',
+ 'users': [users[1].pk],
+ 'groups': [groups[1].pk],
+ },
+ {
+ 'name': 'Notification Group 6',
+ 'description': 'Baz',
+ 'users': [users[2].pk],
+ 'groups': [groups[2].pk],
+ },
+ ]
+
+
+class NotificationTest(APIViewTestCases.APIViewTestCase):
+ model = Notification
+ brief_fields = ['display', 'event_type', 'id', 'object_id', 'object_type', 'read', 'url', 'user']
+
+ @classmethod
+ def setUpTestData(cls):
+ users = (
+ User(username='User 1'),
+ User(username='User 2'),
+ User(username='User 3'),
+ User(username='User 4'),
+ )
+ User.objects.bulk_create(users)
+ 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)
+
+ notifications = (
+ Notification(
+ object=sites[0],
+ event_type=OBJECT_CREATED,
+ user=users[0],
+ ),
+ Notification(
+ object=sites[1],
+ event_type=OBJECT_UPDATED,
+ user=users[1],
+ ),
+ Notification(
+ object=sites[2],
+ event_type=OBJECT_DELETED,
+ user=users[2],
+ ),
+ )
+ Notification.objects.bulk_create(notifications)
+
+ cls.create_data = [
+ {
+ 'object_type': 'dcim.site',
+ 'object_id': sites[0].pk,
+ 'user': users[3].pk,
+ 'event_type': OBJECT_CREATED,
+ },
+ {
+ 'object_type': 'dcim.site',
+ 'object_id': sites[1].pk,
+ 'user': users[3].pk,
+ 'event_type': OBJECT_UPDATED,
+ },
+ {
+ 'object_type': 'dcim.site',
+ 'object_id': sites[2].pk,
+ 'user': users[3].pk,
+ 'event_type': OBJECT_DELETED,
+ },
+ ]
diff --git a/netbox/extras/tests/test_conditions.py b/netbox/extras/tests/test_conditions.py
index dd528b918dc..dfe460f99d1 100644
--- a/netbox/extras/tests/test_conditions.py
+++ b/netbox/extras/tests/test_conditions.py
@@ -1,6 +1,7 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
+from core.events import *
from dcim.choices import SiteStatusChoices
from dcim.models import Site
from extras.conditions import Condition, ConditionSet
@@ -230,8 +231,7 @@ class ConditionSetTest(TestCase):
"""
event_rule = EventRule(
name='Event Rule 1',
- type_create=True,
- type_update=True,
+ event_types=[OBJECT_CREATED, OBJECT_UPDATED],
conditions={
'attr': 'status.value',
'value': 'active',
@@ -251,8 +251,7 @@ class ConditionSetTest(TestCase):
"""
event_rule = EventRule(
name='Event Rule 1',
- type_create=True,
- type_update=True,
+ event_types=[OBJECT_CREATED, OBJECT_UPDATED],
conditions={
"attr": "status.value",
"value": ["planned", "staging"],
@@ -273,8 +272,7 @@ class ConditionSetTest(TestCase):
"""
event_rule = EventRule(
name='Event Rule 1',
- type_create=True,
- type_update=True,
+ event_types=[OBJECT_CREATED, OBJECT_UPDATED],
conditions={
"attr": "status.value",
"value": ["planned", "staging"],
@@ -300,8 +298,7 @@ class ConditionSetTest(TestCase):
webhook = Webhook.objects.create(name='Webhook 100', payload_url='http://example.com/?1', http_method='POST')
form = EventRuleForm({
"name": "Event Rule 1",
- "type_create": True,
- "type_update": True,
+ "event_types": [OBJECT_CREATED, OBJECT_UPDATED],
"action_object_type": ct.pk,
"action_type": "webhook",
"action_choice": webhook.pk,
diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py
index 009ae8798a2..2bc9b5accef 100644
--- a/netbox/extras/tests/test_customfields.py
+++ b/netbox/extras/tests/test_customfields.py
@@ -343,6 +343,74 @@ class CustomFieldTest(TestCase):
instance.refresh_from_db()
self.assertIsNone(instance.custom_field_data.get(cf.name))
+ def test_remove_selected_choice(self):
+ """
+ Removing a ChoiceSet choice that is referenced by an object should raise
+ a ValidationError exception.
+ """
+ CHOICES = (
+ ('a', 'Option A'),
+ ('b', 'Option B'),
+ ('c', 'Option C'),
+ ('d', 'Option D'),
+ )
+
+ # Create a set of custom field choices
+ choice_set = CustomFieldChoiceSet.objects.create(
+ name='Custom Field Choice Set 1',
+ extra_choices=CHOICES
+ )
+
+ # Create a select custom field
+ cf = CustomField.objects.create(
+ name='select_field',
+ type=CustomFieldTypeChoices.TYPE_SELECT,
+ required=False,
+ choice_set=choice_set
+ )
+ cf.object_types.set([self.object_type])
+
+ # Create a multi-select custom field
+ cf_multiselect = CustomField.objects.create(
+ name='multiselect_field',
+ type=CustomFieldTypeChoices.TYPE_MULTISELECT,
+ required=False,
+ choice_set=choice_set
+ )
+ cf_multiselect.object_types.set([self.object_type])
+
+ # Assign a choice for both custom fields on an object
+ instance = Site.objects.first()
+ instance.custom_field_data[cf.name] = 'a'
+ instance.custom_field_data[cf_multiselect.name] = ['b', 'c']
+ instance.save()
+
+ # Attempting to delete a selected choice should fail
+ with self.assertRaises(ValidationError):
+ choice_set.extra_choices = (
+ ('b', 'Option B'),
+ ('c', 'Option C'),
+ ('d', 'Option D'),
+ )
+ choice_set.full_clean()
+
+ # Attempting to delete either of the multi-select choices should fail
+ with self.assertRaises(ValidationError):
+ choice_set.extra_choices = (
+ ('a', 'Option A'),
+ ('b', 'Option B'),
+ ('d', 'Option D'),
+ )
+ choice_set.full_clean()
+
+ # Removing a non-selected choice should succeed
+ choice_set.extra_choices = (
+ ('a', 'Option A'),
+ ('b', 'Option B'),
+ ('c', 'Option C'),
+ )
+ choice_set.full_clean()
+
def test_object_field(self):
value = VLAN.objects.create(name='VLAN 1', vid=1).pk
@@ -1143,7 +1211,7 @@ class CustomFieldAPITest(APITestCase):
def test_uniqueness_validation(self):
# Create a unique custom field
cf_text = CustomField.objects.get(name='text_field')
- cf_text.validation_unique = True
+ cf_text.unique = True
cf_text.save()
# Set a value on site 1
diff --git a/netbox/extras/tests/test_customvalidators.py b/netbox/extras/tests/test_customvalidators.py
index 217fddd184c..9f85b4913d0 100644
--- a/netbox/extras/tests/test_customvalidators.py
+++ b/netbox/extras/tests/test_customvalidators.py
@@ -14,7 +14,7 @@ from utilities.request import NetBoxFakeRequest
class MyValidator(CustomValidator):
- def validate(self, instance):
+ def validate(self, instance, request):
if instance.name != 'foo':
self.fail("Name must be foo!")
@@ -162,7 +162,7 @@ class CustomValidatorTest(TestCase):
Site(name='abcdef123', slug='abcdef123').clean()
@override_settings(CUSTOM_VALIDATORS={'dcim.site': [region_validator]})
- def test_valid(self):
+ def test_related_object(self):
region1 = Region(name='Foo', slug='foo')
region1.save()
region2 = Region(name='Bar', slug='bar')
diff --git a/netbox/extras/tests/test_event_rules.py b/netbox/extras/tests/test_event_rules.py
index 39b896616de..7f7b0b81f31 100644
--- a/netbox/extras/tests/test_event_rules.py
+++ b/netbox/extras/tests/test_event_rules.py
@@ -9,12 +9,12 @@ from django.urls import reverse
from requests import Session
from rest_framework import status
-from core.choices import ObjectChangeActionChoices
+from core.events import *
from core.models import ObjectType
from dcim.choices import SiteStatusChoices
from dcim.models import Site
from extras.choices import EventRuleActionChoices
-from extras.events import enqueue_object, flush_events, serialize_for_event
+from extras.events import enqueue_event, flush_events, serialize_for_event
from extras.models import EventRule, Tag, Webhook
from extras.webhooks import generate_signature, send_webhook
from netbox.context_managers import event_tracking
@@ -46,22 +46,22 @@ class EventRuleTest(APITestCase):
webhook_type = ObjectType.objects.get(app_label='extras', model='webhook')
event_rules = EventRule.objects.bulk_create((
EventRule(
- name='Webhook Event 1',
- type_create=True,
+ name='Event Rule 1',
+ event_types=[OBJECT_CREATED],
action_type=EventRuleActionChoices.WEBHOOK,
action_object_type=webhook_type,
action_object_id=webhooks[0].id
),
EventRule(
- name='Webhook Event 2',
- type_update=True,
+ name='Event Rule 2',
+ event_types=[OBJECT_UPDATED],
action_type=EventRuleActionChoices.WEBHOOK,
action_object_type=webhook_type,
action_object_id=webhooks[0].id
),
EventRule(
- name='Webhook Event 3',
- type_delete=True,
+ name='Event Rule 3',
+ event_types=[OBJECT_DELETED],
action_type=EventRuleActionChoices.WEBHOOK,
action_object_type=webhook_type,
action_object_id=webhooks[0].id
@@ -82,8 +82,7 @@ class EventRuleTest(APITestCase):
"""
event_rule = EventRule(
name='Event Rule 1',
- type_create=True,
- type_update=True,
+ event_types=[OBJECT_CREATED, OBJECT_UPDATED],
conditions={
'and': [
{
@@ -131,8 +130,8 @@ class EventRuleTest(APITestCase):
# Verify that a background task was queued for the new object
self.assertEqual(self.queue.count, 1)
job = self.queue.jobs[0]
- self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_create=True))
- self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_CREATE)
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 1'))
+ self.assertEqual(job.kwargs['event_type'], OBJECT_CREATED)
self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], response.data['id'])
self.assertEqual(len(job.kwargs['data']['tags']), len(response.data['tags']))
@@ -181,8 +180,8 @@ class EventRuleTest(APITestCase):
# Verify that a background task was queued for each new object
self.assertEqual(self.queue.count, 3)
for i, job in enumerate(self.queue.jobs):
- self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_create=True))
- self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_CREATE)
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 1'))
+ self.assertEqual(job.kwargs['event_type'], OBJECT_CREATED)
self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], response.data[i]['id'])
self.assertEqual(len(job.kwargs['data']['tags']), len(response.data[i]['tags']))
@@ -212,8 +211,8 @@ class EventRuleTest(APITestCase):
# Verify that a background task was queued for the updated object
self.assertEqual(self.queue.count, 1)
job = self.queue.jobs[0]
- self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_update=True))
- self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_UPDATE)
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 2'))
+ self.assertEqual(job.kwargs['event_type'], OBJECT_UPDATED)
self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], site.pk)
self.assertEqual(len(job.kwargs['data']['tags']), len(response.data['tags']))
@@ -268,8 +267,8 @@ class EventRuleTest(APITestCase):
# Verify that a background task was queued for each updated object
self.assertEqual(self.queue.count, 3)
for i, job in enumerate(self.queue.jobs):
- self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_update=True))
- self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_UPDATE)
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 2'))
+ self.assertEqual(job.kwargs['event_type'], OBJECT_UPDATED)
self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], data[i]['id'])
self.assertEqual(len(job.kwargs['data']['tags']), len(response.data[i]['tags']))
@@ -294,8 +293,8 @@ class EventRuleTest(APITestCase):
# Verify that a task was queued for the deleted object
self.assertEqual(self.queue.count, 1)
job = self.queue.jobs[0]
- self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_delete=True))
- self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_DELETE)
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 3'))
+ self.assertEqual(job.kwargs['event_type'], OBJECT_DELETED)
self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], site.pk)
self.assertEqual(job.kwargs['snapshots']['prechange']['name'], 'Site 1')
@@ -327,8 +326,8 @@ class EventRuleTest(APITestCase):
# Verify that a background task was queued for each deleted object
self.assertEqual(self.queue.count, 3)
for i, job in enumerate(self.queue.jobs):
- self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_delete=True))
- self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_DELETE)
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(name='Event Rule 3'))
+ self.assertEqual(job.kwargs['event_type'], OBJECT_DELETED)
self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], sites[i].pk)
self.assertEqual(job.kwargs['snapshots']['prechange']['name'], sites[i].name)
@@ -342,7 +341,7 @@ class EventRuleTest(APITestCase):
A dummy implementation of Session.send() to be used for testing.
Always returns a 200 HTTP response.
"""
- event = EventRule.objects.get(type_create=True)
+ event = EventRule.objects.get(name='Event Rule 1')
webhook = event.action_object
signature = generate_signature(request.body, webhook.secret)
@@ -365,12 +364,12 @@ class EventRuleTest(APITestCase):
# Enqueue a webhook for processing
webhooks_queue = {}
site = Site.objects.create(name='Site 1', slug='site-1')
- enqueue_object(
+ enqueue_event(
webhooks_queue,
instance=site,
user=self.user,
request_id=request_id,
- action=ObjectChangeActionChoices.ACTION_CREATE
+ event_type=OBJECT_CREATED
)
flush_events(list(webhooks_queue.values()))
@@ -378,7 +377,7 @@ class EventRuleTest(APITestCase):
job = self.queue.jobs[0]
# Patch the Session object with our dummy_send() method, then process the webhook for sending
- with patch.object(Session, 'send', dummy_send) as mock_send:
+ with patch.object(Session, 'send', dummy_send):
send_webhook(**job.kwargs)
def test_duplicate_triggers(self):
@@ -391,13 +390,36 @@ class EventRuleTest(APITestCase):
request.id = uuid.uuid4()
request.user = self.user
- self.assertEqual(self.queue.count, 0, msg="Unexpected jobs found in queue")
-
+ # Test create & update
with event_tracking(request):
site = Site(name='Site 1', slug='site-1')
site.save()
-
- # Save the site a second time
+ site.description = 'foo'
site.save()
-
self.assertEqual(self.queue.count, 1, msg="Duplicate jobs found in queue")
+ job = self.queue.get_jobs()[0]
+ self.assertEqual(job.kwargs['event_type'], OBJECT_CREATED)
+ self.queue.empty()
+
+ # Test multiple updates
+ site = Site.objects.create(name='Site 2', slug='site-2')
+ with event_tracking(request):
+ site.description = 'foo'
+ site.save()
+ site.description = 'bar'
+ site.save()
+ self.assertEqual(self.queue.count, 1, msg="Duplicate jobs found in queue")
+ job = self.queue.get_jobs()[0]
+ self.assertEqual(job.kwargs['event_type'], OBJECT_UPDATED)
+ self.queue.empty()
+
+ # Test update & delete
+ site = Site.objects.create(name='Site 3', slug='site-3')
+ with event_tracking(request):
+ site.description = 'foo'
+ site.save()
+ site.delete()
+ self.assertEqual(self.queue.count, 1, msg="Duplicate jobs found in queue")
+ job = self.queue.get_jobs()[0]
+ self.assertEqual(job.kwargs['event_type'], OBJECT_DELETED)
+ self.queue.empty()
diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py
index 5c737f7cfe5..9048d5fd979 100644
--- a/netbox/extras/tests/test_filtersets.py
+++ b/netbox/extras/tests/test_filtersets.py
@@ -1,12 +1,12 @@
import uuid
from datetime import datetime, timezone
-from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from circuits.models import Provider
from core.choices import ManagedFileRootPathChoices, ObjectChangeActionChoices
+from core.events import *
from core.models import ObjectChange, ObjectType
from dcim.filtersets import SiteFilterSet
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
@@ -15,17 +15,15 @@ from extras.choices import *
from extras.filtersets import *
from extras.models import *
from tenancy.models import Tenant, TenantGroup
+from users.models import Group, User
from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests, create_tags
from virtualization.models import Cluster, ClusterGroup, ClusterType
-User = get_user_model()
-
-
class CustomFieldTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CustomField.objects.all()
filterset = CustomFieldFilterSet
- ignore_fields = ('default',)
+ ignore_fields = ('default', 'related_object_filter')
@classmethod
def setUpTestData(cls):
@@ -254,7 +252,7 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
class EventRuleTestCase(TestCase, BaseFilterSetTests):
queryset = EventRule.objects.all()
filterset = EventRuleFilterSet
- ignore_fields = ('action_data', 'conditions')
+ ignore_fields = ('action_data', 'conditions', 'event_types')
@classmethod
def setUpTestData(cls):
@@ -295,11 +293,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
name='Event Rule 1',
action_object=webhooks[0],
enabled=True,
- type_create=True,
- type_update=False,
- type_delete=False,
- type_job_start=False,
- type_job_end=False,
+ event_types=[OBJECT_CREATED],
action_type=EventRuleActionChoices.WEBHOOK,
description='foobar1'
),
@@ -307,11 +301,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
name='Event Rule 2',
action_object=webhooks[1],
enabled=True,
- type_create=False,
- type_update=True,
- type_delete=False,
- type_job_start=False,
- type_job_end=False,
+ event_types=[OBJECT_UPDATED],
action_type=EventRuleActionChoices.WEBHOOK,
description='foobar2'
),
@@ -319,11 +309,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
name='Event Rule 3',
action_object=webhooks[2],
enabled=False,
- type_create=False,
- type_update=False,
- type_delete=True,
- type_job_start=False,
- type_job_end=False,
+ event_types=[OBJECT_DELETED],
action_type=EventRuleActionChoices.WEBHOOK,
description='foobar3'
),
@@ -331,22 +317,14 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
name='Event Rule 4',
action_object=scripts[0],
enabled=False,
- type_create=False,
- type_update=False,
- type_delete=False,
- type_job_start=True,
- type_job_end=False,
+ event_types=[JOB_STARTED],
action_type=EventRuleActionChoices.SCRIPT,
),
EventRule(
name='Event Rule 5',
action_object=scripts[1],
enabled=False,
- type_create=False,
- type_update=False,
- type_delete=False,
- type_job_start=False,
- type_job_end=True,
+ event_types=[JOB_COMPLETED],
action_type=EventRuleActionChoices.SCRIPT,
),
)
@@ -387,25 +365,9 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
params = {'enabled': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
- def test_type_create(self):
- params = {'type_create': True}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
-
- def test_type_update(self):
- params = {'type_update': True}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
-
- def test_type_delete(self):
- params = {'type_delete': True}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
-
- def test_type_job_start(self):
- params = {'type_job_start': True}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
-
- def test_type_job_end(self):
- params = {'type_job_end': True}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ def test_event_type(self):
+ params = {'event_type': [OBJECT_CREATED, OBJECT_UPDATED]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class CustomLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
@@ -1136,6 +1098,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
'asnrange',
'cable',
'circuit',
+ 'circuitgroup',
+ 'circuitgroupassignment',
'circuittermination',
'circuittype',
'cluster',
@@ -1187,6 +1151,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
'rack',
'rackreservation',
'rackrole',
+ 'racktype',
'rearport',
'region',
'rir',
@@ -1370,3 +1335,65 @@ class ChangeLoggedFilterSetTestCase(TestCase):
params = {'modified_by_request': self.create_update_request_id}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.queryset.count(), 4)
+
+
+class NotificationGroupTestCase(TestCase, BaseFilterSetTests):
+ queryset = NotificationGroup.objects.all()
+ filterset = NotificationGroupFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ users = (
+ User(username='User 1'),
+ User(username='User 2'),
+ User(username='User 3'),
+ )
+ User.objects.bulk_create(users)
+
+ groups = (
+ Group(name='Group 1'),
+ Group(name='Group 2'),
+ Group(name='Group 3'),
+ )
+ Group.objects.bulk_create(groups)
+
+ 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)
+
+ tenants = (
+ Tenant(name='Tenant 1', slug='tenant-1'),
+ Tenant(name='Tenant 2', slug='tenant-2'),
+ Tenant(name='Tenant 3', slug='tenant-3'),
+ )
+ Tenant.objects.bulk_create(tenants)
+
+ notification_groups = (
+ NotificationGroup(name='Notification Group 1'),
+ NotificationGroup(name='Notification Group 2'),
+ NotificationGroup(name='Notification Group 3'),
+ )
+ NotificationGroup.objects.bulk_create(notification_groups)
+ notification_groups[0].users.add(users[0])
+ notification_groups[1].users.add(users[1])
+ notification_groups[2].users.add(users[2])
+ notification_groups[0].groups.add(groups[0])
+ notification_groups[1].groups.add(groups[1])
+ notification_groups[2].groups.add(groups[2])
+
+ def test_user(self):
+ users = User.objects.filter(username__startswith='User')
+ params = {'user': [users[0].username, users[1].username]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'user_id': [users[0].pk, users[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_group(self):
+ groups = Group.objects.all()
+ params = {'group': [groups[0].name, groups[1].name]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'group_id': [groups[0].pk, groups[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py
index c92a1bc548a..188a06a3f29 100644
--- a/netbox/extras/tests/test_models.py
+++ b/netbox/extras/tests/test_models.py
@@ -49,11 +49,11 @@ class ConfigContextTest(TestCase):
sitegroup = SiteGroup.objects.create(name='Site Group')
site = Site.objects.create(name='Site 1', slug='site-1', region=region, group=sitegroup)
location = Location.objects.create(name='Location 1', slug='location-1', site=site)
- platform = Platform.objects.create(name='Platform')
+ Platform.objects.create(name='Platform')
tenantgroup = TenantGroup.objects.create(name='Tenant Group')
- tenant = Tenant.objects.create(name='Tenant', group=tenantgroup)
- tag1 = Tag.objects.create(name='Tag', slug='tag')
- tag2 = Tag.objects.create(name='Tag2', slug='tag2')
+ Tenant.objects.create(name='Tenant', group=tenantgroup)
+ Tag.objects.create(name='Tag', slug='tag')
+ Tag.objects.create(name='Tag2', slug='tag2')
Device.objects.create(
name='Device 1',
diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py
index cbede195b4c..5d82fae4ca8 100644
--- a/netbox/extras/tests/test_views.py
+++ b/netbox/extras/tests/test_views.py
@@ -1,15 +1,14 @@
-from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
+from core.events import *
from core.models import ObjectType
from dcim.models import DeviceType, Manufacturer, Site
from extras.choices import *
from extras.models import *
+from users.models import Group, User
from utilities.testing import ViewTestCases, TestCase
-User = get_user_model()
-
class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = CustomField
@@ -396,9 +395,9 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
site_type = ObjectType.objects.get_for_model(Site)
event_rules = (
- EventRule(name='EventRule 1', type_create=True, action_object=webhooks[0]),
- EventRule(name='EventRule 2', type_create=True, action_object=webhooks[1]),
- EventRule(name='EventRule 3', type_create=True, action_object=webhooks[2]),
+ EventRule(name='EventRule 1', event_types=[OBJECT_CREATED], action_object=webhooks[0]),
+ EventRule(name='EventRule 2', event_types=[OBJECT_CREATED], action_object=webhooks[1]),
+ EventRule(name='EventRule 3', event_types=[OBJECT_CREATED], action_object=webhooks[2]),
)
for event in event_rules:
event.save()
@@ -408,9 +407,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.form_data = {
'name': 'Event X',
'object_types': [site_type.pk],
- 'type_create': False,
- 'type_update': True,
- 'type_delete': True,
+ 'event_types': [OBJECT_UPDATED, OBJECT_DELETED],
'conditions': None,
'action_type': 'webhook',
'action_object_type': webhook_ct.pk,
@@ -420,8 +417,8 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
- "name,object_types,type_create,action_type,action_object",
- "Webhook 4,dcim.site,True,webhook,Webhook 1",
+ 'name,object_types,event_types,action_type,action_object',
+ f'Webhook 4,dcim.site,"{OBJECT_CREATED},{OBJECT_UPDATED}",webhook,Webhook 1',
)
cls.csv_update_data = (
@@ -432,7 +429,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
cls.bulk_edit_data = {
- 'type_update': True,
+ 'description': 'New description',
}
@@ -620,3 +617,166 @@ class CustomLinkTest(TestCase):
response = self.client.get(site.get_absolute_url(), follow=True)
self.assertEqual(response.status_code, 200)
self.assertIn(f'FOO {site.name} BAR', str(response.content))
+
+
+class SubscriptionTestCase(
+ ViewTestCases.CreateObjectViewTestCase,
+ ViewTestCases.DeleteObjectViewTestCase,
+ ViewTestCases.ListObjectsViewTestCase,
+ ViewTestCases.BulkDeleteObjectsViewTestCase
+):
+ model = Subscription
+
+ @classmethod
+ def setUpTestData(cls):
+ site_ct = ContentType.objects.get_for_model(Site)
+ 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.objects.bulk_create(sites)
+
+ cls.form_data = {
+ 'object_type': site_ct.pk,
+ 'object_id': sites[3].pk,
+ }
+
+ def setUp(self):
+ super().setUp()
+
+ sites = Site.objects.all()
+ user = self.user
+
+ subscriptions = (
+ Subscription(object=sites[0], user=user),
+ Subscription(object=sites[1], user=user),
+ Subscription(object=sites[2], user=user),
+ )
+ Subscription.objects.bulk_create(subscriptions)
+
+ def _get_url(self, action, instance=None):
+ if action == 'list':
+ return reverse('account:subscriptions')
+ return super()._get_url(action, instance)
+
+ def test_list_objects_anonymous(self):
+ self.client.logout()
+ url = reverse('account:subscriptions')
+ login_url = reverse('login')
+ self.assertRedirects(self.client.get(url), f'{login_url}?next={url}')
+
+ def test_list_objects_with_permission(self):
+ return
+
+ def test_list_objects_with_constrained_permission(self):
+ return
+
+
+class NotificationGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = NotificationGroup
+
+ @classmethod
+ def setUpTestData(cls):
+ users = (
+ User(username='User 1'),
+ User(username='User 2'),
+ User(username='User 3'),
+ )
+ User.objects.bulk_create(users)
+ groups = (
+ Group(name='Group 1'),
+ Group(name='Group 2'),
+ Group(name='Group 3'),
+ )
+ Group.objects.bulk_create(groups)
+
+ notification_groups = (
+ NotificationGroup(name='Notification Group 1'),
+ NotificationGroup(name='Notification Group 2'),
+ NotificationGroup(name='Notification Group 3'),
+ )
+ NotificationGroup.objects.bulk_create(notification_groups)
+ for i, notification_group in enumerate(notification_groups):
+ notification_group.users.add(users[i])
+ notification_group.groups.add(groups[i])
+
+ cls.form_data = {
+ 'name': 'Notification Group X',
+ 'description': 'Blah',
+ 'users': [users[0].pk, users[1].pk],
+ 'groups': [groups[0].pk, groups[1].pk],
+ }
+
+ cls.csv_data = (
+ 'name,description,users,groups',
+ 'Notification Group 4,Foo,"User 1,User 2","Group 1,Group 2"',
+ 'Notification Group 5,Bar,"User 1,User 2","Group 1,Group 2"',
+ 'Notification Group 6,Baz,"User 1,User 2","Group 1,Group 2"',
+ )
+
+ cls.csv_update_data = (
+ "id,name",
+ f"{notification_groups[0].pk},Notification Group 7",
+ f"{notification_groups[1].pk},Notification Group 8",
+ f"{notification_groups[2].pk},Notification Group 9",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ }
+
+
+class NotificationTestCase(
+ ViewTestCases.DeleteObjectViewTestCase,
+ ViewTestCases.ListObjectsViewTestCase,
+ ViewTestCases.BulkDeleteObjectsViewTestCase
+):
+ model = Notification
+
+ @classmethod
+ def setUpTestData(cls):
+ site_ct = ContentType.objects.get_for_model(Site)
+ 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.objects.bulk_create(sites)
+
+ cls.form_data = {
+ 'object_type': site_ct.pk,
+ 'object_id': sites[3].pk,
+ }
+
+ def setUp(self):
+ super().setUp()
+
+ sites = Site.objects.all()
+ user = self.user
+
+ notifications = (
+ Notification(object=sites[0], user=user),
+ Notification(object=sites[1], user=user),
+ Notification(object=sites[2], user=user),
+ )
+ Notification.objects.bulk_create(notifications)
+
+ def _get_url(self, action, instance=None):
+ if action == 'list':
+ return reverse('account:notifications')
+ return super()._get_url(action, instance)
+
+ def test_list_objects_anonymous(self):
+ self.client.logout()
+ url = reverse('account:notifications')
+ login_url = reverse('login')
+ self.assertRedirects(self.client.get(url), f'{login_url}?next={url}')
+
+ def test_list_objects_with_permission(self):
+ return
+
+ def test_list_objects_with_constrained_permission(self):
+ return
diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py
index f2e11e71ee8..b13af1db9c3 100644
--- a/netbox/extras/urls.py
+++ b/netbox/extras/urls.py
@@ -53,6 +53,24 @@ urlpatterns = [
path('bookmarks/delete/', views.BookmarkBulkDeleteView.as_view(), name='bookmark_bulk_delete'),
path('bookmarks//', include(get_model_urls('extras', 'bookmark'))),
+ # Notification groups
+ path('notification-groups/', views.NotificationGroupListView.as_view(), name='notificationgroup_list'),
+ path('notification-groups/add/', views.NotificationGroupEditView.as_view(), name='notificationgroup_add'),
+ path('notification-groups/import/', views.NotificationGroupBulkImportView.as_view(), name='notificationgroup_import'),
+ path('notification-groups/edit/', views.NotificationGroupBulkEditView.as_view(), name='notificationgroup_bulk_edit'),
+ path('notification-groups/delete/', views.NotificationGroupBulkDeleteView.as_view(), name='notificationgroup_bulk_delete'),
+ path('notification-groups//', include(get_model_urls('extras', 'notificationgroup'))),
+
+ # Notifications
+ path('notifications/', views.NotificationsView.as_view(), name='notifications'),
+ path('notifications/delete/', views.NotificationBulkDeleteView.as_view(), name='notification_bulk_delete'),
+ path('notifications//', include(get_model_urls('extras', 'notification'))),
+
+ # Subscriptions
+ path('subscriptions/add/', views.SubscriptionCreateView.as_view(), name='subscription_add'),
+ path('subscriptions/delete/', views.SubscriptionBulkDeleteView.as_view(), name='subscription_bulk_delete'),
+ path('subscriptions//', include(get_model_urls('extras', 'subscription'))),
+
# Webhooks
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),
@@ -121,11 +139,6 @@ urlpatterns = [
path('scripts//jobs/', views.ScriptJobsView.as_view(), name='script_jobs'),
path('script-modules//', include(get_model_urls('extras', 'scriptmodule'))),
- # Redirects for legacy script URLs
- # TODO: Remove in NetBox v4.1
- path('scripts///', views.LegacyScriptRedirectView.as_view()),
- path('scripts////', views.LegacyScriptRedirectView.as_view()),
-
# Markdown
path('render/markdown/', views.RenderMarkdownView.as_view(), name="render_markdown"),
]
diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py
index e67b9b50c79..28d2e13f07b 100644
--- a/netbox/extras/utils.py
+++ b/netbox/extras/utils.py
@@ -1,5 +1,19 @@
+import importlib
+
+from django.core.exceptions import ImproperlyConfigured
from taggit.managers import _TaggableManager
+from netbox.context import current_request
+from .validators import CustomValidator
+
+__all__ = (
+ 'image_upload',
+ 'is_report',
+ 'is_script',
+ 'is_taggable',
+ 'run_validators',
+)
+
def is_taggable(obj):
"""
@@ -48,3 +62,25 @@ def is_report(obj):
return issubclass(obj, Report) and obj != Report
except TypeError:
return False
+
+
+def run_validators(instance, validators):
+ """
+ Run the provided iterable of CustomValidators for the instance.
+ """
+ request = current_request.get()
+ for validator in validators:
+
+ # Loading a validator class by dotted path
+ if type(validator) is str:
+ module, cls = validator.rsplit('.', 1)
+ validator = getattr(importlib.import_module(module), cls)()
+
+ # Constructing a new instance on the fly from a ruleset
+ elif type(validator) is dict:
+ validator = CustomValidator(validator)
+
+ elif not issubclass(validator.__class__, CustomValidator):
+ raise ImproperlyConfigured(f"Invalid value for custom validator: {validator}")
+
+ validator(instance, request)
diff --git a/netbox/extras/validators.py b/netbox/extras/validators.py
index 082f87d642d..306acf01bb8 100644
--- a/netbox/extras/validators.py
+++ b/netbox/extras/validators.py
@@ -1,4 +1,3 @@
-import inspect
import operator
from django.core import validators
@@ -123,13 +122,7 @@ class CustomValidator:
)
# Execute custom validation logic (if any)
- # TODO: Remove in v4.1
- # Inspect the validate() method, which may have been overridden, to determine
- # whether we should pass the request (maintains backward compatibility for pre-v4.0)
- if 'request' in inspect.signature(self.validate).parameters:
- self.validate(instance, request)
- else:
- self.validate(instance)
+ self.validate(instance, request)
@staticmethod
def _get_request_attr(request, name):
diff --git a/netbox/extras/views.py b/netbox/extras/views.py
index 4d33324055f..3218422605c 100644
--- a/netbox/extras/views.py
+++ b/netbox/extras/views.py
@@ -6,6 +6,8 @@ from django.db.models import Count, Q
from django.http import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
+from django.utils import timezone
+from django.utils.module_loading import import_string
from django.utils.translation import gettext as _
from django.views.generic import View
@@ -33,7 +35,6 @@ from virtualization.models import VirtualMachine
from . import filtersets, forms, tables
from .constants import LOG_LEVEL_RANK
from .models import *
-from .scripts import run_script
from .tables import ReportResultsTable, ScriptResultsTable
@@ -356,6 +357,144 @@ class BookmarkBulkDeleteView(generic.BulkDeleteView):
return Bookmark.objects.filter(user=request.user)
+#
+# Notification groups
+#
+
+class NotificationGroupListView(generic.ObjectListView):
+ queryset = NotificationGroup.objects.all()
+ filterset = filtersets.NotificationGroupFilterSet
+ filterset_form = forms.NotificationGroupFilterForm
+ table = tables.NotificationGroupTable
+
+
+@register_model_view(NotificationGroup)
+class NotificationGroupView(generic.ObjectView):
+ queryset = NotificationGroup.objects.all()
+
+
+@register_model_view(NotificationGroup, 'edit')
+class NotificationGroupEditView(generic.ObjectEditView):
+ queryset = NotificationGroup.objects.all()
+ form = forms.NotificationGroupForm
+
+
+@register_model_view(NotificationGroup, 'delete')
+class NotificationGroupDeleteView(generic.ObjectDeleteView):
+ queryset = NotificationGroup.objects.all()
+
+
+class NotificationGroupBulkImportView(generic.BulkImportView):
+ queryset = NotificationGroup.objects.all()
+ model_form = forms.NotificationGroupImportForm
+
+
+class NotificationGroupBulkEditView(generic.BulkEditView):
+ queryset = NotificationGroup.objects.all()
+ filterset = filtersets.NotificationGroupFilterSet
+ table = tables.NotificationGroupTable
+ form = forms.NotificationGroupBulkEditForm
+
+
+class NotificationGroupBulkDeleteView(generic.BulkDeleteView):
+ queryset = NotificationGroup.objects.all()
+ filterset = filtersets.NotificationGroupFilterSet
+ table = tables.NotificationGroupTable
+
+
+#
+# Notifications
+#
+
+class NotificationsView(LoginRequiredMixin, View):
+ """
+ HTMX-only user-specific notifications list.
+ """
+ def get(self, request):
+ return render(request, 'htmx/notifications.html', {
+ 'notifications': request.user.notifications.unread(),
+ 'total_count': request.user.notifications.count(),
+ })
+
+
+@register_model_view(Notification, 'read')
+class NotificationReadView(LoginRequiredMixin, View):
+ """
+ Mark the Notification read and redirect the user to its attached object.
+ """
+ def get(self, request, pk):
+ # Mark the Notification as read
+ notification = get_object_or_404(request.user.notifications, pk=pk)
+ notification.read = timezone.now()
+ notification.save()
+
+ # Redirect to the object if it has a URL (deleted objects will not)
+ if hasattr(notification.object, 'get_absolute_url'):
+ return redirect(notification.object.get_absolute_url())
+
+ return redirect('account:notifications')
+
+
+@register_model_view(Notification, 'dismiss')
+class NotificationDismissView(LoginRequiredMixin, View):
+ """
+ A convenience view which allows deleting notifications with one click.
+ """
+ def get(self, request, pk):
+ notification = get_object_or_404(request.user.notifications, pk=pk)
+ notification.delete()
+
+ if htmx_partial(request):
+ return render(request, 'htmx/notifications.html', {
+ 'notifications': request.user.notifications.unread()[:10],
+ })
+
+ return redirect('account:notifications')
+
+
+@register_model_view(Notification, 'delete')
+class NotificationDeleteView(generic.ObjectDeleteView):
+
+ def get_queryset(self, request):
+ return Notification.objects.filter(user=request.user)
+
+
+class NotificationBulkDeleteView(generic.BulkDeleteView):
+ table = tables.NotificationTable
+
+ def get_queryset(self, request):
+ return Notification.objects.filter(user=request.user)
+
+
+#
+# Subscriptions
+#
+
+class SubscriptionCreateView(generic.ObjectEditView):
+ form = forms.SubscriptionForm
+
+ def get_queryset(self, request):
+ return Subscription.objects.filter(user=request.user)
+
+ def alter_object(self, obj, request, url_args, url_kwargs):
+ obj.user = request.user
+ return obj
+
+
+@register_model_view(Subscription, 'delete')
+class SubscriptionDeleteView(generic.ObjectDeleteView):
+
+ def get_queryset(self, request):
+ return Subscription.objects.filter(user=request.user)
+
+
+class SubscriptionBulkDeleteView(generic.BulkDeleteView):
+ table = tables.SubscriptionTable
+
+ def get_queryset(self, request):
+ return Subscription.objects.filter(user=request.user)
+
+
#
# Webhooks
#
@@ -1032,10 +1171,9 @@ class ScriptView(BaseScriptView):
if not get_workers_for_queue('default'):
messages.error(request, _("Unable to run script: RQ worker process not running."))
elif form.is_valid():
- job = Job.enqueue(
- run_script,
+ ScriptJob = import_string("extras.jobs.ScriptJob")
+ job = ScriptJob.enqueue(
instance=script,
- name=script_class.class_name,
user=request.user,
schedule_at=form.cleaned_data.pop('_schedule_at'),
interval=form.cleaned_data.pop('_interval'),
@@ -1091,25 +1229,6 @@ class ScriptJobsView(BaseScriptView):
})
-class LegacyScriptRedirectView(ContentTypePermissionRequiredMixin, View):
- """
- Redirect legacy (pre-v4.0) script URLs. Examples:
- /extras/scripts/// --> /extras/scripts//
- /extras/scripts///source/ --> /extras/scripts//source/
- /extras/scripts///jobs/ --> /extras/scripts//jobs/
- """
- def get_required_permission(self):
- return 'extras.view_script'
-
- def get(self, request, module, name, path=''):
- module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__regex=f"^{module}\\.")
- script = get_object_or_404(Script.objects.all(), module=module, name=name)
-
- url = reverse('extras:script', kwargs={'pk': script.pk})
-
- return redirect(f'{url}{path}')
-
-
class ScriptResultView(TableMixin, generic.ObjectView):
queryset = Job.objects.all()
@@ -1122,7 +1241,10 @@ class ScriptResultView(TableMixin, generic.ObjectView):
table = None
index = 0
- log_threshold = LOG_LEVEL_RANK.get(request.GET.get('log_threshold', LogLevelChoices.LOG_DEFAULT))
+ try:
+ log_threshold = LOG_LEVEL_RANK[request.GET.get('log_threshold', LogLevelChoices.LOG_DEBUG)]
+ except KeyError:
+ log_threshold = LOG_LEVEL_RANK[LogLevelChoices.LOG_DEBUG]
if job.data:
if 'log' in job.data:
@@ -1179,12 +1301,16 @@ class ScriptResultView(TableMixin, generic.ObjectView):
if job.completed:
table = self.get_table(job, request, bulk_actions=False)
+ log_threshold = request.GET.get('log_threshold', LogLevelChoices.LOG_DEBUG)
+ if log_threshold not in LOG_LEVEL_RANK:
+ log_threshold = LogLevelChoices.LOG_DEBUG
+
context = {
'script': job.object,
'job': job,
'table': table,
'log_levels': dict(LogLevelChoices),
- 'log_threshold': request.GET.get('log_threshold', LogLevelChoices.LOG_DEFAULT)
+ 'log_threshold': log_threshold,
}
if job.data and 'log' in job.data:
@@ -1199,6 +1325,9 @@ class ScriptResultView(TableMixin, generic.ObjectView):
# If this is an HTMX request, return only the result HTML
if htmx_partial(request):
+ if request.GET.get('log'):
+ # If log=True, render only the log table
+ return render(request, 'htmx/table.html', context)
response = render(request, 'extras/htmx/script_result.html', context)
if job.completed or not job.started:
response.status_code = 286
diff --git a/netbox/extras/webhooks.py b/netbox/extras/webhooks.py
index 53ec161d783..889c97ac27b 100644
--- a/netbox/extras/webhooks.py
+++ b/netbox/extras/webhooks.py
@@ -25,7 +25,7 @@ def generate_signature(request_body, secret):
@job('default')
-def send_webhook(event_rule, model_name, event, data, timestamp, username, request_id=None, snapshots=None):
+def send_webhook(event_rule, model_name, event_type, data, timestamp, username, request_id=None, snapshots=None):
"""
Make a POST request to the defined Webhook
"""
@@ -33,7 +33,7 @@ def send_webhook(event_rule, model_name, event, data, timestamp, username, reque
# Prepare context data for headers & body templates
context = {
- 'event': WEBHOOK_EVENT_TYPES[event],
+ 'event': WEBHOOK_EVENT_TYPES.get(event_type, event_type),
'timestamp': timestamp,
'model': model_name,
'username': username,
diff --git a/netbox/ipam/api/nested_serializers.py b/netbox/ipam/api/nested_serializers.py
index 95b5ab11df0..8b10f29dfa1 100644
--- a/netbox/ipam/api/nested_serializers.py
+++ b/netbox/ipam/api/nested_serializers.py
@@ -1,3 +1,5 @@
+import warnings
+
from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
@@ -5,6 +7,7 @@ from ipam import models
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
from .field_serializers import IPAddressField
+from .serializers_.nested import NestedIPAddressSerializer
__all__ = [
'NestedAggregateSerializer',
@@ -25,6 +28,12 @@ __all__ = [
'NestedVRFSerializer',
]
+# TODO: Remove in v4.2
+warnings.warn(
+ "Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
+ DeprecationWarning
+)
+
#
# ASN ranges
@@ -177,19 +186,6 @@ class NestedIPRangeSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address']
-#
-# IP addresses
-#
-
-class NestedIPAddressSerializer(WritableNestedSerializer):
- family = serializers.IntegerField(read_only=True)
- address = IPAddressField()
-
- class Meta:
- model = models.IPAddress
- fields = ['id', 'url', 'display_url', 'display', 'family', 'address']
-
-
#
# Services
#
diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py
index 1f5f210288b..0bdfc538533 100644
--- a/netbox/ipam/api/serializers.py
+++ b/netbox/ipam/api/serializers.py
@@ -5,4 +5,3 @@ from .serializers_.vlans import *
from .serializers_.ip import *
from .serializers_.fhrpgroups import *
from .serializers_.services import *
-from .nested_serializers import *
diff --git a/netbox/ipam/api/serializers_/ip.py b/netbox/ipam/api/serializers_/ip.py
index 73fd0906442..535ffcec1bb 100644
--- a/netbox/ipam/api/serializers_/ip.py
+++ b/netbox/ipam/api/serializers_/ip.py
@@ -11,11 +11,11 @@ from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model
from .asns import RIRSerializer
+from .nested import NestedIPAddressSerializer
from .roles import RoleSerializer
from .vlans import VLANSerializer
from .vrfs import VRFSerializer
from ..field_serializers import IPAddressField, IPNetworkField
-from ..nested_serializers import *
__all__ = (
'AggregateSerializer',
diff --git a/netbox/ipam/api/serializers_/nested.py b/netbox/ipam/api/serializers_/nested.py
new file mode 100644
index 00000000000..5297565bb9b
--- /dev/null
+++ b/netbox/ipam/api/serializers_/nested.py
@@ -0,0 +1,18 @@
+from rest_framework import serializers
+
+from ipam import models
+from netbox.api.serializers import WritableNestedSerializer
+from ..field_serializers import IPAddressField
+
+__all__ = (
+ 'NestedIPAddressSerializer',
+)
+
+
+class NestedIPAddressSerializer(WritableNestedSerializer):
+ family = serializers.IntegerField(read_only=True)
+ address = IPAddressField()
+
+ class Meta:
+ model = models.IPAddress
+ fields = ['id', 'url', 'display_url', 'display', 'family', 'address']
diff --git a/netbox/ipam/api/serializers_/roles.py b/netbox/ipam/api/serializers_/roles.py
index 9a97a8570e4..99fd6f4705a 100644
--- a/netbox/ipam/api/serializers_/roles.py
+++ b/netbox/ipam/api/serializers_/roles.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from ipam.models import Role
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
diff --git a/netbox/ipam/api/serializers_/services.py b/netbox/ipam/api/serializers_/services.py
index e0b2014f622..61b330d012b 100644
--- a/netbox/ipam/api/serializers_/services.py
+++ b/netbox/ipam/api/serializers_/services.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from dcim.api.serializers_.devices import DeviceSerializer
from ipam.choices import *
from ipam.models import IPAddress, Service, ServiceTemplate
diff --git a/netbox/ipam/api/serializers_/vlans.py b/netbox/ipam/api/serializers_/vlans.py
index 5525545a86f..608fcf0b47b 100644
--- a/netbox/ipam/api/serializers_/vlans.py
+++ b/netbox/ipam/api/serializers_/vlans.py
@@ -6,7 +6,7 @@ from dcim.api.serializers_.sites import SiteSerializer
from ipam.choices import *
from ipam.constants import VLANGROUP_SCOPE_TYPES
from ipam.models import VLAN, VLANGroup
-from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
+from netbox.api.fields import ChoiceField, ContentTypeField, IntegerRangeSerializer, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model
@@ -32,6 +32,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
)
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
scope = serializers.SerializerMethodField(read_only=True)
+ vid_ranges = IntegerRangeSerializer(many=True, required=False)
utilization = serializers.CharField(read_only=True)
# Related object counts
@@ -40,8 +41,8 @@ class VLANGroupSerializer(NetBoxModelSerializer):
class Meta:
model = VLANGroup
fields = [
- 'id', 'url', 'display_url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'min_vid',
- 'max_vid', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization'
+ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'vid_ranges',
+ 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization'
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count')
validators = []
diff --git a/netbox/ipam/api/serializers_/vrfs.py b/netbox/ipam/api/serializers_/vrfs.py
index ad54dc09557..a23909108c1 100644
--- a/netbox/ipam/api/serializers_/vrfs.py
+++ b/netbox/ipam/api/serializers_/vrfs.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from ipam.models import RouteTarget, VRF
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py
index cac90bb873e..ffd4d5b7de1 100644
--- a/netbox/ipam/api/views.py
+++ b/netbox/ipam/api/views.py
@@ -186,13 +186,13 @@ class AvailableObjectsView(ObjectValidationMixin, APIView):
"""
Return the parent object.
"""
- raise NotImplemented()
+ raise NotImplementedError()
def get_available_objects(self, parent, limit=None):
"""
Return all available objects for the parent.
"""
- raise NotImplemented()
+ raise NotImplementedError()
def get_extra_context(self, parent):
"""
@@ -250,7 +250,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView):
# Determine if the requested number of objects is available
if not self.check_sufficient_available(serializer.validated_data, available_objects):
return Response(
- {"detail": f"Insufficient resources are available to satisfy the request"},
+ {"detail": "Insufficient resources are available to satisfy the request"},
status=status.HTTP_409_CONFLICT
)
diff --git a/netbox/ipam/apps.py b/netbox/ipam/apps.py
index 244ec7d6d0e..c118d5464fd 100644
--- a/netbox/ipam/apps.py
+++ b/netbox/ipam/apps.py
@@ -7,7 +7,7 @@ class IPAMConfig(AppConfig):
def ready(self):
from netbox.models.features import register_models
- from . import signals, search
+ from . import signals, search # noqa: F401
# Register models
register_models(*self.get_models())
diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py
index 5cdfac34ed3..894219c646a 100644
--- a/netbox/ipam/filtersets.py
+++ b/netbox/ipam/filtersets.py
@@ -458,7 +458,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
return queryset.filter(
Q(vrf=vrf) |
Q(vrf__export_targets__in=vrf.import_targets.all())
- )
+ ).distinct()
class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
@@ -738,7 +738,7 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
return queryset.filter(
Q(vrf=vrf) |
Q(vrf__export_targets__in=vrf.import_targets.all())
- )
+ ).distinct()
def filter_device(self, queryset, name, value):
devices = Device.objects.filter(**{'{}__in'.format(name): value})
@@ -911,10 +911,13 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
cluster = django_filters.NumberFilter(
method='filter_scope'
)
+ contains_vid = django_filters.NumberFilter(
+ method='filter_contains_vid'
+ )
class Meta:
model = VLANGroup
- fields = ('id', 'name', 'slug', 'min_vid', 'max_vid', 'description', 'scope_id')
+ fields = ('id', 'name', 'slug', 'description', 'scope_id')
def search(self, queryset, name, value):
if not value.strip():
@@ -932,6 +935,21 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
scope_id=value
)
+ def filter_contains_vid(self, queryset, name, value):
+ """
+ Return all VLANGroups which contain the given VLAN ID.
+ """
+ table_name = VLANGroup._meta.db_table
+ # TODO: See if this can be optimized without compromising queryset integrity
+ # Expand VLAN ID ranges to query by integer
+ groups = VLANGroup.objects.raw(
+ f'SELECT id FROM {table_name}, unnest(vid_ranges) vid_range WHERE %s <@ vid_range',
+ params=(value,)
+ )
+ return queryset.filter(
+ pk__in=[g.id for g in groups]
+ )
+
class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
@@ -1017,6 +1035,16 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
to_field_name='identifier',
label=_('L2VPN'),
)
+ interface_id = django_filters.ModelChoiceFilter(
+ queryset=Interface.objects.all(),
+ method='filter_interface_id',
+ label=_('Assigned interface')
+ )
+ vminterface_id = django_filters.ModelChoiceFilter(
+ queryset=VMInterface.objects.all(),
+ method='filter_vminterface_id',
+ label=_('Assigned VM interface')
+ )
class Meta:
model = VLAN
@@ -1044,6 +1072,22 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
def get_for_virtualmachine(self, queryset, name, value):
return queryset.get_for_virtualmachine(value)
+ def filter_interface_id(self, queryset, name, value):
+ if value is None:
+ return queryset.none()
+ return queryset.filter(
+ Q(interfaces_as_tagged=value) |
+ Q(interfaces_as_untagged=value)
+ )
+
+ def filter_vminterface_id(self, queryset, name, value):
+ if value is None:
+ return queryset.none()
+ return queryset.filter(
+ Q(vminterfaces_as_tagged=value) |
+ Q(vminterfaces_as_untagged=value)
+ )
+
class ServiceTemplateFilterSet(NetBoxModelFilterSet):
port = NumericArrayFilter(
diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py
index c7f64ab1d87..f4a7eabb7da 100644
--- a/netbox/ipam/forms/bulk_edit.py
+++ b/netbox/ipam/forms/bulk_edit.py
@@ -12,6 +12,7 @@ from tenancy.models import Tenant
from utilities.forms import add_blank_choice
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
+ NumericRangeArrayField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect
@@ -221,6 +222,19 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
'group_id': '$site_group',
}
)
+ vlan_group = DynamicModelChoiceField(
+ queryset=VLANGroup.objects.all(),
+ required=False,
+ label=_('VLAN Group')
+ )
+ vlan = DynamicModelChoiceField(
+ queryset=VLAN.objects.all(),
+ required=False,
+ label=_('VLAN'),
+ query_params={
+ 'group_id': '$vlan_group',
+ }
+ )
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
@@ -269,9 +283,10 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
FieldSet('tenant', 'status', 'role', 'description'),
FieldSet('region', 'site_group', 'site', name=_('Site')),
FieldSet('vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
+ FieldSet('vlan_group', 'vlan', name=_('VLAN Assignment')),
)
nullable_fields = (
- 'site', 'vrf', 'tenant', 'role', 'description', 'comments',
+ 'site', 'vlan', 'vrf', 'tenant', 'role', 'description', 'comments',
)
@@ -408,18 +423,6 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
- min_vid = forms.IntegerField(
- min_value=VLAN_VID_MIN,
- max_value=VLAN_VID_MAX,
- required=False,
- label=_('Minimum child VLAN VID')
- )
- max_vid = forms.IntegerField(
- min_value=VLAN_VID_MIN,
- max_value=VLAN_VID_MAX,
- required=False,
- label=_('Maximum child VLAN VID')
- )
description = forms.CharField(
label=_('Description'),
max_length=200,
@@ -483,10 +486,14 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
'group_id': '$clustergroup',
}
)
+ vid_ranges = NumericRangeArrayField(
+ label=_('VLAN ID ranges'),
+ required=False
+ )
model = VLANGroup
fieldsets = (
- FieldSet('site', 'min_vid', 'max_vid', 'description'),
+ FieldSet('site', 'vid_ranges', 'description'),
FieldSet(
'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', name=_('Scope')
),
diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py
index bfff1f4f4f4..dea250c7921 100644
--- a/netbox/ipam/forms/bulk_import.py
+++ b/netbox/ipam/forms/bulk_import.py
@@ -9,7 +9,8 @@ from ipam.models import *
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms.fields import (
- CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField
+ CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField,
+ NumericRangeArrayField,
)
from virtualization.models import VirtualMachine, VMInterface
@@ -411,22 +412,13 @@ class VLANGroupImportForm(NetBoxModelImportForm):
required=False,
label=_('Scope type (app & model)')
)
- min_vid = forms.IntegerField(
- min_value=VLAN_VID_MIN,
- max_value=VLAN_VID_MAX,
- required=False,
- label=_('Minimum child VLAN VID (default: {minimum})').format(minimum=VLAN_VID_MIN)
- )
- max_vid = forms.IntegerField(
- min_value=VLAN_VID_MIN,
- max_value=VLAN_VID_MAX,
- required=False,
- label=_('Maximum child VLAN VID (default: {maximum})').format(maximum=VLAN_VID_MIN)
+ vid_ranges = NumericRangeArrayField(
+ required=False
)
class Meta:
model = VLANGroup
- fields = ('name', 'slug', 'scope_type', 'scope_id', 'min_vid', 'max_vid', 'description', 'tags')
+ fields = ('name', 'slug', 'scope_type', 'scope_id', 'vid_ranges', 'description', 'tags')
labels = {
'scope_id': 'Scope ID',
}
diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py
index 80fb0422656..a326943217d 100644
--- a/netbox/ipam/forms/filtersets.py
+++ b/netbox/ipam/forms/filtersets.py
@@ -413,7 +413,7 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
FieldSet('q', 'filter_id', 'tag'),
FieldSet('region', 'sitegroup', 'site', 'location', 'rack', name=_('Location')),
FieldSet('cluster_group', 'cluster', name=_('Cluster')),
- FieldSet('min_vid', 'max_vid', name=_('VLAN ID')),
+ FieldSet('contains_vid', name=_('VLANs')),
)
model = VLANGroup
region = DynamicModelMultipleChoiceField(
@@ -441,18 +441,6 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
required=False,
label=_('Rack')
)
- min_vid = forms.IntegerField(
- required=False,
- min_value=VLAN_VID_MIN,
- max_value=VLAN_VID_MAX,
- label=_('Minimum VID')
- )
- max_vid = forms.IntegerField(
- required=False,
- min_value=VLAN_VID_MIN,
- max_value=VLAN_VID_MAX,
- label=_('Maximum VID')
- )
cluster = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,
@@ -463,6 +451,11 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm):
required=False,
label=_('Cluster group')
)
+ contains_vid = forms.IntegerField(
+ min_value=0,
+ required=False,
+ label=_('Contains VLAN ID')
+ )
tag = TagFilterField(model)
diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py
index 4e405a0355f..156e7c43562 100644
--- a/netbox/ipam/forms/model_forms.py
+++ b/netbox/ipam/forms/model_forms.py
@@ -1,9 +1,9 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.utils.translation import gettext_lazy as _
-from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
+from dcim.models import Device, Interface, Site
from ipam.choices import *
from ipam.constants import *
from ipam.formfields import IPNetworkFormField
@@ -14,11 +14,13 @@ from utilities.exceptions import PermissionsViolation
from utilities.forms import add_blank_choice
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
- SlugField,
+ NumericRangeArrayField, SlugField
)
from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
-from utilities.forms.widgets import DatePicker
-from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
+from utilities.forms.utils import get_field_value
+from utilities.forms.widgets import DatePicker, HTMXSelect
+from utilities.templatetags.builtins.filters import bettertitle
+from virtualization.models import VirtualMachine, VMInterface
__all__ = (
'AggregateForm',
@@ -562,91 +564,34 @@ class FHRPGroupAssignmentForm(forms.ModelForm):
class VLANGroupForm(NetBoxModelForm):
- scope_type = ContentTypeChoiceField(
- label=_('Scope type'),
- queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
- required=False
- )
- region = DynamicModelChoiceField(
- label=_('Region'),
- queryset=Region.objects.all(),
- required=False,
- initial_params={
- 'sites': '$site'
- }
- )
- sitegroup = DynamicModelChoiceField(
- queryset=SiteGroup.objects.all(),
- required=False,
- initial_params={
- 'sites': '$site'
- },
- label=_('Site group')
- )
- site = DynamicModelChoiceField(
- label=_('Site'),
- queryset=Site.objects.all(),
- required=False,
- initial_params={
- 'locations': '$location'
- },
- query_params={
- 'region_id': '$region',
- 'group_id': '$sitegroup',
- }
- )
- location = DynamicModelChoiceField(
- label=_('Location'),
- queryset=Location.objects.all(),
- required=False,
- initial_params={
- 'racks': '$rack'
- },
- query_params={
- 'site_id': '$site',
- }
- )
- rack = DynamicModelChoiceField(
- label=_('Rack'),
- queryset=Rack.objects.all(),
- required=False,
- query_params={
- 'site_id': '$site',
- 'location_id': '$location',
- }
- )
- clustergroup = DynamicModelChoiceField(
- queryset=ClusterGroup.objects.all(),
- required=False,
- initial_params={
- 'clusters': '$cluster'
- },
- label=_('Cluster group')
- )
- cluster = DynamicModelChoiceField(
- label=_('Cluster'),
- queryset=Cluster.objects.all(),
- required=False,
- query_params={
- 'group_id': '$clustergroup',
- }
- )
slug = SlugField()
+ vid_ranges = NumericRangeArrayField(
+ label=_('VLAN IDs')
+ )
+ scope_type = ContentTypeChoiceField(
+ queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
+ widget=HTMXSelect(),
+ required=False,
+ label=_('Scope type')
+ )
+ scope = DynamicModelChoiceField(
+ label=_('Scope'),
+ queryset=Site.objects.none(), # Initial queryset
+ required=False,
+ disabled=True,
+ selector=True
+ )
fieldsets = (
FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
- FieldSet('min_vid', 'max_vid', name=_('Child VLANs')),
- FieldSet(
- 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster',
- name=_('Scope')
- ),
+ FieldSet('vid_ranges', name=_('Child VLANs')),
+ FieldSet('scope_type', 'scope', name=_('Scope')),
)
class Meta:
model = VLANGroup
fields = [
- 'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
- 'clustergroup', 'cluster', 'min_vid', 'max_vid', 'tags',
+ 'name', 'slug', 'description', 'vid_ranges', 'scope_type', 'tags',
]
def __init__(self, *args, **kwargs):
@@ -654,21 +599,30 @@ class VLANGroupForm(NetBoxModelForm):
initial = kwargs.get('initial', {})
if instance is not None and instance.scope:
- initial[instance.scope_type.model] = instance.scope
-
+ initial['scope'] = instance.scope
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
+ if scope_type_id := get_field_value(self, 'scope_type'):
+ try:
+ scope_type = ContentType.objects.get(pk=scope_type_id)
+ model = scope_type.model_class()
+ self.fields['scope'].queryset = model.objects.all()
+ self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
+ self.fields['scope'].disabled = False
+ self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
+ except ObjectDoesNotExist:
+ pass
+
+ if self.instance and scope_type_id != self.instance.scope_type_id:
+ self.initial['scope'] = None
+
def clean(self):
super().clean()
- # Assign scope based on scope_type
- if self.cleaned_data.get('scope_type'):
- scope_field = self.cleaned_data['scope_type'].model
- self.instance.scope = self.cleaned_data.get(scope_field)
- else:
- self.instance.scope_id = None
+ # Assign the selected scope (if any)
+ self.instance.scope = self.cleaned_data.get('scope')
class VLANForm(TenancyForm, NetBoxModelForm):
diff --git a/netbox/ipam/graphql/mixins.py b/netbox/ipam/graphql/mixins.py
index 73cc60ec413..692741871f8 100644
--- a/netbox/ipam/graphql/mixins.py
+++ b/netbox/ipam/graphql/mixins.py
@@ -1,7 +1,6 @@
from typing import Annotated, List
import strawberry
-import strawberry_django
__all__ = (
'IPAddressesMixin',
@@ -11,9 +10,9 @@ __all__ = (
@strawberry.type
class IPAddressesMixin:
- ip_addresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]]
+ ip_addresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]] # noqa: F821
@strawberry.type
class VLANGroupsMixin:
- vlan_groups: List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]]
+ vlan_groups: List[Annotated["VLANGroupType", strawberry.lazy('ipam.graphql.types')]] # noqa: F821
diff --git a/netbox/ipam/graphql/schema.py b/netbox/ipam/graphql/schema.py
index c02788c3a79..072f8cbcd46 100644
--- a/netbox/ipam/graphql/schema.py
+++ b/netbox/ipam/graphql/schema.py
@@ -3,88 +3,55 @@ from typing import List
import strawberry
import strawberry_django
-from ipam import models
from .types import *
-@strawberry.type
+@strawberry.type(name="Query")
class IPAMQuery:
- @strawberry.field
- def asn(self, id: int) -> ASNType:
- return models.ASN.objects.get(pk=id)
+ asn: ASNType = strawberry_django.field()
asn_list: List[ASNType] = strawberry_django.field()
- @strawberry.field
- def asn_range(self, id: int) -> ASNRangeType:
- return models.ASNRange.objects.get(pk=id)
+ asn_range: ASNRangeType = strawberry_django.field()
asn_range_list: List[ASNRangeType] = strawberry_django.field()
- @strawberry.field
- def aggregate(self, id: int) -> AggregateType:
- return models.Aggregate.objects.get(pk=id)
+ aggregate: AggregateType = strawberry_django.field()
aggregate_list: List[AggregateType] = strawberry_django.field()
- @strawberry.field
- def ip_address(self, id: int) -> IPAddressType:
- return models.IPAddress.objects.get(pk=id)
+ ip_address: IPAddressType = strawberry_django.field()
ip_address_list: List[IPAddressType] = strawberry_django.field()
- @strawberry.field
- def ip_range(self, id: int) -> IPRangeType:
- return models.IPRange.objects.get(pk=id)
+ ip_range: IPRangeType = strawberry_django.field()
ip_range_list: List[IPRangeType] = strawberry_django.field()
- @strawberry.field
- def prefix(self, id: int) -> PrefixType:
- return models.Prefix.objects.get(pk=id)
+ prefix: PrefixType = strawberry_django.field()
prefix_list: List[PrefixType] = strawberry_django.field()
- @strawberry.field
- def rir(self, id: int) -> RIRType:
- return models.RIR.objects.get(pk=id)
+ rir: RIRType = strawberry_django.field()
rir_list: List[RIRType] = strawberry_django.field()
- @strawberry.field
- def role(self, id: int) -> RoleType:
- return models.Role.objects.get(pk=id)
+ role: RoleType = strawberry_django.field()
role_list: List[RoleType] = strawberry_django.field()
- @strawberry.field
- def route_target(self, id: int) -> RouteTargetType:
- return models.RouteTarget.objects.get(pk=id)
+ route_target: RouteTargetType = strawberry_django.field()
route_target_list: List[RouteTargetType] = strawberry_django.field()
- @strawberry.field
- def service(self, id: int) -> ServiceType:
- return models.Service.objects.get(pk=id)
+ service: ServiceType = strawberry_django.field()
service_list: List[ServiceType] = strawberry_django.field()
- @strawberry.field
- def service_template(self, id: int) -> ServiceTemplateType:
- return models.ServiceTemplate.objects.get(pk=id)
+ service_template: ServiceTemplateType = strawberry_django.field()
service_template_list: List[ServiceTemplateType] = strawberry_django.field()
- @strawberry.field
- def fhrp_group(self, id: int) -> FHRPGroupType:
- return models.FHRPGroup.objects.get(pk=id)
+ fhrp_group: FHRPGroupType = strawberry_django.field()
fhrp_group_list: List[FHRPGroupType] = strawberry_django.field()
- @strawberry.field
- def fhrp_group_assignment(self, id: int) -> FHRPGroupAssignmentType:
- return models.FHRPGroupAssignment.objects.get(pk=id)
+ fhrp_group_assignment: FHRPGroupAssignmentType = strawberry_django.field()
fhrp_group_assignment_list: List[FHRPGroupAssignmentType] = strawberry_django.field()
- @strawberry.field
- def vlan(self, id: int) -> VLANType:
- return models.VLAN.objects.get(pk=id)
+ vlan: VLANType = strawberry_django.field()
vlan_list: List[VLANType] = strawberry_django.field()
- @strawberry.field
- def vlan_group(self, id: int) -> VLANGroupType:
- return models.VLANGroup.objects.get(pk=id)
+ vlan_group: VLANGroupType = strawberry_django.field()
vlan_group_list: List[VLANGroupType] = strawberry_django.field()
- @strawberry.field
- def vrf(self, id: int) -> VRFType:
- return models.VRF.objects.get(pk=id)
+ vrf: VRFType = strawberry_django.field()
vrf_list: List[VRFType] = strawberry_django.field()
diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py
index 36e09eaac25..46d45816e73 100644
--- a/netbox/ipam/graphql/types.py
+++ b/netbox/ipam/graphql/types.py
@@ -251,6 +251,7 @@ class VLANType(NetBoxObjectType):
class VLANGroupType(OrganizationalObjectType):
vlans: List[VLANType]
+ vid_ranges: List[str]
@strawberry_django.field
def scope(self) -> Annotated[Union[
diff --git a/netbox/ipam/migrations/0070_vlangroup_vlan_id_ranges.py b/netbox/ipam/migrations/0070_vlangroup_vlan_id_ranges.py
new file mode 100644
index 00000000000..b01941401ff
--- /dev/null
+++ b/netbox/ipam/migrations/0070_vlangroup_vlan_id_ranges.py
@@ -0,0 +1,55 @@
+import django.contrib.postgres.fields
+import django.contrib.postgres.fields.ranges
+from django.db import migrations, models
+from django.db.backends.postgresql.psycopg_any import NumericRange
+
+import ipam.models.vlans
+
+
+def set_vid_ranges(apps, schema_editor):
+ """
+ Convert the min_vid & max_vid fields to a range in the new vid_ranges ArrayField.
+ """
+ VLANGroup = apps.get_model('ipam', 'VLANGroup')
+ for group in VLANGroup.objects.all():
+ group.vid_ranges = [
+ NumericRange(group.min_vid, group.max_vid, bounds='[]')
+ ]
+ group._total_vlan_ids = group.max_vid - group.min_vid + 1
+ group.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ipam', '0069_gfk_indexes'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='vlangroup',
+ name='vid_ranges',
+ field=django.contrib.postgres.fields.ArrayField(
+ base_field=django.contrib.postgres.fields.ranges.IntegerRangeField(),
+ default=ipam.models.vlans.default_vid_ranges,
+ size=None
+ ),
+ ),
+ migrations.AddField(
+ model_name='vlangroup',
+ name='_total_vlan_ids',
+ field=models.PositiveBigIntegerField(default=4094),
+ ),
+ migrations.RunPython(
+ code=set_vid_ranges,
+ reverse_code=migrations.RunPython.noop
+ ),
+ migrations.RemoveField(
+ model_name='vlangroup',
+ name='max_vid',
+ ),
+ migrations.RemoveField(
+ model_name='vlangroup',
+ name='min_vid',
+ ),
+ ]
diff --git a/netbox/ipam/models/asns.py b/netbox/ipam/models/asns.py
index ba483a74533..eb47426b231 100644
--- a/netbox/ipam/models/asns.py
+++ b/netbox/ipam/models/asns.py
@@ -1,6 +1,5 @@
from django.core.exceptions import ValidationError
from django.db import models
-from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from ipam.fields import ASNField
@@ -54,9 +53,6 @@ class ASNRange(OrganizationalModel):
def __str__(self):
return f'{self.name} ({self.range_as_string()})'
- def get_absolute_url(self):
- return reverse('ipam:asnrange', args=[self.pk])
-
@property
def range(self):
return range(self.start, self.end + 1)
@@ -128,9 +124,6 @@ class ASN(PrimaryModel):
def __str__(self):
return f'AS{self.asn_with_asdot}'
- def get_absolute_url(self):
- return reverse('ipam:asn', args=[self.pk])
-
@property
def asn_asdot(self):
"""
@@ -149,3 +142,7 @@ class ASN(PrimaryModel):
return f'{self.asn} ({self.asn // 65536}.{self.asn % 65536})'
else:
return self.asn
+
+ @property
+ def prefixed_name(self):
+ return f'AS{self.asn_with_asdot}'
diff --git a/netbox/ipam/models/fhrp.py b/netbox/ipam/models/fhrp.py
index c3a7084b61b..28bb37ef371 100644
--- a/netbox/ipam/models/fhrp.py
+++ b/netbox/ipam/models/fhrp.py
@@ -1,7 +1,6 @@
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
-from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from ipam.choices import *
@@ -71,9 +70,6 @@ class FHRPGroup(PrimaryModel):
return name
- def get_absolute_url(self):
- return reverse('ipam:fhrpgroup', args=[self.pk])
-
class FHRPGroupAssignment(ChangeLoggedModel):
interface_type = models.ForeignKey(
diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py
index 0b8e3a8dfc8..a540b5810ce 100644
--- a/netbox/ipam/models/ip.py
+++ b/netbox/ipam/models/ip.py
@@ -4,7 +4,6 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F
from django.db.models.functions import Cast
-from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
@@ -71,9 +70,6 @@ class RIR(OrganizationalModel):
verbose_name = _('RIR')
verbose_name_plural = _('RIRs')
- def get_absolute_url(self):
- return reverse('ipam:rir', args=[self.pk])
-
class Aggregate(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
"""
@@ -118,9 +114,6 @@ class Aggregate(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
def __str__(self):
return str(self.prefix)
- def get_absolute_url(self):
- return reverse('ipam:aggregate', args=[self.pk])
-
def clean(self):
super().clean()
@@ -203,9 +196,6 @@ class Role(OrganizationalModel):
def __str__(self):
return self.name
- def get_absolute_url(self):
- return reverse('ipam:role', args=[self.pk])
-
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
"""
@@ -303,9 +293,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
def __str__(self):
return str(self.prefix)
- def get_absolute_url(self):
- return reverse('ipam:prefix', args=[self.pk])
-
def clean(self):
super().clean()
@@ -551,9 +538,6 @@ class IPRange(ContactsMixin, PrimaryModel):
def __str__(self):
return self.name
- def get_absolute_url(self):
- return reverse('ipam:iprange', args=[self.pk])
-
def clean(self):
super().clean()
@@ -798,9 +782,6 @@ class IPAddress(ContactsMixin, PrimaryModel):
self._original_assigned_object_id = self.__dict__.get('assigned_object_id')
self._original_assigned_object_type_id = self.__dict__.get('assigned_object_type_id')
- def get_absolute_url(self):
- return reverse('ipam:ipaddress', args=[self.pk])
-
def get_duplicates(self):
return IPAddress.objects.filter(
vrf=self.vrf,
diff --git a/netbox/ipam/models/services.py b/netbox/ipam/models/services.py
index 71f34c66c32..bb4049781b7 100644
--- a/netbox/ipam/models/services.py
+++ b/netbox/ipam/models/services.py
@@ -2,7 +2,6 @@ from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
-from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from ipam.choices import *
@@ -59,9 +58,6 @@ class ServiceTemplate(ServiceBase, PrimaryModel):
verbose_name = _('service template')
verbose_name_plural = _('service templates')
- def get_absolute_url(self):
- return reverse('ipam:servicetemplate', args=[self.pk])
-
class Service(ContactsMixin, ServiceBase, PrimaryModel):
"""
@@ -102,9 +98,6 @@ class Service(ContactsMixin, ServiceBase, PrimaryModel):
verbose_name = _('service')
verbose_name_plural = _('services')
- def get_absolute_url(self):
- return reverse('ipam:service', args=[self.pk])
-
@property
def parent(self):
return self.device or self.virtual_machine
diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py
index 7434bd0b449..23f7c41c7e8 100644
--- a/netbox/ipam/models/vlans.py
+++ b/netbox/ipam/models/vlans.py
@@ -1,8 +1,9 @@
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
+from django.contrib.postgres.fields import ArrayField, IntegerRangeField
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
-from django.urls import reverse
+from django.db.backends.postgresql.psycopg_any import NumericRange
from django.utils.translation import gettext_lazy as _
from dcim.models import Interface
@@ -10,6 +11,7 @@ from ipam.choices import *
from ipam.constants import *
from ipam.querysets import VLANQuerySet, VLANGroupQuerySet
from netbox.models import OrganizationalModel, PrimaryModel
+from utilities.data import check_ranges_overlap, ranges_to_string
from virtualization.models import VMInterface
__all__ = (
@@ -18,9 +20,16 @@ __all__ = (
)
+def default_vid_ranges():
+ return [
+ NumericRange(VLAN_VID_MIN, VLAN_VID_MAX, bounds='[]')
+ ]
+
+
class VLANGroup(OrganizationalModel):
"""
- A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
+ A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique. Each group must
+ define one or more ranges of valid VLAN IDs, and may be assigned a specific scope.
"""
name = models.CharField(
verbose_name=_('name'),
@@ -45,23 +54,13 @@ class VLANGroup(OrganizationalModel):
ct_field='scope_type',
fk_field='scope_id'
)
- min_vid = models.PositiveSmallIntegerField(
- verbose_name=_('minimum VLAN ID'),
- default=VLAN_VID_MIN,
- validators=(
- MinValueValidator(VLAN_VID_MIN),
- MaxValueValidator(VLAN_VID_MAX)
- ),
- help_text=_('Lowest permissible ID of a child VLAN')
+ vid_ranges = ArrayField(
+ IntegerRangeField(),
+ verbose_name=_('VLAN ID ranges'),
+ default=default_vid_ranges
)
- max_vid = models.PositiveSmallIntegerField(
- verbose_name=_('maximum VLAN ID'),
- default=VLAN_VID_MAX,
- validators=(
- MinValueValidator(VLAN_VID_MIN),
- MaxValueValidator(VLAN_VID_MAX)
- ),
- help_text=_('Highest permissible ID of a child VLAN')
+ _total_vlan_ids = models.PositiveBigIntegerField(
+ default=VLAN_VID_MAX - VLAN_VID_MIN + 1
)
objects = VLANGroupQuerySet.as_manager()
@@ -84,9 +83,6 @@ class VLANGroup(OrganizationalModel):
verbose_name = _('VLAN group')
verbose_name_plural = _('VLAN groups')
- def get_absolute_url(self):
- return reverse('ipam:vlangroup', args=[self.pk])
-
def clean(self):
super().clean()
@@ -96,17 +92,33 @@ class VLANGroup(OrganizationalModel):
if self.scope_id and not self.scope_type:
raise ValidationError(_("Cannot set scope_id without scope_type."))
- # Validate min/max child VID limits
- if self.max_vid < self.min_vid:
- raise ValidationError({
- 'max_vid': _("Maximum child VID must be greater than or equal to minimum child VID")
- })
+ # Validate VID ranges
+ if self.vid_ranges and check_ranges_overlap(self.vid_ranges):
+ raise ValidationError({'vid_ranges': _("Ranges cannot overlap.")})
+ for vid_range in self.vid_ranges:
+ if vid_range.lower > vid_range.upper:
+ raise ValidationError({
+ 'vid_ranges': _(
+ "Maximum child VID must be greater than or equal to minimum child VID ({value})"
+ ).format(value=vid_range)
+ })
+
+ def save(self, *args, **kwargs):
+ self._total_vlan_ids = 0
+ for vid_range in self.vid_ranges:
+ self._total_vlan_ids += vid_range.upper - vid_range.lower + 1
+
+ super().save(*args, **kwargs)
def get_available_vids(self):
"""
Return all available VLANs within this group.
"""
- available_vlans = {vid for vid in range(self.min_vid, self.max_vid + 1)}
+ available_vlans = set()
+ for vlan_range in self.vid_ranges:
+ available_vlans = available_vlans.union({
+ vid for vid in range(vlan_range.lower, vlan_range.upper)
+ })
available_vlans -= set(VLAN.objects.filter(group=self).values_list('vid', flat=True))
return sorted(available_vlans)
@@ -126,6 +138,10 @@ class VLANGroup(OrganizationalModel):
"""
return VLAN.objects.filter(group=self).order_by('vid')
+ @property
+ def vid_ranges_list(self):
+ return ranges_to_string(self.vid_ranges)
+
class VLAN(PrimaryModel):
"""
@@ -217,9 +233,6 @@ class VLAN(PrimaryModel):
def __str__(self):
return f'{self.name} ({self.vid})'
- def get_absolute_url(self):
- return reverse('ipam:vlan', args=[self.pk])
-
def clean(self):
super().clean()
@@ -231,13 +244,14 @@ class VLAN(PrimaryModel):
).format(group=self.group, scope=self.group.scope, site=self.site)
)
- # Validate group min/max VIDs
- if self.group and not self.group.min_vid <= self.vid <= self.group.max_vid:
- raise ValidationError({
- 'vid': _(
- "VID must be between {minimum} and {maximum} for VLANs in group {group}"
- ).format(minimum=self.group.min_vid, maximum=self.group.max_vid, group=self.group)
- })
+ # Check that the VLAN ID is permitted in the assigned group (if any)
+ if self.group:
+ if not any([self.vid in r for r in self.group.vid_ranges]):
+ raise ValidationError({
+ 'vid': _(
+ "VID must be in ranges {ranges} for VLANs in group {group}"
+ ).format(ranges=ranges_to_string(self.group.vid_ranges), group=self.group)
+ })
def get_status_color(self):
return VLANStatusChoices.colors.get(self.status)
diff --git a/netbox/ipam/models/vrfs.py b/netbox/ipam/models/vrfs.py
index 1f9b928f588..26afb7927ca 100644
--- a/netbox/ipam/models/vrfs.py
+++ b/netbox/ipam/models/vrfs.py
@@ -1,11 +1,9 @@
from django.db import models
-from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from ipam.constants import *
from netbox.models import PrimaryModel
-
__all__ = (
'RouteTarget',
'VRF',
@@ -67,9 +65,6 @@ class VRF(PrimaryModel):
return f'{self.name} ({self.rd})'
return self.name
- def get_absolute_url(self):
- return reverse('ipam:vrf', args=[self.pk])
-
class RouteTarget(PrimaryModel):
"""
@@ -96,6 +91,3 @@ class RouteTarget(PrimaryModel):
def __str__(self):
return self.name
-
- def get_absolute_url(self):
- return reverse('ipam:routetarget', args=[self.pk])
diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py
index a3f37fe3ca6..771e9b3b9d4 100644
--- a/netbox/ipam/querysets.py
+++ b/netbox/ipam/querysets.py
@@ -9,6 +9,7 @@ from utilities.querysets import RestrictedQuerySet
__all__ = (
'ASNRangeQuerySet',
'PrefixQuerySet',
+ 'VLANGroupQuerySet',
'VLANQuerySet',
)
@@ -63,7 +64,7 @@ class VLANGroupQuerySet(RestrictedQuerySet):
return self.annotate(
vlan_count=count_related(VLAN, 'group'),
- utilization=Round(F('vlan_count') / (F('max_vid') - F('min_vid') + 1.0) * 100, 2)
+ utilization=Round(F('vlan_count') * 100.0 / F('_total_vlan_ids'), 2)
)
diff --git a/netbox/ipam/search.py b/netbox/ipam/search.py
index a1cddbb1a8e..16a8eba3c51 100644
--- a/netbox/ipam/search.py
+++ b/netbox/ipam/search.py
@@ -19,6 +19,7 @@ class ASNIndex(SearchIndex):
model = models.ASN
fields = (
('asn', 100),
+ ('prefixed_name', 110),
('description', 500),
)
display_attrs = ('rir', 'tenant', 'description')
@@ -28,6 +29,7 @@ class ASNIndex(SearchIndex):
class ASNRangeIndex(SearchIndex):
model = models.ASNRange
fields = (
+ ('name', 100),
('description', 500),
)
display_attrs = ('rir', 'tenant', 'description')
@@ -154,9 +156,8 @@ class VLANGroupIndex(SearchIndex):
('name', 100),
('slug', 110),
('description', 500),
- ('max_vid', 2000),
)
- display_attrs = ('scope_type', 'min_vid', 'max_vid', 'description')
+ display_attrs = ('scope_type', 'description')
@register_search
diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py
index 10dea3a9283..8ec7a596725 100644
--- a/netbox/ipam/tables/ip.py
+++ b/netbox/ipam/tables/ip.py
@@ -51,7 +51,7 @@ IPADDRESS_LINK = """
{% if record.pk %}
{{ record.address }}
{% elif perms.ipam.add_ipaddress %}
- {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
+ {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
{% else %}
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
{% endif %}
@@ -86,7 +86,8 @@ class RIRTable(NetBoxTable):
linkify=True
)
is_private = columns.BooleanColumn(
- verbose_name=_('Private')
+ verbose_name=_('Private'),
+ false_mark=None
)
aggregate_count = columns.LinkedCountColumn(
viewname='ipam:aggregate_list',
@@ -258,10 +259,12 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
linkify=True
)
is_pool = columns.BooleanColumn(
- verbose_name=_('Pool')
+ verbose_name=_('Pool'),
+ false_mark=None
)
mark_utilized = columns.BooleanColumn(
- verbose_name=_('Marked Utilized')
+ verbose_name=_('Marked Utilized'),
+ false_mark=None
)
utilization = PrefixUtilizationColumn(
verbose_name=_('Utilization'),
@@ -314,7 +317,8 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
linkify=True
)
mark_utilized = columns.BooleanColumn(
- verbose_name=_('Marked Utilized')
+ verbose_name=_('Marked Utilized'),
+ false_mark=None
)
utilization = columns.UtilizationColumn(
verbose_name=_('Utilization'),
@@ -386,7 +390,8 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
assigned = columns.BooleanColumn(
accessor='assigned_object_id',
linkify=lambda record: record.assigned_object.get_absolute_url(),
- verbose_name=_('Assigned')
+ verbose_name=_('Assigned'),
+ false_mark=None
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py
index 11de0381c4b..5387ce24ce8 100644
--- a/netbox/ipam/tables/vlans.py
+++ b/netbox/ipam/tables/vlans.py
@@ -72,6 +72,10 @@ class VLANGroupTable(NetBoxTable):
linkify=True,
orderable=False
)
+ vid_ranges_list = tables.Column(
+ verbose_name=_('VID Ranges'),
+ orderable=False
+ )
vlan_count = columns.LinkedCountColumn(
viewname='ipam:vlan_list',
url_params={'group_id': 'pk'},
@@ -91,7 +95,7 @@ class VLANGroupTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = VLANGroup
fields = (
- 'pk', 'id', 'name', 'scope_type', 'scope', 'min_vid', 'max_vid', 'vlan_count', 'slug', 'description',
+ 'pk', 'id', 'name', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count', 'slug', 'description',
'tags', 'created', 'last_updated', 'actions', 'utilization',
)
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'utilization', 'description')
@@ -211,6 +215,7 @@ class InterfaceVLANTable(NetBoxTable):
)
tagged = columns.BooleanColumn(
verbose_name=_('Tagged'),
+ false_mark=None
)
site = tables.Column(
verbose_name=_('Site'),
diff --git a/netbox/ipam/tables/vrfs.py b/netbox/ipam/tables/vrfs.py
index 174b9918951..5fd9cbfb610 100644
--- a/netbox/ipam/tables/vrfs.py
+++ b/netbox/ipam/tables/vrfs.py
@@ -30,7 +30,8 @@ class VRFTable(TenancyColumnsMixin, NetBoxTable):
verbose_name=_('RD')
)
enforce_unique = columns.BooleanColumn(
- verbose_name=_('Unique')
+ verbose_name=_('Unique'),
+ false_mark=None
)
import_targets = columns.TemplateColumn(
verbose_name=_('Import Targets'),
diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py
index 2cf7a2f1c80..1d2cdf1b714 100644
--- a/netbox/ipam/tests/test_api.py
+++ b/netbox/ipam/tests/test_api.py
@@ -8,6 +8,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer,
from ipam.choices import *
from ipam.models import *
from tenancy.models import Tenant
+from utilities.data import string_to_ranges
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_warnings
@@ -699,8 +700,6 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase):
device1.primary_ip4 = ip_addresses[0]
device1.save()
- ip2 = ip_addresses[1]
-
url = reverse('ipam-api:ipaddress-detail', kwargs={'pk': ip1.pk})
self.add_permissions('ipam.change_ipaddress')
@@ -767,6 +766,7 @@ class FHRPGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'priority': 100,
}
+ user_permissions = ('ipam.view_fhrpgroup', )
@classmethod
def setUpTestData(cls):
@@ -882,8 +882,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
vlangroup = VLANGroup.objects.create(
name='VLAN Group X',
slug='vlan-group-x',
- min_vid=MIN_VID,
- max_vid=MAX_VID
+ vid_ranges=string_to_ranges(f"{MIN_VID}-{MAX_VID}")
)
# Create a set of VLANs within the group
diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py
index 8f07a241a19..4e38b14503b 100644
--- a/netbox/ipam/tests/test_filtersets.py
+++ b/netbox/ipam/tests/test_filtersets.py
@@ -1,4 +1,5 @@
from django.contrib.contenttypes.models import ContentType
+from django.db.backends.postgresql.psycopg_any import NumericRange
from django.test import TestCase
from netaddr import IPNetwork
@@ -1465,6 +1466,7 @@ class FHRPGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VLANGroup.objects.all()
filterset = VLANGroupFilterSet
+ ignore_fields = ('vid_ranges',)
@classmethod
def setUpTestData(cls):
@@ -1494,14 +1496,55 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
cluster.save()
vlan_groups = (
- VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=region, description='foobar1'),
- VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sitegroup, description='foobar2'),
- VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=site, description='foobar3'),
- VLANGroup(name='VLAN Group 4', slug='vlan-group-4', scope=location),
- VLANGroup(name='VLAN Group 5', slug='vlan-group-5', scope=rack),
- VLANGroup(name='VLAN Group 6', slug='vlan-group-6', scope=clustergroup),
- VLANGroup(name='VLAN Group 7', slug='vlan-group-7', scope=cluster),
- VLANGroup(name='VLAN Group 8', slug='vlan-group-8'),
+ VLANGroup(
+ name='VLAN Group 1',
+ slug='vlan-group-1',
+ vid_ranges=[NumericRange(1, 11), NumericRange(100, 200)],
+ scope=region,
+ description='foobar1'
+ ),
+ VLANGroup(
+ name='VLAN Group 2',
+ slug='vlan-group-2',
+ vid_ranges=[NumericRange(1, 11), NumericRange(200, 300)],
+ scope=sitegroup,
+ description='foobar2'
+ ),
+ VLANGroup(
+ name='VLAN Group 3',
+ slug='vlan-group-3',
+ vid_ranges=[NumericRange(1, 11), NumericRange(300, 400)],
+ scope=site,
+ description='foobar3'
+ ),
+ VLANGroup(
+ name='VLAN Group 4',
+ slug='vlan-group-4',
+ vid_ranges=[NumericRange(1, 11), NumericRange(400, 500)],
+ scope=location
+ ),
+ VLANGroup(
+ name='VLAN Group 5',
+ slug='vlan-group-5',
+ vid_ranges=[NumericRange(1, 11), NumericRange(500, 600)],
+ scope=rack
+ ),
+ VLANGroup(
+ name='VLAN Group 6',
+ slug='vlan-group-6',
+ vid_ranges=[NumericRange(1, 11), NumericRange(600, 700)],
+ scope=clustergroup
+ ),
+ VLANGroup(
+ name='VLAN Group 7',
+ slug='vlan-group-7',
+ vid_ranges=[NumericRange(1, 11), NumericRange(700, 800)],
+ scope=cluster
+ ),
+ VLANGroup(
+ name='VLAN Group 8',
+ slug='vlan-group-8'
+ ),
)
VLANGroup.objects.bulk_create(vlan_groups)
@@ -1521,6 +1564,12 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_contains_vid(self):
+ params = {'contains_vid': 123}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'contains_vid': 1}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
+
def test_region(self):
params = {'region': Region.objects.first().pk}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@@ -1609,6 +1658,13 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Device.objects.bulk_create(devices)
+ interfaces = (
+ Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+ Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+ Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+ )
+ Interface.objects.bulk_create(interfaces)
+
cluster_groups = (
ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'),
@@ -1631,6 +1687,13 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
)
VirtualMachine.objects.bulk_create(virtual_machines)
+ vm_interfaces = (
+ VMInterface(virtual_machine=virtual_machines[0], name='VM Interface 1'),
+ VMInterface(virtual_machine=virtual_machines[1], name='VM Interface 2'),
+ VMInterface(virtual_machine=virtual_machines[2], name='VM Interface 3'),
+ )
+ VMInterface.objects.bulk_create(vm_interfaces)
+
groups = (
# Scoped VLAN groups
VLANGroup(name='Region 1', slug='region-1', scope=regions[0]),
@@ -1724,6 +1787,22 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
)
VLAN.objects.bulk_create(vlans)
+ # Assign VLANs to device interfaces
+ interfaces[0].untagged_vlan = vlans[0]
+ interfaces[0].tagged_vlans.add(vlans[1])
+ interfaces[1].untagged_vlan = vlans[2]
+ interfaces[1].tagged_vlans.add(vlans[3])
+ interfaces[2].untagged_vlan = vlans[4]
+ interfaces[2].tagged_vlans.add(vlans[5])
+
+ # Assign VLANs to VM interfaces
+ vm_interfaces[0].untagged_vlan = vlans[0]
+ vm_interfaces[0].tagged_vlans.add(vlans[1])
+ vm_interfaces[1].untagged_vlan = vlans[2]
+ vm_interfaces[1].tagged_vlans.add(vlans[3])
+ vm_interfaces[2].untagged_vlan = vlans[4]
+ vm_interfaces[2].tagged_vlans.add(vlans[5])
+
def test_q(self):
params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@@ -1808,6 +1887,16 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'available_at_site': site_id}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) # 4 scoped + 1 global group + 1 global
+ def test_interface(self):
+ interface_id = Interface.objects.first().pk
+ params = {'interface_id': interface_id}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_vminterface(self):
+ vminterface_id = VMInterface.objects.first().pk
+ params = {'vminterface_id': vminterface_id}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ServiceTemplate.objects.all()
diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py
index d0f42e8a6bb..8a5d918a9f2 100644
--- a/netbox/ipam/tests/test_models.py
+++ b/netbox/ipam/tests/test_models.py
@@ -1,6 +1,7 @@
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from netaddr import IPNetwork, IPSet
+from utilities.data import string_to_ranges
from ipam.choices import *
from ipam.models import *
@@ -509,8 +510,7 @@ class TestVLANGroup(TestCase):
vlangroup = VLANGroup.objects.create(
name='VLAN Group 1',
slug='vlan-group-1',
- min_vid=100,
- max_vid=199
+ vid_ranges=string_to_ranges('100-199'),
)
VLAN.objects.bulk_create((
VLAN(name='VLAN 100', vid=100, group=vlangroup),
@@ -533,3 +533,27 @@ class TestVLANGroup(TestCase):
VLAN.objects.create(name='VLAN 104', vid=104, group=vlangroup)
self.assertEqual(vlangroup.get_next_available_vid(), 105)
+
+ def test_vid_validation(self):
+ vlangroup = VLANGroup.objects.first()
+
+ vlan = VLAN(vid=1, name='VLAN 1', group=vlangroup)
+ with self.assertRaises(ValidationError):
+ vlan.full_clean()
+
+ vlan = VLAN(vid=109, name='VLAN 109', group=vlangroup)
+ vlan.full_clean()
+
+ def test_overlapping_vlan(self):
+ vlangroup = VLANGroup(
+ name='VLAN Group 1',
+ slug='vlan-group-1',
+ vid_ranges=string_to_ranges('2-4,3-5'),
+ )
+ with self.assertRaises(ValidationError):
+ vlangroup.full_clean()
+
+ # make sure single vlan range works
+ vlangroup.vid_ranges = string_to_ranges('2-2')
+ vlangroup.full_clean()
+ vlangroup.save()
diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py
index bc42341ba37..95b31187886 100644
--- a/netbox/ipam/tests/test_views.py
+++ b/netbox/ipam/tests/test_views.py
@@ -50,7 +50,7 @@ class ASNRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
- f"name,slug,rir,tenant,start,end,description",
+ "name,slug,rir,tenant,start,end,description",
f"ASN Range 4,asn-range-4,{rirs[1].name},{tenants[1].name},400,499,Fourth range",
f"ASN Range 5,asn-range-5,{rirs[1].name},{tenants[1].name},500,599,Fifth range",
f"ASN Range 6,asn-range-6,{rirs[1].name},{tenants[1].name},600,699,Sixth range",
@@ -764,21 +764,20 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
cls.form_data = {
'name': 'VLAN Group X',
'slug': 'vlan-group-x',
- 'min_vid': 1,
- 'max_vid': 4094,
'description': 'A new VLAN group',
+ 'vid_ranges': '100-199,300-399',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
- f"name,slug,scope_type,scope_id,description",
- f"VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
+ "name,slug,scope_type,scope_id,description",
+ "VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
f"VLAN Group 5,vlan-group-5,dcim.site,{sites[0].pk},Fifth VLAN group",
f"VLAN Group 6,vlan-group-6,dcim.site,{sites[1].pk},Sixth VLAN group",
)
cls.csv_update_data = (
- f"id,name,description",
+ "id,name,description",
f"{vlan_groups[0].pk},VLAN Group 7,Fourth VLAN group7",
f"{vlan_groups[1].pk},VLAN Group 8,Fifth VLAN group8",
f"{vlan_groups[2].pk},VLAN Group 9,Sixth VLAN group9",
diff --git a/netbox/ipam/utils.py b/netbox/ipam/utils.py
index 21b90fbcd46..3297abd8fb4 100644
--- a/netbox/ipam/utils.py
+++ b/netbox/ipam/utils.py
@@ -90,44 +90,58 @@ def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
return output
-def add_available_vlans(vlans, vlan_group=None):
+def available_vlans_from_range(vlans, vlan_group, vid_range):
"""
Create fake records for all gaps between used VLANs
"""
- min_vid = vlan_group.min_vid if vlan_group else VLAN_VID_MIN
- max_vid = vlan_group.max_vid if vlan_group else VLAN_VID_MAX
+ min_vid = int(vid_range.lower) if vid_range else VLAN_VID_MIN
+ max_vid = int(vid_range.upper) if vid_range else VLAN_VID_MAX
if not vlans:
return [{
'vid': min_vid,
'vlan_group': vlan_group,
- 'available': max_vid - min_vid + 1
+ 'available': max_vid - min_vid
}]
- prev_vid = max_vid
+ prev_vid = min_vid - 1
new_vlans = []
for vlan in vlans:
+
+ # Ignore VIDs outside the range
+ if not min_vid <= vlan.vid < max_vid:
+ continue
+
+ # Annotate any available VIDs between the previous (or minimum) VID
+ # and the current VID
if vlan.vid - prev_vid > 1:
new_vlans.append({
'vid': prev_vid + 1,
'vlan_group': vlan_group,
'available': vlan.vid - prev_vid - 1,
})
+
prev_vid = vlan.vid
- if vlans[0].vid > min_vid:
- new_vlans.append({
- 'vid': min_vid,
- 'vlan_group': vlan_group,
- 'available': vlans[0].vid - min_vid,
- })
+ # Annotate any remaining available VLANs
if prev_vid < max_vid:
new_vlans.append({
'vid': prev_vid + 1,
'vlan_group': vlan_group,
- 'available': max_vid - prev_vid,
+ 'available': max_vid - prev_vid - 1,
})
+ return new_vlans
+
+
+def add_available_vlans(vlans, vlan_group):
+ """
+ Create fake records for all gaps between used VLANs
+ """
+ new_vlans = []
+ for vid_range in vlan_group.vid_ranges:
+ new_vlans.extend(available_vlans_from_range(vlans, vlan_group, vid_range))
+
vlans = list(vlans) + new_vlans
vlans.sort(key=lambda v: v.vid if type(v) is VLAN else v['vid'])
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index 12c86c53315..67d56f15e77 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -7,6 +7,7 @@ from django.utils.translation import gettext as _
from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet
+from dcim.forms import InterfaceFilterForm
from dcim.models import Interface, Site
from netbox.views import generic
from tenancy.views import ObjectContactsView
@@ -14,6 +15,7 @@ from utilities.query import count_related
from utilities.tables import get_table_ordering
from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet
+from virtualization.forms import VMInterfaceFilterForm
from virtualization.models import VMInterface
from . import filtersets, forms, tables
from .choices import PrefixStatusChoices
@@ -206,6 +208,7 @@ class ASNRangeASNsView(generic.ObjectChildrenView):
child_model = ASN
table = tables.ASNTable
filterset = filtersets.ASNFilterSet
+ filterset_form = forms.ASNFilterForm
tab = ViewTab(
label=_('ASNs'),
badge=lambda x: x.get_child_asns().count(),
@@ -337,6 +340,7 @@ class AggregatePrefixesView(generic.ObjectChildrenView):
child_model = Prefix
table = tables.PrefixTable
filterset = filtersets.PrefixFilterSet
+ filterset_form = forms.PrefixFilterForm
template_name = 'ipam/aggregate/prefixes.html'
tab = ViewTab(
label=_('Prefixes'),
@@ -523,6 +527,7 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
child_model = Prefix
table = tables.PrefixTable
filterset = filtersets.PrefixFilterSet
+ filterset_form = forms.PrefixFilterForm
template_name = 'ipam/prefix/prefixes.html'
tab = ViewTab(
label=_('Child Prefixes'),
@@ -558,6 +563,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
child_model = IPRange
table = tables.IPRangeTable
filterset = filtersets.IPRangeFilterSet
+ filterset_form = forms.IPRangeFilterForm
template_name = 'ipam/prefix/ip_ranges.html'
tab = ViewTab(
label=_('Child Ranges'),
@@ -584,6 +590,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
child_model = IPAddress
table = tables.IPAddressTable
filterset = filtersets.IPAddressFilterSet
+ filterset_form = forms.IPAddressFilterForm
template_name = 'ipam/prefix/ip_addresses.html'
tab = ViewTab(
label=_('IP Addresses'),
@@ -683,6 +690,7 @@ class IPRangeIPAddressesView(generic.ObjectChildrenView):
child_model = IPAddress
table = tables.IPAddressTable
filterset = filtersets.IPAddressFilterSet
+ filterset_form = forms.IPRangeFilterForm
template_name = 'ipam/iprange/ip_addresses.html'
tab = ViewTab(
label=_('IP Addresses'),
@@ -885,6 +893,7 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView):
child_model = IPAddress
table = tables.IPAddressTable
filterset = filtersets.IPAddressFilterSet
+ filterset_form = forms.IPAddressFilterForm
tab = ViewTab(
label=_('Related IPs'),
badge=lambda x: x.get_related_ips().count(),
@@ -906,7 +915,7 @@ class IPAddressContactsView(ObjectContactsView):
#
class VLANGroupListView(generic.ObjectListView):
- queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
+ queryset = VLANGroup.objects.annotate_utilization()
filterset = filtersets.VLANGroupFilterSet
filterset_form = forms.VLANGroupFilterForm
table = tables.VLANGroupTable
@@ -914,7 +923,7 @@ class VLANGroupListView(generic.ObjectListView):
@register_model_view(VLANGroup)
class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
- queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
+ queryset = VLANGroup.objects.annotate_utilization()
def get_extra_context(self, request, instance):
return {
@@ -957,6 +966,7 @@ class VLANGroupVLANsView(generic.ObjectChildrenView):
child_model = VLAN
table = tables.VLANTable
filterset = filtersets.VLANFilterSet
+ filterset_form = forms.VLANFilterForm
tab = ViewTab(
label=_('VLANs'),
badge=lambda x: x.get_child_vlans().count(),
@@ -1112,6 +1122,7 @@ class VLANInterfacesView(generic.ObjectChildrenView):
child_model = Interface
table = tables.VLANDevicesTable
filterset = InterfaceFilterSet
+ filterset_form = InterfaceFilterForm
tab = ViewTab(
label=_('Device Interfaces'),
badge=lambda x: x.get_interfaces().count(),
@@ -1129,6 +1140,7 @@ class VLANVMInterfacesView(generic.ObjectChildrenView):
child_model = VMInterface
table = tables.VLANVirtualMachinesTable
filterset = VMInterfaceFilterSet
+ filterset_form = VMInterfaceFilterForm
tab = ViewTab(
label=_('VM Interfaces'),
badge=lambda x: x.get_vminterfaces().count(),
diff --git a/netbox/netbox/admin.py b/netbox/netbox/admin.py
deleted file mode 100644
index cdfacc1418a..00000000000
--- a/netbox/netbox/admin.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from django.conf import settings
-from django.contrib.admin import site as admin_site
-from taggit.models import Tag
-
-
-# Override default AdminSite attributes so we can avoid creating and
-# registering our own class
-admin_site.site_header = 'NetBox Administration'
-admin_site.site_title = 'NetBox'
-admin_site.site_url = '/{}'.format(settings.BASE_PATH)
-admin_site.index_template = 'admin/index.html'
-
-# Unregister the unused stock Tag model provided by django-taggit
-admin_site.unregister(Tag)
diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py
index 08ffd0bc45d..e7d1ef5745b 100644
--- a/netbox/netbox/api/fields.py
+++ b/netbox/netbox/api/fields.py
@@ -1,4 +1,5 @@
from django.core.exceptions import ObjectDoesNotExist
+from django.db.backends.postgresql.psycopg_any import NumericRange
from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
@@ -11,6 +12,7 @@ __all__ = (
'ChoiceField',
'ContentTypeField',
'IPNetworkSerializer',
+ 'IntegerRangeSerializer',
'RelatedObjectCountField',
'SerializedPKRelatedField',
)
@@ -154,3 +156,19 @@ class RelatedObjectCountField(serializers.ReadOnlyField):
self.relation = relation
super().__init__(**kwargs)
+
+
+class IntegerRangeSerializer(serializers.Serializer):
+ """
+ Represents a range of integers.
+ """
+ def to_internal_value(self, data):
+ if not isinstance(data, (list, tuple)) or len(data) != 2:
+ raise ValidationError(_("Ranges must be specified in the form (lower, upper)."))
+ if type(data[0]) is not int or type(data[1]) is not int:
+ raise ValidationError(_("Range boundaries must be defined as integers."))
+
+ return NumericRange(data[0], data[1], bounds='[]')
+
+ def to_representation(self, instance):
+ return instance.lower, instance.upper - 1
diff --git a/netbox/netbox/api/serializers/fields.py b/netbox/netbox/api/serializers/fields.py
index 4fee3604376..74782bddfcf 100644
--- a/netbox/netbox/api/serializers/fields.py
+++ b/netbox/netbox/api/serializers/fields.py
@@ -1,5 +1,8 @@
+from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
+from utilities.views import get_viewname
+
__all__ = (
'NetBoxAPIHyperlinkedIdentityField',
'NetBoxURLHyperlinkedIdentityField',
@@ -30,12 +33,10 @@ class BaseNetBoxHyperlinkedIdentityField(serializers.HyperlinkedIdentityField):
lookup_value = getattr(obj, self.lookup_field)
kwargs = {self.lookup_url_kwarg: lookup_value}
- model_name = self.parent.Meta.model._meta.model_name
- app_name = self.parent.Meta.model._meta.app_label
- view_name = self.get_view_name(app_name, model_name)
+ view_name = self.get_view_name(obj)
return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
- def get_view_name(self, app_name, model_name):
+ def get_view_name(self, model):
raise NotImplementedError(_('{class_name} must implement get_view_name()').format(
class_name=self.__class__.__name__
))
@@ -43,11 +44,11 @@ class BaseNetBoxHyperlinkedIdentityField(serializers.HyperlinkedIdentityField):
class NetBoxAPIHyperlinkedIdentityField(BaseNetBoxHyperlinkedIdentityField):
- def get_view_name(self, app_name, model_name):
- return f'{app_name}-api:{model_name}-detail'
+ def get_view_name(self, model):
+ return get_viewname(model=model, action='detail', rest_api=True)
class NetBoxURLHyperlinkedIdentityField(BaseNetBoxHyperlinkedIdentityField):
- def get_view_name(self, app_name, model_name):
- return f'{app_name}:{model_name}'
+ def get_view_name(self, model):
+ return get_viewname(model=model)
diff --git a/netbox/netbox/api/serializers/nested.py b/netbox/netbox/api/serializers/nested.py
index af4ae35cbed..4a5fc621452 100644
--- a/netbox/netbox/api/serializers/nested.py
+++ b/netbox/netbox/api/serializers/nested.py
@@ -1,5 +1,3 @@
-from rest_framework import serializers
-
from extras.models import Tag
from utilities.api import get_related_object_by_attrs
from .base import BaseModelSerializer
@@ -21,7 +19,7 @@ class WritableNestedSerializer(BaseModelSerializer):
return get_related_object_by_attrs(queryset, data)
-# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers
+# Declared here for use by PrimaryModelSerializer
class NestedTagSerializer(WritableNestedSerializer):
class Meta:
diff --git a/netbox/netbox/authentication/__init__.py b/netbox/netbox/authentication/__init__.py
index 55fd91d4d56..f80454f9998 100644
--- a/netbox/netbox/authentication/__init__.py
+++ b/netbox/netbox/authentication/__init__.py
@@ -2,7 +2,6 @@ import logging
from collections import defaultdict
from django.conf import settings
-from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured
@@ -10,23 +9,21 @@ from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from users.constants import CONSTRAINT_TOKEN_USER
-from users.models import Group, ObjectPermission
+from users.models import Group, ObjectPermission, User
from utilities.permissions import (
permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_type,
)
from .misc import _mirror_groups
-UserModel = get_user_model()
-
AUTH_BACKEND_ATTRS = {
# backend name: title, MDI icon name
'amazon': ('Amazon AWS', 'aws'),
'apple': ('Apple', 'apple'),
'auth0': ('Auth0', None),
- 'azuread-oauth2': ('Microsoft Azure AD', 'microsoft'),
- 'azuread-b2c-oauth2': ('Microsoft Azure AD', 'microsoft'),
- 'azuread-tenant-oauth2': ('Microsoft Azure AD', 'microsoft'),
- 'azuread-v2-tenant-oauth2': ('Microsoft Azure AD', 'microsoft'),
+ 'entraid-oauth2': ('Microsoft Entra ID', 'microsoft'),
+ 'entraid-b2c-oauth2': ('Microsoft Entra ID', 'microsoft'),
+ 'entraid-tenant-oauth2': ('Microsoft Entra ID', 'microsoft'),
+ 'entraid-v2-tenant-oauth2': ('Microsoft Entra ID', 'microsoft'),
'bitbucket': ('BitBucket', 'bitbucket'),
'bitbucket-oauth2': ('BitBucket', 'bitbucket'),
'digitalocean': ('DigitalOcean', 'digital-ocean'),
@@ -49,12 +46,15 @@ AUTH_BACKEND_ATTRS = {
'okta-openidconnect': ('Okta (OIDC)', None),
'salesforce-oauth2': ('Salesforce', 'salesforce'),
}
+# Override with potential user configuration
+AUTH_BACKEND_ATTRS.update(getattr(settings, 'SOCIAL_AUTH_BACKEND_ATTRS', {}))
def get_auth_backend_display(name):
"""
- Return the user-friendly name and icon name for a remote authentication backend, if known. Defaults to the
- raw backend name and no icon.
+ Return the user-friendly name and icon name for a remote authentication backend, if
+ known. Obtained from the defaults dictionary AUTH_BACKEND_ATTRS, overridden by the
+ setting `SOCIAL_AUTH_BACKEND_ATTRS`. Defaults to the raw backend name and no icon.
"""
return AUTH_BACKEND_ATTRS.get(name, (name, None))
@@ -215,15 +215,15 @@ class RemoteUserBackend(_RemoteUserBackend):
# instead we use get_or_create when creating unknown users since it has
# built-in safeguards for multiple threads.
if self.create_unknown_user:
- user, created = UserModel._default_manager.get_or_create(**{
- UserModel.USERNAME_FIELD: username
+ user, created = User._default_manager.get_or_create(**{
+ User.USERNAME_FIELD: username
})
if created:
user = self.configure_user(request, user)
else:
try:
- user = UserModel._default_manager.get_by_natural_key(username)
- except UserModel.DoesNotExist:
+ user = User._default_manager.get_by_natural_key(username)
+ except User.DoesNotExist:
pass
if self.user_can_authenticate(user):
if settings.REMOTE_AUTH_GROUP_SYNC_ENABLED:
diff --git a/netbox/netbox/choices.py b/netbox/netbox/choices.py
index fe941056fc0..5c3110745ff 100644
--- a/netbox/netbox/choices.py
+++ b/netbox/netbox/choices.py
@@ -7,8 +7,10 @@ __all__ = (
'ButtonColorChoices',
'ColorChoices',
'CSVDelimiterChoices',
+ 'DistanceUnitChoices',
'ImportFormatChoices',
'ImportMethodChoices',
+ 'WeightUnitChoices',
)
@@ -81,10 +83,7 @@ class ColorChoices(ChoiceSet):
#
class ButtonColorChoices(ChoiceSet):
- """
- Map standard button color choices to Bootstrap 3 button classes
- """
- DEFAULT = 'outline-dark'
+ DEFAULT = 'default'
BLUE = 'blue'
INDIGO = 'indigo'
PURPLE = 'purple'
@@ -160,3 +159,39 @@ class CSVDelimiterChoices(ChoiceSet):
(SEMICOLON, _('Semicolon')),
(TAB, _('Tab')),
]
+
+
+class DistanceUnitChoices(ChoiceSet):
+
+ # Metric
+ UNIT_KILOMETER = 'km'
+ UNIT_METER = 'm'
+
+ # Imperial
+ UNIT_MILE = 'mi'
+ UNIT_FOOT = 'ft'
+
+ CHOICES = (
+ (UNIT_KILOMETER, _('Kilometers')),
+ (UNIT_METER, _('Meters')),
+ (UNIT_MILE, _('Miles')),
+ (UNIT_FOOT, _('Feet')),
+ )
+
+
+class WeightUnitChoices(ChoiceSet):
+
+ # Metric
+ UNIT_KILOGRAM = 'kg'
+ UNIT_GRAM = 'g'
+
+ # Imperial
+ UNIT_POUND = 'lb'
+ UNIT_OUNCE = 'oz'
+
+ CHOICES = (
+ (UNIT_KILOGRAM, _('Kilograms')),
+ (UNIT_GRAM, _('Grams')),
+ (UNIT_POUND, _('Pounds')),
+ (UNIT_OUNCE, _('Ounces')),
+ )
diff --git a/netbox/netbox/config/__init__.py b/netbox/netbox/config/__init__.py
index 1c16d6769a9..23108f1d28c 100644
--- a/netbox/netbox/config/__init__.py
+++ b/netbox/netbox/config/__init__.py
@@ -85,7 +85,7 @@ class Config:
logger.debug("Loaded configuration data from database")
except DatabaseError:
# The database may not be available yet (e.g. when running a management command)
- logger.warning(f"Skipping config initialization (database unavailable)")
+ logger.warning("Skipping config initialization (database unavailable)")
return
revision.activate()
diff --git a/netbox/netbox/configuration_testing.py b/netbox/netbox/configuration_testing.py
index 346cd89d20f..cec05cabbf6 100644
--- a/netbox/netbox/configuration_testing.py
+++ b/netbox/netbox/configuration_testing.py
@@ -39,8 +39,6 @@ REDIS = {
SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
-DJANGO_ADMIN_ENABLED = True
-
DEFAULT_PERMISSIONS = {}
LOGGING = {
diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py
index e797f4f295e..b8c679ec04a 100644
--- a/netbox/netbox/constants.py
+++ b/netbox/netbox/constants.py
@@ -1,7 +1,3 @@
-# Prefix for nested serializers
-# TODO: Remove in v4.1
-NESTED_SERIALIZER_PREFIX = 'Nested'
-
# RQ queue names
RQ_QUEUE_DEFAULT = 'default'
RQ_QUEUE_HIGH = 'high'
@@ -27,6 +23,9 @@ ADVISORY_LOCK_KEYS = {
'wirelesslangroup': 105600,
'inventoryitem': 105700,
'inventoryitemtemplate': 105800,
+
+ # Jobs
+ 'job-schedules': 110100,
}
# Default view action permission mapping
diff --git a/netbox/netbox/data_backends.py b/netbox/netbox/data_backends.py
index d5bab75c1a8..e3a3de4d17f 100644
--- a/netbox/netbox/data_backends.py
+++ b/netbox/netbox/data_backends.py
@@ -50,4 +50,4 @@ class DataBackend:
2. Yields the local path at which data has been replicated
3. Performs any necessary cleanup
"""
- raise NotImplemented()
+ raise NotImplementedError()
diff --git a/netbox/netbox/events.py b/netbox/netbox/events.py
new file mode 100644
index 00000000000..e48c3281512
--- /dev/null
+++ b/netbox/netbox/events.py
@@ -0,0 +1,79 @@
+from dataclasses import dataclass
+
+from netbox.registry import registry
+
+EVENT_TYPE_KIND_INFO = 'info'
+EVENT_TYPE_KIND_SUCCESS = 'success'
+EVENT_TYPE_KIND_WARNING = 'warning'
+EVENT_TYPE_KIND_DANGER = 'danger'
+
+__all__ = (
+ 'EVENT_TYPE_KIND_DANGER',
+ 'EVENT_TYPE_KIND_INFO',
+ 'EVENT_TYPE_KIND_SUCCESS',
+ 'EVENT_TYPE_KIND_WARNING',
+ 'EventType',
+ 'get_event_type',
+ 'get_event_type_choices',
+ 'get_event_text',
+)
+
+
+def get_event_type(name):
+ return registry['event_types'].get(name)
+
+
+def get_event_text(name):
+ if event := registry['event_types'].get(name):
+ return event.text
+ return ''
+
+
+def get_event_type_choices():
+ return [
+ (event.name, event.text) for event in registry['event_types'].values()
+ ]
+
+
+@dataclass
+class EventType:
+ """
+ A type of event which can occur in NetBox. Event rules can be defined to automatically
+ perform some action in response to an event.
+
+ Args:
+ name: The unique name under which the event is registered.
+ text: The human-friendly event name. This should support translation.
+ kind: The event's classification (info, success, warning, or danger). The default type is info.
+ destructive: Indicates that the associated object was destroyed as a result of the event (default: False).
+ """
+ name: str
+ text: str
+ kind: str = EVENT_TYPE_KIND_INFO
+ destructive: bool = False
+
+ def __str__(self):
+ return self.text
+
+ def register(self):
+ if self.name in registry['event_types']:
+ raise Exception(f"An event type named {self.name} has already been registered!")
+ registry['event_types'][self.name] = self
+
+ @property
+ def color(self):
+ return {
+ EVENT_TYPE_KIND_INFO: 'blue',
+ EVENT_TYPE_KIND_SUCCESS: 'green',
+ EVENT_TYPE_KIND_WARNING: 'orange',
+ EVENT_TYPE_KIND_DANGER: 'red',
+ }.get(self.kind)
+
+ @property
+ def icon(self):
+ return {
+ EVENT_TYPE_KIND_INFO: 'mdi mdi-information',
+ EVENT_TYPE_KIND_SUCCESS: 'mdi mdi-check-circle',
+ EVENT_TYPE_KIND_WARNING: 'mdi mdi-alert-box',
+ EVENT_TYPE_KIND_DANGER: 'mdi mdi-alert-octagon',
+ }.get(self.kind)
diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py
index ac43fe57f44..637a40bf114 100644
--- a/netbox/netbox/filtersets.py
+++ b/netbox/netbox/filtersets.py
@@ -133,7 +133,7 @@ class BaseFilterSet(django_filters.FilterSet):
django_filters.ModelChoiceFilter,
django_filters.ModelMultipleChoiceFilter,
TagFilter
- )) or existing_filter.extra.get('choices'):
+ )):
# These filter types support only negation
return FILTER_NEGATION_LOOKUP_MAP
@@ -172,6 +172,7 @@ class BaseFilterSet(django_filters.FilterSet):
# Create new filters for each lookup expression in the map
for lookup_name, lookup_expr in lookup_map.items():
new_filter_name = f'{existing_filter_name}__{lookup_name}'
+ existing_filter_extra = deepcopy(existing_filter.extra)
try:
if existing_filter_name in cls.declared_filters:
@@ -179,14 +180,18 @@ class BaseFilterSet(django_filters.FilterSet):
# create the new filter with the same type because there is no guarantee the defined type
# is the same as the default type for the field
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
- filter_cls = django_filters.BooleanFilter if lookup_expr == 'empty' else type(existing_filter)
+ filter_cls = type(existing_filter)
+ if lookup_expr == 'empty':
+ filter_cls = django_filters.BooleanFilter
+ for param_to_remove in ('choices', 'null_value'):
+ existing_filter_extra.pop(param_to_remove, None)
new_filter = filter_cls(
field_name=field_name,
lookup_expr=lookup_expr,
label=existing_filter.label,
exclude=existing_filter.exclude,
distinct=existing_filter.distinct,
- **existing_filter.extra
+ **existing_filter_extra
)
elif hasattr(existing_filter, 'custom_field'):
# Filter is for a custom field
diff --git a/netbox/netbox/forms/__init__.py b/netbox/netbox/forms/__init__.py
index fa82689a5de..f88fb18bc92 100644
--- a/netbox/netbox/forms/__init__.py
+++ b/netbox/netbox/forms/__init__.py
@@ -1,7 +1,7 @@
import re
from django import forms
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
from netbox.search import LookupTypes
from netbox.search.backends import search_backend
@@ -36,7 +36,8 @@ class SearchForm(forms.Form):
lookup = forms.ChoiceField(
choices=LOOKUP_CHOICES,
initial=LookupTypes.PARTIAL,
- required=False
+ required=False,
+ label=_('Lookup')
)
def __init__(self, *args, **kwargs):
diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py
index d59f61ef9b9..74ac4b0e005 100644
--- a/netbox/netbox/forms/base.py
+++ b/netbox/netbox/forms/base.py
@@ -60,6 +60,8 @@ class NetBoxModelForm(CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms
if value in self.fields[cf_name].empty_values:
self.instance.custom_field_data[key] = None
else:
+ if customfield.type == CustomFieldTypeChoices.TYPE_JSON and type(value) is str:
+ value = json.loads(value)
self.instance.custom_field_data[key] = customfield.serialize(value)
return super().clean()
diff --git a/netbox/netbox/graphql/filter_mixins.py b/netbox/netbox/graphql/filter_mixins.py
index 5075e9aa282..2044a1ddeeb 100644
--- a/netbox/netbox/graphql/filter_mixins.py
+++ b/netbox/netbox/graphql/filter_mixins.py
@@ -1,4 +1,4 @@
-from functools import partial, partialmethod, wraps
+from functools import partialmethod
from typing import List
import django_filters
@@ -6,6 +6,7 @@ import strawberry
import strawberry_django
from django.core.exceptions import FieldDoesNotExist
from strawberry import auto
+
from ipam.fields import ASNField
from netbox.graphql.scalars import BigInt
from utilities.fields import ColorField, CounterCacheField
@@ -47,7 +48,7 @@ def map_strawberry_type(field):
pass
elif isinstance(field, NumericArrayFilter):
should_create_function = True
- attr_type = int
+ attr_type = int | None
elif isinstance(field, TreeNodeMultipleChoiceFilter):
should_create_function = True
attr_type = List[str] | None
@@ -108,8 +109,7 @@ def map_strawberry_type(field):
elif issubclass(type(field), django_filters.TypedMultipleChoiceFilter):
pass
elif issubclass(type(field), django_filters.MultipleChoiceFilter):
- should_create_function = True
- attr_type = List[str] | None
+ attr_type = str | None
elif issubclass(type(field), django_filters.TypedChoiceFilter):
pass
elif issubclass(type(field), django_filters.ChoiceFilter):
@@ -201,4 +201,9 @@ def autotype_decorator(filterset):
class BaseFilterMixin:
def filter_by_filterset(self, queryset, key):
- return self.filterset(data={key: getattr(self, key)}, queryset=queryset).qs
+ filterset = self.filterset(data={key: getattr(self, key)}, queryset=queryset)
+ if not filterset.is_valid():
+ # We could raise validation error but strawberry logs it all to the
+ # console i.e. raise ValidationError(f"{k}: {v[0]}")
+ return filterset.qs.none()
+ return filterset.qs
diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py
index 2b4c83405bb..a7609c9d206 100644
--- a/netbox/netbox/graphql/schema.py
+++ b/netbox/netbox/graphql/schema.py
@@ -1,5 +1,7 @@
import strawberry
+from django.conf import settings
from strawberry_django.optimizer import DjangoOptimizerExtension
+from strawberry.extensions import MaxAliasesLimiter
from strawberry.schema.config import StrawberryConfig
from circuits.graphql.schema import CircuitsQuery
@@ -36,6 +38,7 @@ schema = strawberry.Schema(
query=Query,
config=StrawberryConfig(auto_camel_case=False),
extensions=[
- DjangoOptimizerExtension,
+ DjangoOptimizerExtension(prefetch_custom_queryset=True),
+ MaxAliasesLimiter(max_alias_count=settings.GRAPHQL_MAX_ALIASES),
]
)
diff --git a/netbox/netbox/graphql/views.py b/netbox/netbox/graphql/views.py
index b347d71b42a..85a01f02501 100644
--- a/netbox/netbox/graphql/views.py
+++ b/netbox/netbox/graphql/views.py
@@ -1,10 +1,6 @@
-import json
-
from django.conf import settings
from django.contrib.auth.views import redirect_to_login
from django.http import HttpResponseNotFound, HttpResponseForbidden
-from django.http import HttpResponse
-from django.template import loader
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.exceptions import AuthenticationFailed
@@ -46,9 +42,3 @@ class NetBoxGraphQLView(GraphQLView):
return HttpResponseForbidden("No credentials provided.")
return super().dispatch(request, *args, **kwargs)
-
- def render_graphql_ide(self, request):
- template = loader.get_template("graphiql.html")
- context = {"SUBSCRIPTION_ENABLED": json.dumps(self.subscriptions_enabled)}
-
- return HttpResponse(template.render(context, request))
diff --git a/netbox/netbox/jobs.py b/netbox/netbox/jobs.py
new file mode 100644
index 00000000000..087c2489687
--- /dev/null
+++ b/netbox/netbox/jobs.py
@@ -0,0 +1,134 @@
+import logging
+from abc import ABC, abstractmethod
+from datetime import timedelta
+
+from django.utils.functional import classproperty
+from django_pglocks import advisory_lock
+from rq.timeouts import JobTimeoutException
+
+from core.choices import JobStatusChoices
+from core.models import Job, ObjectType
+from netbox.constants import ADVISORY_LOCK_KEYS
+
+__all__ = (
+ 'JobRunner',
+)
+
+
+class JobRunner(ABC):
+ """
+ Background Job helper class.
+
+ This class handles the execution of a background job. It is responsible for maintaining its state, reporting errors,
+ and scheduling recurring jobs.
+ """
+
+ class Meta:
+ pass
+
+ def __init__(self, job):
+ """
+ Args:
+ job: The specific `Job` this `JobRunner` is executing.
+ """
+ self.job = job
+
+ @classproperty
+ def name(cls):
+ return getattr(cls.Meta, 'name', cls.__name__)
+
+ @abstractmethod
+ def run(self, *args, **kwargs):
+ """
+ Run the job.
+
+ A `JobRunner` class needs to implement this method to execute all commands of the job.
+ """
+ pass
+
+ @classmethod
+ def handle(cls, job, *args, **kwargs):
+ """
+ Handle the execution of a `Job`.
+
+ This method is called by the Job Scheduler to handle the execution of all job commands. It will maintain the
+ job's metadata and handle errors. For periodic jobs, a new job is automatically scheduled using its `interval`.
+ """
+ try:
+ job.start()
+ cls(job).run(*args, **kwargs)
+ job.terminate()
+
+ except Exception as e:
+ job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
+ if type(e) is JobTimeoutException:
+ logging.error(e)
+
+ # If the executed job is a periodic job, schedule its next execution at the specified interval.
+ finally:
+ if job.interval:
+ new_scheduled_time = (job.scheduled or job.started) + timedelta(minutes=job.interval)
+ cls.enqueue(
+ instance=job.object,
+ user=job.user,
+ schedule_at=new_scheduled_time,
+ interval=job.interval,
+ **kwargs,
+ )
+
+ @classmethod
+ def get_jobs(cls, instance=None):
+ """
+ Get all jobs of this `JobRunner` related to a specific instance.
+ """
+ jobs = Job.objects.filter(name=cls.name)
+
+ if instance:
+ object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False)
+ jobs = jobs.filter(
+ object_type=object_type,
+ object_id=instance.pk,
+ )
+
+ return jobs
+
+ @classmethod
+ def enqueue(cls, *args, **kwargs):
+ """
+ Enqueue a new `Job`.
+
+ This method is a wrapper of `Job.enqueue()` using `handle()` as function callback. See its documentation for
+ parameters.
+ """
+ name = kwargs.pop('name', None) or cls.name
+ return Job.enqueue(cls.handle, name=name, *args, **kwargs)
+
+ @classmethod
+ @advisory_lock(ADVISORY_LOCK_KEYS['job-schedules'])
+ def enqueue_once(cls, instance=None, schedule_at=None, interval=None, *args, **kwargs):
+ """
+ Enqueue a new `Job` once, i.e. skip duplicate jobs.
+
+ Like `enqueue()`, this method adds a new `Job` to the job queue. However, if there's already a job of this
+ class scheduled for `instance`, the existing job will be updated if necessary. This ensures that a particular
+ schedule is only set up once at any given time, i.e. multiple calls to this method are idempotent.
+
+ Note that this does not forbid running additional jobs with the `enqueue()` method, e.g. to schedule an
+ immediate synchronization job in addition to a periodic synchronization schedule.
+
+ For additional parameters see `enqueue()`.
+
+ Args:
+ instance: The NetBox object to which this job pertains (optional)
+ schedule_at: Schedule the job to be executed at the passed date and time
+ interval: Recurrence interval (in minutes)
+ """
+ job = cls.get_jobs(instance).filter(status__in=JobStatusChoices.ENQUEUED_STATE_CHOICES).first()
+ if job:
+ # If the job parameters haven't changed, don't schedule a new job and keep the current schedule. Otherwise,
+ # delete the existing job and schedule a new job instead.
+ if (schedule_at and job.scheduled == schedule_at) and (job.interval == interval):
+ return job
+ job.delete()
+
+ return cls.enqueue(instance=instance, schedule_at=schedule_at, interval=interval, *args, **kwargs)
diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py
index 3d94734eee9..8012965a46f 100644
--- a/netbox/netbox/middleware.py
+++ b/netbox/netbox/middleware.py
@@ -36,6 +36,11 @@ class CoreMiddleware:
with event_tracking(request):
response = self.get_response(request)
+ # Check if language cookie should be renewed
+ if request.user.is_authenticated and settings.SESSION_SAVE_EVERY_REQUEST:
+ if language := request.user.config.get('locale.language'):
+ response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
+
# Attach the unique request ID as an HTTP header.
response['X-Request-ID'] = request.id
diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py
index 2c262b25867..b1f7cfd4891 100644
--- a/netbox/netbox/models/__init__.py
+++ b/netbox/netbox/models/__init__.py
@@ -3,6 +3,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import ValidationError
from django.db import models
+from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
@@ -29,6 +30,7 @@ class NetBoxFeatureSet(
CustomValidationMixin,
ExportTemplatesMixin,
JournalingMixin,
+ NotificationsMixin,
TagsMixin,
EventRulesMixin
):
@@ -39,6 +41,9 @@ class NetBoxFeatureSet(
def docs_url(self):
return f'{settings.STATIC_URL}docs/models/{self._meta.app_label}/{self._meta.model_name}/'
+ def get_absolute_url(self):
+ return reverse(f'{self._meta.app_label}:{self._meta.model_name}', args=[self.pk])
+
#
# Base model classes
@@ -161,7 +166,7 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
super().clean()
# An MPTT model cannot be its own parent
- if self.pk and self.parent and self.parent in self.get_descendants(include_self=True):
+ if not self._state.adding and self.parent and self.parent in self.get_descendants(include_self=True):
raise ValidationError({
"parent": "Cannot assign self or child {type} as parent.".format(type=self._meta.verbose_name)
})
diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py
index 0393bf25dfd..a972277705c 100644
--- a/netbox/netbox/models/features.py
+++ b/netbox/netbox/models/features.py
@@ -34,6 +34,7 @@ __all__ = (
'ImageAttachmentsMixin',
'JobsMixin',
'JournalingMixin',
+ 'NotificationsMixin',
'SyncedDataMixin',
'TagsMixin',
'register_models',
@@ -287,8 +288,8 @@ class CustomFieldsMixin(models.Model):
))
# Validate uniqueness if enforced
- if custom_fields[field_name].validation_unique and value not in CUSTOMFIELD_EMPTY_VALUES:
- if self._meta.model.objects.filter(**{
+ if custom_fields[field_name].unique and value not in CUSTOMFIELD_EMPTY_VALUES:
+ if self._meta.model.objects.exclude(pk=self.pk).filter(**{
f'custom_field_data__{field_name}': value
}).exists():
raise ValidationError(_("Custom field '{name}' must have a unique value.").format(
@@ -377,6 +378,20 @@ class BookmarksMixin(models.Model):
abstract = True
+class NotificationsMixin(models.Model):
+ """
+ Enables support for user notifications.
+ """
+ subscriptions = GenericRelation(
+ to='extras.Subscription',
+ content_type_field='object_type',
+ object_id_field='object_id'
+ )
+
+ class Meta:
+ abstract = True
+
+
class JobsMixin(models.Model):
"""
Enables support for job results.
@@ -393,14 +408,9 @@ class JobsMixin(models.Model):
def get_latest_jobs(self):
"""
- Return a dictionary mapping of the most recent jobs for this instance.
+ Return a list of the most recent jobs for this instance.
"""
- return {
- job.name: job
- for job in self.jobs.filter(
- status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
- ).order_by('name', '-created').distinct('name').defer('data')
- }
+ return self.jobs.filter(status__in=JobStatusChoices.TERMINAL_STATE_CHOICES).order_by('-created').defer('data')
class JournalingMixin(models.Model):
@@ -582,13 +592,14 @@ FEATURES_MAP = {
'custom_fields': CustomFieldsMixin,
'custom_links': CustomLinksMixin,
'custom_validation': CustomValidationMixin,
+ 'event_rules': EventRulesMixin,
'export_templates': ExportTemplatesMixin,
'image_attachments': ImageAttachmentsMixin,
'jobs': JobsMixin,
'journaling': JournalingMixin,
+ 'notifications': NotificationsMixin,
'synced_data': SyncedDataMixin,
'tags': TagsMixin,
- 'event_rules': EventRulesMixin,
}
registry['model_features'].update({
diff --git a/netbox/netbox/models/mixins.py b/netbox/netbox/models/mixins.py
new file mode 100644
index 00000000000..804e0b71ada
--- /dev/null
+++ b/netbox/netbox/models/mixins.py
@@ -0,0 +1,97 @@
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+from netbox.choices import *
+from utilities.conversion import to_grams, to_meters
+
+__all__ = (
+ 'DistanceMixin',
+ 'WeightMixin',
+)
+
+
+class WeightMixin(models.Model):
+ weight = models.DecimalField(
+ verbose_name=_('weight'),
+ max_digits=8,
+ decimal_places=2,
+ blank=True,
+ null=True
+ )
+ weight_unit = models.CharField(
+ verbose_name=_('weight unit'),
+ max_length=50,
+ choices=WeightUnitChoices,
+ blank=True,
+ )
+ # Stores the normalized weight (in grams) for database ordering
+ _abs_weight = models.PositiveBigIntegerField(
+ blank=True,
+ null=True
+ )
+
+ class Meta:
+ abstract = True
+
+ def save(self, *args, **kwargs):
+
+ # Store the given weight (if any) in grams for use in database ordering
+ if self.weight and self.weight_unit:
+ self._abs_weight = to_grams(self.weight, self.weight_unit)
+ else:
+ self._abs_weight = None
+
+ super().save(*args, **kwargs)
+
+ def clean(self):
+ super().clean()
+
+ # Validate weight and weight_unit
+ if self.weight and not self.weight_unit:
+ raise ValidationError(_("Must specify a unit when setting a weight"))
+
+
+class DistanceMixin(models.Model):
+ distance = models.DecimalField(
+ verbose_name=_('distance'),
+ max_digits=8,
+ decimal_places=2,
+ blank=True,
+ null=True
+ )
+ distance_unit = models.CharField(
+ verbose_name=_('distance unit'),
+ max_length=50,
+ choices=DistanceUnitChoices,
+ blank=True,
+ )
+ # Stores the normalized distance (in meters) for database ordering
+ _abs_distance = models.DecimalField(
+ max_digits=10,
+ decimal_places=4,
+ blank=True,
+ null=True
+ )
+
+ class Meta:
+ abstract = True
+
+ def save(self, *args, **kwargs):
+ # Store the given distance (if any) in meters for use in database ordering
+ if self.distance is not None and self.distance_unit:
+ self._abs_distance = to_meters(self.distance, self.distance_unit)
+ else:
+ self._abs_distance = None
+
+ # Clear distance_unit if no distance is defined
+ if self.distance is None:
+ self.distance_unit = ''
+
+ super().save(*args, **kwargs)
+
+ def clean(self):
+ super().clean()
+
+ # Validate distance and distance_unit
+ if self.distance and not self.distance_unit:
+ raise ValidationError(_("Must specify a unit when setting a distance"))
diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py
index 6db7ac14ca8..9d8ffaaf897 100644
--- a/netbox/netbox/navigation/menu.py
+++ b/netbox/netbox/navigation/menu.py
@@ -20,19 +20,6 @@ ORGANIZATION_MENU = Menu(
get_model_item('dcim', 'location', _('Locations')),
),
),
- MenuGroup(
- label=_('Racks'),
- items=(
- get_model_item('dcim', 'rack', _('Racks')),
- get_model_item('dcim', 'rackrole', _('Rack Roles')),
- get_model_item('dcim', 'rackreservation', _('Reservations')),
- MenuItem(
- link='dcim:rack_elevation_list',
- link_text=_('Elevations'),
- permissions=['dcim.view_rack']
- ),
- ),
- ),
MenuGroup(
label=_('Tenancy'),
items=(
@@ -52,6 +39,32 @@ ORGANIZATION_MENU = Menu(
),
)
+RACKS_MENU = Menu(
+ label=_('Racks'),
+ icon_class='mdi mdi-door-sliding',
+ groups=(
+ MenuGroup(
+ label=_('Racks'),
+ items=(
+ get_model_item('dcim', 'rack', _('Racks')),
+ get_model_item('dcim', 'rackrole', _('Rack Roles')),
+ get_model_item('dcim', 'rackreservation', _('Reservations')),
+ MenuItem(
+ link='dcim:rack_elevation_list',
+ link_text=_('Elevations'),
+ permissions=['dcim.view_rack']
+ ),
+ ),
+ ),
+ MenuGroup(
+ label=_('Rack Types'),
+ items=(
+ get_model_item('dcim', 'racktype', _('Rack Types')),
+ ),
+ ),
+ ),
+)
+
DEVICES_MENU = Menu(
label=_('Devices'),
icon_class='mdi mdi-server',
@@ -258,6 +271,8 @@ CIRCUITS_MENU = Menu(
items=(
get_model_item('circuits', 'circuit', _('Circuits')),
get_model_item('circuits', 'circuittype', _('Circuit Types')),
+ get_model_item('circuits', 'circuitgroup', _('Circuit Groups')),
+ get_model_item('circuits', 'circuitgroupassignment', _('Group Assignments')),
get_model_item('circuits', 'circuittermination', _('Circuit Terminations')),
),
),
@@ -355,6 +370,7 @@ OPERATIONS_MENU = Menu(
MenuGroup(
label=_('Logging'),
items=(
+ get_model_item('extras', 'notificationgroup', _('Notification Groups')),
get_model_item('extras', 'journalentry', _('Journal Entries'), actions=['import']),
get_model_item('core', 'objectchange', _('Change Log'), actions=[]),
),
@@ -370,57 +386,57 @@ ADMIN_MENU = Menu(
label=_('Authentication'),
items=(
MenuItem(
- link=f'users:user_list',
+ link='users:user_list',
link_text=_('Users'),
auth_required=True,
- permissions=[f'users.view_user'],
+ permissions=['users.view_user'],
buttons=(
MenuItemButton(
- link=f'users:user_add',
+ link='users:user_add',
title='Add',
icon_class='mdi mdi-plus-thick',
- permissions=[f'users.add_user']
+ permissions=['users.add_user']
),
MenuItemButton(
- link=f'users:user_import',
+ link='users:user_import',
title='Import',
icon_class='mdi mdi-upload',
- permissions=[f'users.add_user']
+ permissions=['users.add_user']
)
)
),
MenuItem(
- link=f'users:group_list',
+ link='users:group_list',
link_text=_('Groups'),
auth_required=True,
- permissions=[f'users.view_group'],
+ permissions=['users.view_group'],
buttons=(
MenuItemButton(
- link=f'users:group_add',
+ link='users:group_add',
title='Add',
icon_class='mdi mdi-plus-thick',
- permissions=[f'users.add_group']
+ permissions=['users.add_group']
),
MenuItemButton(
- link=f'users:group_import',
+ link='users:group_import',
title='Import',
icon_class='mdi mdi-upload',
- permissions=[f'users.add_group']
+ permissions=['users.add_group']
)
)
),
MenuItem(
- link=f'users:token_list',
+ link='users:token_list',
link_text=_('API Tokens'),
auth_required=True,
- permissions=[f'users.view_token'],
+ permissions=['users.view_token'],
buttons=get_model_buttons('users', 'token')
),
MenuItem(
- link=f'users:objectpermission_list',
+ link='users:objectpermission_list',
link_text=_('Permissions'),
auth_required=True,
- permissions=[f'users.view_objectpermission'],
+ permissions=['users.view_objectpermission'],
buttons=get_model_buttons('users', 'objectpermission', actions=['add'])
),
),
@@ -433,6 +449,11 @@ ADMIN_MENU = Menu(
link_text=_('System'),
auth_required=True
),
+ MenuItem(
+ link='core:plugin_list',
+ link_text=_('Plugins'),
+ auth_required=True
+ ),
MenuItem(
link='core:configrevision_list',
link_text=_('Configuration History'),
@@ -451,6 +472,7 @@ ADMIN_MENU = Menu(
MENUS = [
ORGANIZATION_MENU,
+ RACKS_MENU,
DEVICES_MENU,
CONNECTIONS_MENU,
WIRELESS_MENU,
@@ -462,16 +484,13 @@ MENUS = [
PROVISIONING_MENU,
CUSTOMIZATION_MENU,
OPERATIONS_MENU,
- ADMIN_MENU,
]
-#
-# Add plugin menus
-#
-
+# 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
@@ -485,3 +504,6 @@ if registry['plugins']['menu_items']:
groups=groups
)
MENUS.append(plugins_menu)
+
+# Add the admin menu last
+MENUS.append(ADMIN_MENU)
diff --git a/netbox/netbox/plugins/templates.py b/netbox/netbox/plugins/templates.py
index 5fa1959b8a7..e1f4b7a47d1 100644
--- a/netbox/netbox/plugins/templates.py
+++ b/netbox/netbox/plugins/templates.py
@@ -38,6 +38,10 @@ class PluginTemplateExtension:
return get_template(template_name).render({**self.context, **extra_context})
+ #
+ # Global methods
+ #
+
def navbar(self):
"""
Content that will be rendered inside the top navigation menu. Content should be returned as an HTML
@@ -45,6 +49,37 @@ class PluginTemplateExtension:
"""
raise NotImplementedError
+ #
+ # Object list views
+ #
+
+ def list_buttons(self):
+ """
+ Buttons that will be rendered and added to the existing list of buttons on the list view. Content
+ should be returned as an HTML string. Note that content does not need to be marked as safe because this is
+ automatically handled.
+ """
+ raise NotImplementedError
+
+ #
+ # Object detail views
+ #
+
+ def buttons(self):
+ """
+ Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content
+ should be returned as an HTML string. Note that content does not need to be marked as safe because this is
+ automatically handled.
+ """
+ raise NotImplementedError
+
+ def alerts(self):
+ """
+ Arbitrary content to be inserted at the top of an object's detail view. Content should be returned as an
+ HTML string. Note that content does not need to be marked as safe because this is automatically handled.
+ """
+ raise NotImplementedError
+
def left_page(self):
"""
Content that will be rendered on the left of the detail page view. Content should be returned as an
@@ -65,19 +100,3 @@ class PluginTemplateExtension:
HTML string. Note that content does not need to be marked as safe because this is automatically handled.
"""
raise NotImplementedError
-
- def buttons(self):
- """
- Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content
- should be returned as an HTML string. Note that content does not need to be marked as safe because this is
- automatically handled.
- """
- raise NotImplementedError
-
- def list_buttons(self):
- """
- Buttons that will be rendered and added to the existing list of buttons on the list view. Content
- should be returned as an HTML string. Note that content does not need to be marked as safe because this is
- automatically handled.
- """
- raise NotImplementedError
diff --git a/netbox/netbox/plugins/urls.py b/netbox/netbox/plugins/urls.py
index 075bda81182..7a9f30c7eff 100644
--- a/netbox/netbox/plugins/urls.py
+++ b/netbox/netbox/plugins/urls.py
@@ -3,13 +3,11 @@ from importlib import import_module
from django.apps import apps
from django.conf import settings
from django.conf.urls import include
-from django.contrib.admin.views.decorators import staff_member_required
from django.urls import path
from django.utils.module_loading import import_string, module_has_submodule
from . import views
-# Initialize URL base, API, and admin URL patterns for plugins
plugin_patterns = []
plugin_api_patterns = [
path('', views.PluginsAPIRootView.as_view(), name='api-root'),
diff --git a/netbox/netbox/plugins/views.py b/netbox/netbox/plugins/views.py
index 777a4c69e4d..6a10f2e2c0e 100644
--- a/netbox/netbox/plugins/views.py
+++ b/netbox/netbox/plugins/views.py
@@ -2,9 +2,7 @@ from collections import OrderedDict
from django.apps import apps
from django.conf import settings
-from django.shortcuts import render
from django.urls.exceptions import NoReverseMatch
-from django.views.generic import View
from drf_spectacular.utils import extend_schema
from rest_framework import permissions
from rest_framework.response import Response
diff --git a/netbox/netbox/preferences.py b/netbox/netbox/preferences.py
index d911aabb08e..4fdb7e31fe7 100644
--- a/netbox/netbox/preferences.py
+++ b/netbox/netbox/preferences.py
@@ -1,5 +1,5 @@
from django.conf import settings
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
from netbox.registry import registry
from users.preferences import UserPreference
diff --git a/netbox/netbox/registry.py b/netbox/netbox/registry.py
index d783647ec8b..0920cbccf96 100644
--- a/netbox/netbox/registry.py
+++ b/netbox/netbox/registry.py
@@ -25,6 +25,7 @@ registry = Registry({
'counter_fields': collections.defaultdict(dict),
'data_backends': dict(),
'denormalized_fields': collections.defaultdict(list),
+ 'event_types': dict(),
'model_features': dict(),
'models': collections.defaultdict(set),
'plugins': dict(),
diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py
index 227a79205e1..12243e9b64e 100644
--- a/netbox/netbox/search/backends.py
+++ b/netbox/netbox/search/backends.py
@@ -8,6 +8,7 @@ from django.db.models.fields.related import ForeignKey
from django.db.models.functions import window
from django.db.models.signals import post_delete, post_save
from django.utils.module_loading import import_string
+from django.utils.translation import gettext_lazy as _
import netaddr
from netaddr.core import AddrFormatError
@@ -39,7 +40,7 @@ class SearchBackend:
# Organize choices by category
categories = defaultdict(dict)
for label, idx in registry['search'].items():
- categories[idx.get_category()][label] = title(idx.model._meta.verbose_name)
+ categories[idx.get_category()][label] = _(title(idx.model._meta.verbose_name))
# Compile a nested tuple of choices for form rendering
results = (
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index 42ae8cb3eb7..2b057b9ab99 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -5,14 +5,12 @@ import os
import platform
import sys
import warnings
-from urllib.parse import urlencode, urlsplit
+from urllib.parse import urlencode
-import django
import requests
from django.contrib.messages import constants as messages
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.validators import URLValidator
-from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from netbox.config import PARAMS as CONFIG_PARAMS
@@ -21,7 +19,6 @@ from netbox.plugins import PluginConfig
from utilities.release import load_release_data
from utilities.string import trailing_slash
-
#
# Environment setup
#
@@ -63,7 +60,17 @@ for parameter in ('ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY', 'REDIS'):
ADMINS = getattr(configuration, 'ADMINS', [])
ALLOW_TOKEN_RETRIEVAL = getattr(configuration, 'ALLOW_TOKEN_RETRIEVAL', True)
ALLOWED_HOSTS = getattr(configuration, 'ALLOWED_HOSTS') # Required
-AUTH_PASSWORD_VALIDATORS = getattr(configuration, 'AUTH_PASSWORD_VALIDATORS', [])
+AUTH_PASSWORD_VALIDATORS = getattr(configuration, 'AUTH_PASSWORD_VALIDATORS', [
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ "OPTIONS": {
+ "min_length": 12,
+ },
+ },
+ {
+ "NAME": "utilities.password_validation.AlphanumericPasswordValidator",
+ },
+])
BASE_PATH = trailing_slash(getattr(configuration, 'BASE_PATH', ''))
CHANGELOG_SKIP_EMPTY_CHANGES = getattr(configuration, 'CHANGELOG_SKIP_EMPTY_CHANGES', True)
CENSUS_REPORTING_ENABLED = getattr(configuration, 'CENSUS_REPORTING_ENABLED', True)
@@ -84,6 +91,16 @@ DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', {
'extras.add_bookmark': ({'user': '$user'},),
'extras.change_bookmark': ({'user': '$user'},),
'extras.delete_bookmark': ({'user': '$user'},),
+ # Permit users to manage their own notifications
+ 'extras.view_notification': ({'user': '$user'},),
+ 'extras.add_notification': ({'user': '$user'},),
+ 'extras.change_notification': ({'user': '$user'},),
+ 'extras.delete_notification': ({'user': '$user'},),
+ # Permit users to manage their own subscriptions
+ 'extras.view_subscription': ({'user': '$user'},),
+ 'extras.add_subscription': ({'user': '$user'},),
+ 'extras.change_subscription': ({'user': '$user'},),
+ 'extras.delete_subscription': ({'user': '$user'},),
# Permit users to manage their own API tokens
'users.view_token': ({'user': '$user'},),
'users.add_token': ({'user': '$user'},),
@@ -91,7 +108,6 @@ DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', {
'users.delete_token': ({'user': '$user'},),
})
DEVELOPER = getattr(configuration, 'DEVELOPER', False)
-DJANGO_ADMIN_ENABLED = getattr(configuration, 'DJANGO_ADMIN_ENABLED', False)
DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
EMAIL = getattr(configuration, 'EMAIL', {})
EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', (
@@ -100,8 +116,10 @@ EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', (
EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {})
FILE_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'FILE_UPLOAD_MAX_MEMORY_SIZE', 2621440)
+GRAPHQL_MAX_ALIASES = getattr(configuration, 'GRAPHQL_MAX_ALIASES', 10)
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
+ISOLATED_DEPLOYMENT = getattr(configuration, 'ISOLATED_DEPLOYMENT', False)
JINJA2_FILTERS = getattr(configuration, 'JINJA2_FILTERS', {})
LANGUAGE_CODE = getattr(configuration, 'DEFAULT_LANGUAGE', 'en-us')
LANGUAGE_COOKIE_PATH = CSRF_COOKIE_PATH
@@ -149,6 +167,7 @@ SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', None)
SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0)
+SENTRY_SEND_DEFAULT_PII = getattr(configuration, 'SENTRY_SEND_DEFAULT_PII', False)
SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE', 0)
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
@@ -178,7 +197,7 @@ if len(SECRET_KEY) < 50:
if RELEASE_CHECK_URL:
try:
URLValidator()(RELEASE_CHECK_URL)
- except ValidationError as e:
+ except ValidationError:
raise ImproperlyConfigured(
"RELEASE_CHECK_URL must be a valid URL. Example: https://api.github.com/repos/netbox-community/netbox"
)
@@ -227,6 +246,23 @@ if STORAGE_BACKEND is not None:
return globals().get(name, default)
storages.utils.setting = _setting
+ # django-storage-swift
+ elif STORAGE_BACKEND == 'swift.storage.SwiftStorage':
+ try:
+ import swift.utils # noqa: F401
+ except ModuleNotFoundError as e:
+ if getattr(e, 'name') == 'swift':
+ raise ImproperlyConfigured(
+ f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storage-swift is not present. "
+ "It can be installed by running 'pip install django-storage-swift'."
+ )
+ raise e
+
+ # Load all SWIFT_* settings from the user configuration
+ for param, value in STORAGE_CONFIG.items():
+ if param.startswith('SWIFT_'):
+ globals()[param] = value
+
if STORAGE_CONFIG and STORAGE_BACKEND is None:
warnings.warn(
"STORAGE_CONFIG has been set in configuration.py but STORAGE_BACKEND is not defined. STORAGE_CONFIG will be "
@@ -334,7 +370,6 @@ SERVER_EMAIL = EMAIL.get('FROM_EMAIL')
#
INSTALLED_APPS = [
- 'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
@@ -372,12 +407,9 @@ INSTALLED_APPS = [
]
if not DEBUG:
INSTALLED_APPS.remove('debug_toolbar')
-if not DJANGO_ADMIN_ENABLED:
- INSTALLED_APPS.remove('django.contrib.admin')
# Middleware
MIDDLEWARE = [
- "strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware",
'corsheaders.middleware.CorsMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
@@ -392,6 +424,13 @@ MIDDLEWARE = [
'netbox.middleware.CoreMiddleware',
'netbox.middleware.MaintenanceModeMiddleware',
]
+
+if DEBUG:
+ MIDDLEWARE = [
+ "strawberry_django.middlewares.debug_toolbar.DebugToolbarMiddleware",
+ *MIDDLEWARE,
+ ]
+
if METRICS_ENABLED:
# If metrics are enabled, add the before & after Prometheus middleware
MIDDLEWARE = [
@@ -504,7 +543,6 @@ EXEMPT_EXCLUDE_MODELS = (
# All URLs starting with a string listed here are exempt from maintenance mode enforcement
MAINTENANCE_EXEMPT_PATHS = (
- f'/{BASE_PATH}admin/',
f'/{BASE_PATH}extras/config-revisions/', # Allow modifying the configuration
LOGIN_URL,
LOGIN_REDIRECT_URL,
@@ -529,7 +567,7 @@ if SENTRY_ENABLED:
release=RELEASE.full_version,
sample_rate=SENTRY_SAMPLE_RATE,
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
- send_default_pii=True,
+ send_default_pii=SENTRY_SEND_DEFAULT_PII,
http_proxy=HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
https_proxy=HTTP_PROXIES.get('https') if HTTP_PROXIES else None
)
@@ -550,7 +588,7 @@ CENSUS_PARAMS = {
'python_version': sys.version.split()[0],
'deployment_id': DEPLOYMENT_ID,
}
-if CENSUS_REPORTING_ENABLED and not DEBUG and 'test' not in sys.argv:
+if CENSUS_REPORTING_ENABLED and not ISOLATED_DEPLOYMENT and not DEBUG and 'test' not in sys.argv:
try:
# Report anonymous census data
requests.get(f'{CENSUS_URL}?{urlencode(CENSUS_PARAMS)}', timeout=3, proxies=HTTP_PROXIES)
@@ -714,11 +752,16 @@ RQ_QUEUES.update({
# Supported translation languages
LANGUAGES = (
+ ('cs', _('Czech')),
+ ('da', _('Danish')),
('de', _('German')),
('en', _('English')),
('es', _('Spanish')),
('fr', _('French')),
+ ('it', _('Italian')),
('ja', _('Japanese')),
+ ('nl', _('Dutch')),
+ ('pl', _('Polish')),
('pt', _('Portuguese')),
('ru', _('Russian')),
('tr', _('Turkish')),
@@ -733,6 +776,7 @@ LOCALE_PATHS = (
# Strawberry (GraphQL)
#
STRAWBERRY_DJANGO = {
+ "DEFAULT_PK_FIELD_NAME": "id",
"TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True,
"USE_DEPRECATED_FILTERS": True,
}
@@ -741,6 +785,8 @@ STRAWBERRY_DJANGO = {
# Plugins
#
+PLUGIN_CATALOG_URL = 'https://api.netbox.oss.netboxlabs.com/v1/plugins'
+
# Register any configured plugins
for plugin_name in PLUGINS:
try:
diff --git a/netbox/netbox/staging.py b/netbox/netbox/staging.py
index 4d37fb7ada4..e6b94640302 100644
--- a/netbox/netbox/staging.py
+++ b/netbox/netbox/staging.py
@@ -80,7 +80,7 @@ class checkout:
Create Change instances for all actions stored in the queue.
"""
if not self.queue:
- logger.debug(f"No queued changes; aborting")
+ logger.debug("No queued changes; aborting")
return
logger.debug(f"Processing {len(self.queue)} queued changes")
diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py
index 2576f70e597..ee142039669 100644
--- a/netbox/netbox/tables/columns.py
+++ b/netbox/netbox/tables/columns.py
@@ -35,6 +35,7 @@ __all__ = (
'ContentTypesColumn',
'CustomFieldColumn',
'CustomLinkColumn',
+ 'DistanceColumn',
'DurationColumn',
'LinkedCountColumn',
'MarkdownColumn',
@@ -173,6 +174,7 @@ class ToggleColumn(tables.CheckBoxColumn):
kwargs['attrs'] = {
'th': {
'class': 'w-1',
+ 'aria-label': _('Select all'),
},
'td': {
'class': 'w-1',
@@ -194,14 +196,23 @@ class BooleanColumn(tables.Column):
Custom implementation of BooleanColumn to render a nicely-formatted checkmark or X icon instead of a Unicode
character.
"""
+ TRUE_MARK = mark_safe(' ')
+ FALSE_MARK = mark_safe(' ')
+ EMPTY_MARK = mark_safe('— ') # Placeholder
+
+ def __init__(self, *args, true_mark=TRUE_MARK, false_mark=FALSE_MARK, **kwargs):
+ self.true_mark = true_mark
+ self.false_mark = false_mark
+ super().__init__(*args, **kwargs)
+
def render(self, value):
- if value:
- rendered = ' '
- elif value is None:
- rendered = '— '
- else:
- rendered = ' '
- return mark_safe(rendered)
+ if value is None:
+ return self.EMPTY_MARK
+ if value and self.true_mark:
+ return self.true_mark
+ if not value and self.false_mark:
+ return self.false_mark
+ return self.EMPTY_MARK
def value(self, value):
return str(value)
@@ -249,7 +260,7 @@ class ActionsColumn(tables.Column):
def render(self, record, table, **kwargs):
# Skip dummy records (e.g. available VLANs) or those with no actions
- if not getattr(record, 'pk', None) or not self.actions:
+ if not getattr(record, 'pk', None) or not (self.actions or self.extra_buttons):
return ''
model = table.Meta.model
@@ -275,7 +286,7 @@ class ActionsColumn(tables.Column):
if len(self.actions) == 1 or (self.split_actions and idx == 0):
dropdown_class = attrs.css_class
button = (
- f''
+ f' '
f' '
)
@@ -321,19 +332,26 @@ class ActionsColumn(tables.Column):
class ChoiceFieldColumn(tables.Column):
"""
Render a model's static ChoiceField with its value from `get_FOO_display()` as a colored badge. Background color is
- set by the instance's get_FOO_color() method, if defined.
+ set by the instance's get_FOO_color() method, if defined, or can be overridden by a "color" callable.
"""
DEFAULT_BG_COLOR = 'secondary'
+ def __init__(self, *args, color=None, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.color = color
+
def render(self, record, bound_column, value):
if value in self.empty_values:
return self.default
- # Determine the background color to use (try calling object.get_FOO_color())
- try:
- bg_color = getattr(record, f'get_{bound_column.name}_color')() or self.DEFAULT_BG_COLOR
- except AttributeError:
- bg_color = self.DEFAULT_BG_COLOR
+ # Determine the background color to use (use "color" callable if given, else try calling object.get_FOO_color())
+ if self.color:
+ bg_color = self.color(record)
+ else:
+ try:
+ bg_color = getattr(record, f'get_{bound_column.name}_color')() or self.DEFAULT_BG_COLOR
+ except AttributeError:
+ bg_color = self.DEFAULT_BG_COLOR
return mark_safe(f'{value} ')
@@ -674,3 +692,16 @@ class ChoicesColumn(tables.Column):
value.append(f'({omitted_count} more)')
return ', '.join(value)
+
+
+class DistanceColumn(TemplateColumn):
+ """
+ Distance with template code for formatting
+ """
+ template_code = """
+ {% load helpers %}
+ {% if record.distance %}{{ record.distance|floatformat:"-2" }} {{ record.distance_unit }}{% endif %}
+ """
+
+ def __init__(self, template_code=template_code, order_by='_abs_distance', **kwargs):
+ super().__init__(template_code=template_code, order_by=order_by, **kwargs)
diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py
index 00e08d4c491..c23c1fa140e 100644
--- a/netbox/netbox/tables/tables.py
+++ b/netbox/netbox/tables/tables.py
@@ -6,6 +6,7 @@ from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import RelatedField
+from django.db.models.fields.reverse_related import ManyToOneRel
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from django.utils.safestring import mark_safe
@@ -103,7 +104,7 @@ class BaseTable(tables.Table):
field = model._meta.get_field(field_name)
except FieldDoesNotExist:
break
- if isinstance(field, RelatedField):
+ if isinstance(field, (RelatedField, ManyToOneRel)):
# Follow ForeignKeys to the related model
prefetch_path.append(field_name)
model = field.remote_field.model
diff --git a/netbox/netbox/tests/dummy_plugin/admin.py b/netbox/netbox/tests/dummy_plugin/admin.py
deleted file mode 100644
index 83bc22ad8f5..00000000000
--- a/netbox/netbox/tests/dummy_plugin/admin.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from django.contrib import admin
-
-from netbox.admin import admin_site
-from .models import DummyModel
-
-
-@admin.register(DummyModel, site=admin_site)
-class DummyModelAdmin(admin.ModelAdmin):
- list_display = ('name', 'number')
diff --git a/netbox/netbox/tests/dummy_plugin/graphql.py b/netbox/netbox/tests/dummy_plugin/graphql.py
index 2651f4e9ea9..a8bbfcea202 100644
--- a/netbox/netbox/tests/dummy_plugin/graphql.py
+++ b/netbox/netbox/tests/dummy_plugin/graphql.py
@@ -13,11 +13,9 @@ class DummyModelType:
pass
-@strawberry.type
+@strawberry.type(name="Query")
class DummyQuery:
- @strawberry.field
- def dummymodel(self, id: int) -> DummyModelType:
- return None
+ dummymodel: DummyModelType = strawberry_django.field()
dummymodel_list: List[DummyModelType] = strawberry_django.field()
diff --git a/netbox/netbox/tests/dummy_plugin/template_content.py b/netbox/netbox/tests/dummy_plugin/template_content.py
index b7157e37014..e9a6b9da1ce 100644
--- a/netbox/netbox/tests/dummy_plugin/template_content.py
+++ b/netbox/netbox/tests/dummy_plugin/template_content.py
@@ -10,6 +10,12 @@ class GlobalContent(PluginTemplateExtension):
class SiteContent(PluginTemplateExtension):
models = ['dcim.site']
+ def buttons(self):
+ return "SITE CONTENT - BUTTONS"
+
+ def alerts(self):
+ return "SITE CONTENT - ALERTS"
+
def left_page(self):
return "SITE CONTENT - LEFT PAGE"
@@ -19,9 +25,6 @@ class SiteContent(PluginTemplateExtension):
def full_width_page(self):
return "SITE CONTENT - FULL WIDTH PAGE"
- def buttons(self):
- return "SITE CONTENT - BUTTONS"
-
def list_buttons(self):
return "SITE CONTENT - LIST BUTTONS"
diff --git a/netbox/netbox/tests/dummy_plugin/views.py b/netbox/netbox/tests/dummy_plugin/views.py
index f6cf6a5c5c2..82f250fc1eb 100644
--- a/netbox/netbox/tests/dummy_plugin/views.py
+++ b/netbox/netbox/tests/dummy_plugin/views.py
@@ -8,7 +8,7 @@ from dcim.models import Site
from utilities.views import register_model_view
from .models import DummyModel
# Trigger registration of custom column
-from .tables import mycol
+from .tables import mycol # noqa: F401
class DummyModelsView(View):
@@ -21,7 +21,7 @@ class DummyModelsView(View):
class DummyModelAddView(View):
def get(self, request):
- return HttpResponse(f"Create an instance")
+ return HttpResponse("Create an instance")
def post(self, request):
instance = DummyModel(
@@ -29,7 +29,7 @@ class DummyModelAddView(View):
number=random.randint(1, 100000)
)
instance.save()
- return HttpResponse(f"Instance created")
+ return HttpResponse("Instance created")
@register_model_view(Site, 'extra', path='other-stuff')
diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py
index 6e049dcafa8..ae6d3f4c299 100644
--- a/netbox/netbox/tests/test_authentication.py
+++ b/netbox/netbox/tests/test_authentication.py
@@ -1,7 +1,6 @@
import datetime
from django.conf import settings
-from django.contrib.auth import get_user_model
from django.test import Client
from django.test.utils import override_settings
from django.urls import reverse
@@ -11,14 +10,11 @@ from rest_framework.test import APIClient
from core.models import ObjectType
from dcim.models import Site
from ipam.models import Prefix
-from users.models import Group, ObjectPermission, Token
+from users.models import Group, ObjectPermission, Token, User
from utilities.testing import TestCase
from utilities.testing.api import APITestCase
-User = get_user_model()
-
-
class TokenAuthenticationTestCase(APITestCase):
@override_settings(LOGIN_REQUIRED=True, EXEMPT_VIEW_PERMISSIONS=['*'])
@@ -110,7 +106,7 @@ class ExternalAuthenticationTestCase(TestCase):
self.assertEqual(settings.REMOTE_AUTH_HEADER, 'HTTP_REMOTE_USER')
# Client should not be authenticated
- response = self.client.get(reverse('home'), follow=True, **headers)
+ self.client.get(reverse('home'), follow=True, **headers)
self.assertNotIn('_auth_user_id', self.client.session)
@override_settings(
diff --git a/netbox/netbox/tests/test_graphql.py b/netbox/netbox/tests/test_graphql.py
index 2cf9ee87b9d..b04d42d2447 100644
--- a/netbox/netbox/tests/test_graphql.py
+++ b/netbox/netbox/tests/test_graphql.py
@@ -1,7 +1,14 @@
+import json
+
from django.test import override_settings
from django.urls import reverse
+from rest_framework import status
-from utilities.testing import disable_warnings, TestCase
+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
class GraphQLTestCase(TestCase):
@@ -34,3 +41,88 @@ class GraphQLTestCase(TestCase):
response = self.client.get(url, **header)
with disable_warnings('django.request'):
self.assertHttpStatus(response, 302) # Redirect to login page
+
+
+class GraphQLAPITestCase(APITestCase):
+
+ @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)
+ Location.objects.create(
+ site=sites[0],
+ name='Location 1',
+ slug='location-1',
+ status=LocationStatusChoices.STATUS_PLANNED
+ ),
+ Location.objects.create(
+ site=sites[1],
+ name='Location 2',
+ slug='location-2',
+ status=LocationStatusChoices.STATUS_STAGING
+ ),
+ Location.objects.create(
+ site=sites[1],
+ name='Location 3',
+ slug='location-3',
+ 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)
+ self.assertHttpStatus(response, status.HTTP_200_OK)
+ data = json.loads(response.content)
+ self.assertNotIn('errors', data)
+ self.assertEqual(len(data['data']['location_list']), 1)
+ self.assertIsNotNone(data['data']['location_list'][0]['site'])
+
+ # Test OR logic
+ query = """{
+ location_list( filters: {
+ status: \"""" + LocationStatusChoices.STATUS_PLANNED + """\",
+ OR: {status: \"""" + LocationStatusChoices.STATUS_STAGING + """\"}
+ }) {
+ id site {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']['location_list']), 2)
+
+ # An invalid request should return an empty list
+ query = '{location_list(filters: {site_id: "99999"}) {id site {id}}}' # Invalid site 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.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))
+ 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)
diff --git a/netbox/netbox/tests/test_import.py b/netbox/netbox/tests/test_import.py
index f382d011201..16711ef7268 100644
--- a/netbox/netbox/tests/test_import.py
+++ b/netbox/netbox/tests/test_import.py
@@ -2,6 +2,7 @@ from django.test import override_settings
from core.models import ObjectType
from dcim.models import *
+from extras.models import CustomField
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
from users.models import ObjectPermission
from utilities.testing import ModelViewTestCase, create_tags
@@ -76,7 +77,6 @@ class CSVImportTestCase(ModelViewTestCase):
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
regions = Region.objects.all()
self.assertEqual(regions.count(), 4)
- region = Region.objects.get(slug="region-4")
self.assertEqual(
list(regions[0].tags.values_list('name', flat=True)),
['Alpha', 'Bravo']
@@ -116,3 +116,28 @@ class CSVImportTestCase(ModelViewTestCase):
# Test POST with permission
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200)
self.assertEqual(Region.objects.count(), 0)
+
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
+ def test_custom_field_defaults(self):
+ self.add_permissions('dcim.add_region')
+ csv_data = [
+ 'name,slug,description',
+ 'Region 1,region-1,abc',
+ ]
+ data = {
+ 'format': ImportFormatChoices.CSV,
+ 'data': self._get_csv_data(csv_data),
+ 'csv_delimiter': CSVDelimiterChoices.AUTO,
+ }
+
+ cf = CustomField.objects.create(
+ name='tcf',
+ type='text',
+ required=False,
+ default='def-cf-text'
+ )
+ cf.object_types.set([ObjectType.objects.get_for_model(self.model)])
+
+ self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
+ region = Region.objects.get(slug='region-1')
+ self.assertEqual(region.cf['tcf'], 'def-cf-text')
diff --git a/netbox/netbox/tests/test_jobs.py b/netbox/netbox/tests/test_jobs.py
new file mode 100644
index 00000000000..cb3024038ae
--- /dev/null
+++ b/netbox/netbox/tests/test_jobs.py
@@ -0,0 +1,129 @@
+from datetime import timedelta
+
+from django.test import TestCase
+from django.utils import timezone
+from django_rq import get_queue
+
+from ..jobs import *
+from core.models import Job
+from core.choices import JobStatusChoices
+
+
+class TestJobRunner(JobRunner):
+ def run(self, *args, **kwargs):
+ pass
+
+
+class JobRunnerTestCase(TestCase):
+ def tearDown(self):
+ super().tearDown()
+
+ # Clear all queues after running each test
+ get_queue('default').connection.flushall()
+ get_queue('high').connection.flushall()
+ get_queue('low').connection.flushall()
+
+ @staticmethod
+ def get_schedule_at(offset=1):
+ # Schedule jobs a week in advance to avoid accidentally running jobs on worker nodes used for testing.
+ return timezone.now() + timedelta(weeks=offset)
+
+
+class JobRunnerTest(JobRunnerTestCase):
+ """
+ Test internal logic of `JobRunner`.
+ """
+
+ def test_name_default(self):
+ self.assertEqual(TestJobRunner.name, TestJobRunner.__name__)
+
+ def test_name_set(self):
+ class NamedJobRunner(TestJobRunner):
+ class Meta:
+ name = 'TestName'
+
+ self.assertEqual(NamedJobRunner.name, 'TestName')
+
+ def test_handle(self):
+ job = TestJobRunner.enqueue(immediate=True)
+
+ self.assertEqual(job.status, JobStatusChoices.STATUS_COMPLETED)
+
+ def test_handle_errored(self):
+ class ErroredJobRunner(TestJobRunner):
+ EXP = Exception('Test error')
+
+ def run(self, *args, **kwargs):
+ raise self.EXP
+
+ job = ErroredJobRunner.enqueue(immediate=True)
+
+ self.assertEqual(job.status, JobStatusChoices.STATUS_ERRORED)
+ self.assertEqual(job.error, repr(ErroredJobRunner.EXP))
+
+
+class EnqueueTest(JobRunnerTestCase):
+ """
+ Test enqueuing of `JobRunner`.
+ """
+
+ def test_enqueue(self):
+ instance = Job()
+ for i in range(1, 3):
+ job = TestJobRunner.enqueue(instance, schedule_at=self.get_schedule_at())
+
+ self.assertIsInstance(job, Job)
+ self.assertEqual(TestJobRunner.get_jobs(instance).count(), i)
+
+ def test_enqueue_once(self):
+ job = TestJobRunner.enqueue_once(instance=Job(), schedule_at=self.get_schedule_at())
+
+ self.assertIsInstance(job, Job)
+ self.assertEqual(job.name, TestJobRunner.__name__)
+
+ def test_enqueue_once_twice_same(self):
+ instance = Job()
+ schedule_at = self.get_schedule_at()
+ job1 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at)
+ job2 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at)
+
+ self.assertEqual(job1, job2)
+ self.assertEqual(TestJobRunner.get_jobs(instance).count(), 1)
+
+ def test_enqueue_once_twice_different_schedule_at(self):
+ instance = Job()
+ job1 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at())
+ job2 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at(2))
+
+ self.assertNotEqual(job1, job2)
+ self.assertRaises(Job.DoesNotExist, job1.refresh_from_db)
+ self.assertEqual(TestJobRunner.get_jobs(instance).count(), 1)
+
+ def test_enqueue_once_twice_different_interval(self):
+ instance = Job()
+ schedule_at = self.get_schedule_at()
+ job1 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at)
+ job2 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at, interval=60)
+
+ self.assertNotEqual(job1, job2)
+ self.assertEqual(job1.interval, None)
+ self.assertEqual(job2.interval, 60)
+ self.assertRaises(Job.DoesNotExist, job1.refresh_from_db)
+ self.assertEqual(TestJobRunner.get_jobs(instance).count(), 1)
+
+ def test_enqueue_once_with_enqueue(self):
+ instance = Job()
+ job1 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at(2))
+ job2 = TestJobRunner.enqueue(instance, schedule_at=self.get_schedule_at())
+
+ self.assertNotEqual(job1, job2)
+ self.assertEqual(TestJobRunner.get_jobs(instance).count(), 2)
+
+ def test_enqueue_once_after_enqueue(self):
+ instance = Job()
+ job1 = TestJobRunner.enqueue(instance, schedule_at=self.get_schedule_at())
+ job2 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at(2))
+
+ self.assertNotEqual(job1, job2)
+ self.assertRaises(Job.DoesNotExist, job1.refresh_from_db)
+ self.assertEqual(TestJobRunner.get_jobs(instance).count(), 1)
diff --git a/netbox/netbox/tests/test_plugins.py b/netbox/netbox/tests/test_plugins.py
index 351fef9e2db..ba44378c5ce 100644
--- a/netbox/netbox/tests/test_plugins.py
+++ b/netbox/netbox/tests/test_plugins.py
@@ -36,12 +36,6 @@ class PluginTest(TestCase):
instance.delete()
self.assertIsNone(instance.pk)
- def test_admin(self):
-
- # Test admin view URL resolution
- url = reverse('admin:dummy_plugin_dummymodel_add')
- self.assertEqual(url, '/admin/dummy_plugin/dummymodel/add/')
-
@override_settings(LOGIN_REQUIRED=False)
def test_views(self):
diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py
index b0175ec043d..08c9a46a82a 100644
--- a/netbox/netbox/urls.py
+++ b/netbox/netbox/urls.py
@@ -77,11 +77,6 @@ _patterns = [
path('api/plugins/', include((plugin_api_patterns, 'plugins-api'))),
]
-# Django admin UI
-if settings.DJANGO_ADMIN_ENABLED:
- from .admin import admin_site
- _patterns.append(path('admin/', admin_site.urls))
-
# django-debug-toolbar
if settings.DEBUG:
import debug_toolbar
diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py
index 4e0e9cbdd91..d8c4c5e27d5 100644
--- a/netbox/netbox/views/generic/bulk_views.py
+++ b/netbox/netbox/views/generic/bulk_views.py
@@ -4,11 +4,12 @@ from copy import deepcopy
from django.contrib import messages
from django.contrib.contenttypes.fields import GenericRel
+from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError
from django.db import transaction, IntegrityError
from django.db.models import ManyToManyField, ProtectedError, RestrictedError
from django.db.models.fields.reverse_related import ManyToManyRel
-from django.forms import HiddenInput, ModelMultipleChoiceField, MultipleHiddenInput
+from django.forms import ModelMultipleChoiceField, MultipleHiddenInput
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
@@ -16,10 +17,12 @@ from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django_tables2.export import TableExport
+from mptt.models import MPTTModel
from core.models import ObjectType
-from extras.models import ExportTemplate
-from extras.signals import clear_events
+from core.signals import clear_events
+from extras.choices import CustomFieldUIEditableChoices
+from extras.models import CustomField, ExportTemplate
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
@@ -107,7 +110,13 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
try:
return template.render_to_response(self.queryset)
except Exception as e:
- messages.error(request, f"There was an error rendering the selected export template ({template.name}): {e}")
+ messages.error(
+ request,
+ _("There was an error rendering the selected export template ({template}): {error}").format(
+ template=template.name,
+ error=e
+ )
+ )
# Strip the `export` param and redirect user to the filtered objects list
query_params = request.GET.copy()
query_params.pop('export')
@@ -196,6 +205,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
return render(request, 'htmx/table.html', {
'table': table,
'filter_chits': filter_chits,
+ 'model': model,
+ 'actions': actions,
})
context = {
@@ -435,6 +446,17 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
if instance.pk and hasattr(instance, 'snapshot'):
instance.snapshot()
+ else:
+ # 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:
+ field_name = f'cf_{cf.name}'
+ if field_name not in record:
+ record[field_name] = cf.default
+
# Instantiate the model form for the object
model_form_kwargs = {
'data': record,
@@ -619,6 +641,10 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
if form.cleaned_data.get('remove_tags', None):
obj.tags.remove(*form.cleaned_data['remove_tags'])
+ # Rebuild the tree for MPTT models
+ if issubclass(self.queryset.model, MPTTModel):
+ self.queryset.model.objects.rebuild()
+
return updated_objects
#
@@ -694,7 +720,10 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
# Retrieve objects being edited
table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False)
if not table.rows:
- messages.warning(request, "No {} were selected.".format(model._meta.verbose_name_plural))
+ messages.warning(
+ request,
+ _("No {object_type} were selected.").format(object_type=model._meta.verbose_name_plural)
+ )
return redirect(self.get_return_url(request))
return render(request, self.template_name, {
@@ -771,8 +800,13 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects):
raise PermissionsViolation
- model_name = self.queryset.model._meta.verbose_name_plural
- messages.success(request, f"Renamed {len(selected_objects)} {model_name}")
+ messages.success(
+ request,
+ _("Renamed {count} {object_type}").format(
+ count=len(selected_objects),
+ object_type=self.queryset.model._meta.verbose_name_plural
+ )
+ )
return redirect(self.get_return_url(request))
except (AbortRequest, PermissionsViolation) as e:
@@ -864,7 +898,10 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
messages.error(request, mark_safe(e.message))
return redirect(self.get_return_url(request))
- msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
+ msg = _("Deleted {count} {object_type}").format(
+ count=deleted_count,
+ object_type=model._meta.verbose_name_plural
+ )
logger.info(msg)
messages.success(request, msg)
return redirect(self.get_return_url(request))
@@ -881,7 +918,10 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
# Retrieve objects being deleted
table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False)
if not table.rows:
- messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
+ messages.warning(
+ request,
+ _("No {object_type} were selected.").format(object_type=model._meta.verbose_name_plural)
+ )
return redirect(self.get_return_url(request))
return render(request, self.template_name, {
@@ -926,7 +966,10 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
selected_objects = self.parent_model.objects.filter(pk__in=pk_list)
if not selected_objects:
- messages.warning(request, "No {} were selected.".format(self.parent_model._meta.verbose_name_plural))
+ messages.warning(
+ request,
+ _("No {object_type} were selected.").format(object_type=self.parent_model._meta.verbose_name_plural)
+ )
return redirect(self.get_return_url(request))
table = self.table(selected_objects, orderable=False)
diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py
index 821d87e170c..451c9c01d2a 100644
--- a/netbox/netbox/views/generic/feature_views.py
+++ b/netbox/netbox/views/generic/feature_views.py
@@ -38,7 +38,7 @@ class ObjectChangeLogView(ConditionalLoginRequiredMixin, View):
base_template = None
tab = ViewTab(
label=_('Changelog'),
- permission='extras.view_objectchange',
+ permission='core.view_objectchange',
weight=10000
)
@@ -204,11 +204,14 @@ class ObjectSyncDataView(LoginRequiredMixin, View):
obj = get_object_or_404(qs, **kwargs)
if not obj.data_file:
- messages.error(request, f"Unable to synchronize data: No data file set.")
+ messages.error(request, _("Unable to synchronize data: No data file set."))
return redirect(obj.get_absolute_url())
obj.sync(save=True)
- messages.success(request, f"Synchronized data for {model._meta.verbose_name} {obj}.")
+ messages.success(request, _("Synchronized data for {object_type} {object}.").format(
+ object_type=model._meta.verbose_name,
+ object=obj
+ ))
return redirect(obj.get_absolute_url())
@@ -230,7 +233,9 @@ class BulkSyncDataView(GetReturnURLMixin, BaseMultiObjectView):
for obj in selected_objects:
obj.sync(save=True)
- model_name = self.queryset.model._meta.verbose_name_plural
- messages.success(request, f"Synced {len(selected_objects)} {model_name}")
+ messages.success(request, _("Synced {count} {object_type}").format(
+ count=len(selected_objects),
+ object_type=self.queryset.model._meta.verbose_name_plural
+ ))
return redirect(self.get_return_url(request))
diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py
index 243ae2547d8..0686e52b7df 100644
--- a/netbox/netbox/views/generic/object_views.py
+++ b/netbox/netbox/views/generic/object_views.py
@@ -13,7 +13,7 @@ from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
-from extras.signals import clear_events
+from core.signals import clear_events
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, PermissionsViolation
from utilities.forms import ConfirmationForm, restrict_form_fields
@@ -87,12 +87,14 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
child_model: The model class which represents the child objects
table: The django-tables2 Table class used to render the child objects list
filterset: A django-filter FilterSet that is applied to the queryset
+ filterset_form: The form class used to render filter options
actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk
action names must be prefixed with `bulk_`. (See ActionsMixin.)
"""
child_model = None
table = None
filterset = None
+ filterset_form = None
template_name = 'generic/object_children.html'
def get_children(self, request, parent):
@@ -144,14 +146,17 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
return render(request, 'htmx/table.html', {
'object': instance,
'table': table,
+ 'model': self.child_model,
})
return render(request, self.get_template_name(), {
'object': instance,
+ 'model': self.child_model,
'child_model': self.child_model,
'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
'table': table,
'table_config': f'{table.name}_config',
+ 'filter_form': self.filterset_form(request.GET) if self.filterset_form else None,
'actions': actions,
'tab': self.tab,
'return_url': request.get_full_path(),
@@ -231,6 +236,8 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
# If this is an HTMX request, return only the rendered form HTML
if htmx_partial(request):
return render(request, self.htmx_template_name, {
+ 'model': model,
+ 'object': obj,
'form': form,
})
@@ -285,6 +292,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
msg = f'{msg} {obj}'
messages.success(request, msg)
+ # If adding another object, redirect back to the edit form
if '_addanother' in request.POST:
redirect_url = request.path
@@ -300,6 +308,12 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
return_url = self.get_return_url(request, obj)
+ # If the object has been created or edited via HTMX, return an HTMX redirect to the object view
+ if request.htmx:
+ return HttpResponse(headers={
+ 'HX-Location': return_url,
+ })
+
return redirect(return_url)
except (AbortRequest, PermissionsViolation) as e:
diff --git a/netbox/project-static/dist/graphiql.css b/netbox/project-static/dist/graphiql.css
deleted file mode 100644
index 3db5b848aa0..00000000000
--- a/netbox/project-static/dist/graphiql.css
+++ /dev/null
@@ -1,13 +0,0 @@
-@font-face{font-family:Roboto;font-style:italic;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Roboto;font-style:italic;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Roboto;font-style:italic;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,d09GMgABAAAAAAMwAA4AAAAABZgAAALdAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoI4ghsLEAABNgIkAxwEIAWDCgcgG3YEyI7DdHsjE9IUV+CFDh74vPL9/MmgO0un0soqjWt7En2kQoCMtXsRxyxkMqP9iO6NfSiUaLJuoRIKnhI0+ImbcWOB5XOAFVmCgxZQQmuBJRhZtsUCXm/492Dyuk2YZJdkdApZeOzyEQgKOwDgRjASBEEBVmAlgACtOHEhpjLyyrACMAB0vaLa6cAw5bc5bvhA2uwO7zXAyKPmkYNnAJgBxLEMDxFLqVBPI6EQ/daTr/QOAgfCngRoZc4UZiL623qCkf/oHVsfRCOuAIbJyF4ajQQKQLmQhNBAA4aygH9b19Xw4iAC8DkKM6WrYw/ABMAOWEAamA7sgBWACgAUSlc3SCmlc95o45idYD92Qt/+5gF19v3FALtB9+7dq/h6/Ljyu/zzYfnngwdlHxO+k39nOcO/e7nPf2vCoo3HVlmNTdnWwW3JZffuVU6cQX14kb3qUGOOJ+mjP9iMeb1Nivq5gXpJUWm+cmVK56e6PjI2uce23hHlG48vyDvym5/5q+wbkjq90rN+z53D6zXqmVUPVshZoVtrZgc4vleS1NNrni6VR8I/vTrpzpPwu1+1Pel4xBIzK16W3KcLNnVGl2RGZHbPXBAvhw4M02Ci/t0BBfw/p79XS9V7CKAMF0++DK9rtI/7MXvGATjz0TEA4K4oef476t9dS555BAoLBYCA6ei/FSzVgvg/cIR45gpTaLWeLiB+oa4xJuTks7r7/xwCmCzlpoJKALCDQmkyEsCsN0mELUADghGsGgAF6c9IXkabDYyqg6WMkZd9z7BT5gaphhhqnOH66aOvkTQhggQLpsk0xBB9DNSLJttgPQTQJBtoIE0JEY2wb+1lhF6GG62XngKUGKLFECMNkW2kZgP10+M31GZUwfojwkU0uAcQkISKFNtqGMlau3vIjjRUjMANjYkDNKeouYh7CRBmuD4CHQgHG6GXET8oT7ZU6QqUStddiABBJPSv6P315AAA) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Roboto;font-style:italic;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Roboto;font-style:italic;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Roboto;font-style:italic;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Roboto;font-style:italic;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Roboto;font-style:italic;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Roboto;font-style:italic;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Roboto;font-style:italic;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,d09GMgABAAAAAANUAA4AAAAABbwAAAMBAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoI0ghgLEAABNgIkAxwEIAWDMgcgG5sECK4GbGM62A+KOMNGmZWUwcdhKI9l4Sh/WwYP/3af9w0W4ERa2bOg405uoSptTooGKkF8HniO5b+Iojvye4dReBbNtVHwcLQTG2gBzQfYOqjJ/XYU/jItwgxa4I3czM4Fj9LAAnlHz+dzgSO71Jqn2QML8H66dROj0qAFLYnRhtm0b89/erW/v8l/LA6we9gCizDBtQzSf4EtkcwDT6RtmgYEQXnDKGQslZyX/CkQSFgBAE4ERggEAgmwACwQgADMsONAJKVkFWEBgAJgwMz1NlLWec3G+jtZu+rXO1i7rx/sZi0AEwB5WVY28FUE1CORQAjvtSPftAwCQQjGAbTUfm4qwrvbNmDEf5pjR4JoxElAiYiMWjQyIAEy4EBGAA4UNKCgIMC7a5Cej2sCAA+SMEEyYA2AMQBWgCmQAObACrAAQAUAJCSDMEDmo7CztfXoRGu7SUeVdbvosOq6N6PHnZ2yf9l3eXPj/q2qXdkjBL+qrix1cYsqzItOvXfRPaMXkUvPeFWoxr7tZB8gfxIhMauBapmSUhO8d3O8wUt0MoI7UAxLzt0/zhCwJnVHrsPYXenm8suPeLYORWqn/3wwK6Qp+frDiYGvxHSXFzoXfpihfmlODl9oFbOqKa8nXbZgd6axNivh4JS8xEZKChij/nuDBPx/MrxQA/WBACCtK44947xa66g/k0YcALjxaesDuBuQP/7x/3bTwmQACVMkAAQYd/7HYBqK1H97hriqWIzlN7cD8Qu1mY6Ql7eR9v8qAcCY/apKqAgArEBCCmOEAExoJiOUENTgBAI3NSBhwSjIbLboV0Blo3PIiN06hxVFfmrr0WtMvzYtWg3SBPDjz58mVY8eLTrpNOm6NfKhidepk6ZAbgbym+oG6PoN0zXxUaBHgx6Demiy6Zq0GdIl3aB6ndo04r7WvSV0/Qa0Nd2+yKcNFCrSvh/6dNKO3xV33aBeEXxNZKTyQUaverfOR49+LZno1XUboBt4oSzpEiXLUSjZDgF8+JHBMIY0KQAA) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Roboto;font-style:italic;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,d09GMgABAAAAABU0AA4AAAAAJLgAABTeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmQbi3YcNgZgAIFkEQwKrkSlZwuBSAABNgIkA4MMBCAFgzIHIBueHrOiVpNataT4nwk2nboHhRIwDgpKyhjHLyLzQxmFwTYyDE5esZ3+2EabADRB2gAnegV3sg2h4vmn/cH/ujNn5kEfUoTVzJCo7tDcxAh1qBL7aK6c2RAfYY5oH5jywGzfVxj2dQKMqiNV1SGa2/3fsqgYgzZIg4jcRiiRIlUD6TaSLHVGBGIUGIlSIiAWaB/Nlf92N3lGYYsKSKjZnfSTB8DmMi27e2FKIBTaKlRVsztJrgQ/v1ar83g3J/7Bm3pohA6p0P68Qebt32Vvzv+J+e5iNnizRruQrw0imsSTJfEmoUCohFIvESLYkJkG86bdWhrvEfNUcXTtnhaEruXzgVaEu0VRWgYqCFQSqCJQjUANMogmzaJVj+izItbskHExWMtGIeDVV4+zjD3+RFc+yF6RlRIHstekRMaC7I2haQkgC2+4KiUBmJDOA0pVozaXNfBR9QCXV2CAnZZ/Pa939bym2tY015bSKkq/1bW5rl2W3bLb9zSVW4Drhr5Xrw/3s6jw6wK1JMm+D+n/woA6vO4yKdplbgIyweLmY2gZzWw+oG+f+/mW70DuJgYtfT7LzTxPyqddT+nC3/NdfLWlUjfjXEzmQ/hpKLyQ98ii2GeJyRwXTdK9mWCse91WkQMY68rJFB88T8t35mpaolV7x53YfELcGYe/k5e+Q8OkBTnHYqOSF4OEEujtXNjCIqJi4hKSUjJyiiqq1KhTr1m7bj36DRk1YdKUaTPmrFizRZJMikLoKiGpjpWa4NUnWmPomkLTHApWNF+toulu2I0Yi3nKgC9LYMKUrGeVRDIh1kjzTns2qSeP9MP0pJk8NMecFu5MvKMmX6zA/fX9Q5TOL5OXchlXyJRSLinno0o+qMoi3UyrVXFduLL6vNeQVxpzV1Mea84LjsgLhbwUIlcyZi3jNgFs8XbW2ZDJIg2tfzlzKEN1ZtUKbMD8DXNXQz5pzDQnsB/gtQLeJN4m5izUdKksg2nSRk5D9WyKQs/IZRNpGuhaSpjhGY1WObToSmatUWx1JnL5ZiO7F4xkJqXyAGWpz01EMiOaMnHN14SjHwXF8xU3i1ZZWLxpN73ceAqTchLyIBv2QRYchjzI1TkEbetj5cxPxG81MA2TYoHqf182swq5rkjT+39QyZjqzKjJ6TL4ACPwvPgGZpVcE6wV0i7YziJlYTFgz06wSoJTcyZeux6CfnM0C5WIWhExayJu64faUNggA4GImLpCRlmSyTJArnQhQdaTUlJopaw1sgZU7ypr6OEVYGgoYhCPTOddtBvLdjIHMufBjQi9q30D8MqGOGCoW0HhivaBxX30m1mMYRKTOyZX24T8t6yqO5dvKWY8MQzAsmM2BOifOGgAttxzR98dn3SWhwPAfk8fm+A/AFev2NuADZ8FqEOHuBI2prgBmrIZBgrWtzvfgonB94d6Td/a27u4n+rD/W5/2MfyH/R7xOPX9W29sx/qp/ut/qDq9O/Rf48AgdPYjW7/N/rfSMgHsINW4FzQnGsrQe1COnTqEn7aIocMixoxWnLsMePiJtgmJT7+OJkeb0rarDmOeQsWLVlGrVpTZUW1GrXq1GvQaP2LmZ7EKSRh4BXwgf9FYOwMVr0KLHcx4+QVV2Bww8AOyAZgR0TFTAKBMZhV3EvUu2AsNqQDS9LuB4/kVg9nIEAakUChYKh0Etsk91wOkcQ08QqFo2oYDIWCw0AMCzosvVYEqoQgyKYVaV4v0TbyETaLINHkqBSblnAxWVLyxFhZiRT0Sioxaa/G0+vRiXi6Zpzgqf6qMzwKSFfUSjihado5YLh79B8qKJo+FF/xdsZkMlr6To3QREwg/1Z5syFRpJPGSR1WRZchQqfBxXCvElCFwlTFk8zNkqOywH1Jozx2tXrde299rYZi3F/j8hyYUCJzj+MouoariaLpw5/zWB0WCylI6bQBtlJsuLccTCwFl1fCy8BJ66uZzMLZRmjB7AZshWCpiXFLqMjZ+pax70kYJ4g3vdADAy+STlWm6dCBArat+kIJvSkOqDI74f6iAA6NRLZV66doUoUfq975RbXQxEgnLi0r3ZerpoaNaNtv8/mYTGpIneZ0iko225hRgGG6ATv8jFaUUQFVCVL6ZPgE2AwMokMDZTmtsllFK0U39mkUrSheCG2eXAF9/PgHgEJfotR+I+o9dmaSuSLeJiIkgrGO+A9EKvYluMiT4dFRQ3pTajHWl9veBQLEMja6I+NcAZBPIQSUPOluNyL7529e9N4yW178bFRuj4sN7tkVOYyfugKg5w2paeMcad1xefLsQSWpM09kB4uLqzoNTXGmScx8wUOVlR8LTv706zKwnzRrdE29H0sexg7yeBbE9/nzNc3zNHXCm5409hjYGLDVoJ4MDuqTFBLMiY5L9ryuwp4SXqdQ+CuWGi42IIFQY6ro8cALgu77TvsSb6Jv7b9xxbjOkP/JQkGGdIzmAxbccBfRMaV17ab6OH+KR4NEzlTuvmgg55yjyo/ZiaWA7KO3jerpxRvkVdVjPk97M9g1R7fFn8Gek9FO5zVe6ONDwK8lVlcLslVyp3v09KACk89xQwUmt85+2eYA7GhJolY3o2BkbMODdnNr+lhgpjFOnbr1/OBYib21aZpysKN9OmVax6cxd/D5qSIpSPpukN+4CIbSDC6CzbQR2F1wtTFvzdtHjnInQ2MDSg0NJmd5k/L2KvwzFd3KPmtoB3g3lJ0pTcCObzcF8NQLDplpnvYEQRGUjJ/cURmn3HTKPmjU7Tj7EwD/mL8sMJCeAvsFbj96Z4hwh008elN4nYEWhV/w3sBFhqVETU68vNhzRDiiRwVkDedsHC0ISHPeZnOxPwqyNFzQ6a9AyDljFvXSpX5nd/S4c/VY4TBr5xSNeX+M7yuGg+ZVgBVfhZEbARbPLLLL+EQWvW+HSGAFEgjB2gc+3P3eJD018Wtmt/jHZ8XdYf5Agz4qPg8+grlb1CPMR4sx/kqh/bh06g3V6cWhBvfrKEjvzKbFUqP8UzdB/Ol3YMueVGqY9OlRHADQoV9l63ahR2W4mX5NvIs30mrXaAeqlhLLMhLLlumj4uXNgRnRgctAZ4k+Kl4C+ik3jrueOf4g05p2t3z/a1reILNNiQPUJsVUfoBaWoAt/Zp4iT9XEKRW4nqY+i0+YI/nQ4NoUPlJPo1N5rMPVs8bKEWOkFoCQnYtOlYoWsI34XKM3XayooVDte/gEwi45CVs9jrLKkqU/6F91E5pwmZsnN7JjJAANBde3pGpR5wiHi9+UAyHMG+pKt9AtnygvLe/DTABfzBuMx8Z/fjNGJFFygbKGVnUhISyRIwBAFMTEyep2yeWqF0Tx3gjYUDboDOLoq360uwh6wWnmKOjO7PmOgOk/D9zUFGT1x1A+hGsyk6txoL1w3O8YQXFg+seG97ljQCFQeCozGjZDT/VNsIqZLh+40/qbvrgXvxizVZYidysC/xB2fExFRMdkeePZqFdlzi92NCCyMYQuAv67jbcSM3E+4BTayTC4V8u3/guJcJ4AXCu3VljZ61nYGdrtc7GJsTGQZRpZG/NBUpX+DitrYH8Y+PIeDxfCtNUgu6C/tmETvY8+ajxE5pgU3w1Eue1TnB5jmH3HDRfM3N1a7/k5r7OxM31ULubE7g1mOo8OEe+ajznfNCx4eCaH9K2ynJANsrq3RXfnUBr7ODMYa1d3nq6Ng6hTCcrQ2hnw2U6W9no3xzdUNfWwUvPwQY4lkxU7+IfiX5NXARWHRPPsyXEgkWQNTxMTj0F1qNZx1QuHZUM96hDR4uylvFNuJT1ni3Kqf69hQfxT2viFZmz4s4U3SyCBzDjLO4c0R4fXd33EtiFG/+f+wtWTlhxj1oxVx0Tf6IbiQFIDfeoDPfSbdzGVa6Nw2KtfJWRAlC2dBaKm9m/P/5A7/CD+7gWleEPcu1K1r5m0jXXeSNV2v+A2dU/90j/OJiHq2mt/b8la/sxvP5l3sAb8v+S9z2tfQhI1/VCtcPLvTOsxpzBUkrhoT3EK+cMdWuZO7MGS2gF4iby2dPAkGVRKjtwVXoPf2lZ8Ffrh7n2d0mHjCWHjBeKzy3lp70Xl3w+5+pgQsPK/KSI7+O/gfw7deoD+sprsO4GJNpdfD3m3HOzYjQdU+95wFNa6d6c6q37SBtVlUnZKHPiiBqzpRM2wTedkVxOL0VoGEq8fx/ybr0HNobG+T/DZdihtMvY466f3ZBAH4qzifM2v3BkD3LkOe7oig2qnMEq1khpPjoE+dt1SwwcvPFIuF+qF1KMhlZ53FxVkQczMc0PJY6BlceunoBPHlP6qJdfpAWuDDyFTyOWlN5/nlCMNsFUL+HwHD29j57ReGU8TjI2GilMJUUTfH3jPWEw0pDPjCQcUXHyaECSO+roydQIv2pfTDGQOQFumkX//qfCUXQ7O+/9igz/zgEO5x1u++yQGIlFdutyrhSv3Yy4xljupLkmrjlSOqhexWM37f65UF4PK+GVsg2L1G3Mc8//NcvRHdRdS3E1fG10U1iOEM1AO8/KnaHmRZ4OVshCu05J9YNVmsTjk94X3eMQB8weyv478BDm+aGGGWAd4eDuh5R6EG1YmWLsfaA4dAQkFPMJTnlRbhtQf6SWT3VaIMQU7nvpkYtchh/7gR1WLLfvw9L4V9xTNHAj76Cpn7JjCHQkdr3qzIo5YO7Qv9NNLo3HCJCjUCv7tcSH2DQV7mUgyzdhl1TuOwrb4PZHrAvko4J58lW+izo1vxQthxE5hG2sBfJVYzDNPgGvYJBZF4K94oiulYLja8xJeAmCKeBMsOe+NDCWtuF0eg1zirwwCy24p3jnwBZ9NIwD5yyfQjd0lOwWDhSPGhMMyCtXO6MaN+nnnCSckWxkSwelgmAgCWR2/DwBV3fRSkzzRg1ZgHJ5l3YQkhwpHxMNN1+n8DgKKy/0NrW3tVFPvAbmE8+3qPnl7Aogu8keoCElQOVaLhh6uJtZS9oYUhQsV6z6us8EX4/xEvXFuuZvfmvlUBM609Kqb6XyLJkDiDUnbg2s9dEIroC++P2K117UlK8ELtty9oW5aLKxlk6o+gzjnC3H02FEZaivJfFIzjz7P6yXe24DSDOjJwTcdHCs33YPcxDemCFcR21xthRvnddLy2JMHwxJD8EsxJw3SCiCaWjzYU4LKW0FPokf64bGILXnpduBhqH7EXjzLf7IK4AJ58f7wBS07YJEh77c3LwwTr3VFFeHem4ZiHXNjKm2dqrTdWi9bXYesq6w5RFdQ+DEy0DQogHGdTV6w465hZJKWIVcqff7Td+uxP2lq/zaGKxDVwvkYXxwthBJQJsG5boSfGQwkYEZfFSEth4DluyswAhPKWcLcJVzxEs7CMlGsgaoO0IcnbgXtwG5b8Zx2zEuiItxUOF27OVUKg9boJwzDtb3kcZov/auX27bDfvQE2PEC2rxDeCnnldJ7t+0T/oNq3UvoTSgfEfSpngyOYcYllQaLJNUQk3r3roFKUPu10d+o9bIfPVcRZER3p0PbBjiDS8iA2hBVL0A63MMrJ8wJhmUNXLPH7ehkgcIuSqiV4h2OjFP8czC274WsrTwzrzwwVvuUxulJa+Zea+PBKvVaExUbZAciVcMVErWe+1y3243jRahGdZbLgdgc1pZuw3tvhvYEZyVZem7klEBzOyT629lFJILyQUrssdRAxG5kPUyuWfycSfcjOwSSUWUTD7EtcPBGWQs+JU2cFQRFjmTWGmqb6V/38DmomcyA8Zo+atUppDValRReG0IOowzUGInHNe5xaGeZp1/cb8F7oJtT5lDBobJUjRl5ttTLmvXrknyQQqdfEiuQDWVyJoyz6wMFiLtntKGl9UsUR3bXR1+cClQsafCLQXYMq6csDwAzW+ByM5iEUA7kUoTVdELcVwCGoPsE0lFl84+w+2CbbPYl/D/471khHss2BIU+gNPnJe+LupQYTKGzSZ9T8QG4HJ3SDXxZr5x3+EdVYmHCtCt0EhTdiegTziEIqVZmg2GI5ojf15NJok75AT9RUXrr+vo+WJFNZpN6187/P1vu2UCU6TcbSw34otto71ytIVMPtD2wAJT4G0AvLEi539dOSQgXGeK402BSFU3E7Mg1bwStUPpa/WtGCt+wfDyseGwgCOHPFoooIgSyqigihrqaO5o+Gv0pH8xQ3HmBL9wDWYmBRZ7YBaQYZZQFirGdFd/bLBBB7f5SuhHF3rD7iKaer/sXCd6bi9V57pCqtkg0PwS15zTpP/Xh53uZEOSf74EPNOsl0NdkC6gnptWCcrgFSMqadxvxPi0vaaNQKaHEWQ/0XjRFSVY01PJr91+7jWZMMQ0Qq8F45WkTAZ+gGRqUcAorIBw2zQNMD+E++aMzfTgjptQ3ESwC7QbZyTlSvAks5q+3wqS6LsC6sxsGUwreQJ0kvV/aOHuz0W+ta1zhcVMltnswAX1aBlryUxplHde/b9VfMh7BOt4vGjkv3HS6XXwojp3WsGXahpyMjEZUx8CbddNNpTrsksM098IMisB4L3fFgXAF+j946+e/0ZXZa5MRUgIwAJW3Pg/BcCqgzRJ/4cdAfBl7TxX9J0inGb5Cxj7p6s+yVU8Sxy1HZqJhlqok+Yo14TGKKcDqO70ovf1NVfqmi91PJOVrqWP2+tpvrPteVV87I+VL9EEy6pS8xMOB4HoaM7ACLAxZHO4RGA8blWJ8nKMmB2V0ocpqW7QWYOZ7D+JKlFzOcoX1kElsqpcXGuTUN7p6/+Y1xPrlZiR4morkeaSclGOFsd++qOXxYzl1B6eFe58Oltc5e+IT9CoTVQzSczYIjC04jc8RVsb8i7Q6rZqJ4hoN0hJgFZArskxuSVHtBu0S7Q79k7pzzmlQFdLpIzcToRA93ckLeCQ8oHQjByMh+dd6QADaxVwMQCmoZCNaYTqaRoj721xdhon6yvw5o871Tn+ARuXrjy7cezQkTu2WtVquom2IZeWKM7szzriwi7KPRjOwrOl6hbxfiaZvvGQ9B6K9aUdgrti24TU+di9cyON3naGdndX67WTWpiAb4EkdeEWaHudJm3evU2Wu1eZmJx3vnOlVVWHj0w1o65s632U9I3DYJdZWF2skW+D37gRfQZMmuOq4ucnVWNAvgGJsacFAA==) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Roboto;font-style:italic;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,d09GMgABAAAAAA9MAA4AAAAAIFwAAA72AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGjQbhlocNgZgAIEAEQwKqiylBguCFgABNgIkA4QoBCAFgzIHIBupGwPuMGwckGFhtxH8MyEbMsSab4QwqaKI5gOnPv8mF8P+xTyVHcbb5D/Pr61z3/vv/5mhhlDCwrGwajAac1aMRiyiyobexbESjDUKI3sjjYx5BK2t2ePAUgRLEzGL1RLeoK0rV4zZVi3+ry715RzSN4Z5LeAENJW/pADAeO6pPAXXIk0EK+HU9yQrhHO3WHh6KWVg8D9jA9WohGXbCoM7tWba29vd/w3NdFO4SQp4swVUtYCSXZW4bO9CmyvwPVOoRPmU2BEI06lQAOwA2FeRUxWmuta9rNAVztY3f+o9z3bjghCqcYziKvP++18RCOMIAID6GM6NG1KdJ+KjGCEMYA+wRwACGNTXjDKMA0eg4ZyVHIuGe3JYDBqeQanxaIiONTkeRsSRGwAgAAMwLswgJQhAvlMADuGVJoNJ46glGwMyQV1AhbxPLkTy2TzyO1ks38vPd7gsX8loF2C+ceEXpSYjgEM+TC9P5ca9mxs+jXhj+ZSyjsh75ZP8W0bLY/K5rMDKBXHQWGttteero8666q4nP330Qzz+lxI9H00BzVOvipYCCIG9tjJetNaSaXdptIeM5J5mKNLrKoqgRAUk6gB6Gr38ypFXqP7J9hGOVBi0qXP9g6Kn/QSkuhQMARQuV1B7CKWFj15+5agABDGyDM+gALgu7vqH1JGNJww3hLWhCZq2MIF9NinPzvM0ek+AKKItQM18cf7aEoB9Sd6r2K88oH7T4H6gYN4bVdggvCoM3ugBAKUXVfDmjVdy384NRx6K2LtfnRGnBidnakxRYbiSqmq/qf2u9hfvjVICxMhIPhRJFbS1dkXtt7Xf89ckGwGS207Z0m1Rd6x3ut4pv3WzeZpJtg/c7JRksZRw8gBUQkDXAnQF9oG4ALEAr+8GiByGrodRZLAADQlRAP1kf/Y/2BR+m3T8q7DMdC891TRLIR2yU03L9zI8M9828/1cN78g1c50LRNycoybnGGbtr+ITM/1HeEGorc/ZaDR7Y8MpEM4tZaAs6Tfbn6Jc9ETPs5jbCJgKJzMycK5Oa6p2sgV09MoBcW5kHwLKkYTVIhArjO048UCAklfXmzADhpJS9we8rgvSD24d8ulNFGvAeX3ivapQNRax5MqrMX7W3LalT7I2bjEbLXoOT6BtkBA+K+L2MNy2n4ib/ic2BaecszW4hlEZ4O2bQ4ZD2vb8u8VJX74o9Zf1kd/KmOqPPQtbFqhFMrpwFv4FrnW6fxy+KmtahmNVLVA4+3CXecQEJCeATtA0Q/Gd1QsFAdhdxJBdPlihB81yFPvwAEhuF96qV7zNMyuNYfpVmWiL2ghWOL0AxkH1cQSt6TEOB2n14XjZg8MtC9YAvWiz4vGv32IkIcEaxwy9Yx45eGEMYoh5vWAkLL4CJUwoctxs2T8wx9/KiQyrel7taNS8zjfpcsfMTPfsYIyrxyYWSIc7u4ksbmo4u1AiSg7YkgEreULCR3QSuohSyxMW4J7NqXMko1hfvqi8EPFt7A/mFDvq3/y/YPfK7Wfm0GyUsR36eJ2lCojRctCDXLfJxwPt+9a8L6j2hUtaCHlQdomVmYQ5fQyWU6opRNrXFf/y8JqoeabIV59i3Y1GiLZv3I4/T/E1h5EI02jkaaosevfmdLnpw1bKl8t+k9efX7j7/YAo+vW8UP+H5+aft9xv7+6Vu/vvcPWw2i66apXm2DpUwnh5dhH7XbSub3Hrqb1smdTd6M6apTCphC7941b++HhAduWOKzy0EWJ2NZ70yeNZXn8+LzM1vqH+t0zrs3gm5TbDqb3GPahyjD8Ut3HFten/G/+XepLDQzDL380DL/iXJK2JJsX8B2LPMoNKb8hWR7YWtun3pqxhs8T67umlAo8h3PqHs5Bg9Bru/5oYcOcPTXzcxfzMtpbJQq1De4nni8ihwGjhrrGZLOfKHmIvd9zUkOmzL8xPI2q+KmLxpXDvmoBTdzp5mYLTel/rv7FRBSsCDWM1npZBsKvluuvpfpL0/PYaj4uPaLpS+Nu/OaUkFe0ns+nnffVQ83HPu6n5oy1BlARDykacrVFbgEv5Gs+4YtrGbtcGPzMbpaP8+ql6pPCInaen2/g8cwhYr1uatayaFqoTC3OyPOb9H80vVt5QIx3Oop2cYGGvgFDYf/C7mSnF+fdfPv5H7MOtJg7WgZYp/n3R39v4/KF/NXPVl5C58rHfXFY6LRxsfa6bDYvprO/jP9sP+9ZihIZOjmAZbHVx9zWiqCpYdZJfAEfvbDdOIdMbTg2RWdP38sjqSSk03a7zNQDL9IOtzPpc5KVpWLSDN0Mwwu7nZ1uYs/44f+qPm4f8uU/bGhvZ9cDq0ayhL4NLB0S7EY0+ogao1Crc4vLGLzz7HqHEWd/c0qYXLiOB2N+5IhTPKORNtq1skx/eVouW8XHp7V5+6HW+neeP7/w+HlDtx1RwwxRAVOGUxEPLR5ytUVOIU9jy/fB6cwbOvRz/YXdmJr9UatQ87oNXugcM2pD0f88nU6O7jV4qGPoFJeZu+oMdejrFq6EKvldglfWTx29OtvJz0MXpd85/Uo+36jcdza9L9ciRWy7A+mTxrDV6h3Z6C2G1HFesVS8LplDQbSlf9eB4T5eOQ4/VTqUJ6+La+jYj/Wlvlr/+o7t2/6n3BC32rnff5LMIoMnj+FZbO0x93VqEMsNnhtEPsQ1xz02akMwvEFVo5tRhvQityWb4PL7b3cu2sUE1n3U1/kVn8v+zQu/Z5x1H3uKU5flStvlWd9wlNtcx82r1q2207dtfdPtooDULtWcNGWZmPCXULtkqP3QQOdsdHz/0nkvS128adFRTs2ci2A+9Ug/c9+iAj6Dli+cuhVKaabfT/4H0WXeE7v0qaUTPC5Fd2lzdBDzCp2r6ZOmzZ9Ir+eNcZ06hNUIg2n1Qwfr/QmG4iXR3GjMSbKrxipY7opa+j4w44PZ0t8aNNjPt+OA3pXWgX3Q+m5haa31pfBds02L2JlRykrYigwKWU88fgrlk1dyi4sr/Y/EwdTgzrJXX/ZNK9tW9tBsXf8IUr8BnWb+c2Aq88vzoM+XZZmBJZWGM+i0+tHaWRVnK66iw+fda1MMuS4B+uD4gcLqGJXOpg5DPxZd6FGGTnMfrZlbdrLshuV5+YObOr8RYzvXi+vSwdlUp1eAu77fsIAudZO7asYZNXrDd02VwgZ91hjzP90vHcepQ+UwP9imi65KKaTpVJlGYWuIx+TRrNHt/r7ioU97M0qUl0zgs+wn9eN/umSycfPdS+FbrUqL3pZRQjOpIpvC1hKPy6WZ5JV00Kgfvu16H/Ip8k9eWXt4mJdu8PjovtVjn/RpmLy99jD0SSzdU2v97risYuxWd6Z1q37EMKjW2Ytmv43Hl5f+73/MitPK1/r/eS5QE3Wz5q/K53th2XwTrCEUABqIWpGZRPYeFAFQbctyGnXD1ahZfkU6D16RL3CW1AljKQm9INuQqbFwATVTAJWoVx6B94x6pS60T+ZENerCnBIHVU14RnWjKpLfc8cy3lJTJVs+soLn5KqU3jdZxTMSTavf1QNrBC+8JbPefTSEl0W12qgmtYqqaKnfXN+xzwh6plnpqWCDvKlL/shUlQ2/BrUSja5WyqcpSLoOBuyYnw5ImFP+Jz/mlFFQVcZZ6hZVwT0psYQd5KOkZs9Zxn5qo+S2H1nBTvJSSvObrGIH2btrs6uG/Vvsp66D6Fil7ThIdfB5qFo5t0gpaev5RKimE0l7w2BqpsCPphF0prSZ2h0Im2EjjEaagxgyyj2Q5iA9Msr9kOYgjoxyT6Q5iCGj3ANpDtIH9OpYpZ9qWL2tZSq1he5RS2MBydCGYoY2uJkTDagjc0oWVJXJSO2iKjiUkuqV2wAnaZr8hHX0IoCdocnUdRWKtdgZJpgeg1AH6oU96Uj5HHusnCxRDDb9eoH+2DM7Vb6F7qk7+SFP28QX2EO81o49YQzW09UwRlzgEZrMQXqH8h92kTsavh3jDPnqXRvVJwiH69m2Dv3PeiVorDIOkyGmyA/xKCBXA8oWrRZM8jF/Lx6hPcAtWhu4AUyKlwiUD0VLrSks8rHSWnxAJSD8NbPcZeujuKj4V9vmKltEFUy2hfw/ZUhb+YBG29V8r+qhbSsViWquDG5xv1WzvGKqdrOl8pe6Hv6e81yt6OPQfLd8olIb8DK9d+i6Nb2r6aB77lf1TltYi499ska2Jcp+UYXONqvClKGOAEQ7TuRTl5oP27gN4oNX3Nb2looANVdm7qoTWXD31x60VI6p6/F/kYq+Tq1bLyphBtj1k5sAVqhOltK2gPmIKnlf3hHTi78Qc1BRV5xFR1u50kgZRhP5iGgHiHxsV/O9akttW6mIU3M93iKy0HiBdjP3d3U98O+Rij5OzbdAJSz8V6M21NrCLB8KocLjvTgf+RDxgdisRG1BbEV2ZV2MaCmqYEGp0lrpdF+hA0abrM1aLz86Ikg8R2dcahLyJeIOsRURlRGb9RqUuai0VQp/USV32ewVF6XTfYsPmPlATV8r8UG+ti3CUwUIAKvncistaMtEpy4fdJ46AMDJ184tAOB3Gvb6a88fv+szdSlgUJgAAARosTZ7QO8rstmC94DYgUk3JXw+QvFF0xdAtJOrlTg0Yp3RXoQjRngiUDmFSl4is1gJzitdYVJi0Flph85MIChp6KiMhYVfk7uYFWeVa+jM3GASUQhU8mEWMxCo/AELv06Mx8DGT+Im8OMP4HsF/xVzeDkp/CP+K4Er+Ev8yWkAoloRSTtJqc3dFSZvcoMb78318f5+2W8557bwsVeI0/XzMRKkZEKu28vtW75zw9plg2FTAMa1WBYEbK0fL6ZYvkeAEuWqG0UgAOAIDOugIoBOOI6yHsAEoFTiZYLK2MtUOR8z+1RUoaFNQMXXb9XRCJ/5SZAoS7IoESKl8tZGK62Ltt76SdB4Gius0wHihWgR6smA2HHDqkUKaYVJKa1k6dkK1YKxEgQ7kJrtzZ+Nj5ImzoBkBYkl1zZEvKp3FqN6WCmiIOL1ghbRtnx1Vr+qb9O1a96ba49PlaiTlgXMCLUQNU4UZIVp4axkEdArs8PEDxlKQfZAA/7rSR5kuD6aK/pOrXCQ70FGCzUBAA==) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Roboto;font-style:italic;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Roboto;font-style:italic;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Roboto;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Roboto;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Roboto;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,d09GMgABAAAAAALsAA4AAAAABWAAAAKbAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoIYgXsLEAABNgIkAxwEIAWCdAcgG0AEAB6HcYyyEjO2Dy0eKLv4XvfsrGs+wIhEBOHOERRRTI2158fc/aln0WYmSJq8uTRSIgUyIVMqpfa/7uYHCqzWDuHREj0f5UuuL+ZAokTaYgiIs5sF5aUutjO7QhBlgMaYvCAIIqqoCggoq0+HjRlX70MGclDLyR3Z8fb0q/ectzCv30obmLesvO5hBhRhcp7kToaLpaRXpL0htKmb5C3rIgzUIwA1fnqrhHSbqXhA3v+sK1wRtcWuhdyg9E5tGXERkaAhroCGeNqCnJxAm6m1Sb58SICvFhXFWnVAAWQoYRjYADJUQQqIYm0uSZKkfpYv1sv21dm9b7kWbV6i3BQ2Z/sOf/hl+ezXH88LRz75pnLuq4/MO/Zx+eyHc3x9VDn3yfx9n1ILyusq3ps75y90fVZ657PJ2iXgF+odHbvzv7Lrm+uTsPR0WJqYcelN7180rHDDnbeWbrx0QHht49uXjCzffOsd5RsvGvHe4yF5o+Ej97/ZMP62+Z+3Wz/08CtZ/FezhpdvG/nb6PMhC9vNvHFx3Du9X47etewROuONg4L0v2eI+L9X7dt0evq+gNihfvWttiuWK4f8VmxWBM/+WK8b8F6Y9evfLf57r9SjuA2URBAobPm/Smni3y3+n1TqgQEACsl5awAI/5AetjNp65A+/38vDAUXaayPL4CMKHYkEFC0DlfIlbAMegyqlmGU2eSTO58TTHX2xLyWvlczc/wY7eDo5WxlYenKyMvNg9Go5MAatqis2Jty2oytLaPupFxOlsgFObsjM05dBxMHVwcMbeFma4xFh8jZxUr2e62Th09I7Bd96I2RI3gzYzqKcsHjqZzGjsamlojTwdmCy9bKFNm7IBcudRU5BU09BQ5eTm5coMaMAw==) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Roboto;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Roboto;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Roboto;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,d09GMgABAAAAAB44AA4AAAAAQKAAAB3hAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGkAbjgwcgTAGYACDFBEMCts8zA4Lg3oAATYCJAOHcAQgBYJ0ByAbBzazETFsHAB5cO4TRclghIL/MhHmoW/sii3JkCwIpmm2o8EQIDh8squu9JqOff+iQjf1biM+8RcrvTvece45JKlkeYjs6P9P9XT17F44fIAcwUEi6lMpFJE7/QM/t95fEYcIjIqRJjGQGgZRKYMR5URGpCKegjKkN0A2mNCCDHoYMKLNwKrDoCz0CH8K3PbrMABNLZi8I53ljHbl084I7Aei8kMtYPer3WN+IMvTyAlb90UTgh6oaMK1IYR1ivIDcHO5B9xTY1F62qQ9HEIjhNkz61vW+HudZavvL020NBMd6YD+zjgKcU/T8/TARaV9smT4+xfkBdsXj3TH3j2yfeQ9lg+03qBvQ9wBwB37GMoQVkRFd6mSKiXg9FinbYGrFHUTCLeqqGT3nsNGZAhuEBGRzNzvNV2uwkxa9CB7bxEPBPBXjjr+TggoogBsBgXLmAkEiTmEJTuICAyIahsQCBSwAFgAAQKYR8NumL32cfYGrTMzkhJA69ykyHjQuigsmQpakAvPTqKCGIQoSYAAClBI2A5uRIss/4QB2tCGlT7mCjUsgAHDt3LvJ0jCj14kSvTam+zU+y+Pv3Xvs/qjhVs3rWUVmnzdV8ecFzzauuRZvVwQvh3vqs7nLOxrfnPeVW/lOV12b9eqk+Az827t88kw5jsvffR2bnP20BoZ8VoqomU/ct6gJfWdrimvJhU8+eSwvFEuy+boVmyo2m10E1ZpqUNBlxlcaNg77hmfm/F2Ae143UrY0nAXzy0JG8mkuz3jZ5n7PxO34COVLwnYdbzneR5KWCRZ04BjJ0acBFRfYD3oqz5taBmtovX/F4+w7l8gQpiLECVGrDjxEhxCdViiI5LQJEuRKk26TFmy5TjqmFzH5TmBrshZJcpUYKh2DksdjgZNmrVo1abdBR06XdSFq1uvfoPGTJgyY86C62667a77HnjokceeeGrRM6+99d5Hnyz57Iuvvlm2YtWadQhzAxAAiwv20gVOjr6V+JlFgCSQjXZUKs4S58m1TGSqgoFAy2BJVtwLODKzaLk0n6AsaosBW45u1ruKoeCKfoUbebwPahazPbl0I6BHR0GODBweasY4TpaqHlDQUDDTcdmLiCALg2Ofha0WmzraagDkKks1OOEAR8B4JAr6WAfrY/0kI6iLLqXUtIyYQNGrJmnB4eBDnQnMD7HwJTA5ws0lp09SIkJIXkYrVQP0TT7AAqLvtk0SCoo0jJ9++W0DAuWyKxCY2wbcGJaPrrdHCSzI+9MAxKo6aPihqLu0kfR9FKykbJ7Had9D3ezAPEB1OQ7+B+eMNQUIkEcAdYfkIiBA/xVo+QpoyFsKJm4E9mEOCxeLY2loxrbQC+NwCo8Ijeg4GseiOMqCE9z4FptFoRiXgFVCeVflk8qryv8hrEZoJLQTLhC6CcOEK6r4zU0CsiQkQiu2h36YhHN4Bzli/KT66Or4u8gekPIuyrnKK8p/79hAaO7AI1yea78A9BjQo3rk2YHcD67eNPp/d9f5yg0ApsV///hqs2MXX1Fe/nj554UB+PkrL5yetz0//5zz3BkQYK/Pfuwh+CwBlA9LzW7VXsdQ5M7EwlanHsd5DRqZ2XvT/vbeZ79RfBMmTZkWJVqMWM+98NIrV40YM+4HbwgUQajeLQb4PyD+DTwGZrcFC78DxrdBvRfcPPTLN9umLdRpAWXkfrLYdejNrDbOng5Ojrvp62g4XHBUQRsmpHTc95NTokBwHxx+zu6jj/fToaiqf3GROhhTTEdiXY9rGW1LM3M62r7dkNaH6VCdd0X7eJs2CSX60LZ6nJ7e1UjqZIzWWV3tMeY8R7sis4d3aJ2k8Y79yZ7o8J50d7J/X7ozMiYxxI09WsecmfjcAa2VOmKOaK3DMEzTfWEY7j+8Z7fZQ0brODb1dF/90G51iQ6cio4eaaSSNWV5NVobz1ZxLZV0mIQLupNMSvdP2vopbKd/uPrm1BfqGEDBlXqWpHr+lENpf9pWxFVCbEcnqc6gLg1Ig0xSTQX4Y7Gm84Ki+Py/W5Wan13gh+0rKkbMpNAkiXUWchLPUzgqiTqCXHLI2F0bKKXc5VsFzYWJsRSpJoVTTWpNfDBAqBUlP8KwlBZSu0x6/gTu+Thhm5L83VjTozrvn+wK0J2k0gxx8d1+H9udNveA8ionCEr+6w6VTo2I1AZb4oLsMnC71Lof+2jn54a49toCh5ZyL1w8kya1nI3w3bVcQU1hi+casA2ljg0oOFVokRuvuUIhdB3jw2pRWwdccR6UCLOVeqSt7OGu9vfcpS4YiKbou0Rk81Q7bU0YckF2YxHzqMygngMbnTw2FwGkvYouIO+2OmQz7IsF5isedr6UELpy+ZuJZMD3OppCv1thaySckOHR9rk6lofOSaLnXKeFH9oImmol39KloaXX/BLPr1Bf7XzAldWt4jb8oMY21MhATsHCZir5gV+A/H3ZVWqz6uQLY8SRqia10N8d5NTxhiMknl6KBAyknZl1+Hc6hoSspAF2yLrktDDEEUkP4S5QZIJL2zx/pMsOH6vU+xbjb1yUFBsgbaia+6GinJ4Jz1NyJIKQi3qinfNSH02HqTDpSAbpRNZKJmGa5i35vnqEUbSwvZFmidKHa1PR9s3e/aBiy3eRsotyDm600fJQFB5Rr12vIA2EkqXPqA3/rYWgQTM1301jJa79AJEBbb/8fW3jQhGAKOLivlWMCTJwEwsDGSjiachUryUHmeJmhikioksURIEgbsHLKyRzMC0CmaFFH7J4+Gv9t1AxlEjLf77WlZCwMHzIyVVTAID4ekxNCTX2C41l0YYQmQ3kckt40p0e8L1vMHsCbjV9PfM6imxpaIRYq9FJPgBZADAOQ36u22ubThyoapr+X+rjiD/9NgT/pwIRq7vjre0EMKWEbw4Hq1oYjLWWKJlgO+DwGGIGexvcoABMn2a0cUDOEo6xeIZhGkWWkrYmUCMK5jSEN7e14mkFLcrJk2e7UFardo4c6pUjq/4XrvKAnvCy13lAa9MoD1P+L50tGb7cVv1oj0ZiLTewTP3/WNaue9+2uEZDMSaKg0TivITMbkP+Uj06Qv48PRftPIGYiTAQdA1oMSaKkLFryCvJipqJow3GeJZdgSQsFfKBXbI0r03OoXcWN/lpLiQ8xsMMZG3HYRr1RRId5REk0WRPGxKcrqUM76ad+dXnlFXe5axIrElK9DNqZIqQdcIVXj1G2DVNQ3GamHnfQqCjBxio65aOpZDZFJKql/XzWKiHbI8QLSIZjgfqU59tzb4h0OU4YD+Ido+KAw8WPiI9SAql918AhP3oNIVds0D4y98j36xRKFug9vWwMSSL4kYnrZtjFcI1IAFgdo3z5AChfSF3Ax+AySdHl7ZkuzzoyNX4NiZ5138FFAq9TrOOR6comDy+InOZQsFkhjRrGQBaa1eSinE7xANVwaCnnbFGVtehpCB40iCLN72ZTMpbi6CTfrVfE7VdhqP1qnSvkc+yQhv9hZCt3kWk1k04GLU+we1cDZdOLP87E535CsKPJmphHMKhxnOP3fmf7/7zbgUnXilNKOiL2XsrO7wga0ptktuqdo872SP39UcruBy/Lv9O+fcXlNERI/p8iYFQY9cHGZT0G75sZ/M5xtDNrRtFnydleurbSxR6oQ2w3HNX1VvYhjATcp1tqNU0jmwxlEiZe/Ydv5l/HyTuIbAfxUnDLLJYgOWWs+/cTYO9YycoJ0YByz3FnlqhgMvoiEOsYAy3B9/MMEDmjjnox0q/kfqgfG/UkKDGnxIFSFt/ThhJ4Oja23nUioF7LvA5zziW0keTniXxIe2nbQS9fi5f4Nbv/249Wl6cGc0pKMxLK6uEUyDf2D209L8Fb5668WFvnlaD9juIre1h0WoZfJCX4ipNNL5Dv67mbSxOUXpzrlzpbpUE2Vhb89ukfTc8nG/0zGqvRUePgHtZ2/3i/QIt3A6h1jIT5Frs7VIL4faOLuHWYvN7VxH0DclLAzclUevxG7eVecPzoqg/cNXZ18XRy/zVd8Hn9wvKZvOIPrEi10s/bituLc/Ory9mghb4FHy3fXG9qkPixVPGJ1rufAb/3xZG9Vl29uEARmZc5EJmeMPhbvzd9wx0En36GP/fsaqGKk7W/cpkcEiRuAtYiRH78rzDjgLHJu4zuAbYJ1tVvyogyMsXVx+zOy9yGjo62U/g1ZzCyPYOCfTP8+LlP7d1KY+Lqr/hS0txuyQmNKWp0lR8smaXNJY7ChF3sx4/VqGUqoyqLP9ZPAWTWguWRgnxTZ44+0cRmOYyK5gVoNT4uA7RfA7bN41H7sne+oW+wjYY/tjnE0ZLOkI5SbEb9khiTPilXrozjG5YqdT0E1uj+50LULN7Vuo97UcLg315lPI0gYAuTHBKywSFuojRAhU2bf1hfsXAt0cCnV0CMWdPxRbVzI2qX6qehYOav/7TGblKPb6HBzhoF6RR86cuLxn8HMINMW+c4rqzlj2rOgqYt8AZ/xRPWFHjZP55evb4nY9SaJdFdF3PxJnwfDd9i0S//JsStLlE5nnxMmVRAXp+DYRq/v24kz9FLRRMayPc/rl8SnlOIfmGUlPLOvIZzDMh1GOjVz8ReSuDlTfzuzzYX7xr2vOZt0DSazCTMemHypvnLUByzOHDgfmhmi5oHuCABz48Em9aWftQQk5gVkI8SPaRBk0U9hErfuzZb27pdUlCeTfV0EglPQh4a7T0bOMFc8JT3SkvG8fvpTwCH3dfBPhGEiYttXDutUenoUtHaGoENv0eby45NiknOj9TOPr68OTS+wHLGmkeCfB9JGx+1rmZxP7ukSBQqy7777PTxYtixP+3sNN/vygseypG/MMT7Gt+RC9qejrd0/qUfrrlEeygVTCIA+Y1wCP1obIDS1qMroCeqopToqesWaOXK8395IvBrqE3VyqGnXMPhUce8bOzirWS3HfBxzPdr/T9RV7edFBiI5mHCT6TkBR71BtkU8xxc8VzdRaG5haELIY93iY7p/JM3WTxJA70c+Pjj97q7JuBiVHepe8zd21YeB6JC9b1mwnajIfvIzHEaHvE0HsY+EbS0BavnVvHd1bCZ9Gt47umFPa8jNjyVM1ahIE/GOOkGrH9kKyGzhyYMjKYQQWaXnLO1XtOAM4nSDshIXsQjZ07R/JtoP9Wur64HvBT8OIfzUpQ6q2SLwurSyzGxbn5Guju/hUmqHISUhKBJkres0B+ZYzlDlb14u+7Mu2lJPg+4ukzyk+nwQIv5HmQa84Wv7syEuM1Edb5fnl2VGMR+/+CYURznzllLYyublUQSW2eDgskum8ZMM5T8zoSeCBDJF7hri8ksfm95j4vQ4paLnUwWa86F5/7xB/KjIktPOQxKFG83HeJ1uVJ9Nzv2ukbe/s9fKQ9xHV1Xq2sSHf6ciCflX4gkWHPcpD6/CYZKTzk5RIbbIjeQ6toFzsjr/LvyTIAfNoy/7w4U0wN2WFfnh25MFZtzs76+7ygJMZHzaEimzK3UDFkNEam+vY/tz/T8iiyb8CX6tUVY1nY/JgHjhO3Lt8iHBPl4fuFFWQKVvGqLpta+THQdtc4e8okA5+zyOFDxlbjqy1eBU1fJS2OLYLPMGkYri7EX4uXPBdEn30+LvJ+90eQLnfCeeXs+yP2sGilJ3fk7P88H6THI1l7s3b3abih2ChrG14Ng5sUF3Do1nZe7T6PLdUu+wpu2u2+Gxcn8mpizWJiAJ9MEqmmdc73Dt5A5kQamwfPdby9a3dbnh77UUg9ltPl/u/uYRLUX4TWrivnzbwkpYsyDQYX62EIr7Tf3yZlTQC1qrDYdMZ0VudsMMvvgw4l3c178py5VH8zq20RI/qYqPb49mvQQl+YR7W0DNTsE99S9tTKwjY6GHOh+EI60nzxEsfMS1KqLGDvBfRY5jy45WHlkyDUUrEPrkfcLjUXvtDxraYmFBec92+LC24v+QKsX0GjrktdWTuGjszJIf1b7o3807YCByi5DPXr+van26RH2PRMVH9jiMKhon4lxPpbHxUKLAEfjntJwuSC8rrb3Jv8f/JgahV9W8oevR58IO5rJX1lZXVoGy46jorrcsIKsVJTtEsAaW9SeXtbd5UZMWfO7h1SDiprbk+37PqlUZn14wE9A25++Psx+RqupX66YDgz3j678KTY6/lwRoNkwRb5nIJK0Iv4Ilxd2VbRVi2yvjURFKV8Ktvqhf+KH/ktLswC7ZMPMhrLRJrK05m2Tq4Otq4udiB4z4+yf4RqKbl+WclBwZkpHZkZQ5kZjj66llZEPSuLcEtror6FDRytTQz0tXfVMxVJt9kVGBAV7RtwsjrTGAzePk3IPBm8o5e8r0NxB5uYhYtPLwxRp4WaqqrsMrHSBs17m/uh05agM/lIhwE5y7YUsqNdWKidbWiwg3NYiK+1+gHbTfW1ltU18bB94hFUOWJslFwDtZxwsZXVUT77XNychcEWptdSfvlZWnEqOMOckuqS1OHUCiB63HdDWdXsC1yEWkGWSzoxDwkVRFm35zSj88/nsLAD02ufZ64u3ukeiT+adTj2eHUOdiA4xw+d7wU+tI7nVc8r7Fw/jO1/z/4w+uFR1aMK2n7MqDu6GDNiuqpnRi5/jC9fqNjdy0xL7ddBy9XFQOjrC/PWVjeDygnbPtXF+IF3l6eQWUMeYLkZc0sj+P5i3DBuzuEldbTwDJ1ZdaroBDIPJNrdT35P+BFP8qtat/NvVS1HvhzyefnWLxoW9XKpaqEUaajKa1qt0cAnyz5PehVOGCWq8YcS+Qnq/N73y+yiKj/mHkXOGCt9K+IW1lBafu7AuD5OpkOGC7saSV0to+irITznYxFpVLDi8EiyFaRFns3+I1HJkNPF60H4jeMdCDSakkb1pphTB6dXx5pc96cThoeXmOOqCmPMt3HryVYDBuUHK/czfAMCOjBvHL182P6wt0li6YC7WPKsNqtKvHu998mSmchr8RjI/pUN5+Ikg6y0WXjdK+sCcjosFlg0oCOQW8Umgk1d7vHigavUHqbVj6MFjCK/k3qYVl/+4qtdQWa2CvmD7uqRdwRMktYgbwZ5xsKUqSzw5s4S2MLIgyneJEoRl/BMdZYHGxJu+BH8DfaN0zdYNx7JfRL/PH8P924ZQk67uWoGnuOU0o+11J4FMsxLjt36+F+YApV75KCaBnTXTp5MZ3SUa/KvJbbHhdfE0RMfh/t7R61lbfPUddKKRt2EifoYO7sE5Ghwt3OQaw/o9RRmM7NBQTrpypPBpOP3bSlke+vwEAc7cpCtPSVki/S2Vl9dQ/2bxjq43Ukl3jaL8ySdgaLeyctz8eqA6ftHmaPHtux9t9/35+/sQHE/T7598C9++Qc0f3N7Q2FzE/nRDNNsJI+5AaQnjN8bf2J8n3nf+g47in3X+v1afwPDH5kfXdf7ZtfHzMfDa/4d103uGve4WrQdUdIafyrpQBITNrj7MHIP0N9N4G2z3li2sbrlC+Z/3WvqJ5HcDhpDztTENBxP1PvMH3bF9lCSYTwUCWEBj9DCq/1JdVd5/n2PbihBiN/jcyi/62UeqeYI2d71hLl6ustx7tt+b6y4KRYdsTlaIsA6JIDRjuoDiqIixpDwCAw1XmGozc0/WLx6pmP/qEbvIsEPr6O1MAaRqiEYS4gxFX6ComUARLZ3M9Bw7ayyU3QCljzQUQ7ehn+15HAEwnDalR1WqBKEPNxNPBYgesrCsVJ5CM9JgkBgBFBd8Gkm0IF1JCwtilOYgbiDtnqtH8+VTGg8PMOrNB4NBq+j1fCH4vlyVctO0QRY+mCvkOPxxCSU2MWfCTely70ygkpKYYH/Ia59b9gKppYalEXR6/vDUdHrGnCKY48PK69j9wCJxuV3QlqpWmr8JuzGcaIYlvZEpGwMsGpCLZYBYxFiH9lhiG2JfTfoD/EWQo6K6RdTRxKf3mFRQqQVREHDkg2GRSFHwtTej9w3MOhzr47pE76JV5zi8twkcQqTuQEmFlppPYyYllhBQPqR42YjQStkILp4HUIyjAON892A2Lt1ckphcaLnY5jjbZbeOYKGcseQDlOfDFUO2StuER8mxM0HwCR6pbmd89sbDQiAKfz2kv6DlyhRx2/3/IzhnWlRU7ajaHkAi2yPGWi4Ttx59aMOAFZI/6kKOVKmephgNZNyBx1h6sNzGS8Zjqhqfqdpsqiroh8lQNH3FezLASeMEXJU5hkslXA1GiRGu7jWeBJmp+gZi/2y3imCXkdfwxiwCiGqOIdTWCjO3vtHcQvrMCJuXgAs3dE+JtluqAa8TIkypM0119ofHXWNMdkF0XwVdCxVoLJTUAG3IOUOmsNYayM57IZgA0Iss2HJDMXMJGyPSB8jlxmJ23ioo8qX3ZeUj0KVieUSiFseWTfWAbf3NGR5LPwCKF2xLXHYtPeIbfWm1RVMU2knGBNzR45RCgrnh+lGiifmEsAoT6zi5pzF64EZRGxB4o4gBkQJn+W161Uxj6FC2yAM4aDsQADkoG5zHqSCdaPCNk8c6+yoLkh2RxeYYAIWiQTCvPIlERwkh0IA/mw60ItuWJ1vWjdZfGlGLLkUQa48VjhU7jl8aqGl7XVpdpaNopGH0vKk+nD0E8zHZakBL5c/x2z7fw7Ur42WQgfmroai7z7tq5Cew2p2lo3ywkMBI4zxlnYDuEEXU5+OfsiT77ACr1uWDwU5bkyc+16aE2Yr9y3KmcJ0MPx8tOiDoNww6nSWkNPyU18gF7WvvYcckRf6EtlzlO+312b9fEB28o/05PaNyS1icoLVjFtHjMG+lL+Sq2hyGhxzgqHuruaNhr3PLKbjqfXhxNqSbapIA4/J3FYaicpB2WpksCSEWYn4TULI0Z7numW3WvbS/AAo00eBcfhtQMRJSMxXxUkob3WV8OblfPkYqX0phdpvBfWluic7pWxcIjwUth1z07OgftNPLD9SESchO7m8dCjqnupqQxT03eBh2jdpNBE6x+GSipOLmBPiZCNW19K5zdK57051wc11GDO5hHIb5ZvmWjq5qJilGhGIo9EE/fdlqWWgs7vaPqopGDQ8zSXK2mvWaRNE2UP40rIW5DHcgiqS3c6g/WE0sgvkjxvAYlA/oN2kJ6eBm9E2+IJ6Q534g+ENjdL2M2+O6cd+cwWMx46WXPtSy26I1N6QSmOuoJ5Z9zRon11UfOTNyf60+HkO9AftCCaFoF034UpTfCol16HcHj5V13pxerwouRy2vpL8hGH2b5lXy8glodM1TAeTZaBuGlec3HyxG2mbAqptMETQ6lOPAGXNZd9zDn8VunXvPwTlZgDw5Z/FNwHgp+H5998Kc/eE9GZowCwUQIDxokkEYHZ/kzg5gk6f7OP/A12ENYj/gdyOYhpKywPaKn3jEtYgaTKzT1vRNljjGCamzrl2b3+0/W3KXKn1s9Y6wr1OIaYe+ihnX71ua/0W36EWplzPtAY6VPUE1xNC6z4hNQe5xqDHsqL42EeqqKJYVjuiFdY49FoiqPSjV4LQwiJUz1fQ0HYNs6SHH/wHf5FDu7MlT1ZsSB4z+0rmSm18rrVAUJ0WmjWU4rdzlaamulErO6hlofO1QGn8UZ/5Qgqvv8mjImuZoCxBr6sKCrq/WY2FDxPahiJFQ5zj/X5nVTpllJ30hylZ5Y+DJdBRMHcKmNuuxrKtzYKaD5VWomUmVWv+R6XtQs/HVKqanTUZIe2FpBuV4bqYghY8MBSXfuz4qy5DCNTb+6s6hVhYfS1NKNZAh3JYGcx2hgTWOTDlhK70Su0TIrByWM8MCawdVpdRtPtg/O4sQQuoBy1xt/dANpb7Rsu2xjQ4PFYUHZgrxAdWnVFdcWJZeYzaPH49Sr5a7prWiotzRN2a/fKaIR6OCjGEyOgieFFKNK8cQSja3C9ICG4SIg3xmyUC8YeowiUAcTUuBYitYw5AZGEUEMPDyB09YZZw6cFlYsTAsDjn43KE1gQSdkOfBwjwf8WkecNCABaBArUWHASYEQUNqbPAKaDkRYg46EURFedGn3Zj8GJpSffiKGKni/I2zOrfESijUKxoMZIR6NNDNITAzmFVpQSRe3RARaETtKighGrPakorRiPRbGaSVJEi6Gj0sHBGyWBKjpYiQRiIfEkSmlhKbY10RhkwZtZJa2OfXNqf0FzdkEQkujgtoSNM4pJMESOSjgSTZqQbjUWZERV6nbsuZw6s2HDlFVHtPgbqQUtOqseJAAA=) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Roboto;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Roboto;font-style:normal;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Roboto;font-style:normal;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Roboto;font-style:normal;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,d09GMgABAAAAAAMAAA4AAAAABWwAAAKuAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoIYgXkLEAABNgIkAxwEIAWDAAcgG0oEAB6D426JQgSiDJGrY+EepR5ejwf4/fWd+/C1EBKYZDS7sRFxHTf9uCJn/m9Of4qsOwRQBbqEex0QSbKziM9Pj42dA85/tYTLU84Cj+f+PIAlq3AtV5GCrQWUqr11TNFedSEUjKs7rSju46fX7RWCSHFAeYQcQRBEKIqiAgIKlGZBdO5a3w4akEBWj6orkgSzThrq5iF0WjfiKGe7e/0dAHkwOR8nW+GblHR72hyEGmzEl02NcDPu9oBKt35NVVBcoyEuIJNhau72SE3EHkhapkdqCiZGhBhliQWUJVETSCQCNfr8o/boWoBjI3miLHqQC4ojH22AaUBxFAUpIBJlJeIVGIvLFI6PlFi4hGYVs0brZ4ZZlT0rbz1SLT+50xlW3X269vh2x+CpO/n7bw02ebvIys0wMkpteMHUIq4PGfxCRBdKjxXGaDRIc42rK+a/qgeebsfBvjGMiQ14cnJjW8fSe6fHlr2NIrgbeH2jS+k9X+md9WJP/5IvZ8LRg1cQ3gz+dJMePnr2/6ZSiy3c9rHc87Zj4tqOx0WLe1U0VR2OOEt9kq4gV/r/NBEyVbPvpL70poCoTunu3LVVZ4nW3xWV8gAKP5VqBMD10Pruq+7/52x5c4B8EQjkzs5oyJ/1JzxT0mgEACA3XjUZACFDut7UuAEqPZepikCuTcprJBVAcSJREzIBeaYSC4kSGAs2BJU5IFLcQjt+sxNAqr55kwOx947iBrvVCRYwpBuDQusVLFWyFCmCVcEwCg8JVsPPK1GwEjxesNZJv6dyHtID6dYP8UnUCvPAemHBGiA+jD6CVgilD8+tWyfSPRiYXwVJDNNkydPUzvrRmeBZvFdArqSTDSCJ3ALcvDp0JBHWjTK8pb0Qvx7N35CkXo0yFRq1qZAgVaJkYiA7H3AA) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Roboto;font-style:normal;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0370-03FF}@font-face{font-family:Roboto;font-style:normal;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:Roboto;font-style:normal;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Roboto;font-style:normal;font-weight:500;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Fira Code;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff;base64,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) format("woff");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Fira Code;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff;base64,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) format("woff");unicode-range:U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Fira Code;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff;base64,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) format("woff");unicode-range:U+1F00-1FFF}@font-face{font-family:Fira Code;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff;base64,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) format("woff");unicode-range:U+0370-03FF}@font-face{font-family:Fira Code;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff;base64,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) format("woff");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Fira Code;font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff;base64,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) format("woff");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}.graphiql-container *{box-sizing:border-box;font-variant-ligatures:none}.graphiql-container,.CodeMirror-info,.CodeMirror-lint-tooltip,.graphiql-dialog,.graphiql-dialog-overlay,.graphiql-tooltip,[data-radix-popper-content-wrapper]{--color-primary: 320, 95%, 43%;--color-secondary: 242, 51%, 61%;--color-tertiary: 188, 100%, 36%;--color-info: 208, 100%, 46%;--color-success: 158, 60%, 42%;--color-warning: 36, 100%, 41%;--color-error: 13, 93%, 58%;--color-neutral: 219, 28%, 32%;--color-base: 219, 28%, 100%;--alpha-secondary: .76;--alpha-tertiary: .5;--alpha-background-heavy: .15;--alpha-background-medium: .1;--alpha-background-light: .07;--font-family: "Roboto", sans-serif;--font-family-mono: "Fira Code", monospace;--font-size-hint:.75rem;--font-size-inline-code:.8125rem;--font-size-body:.9375rem;--font-size-h4:1.125rem;--font-size-h3:1.375rem;--font-size-h2:1.8125rem;--font-weight-regular: 400;--font-weight-medium: 500;--line-height: 1.5;--px-2: 2px;--px-4: 4px;--px-6: 6px;--px-8: 8px;--px-10: 10px;--px-12: 12px;--px-16: 16px;--px-20: 20px;--px-24: 24px;--border-radius-2: 2px;--border-radius-4: 4px;--border-radius-8: 8px;--border-radius-12: 12px;--popover-box-shadow: 0px 6px 20px rgba(59, 76, 106, .13), 0px 1.34018px 4.46726px rgba(59, 76, 106, .0774939), 0px .399006px 1.33002px rgba(59, 76, 106, .0525061);--popover-border: none;--sidebar-width: 60px;--toolbar-width: 40px;--session-header-height: 51px}@media (prefers-color-scheme: dark){body:not(.graphiql-light) .graphiql-container,body:not(.graphiql-light) .CodeMirror-info,body:not(.graphiql-light) .CodeMirror-lint-tooltip,body:not(.graphiql-light) .graphiql-dialog,body:not(.graphiql-light) .graphiql-dialog-overlay,body:not(.graphiql-light) .graphiql-tooltip,body:not(.graphiql-light) [data-radix-popper-content-wrapper]{--color-primary: 338, 100%, 67%;--color-secondary: 243, 100%, 77%;--color-tertiary: 188, 100%, 44%;--color-info: 208, 100%, 72%;--color-success: 158, 100%, 42%;--color-warning: 30, 100%, 80%;--color-error: 13, 100%, 58%;--color-neutral: 219, 29%, 78%;--color-base: 219, 29%, 18%;--popover-box-shadow: none;--popover-border: 1px solid hsl(var(--color-neutral))}}body.graphiql-dark .graphiql-container,body.graphiql-dark .CodeMirror-info,body.graphiql-dark .CodeMirror-lint-tooltip,body.graphiql-dark .graphiql-dialog,body.graphiql-dark .graphiql-dialog-overlay,body.graphiql-dark .graphiql-tooltip,body.graphiql-dark [data-radix-popper-content-wrapper]{--color-primary: 338, 100%, 67%;--color-secondary: 243, 100%, 77%;--color-tertiary: 188, 100%, 44%;--color-info: 208, 100%, 72%;--color-success: 158, 100%, 42%;--color-warning: 30, 100%, 80%;--color-error: 13, 100%, 58%;--color-neutral: 219, 29%, 78%;--color-base: 219, 29%, 18%;--popover-box-shadow: none;--popover-border: 1px solid hsl(var(--color-neutral))}.graphiql-container,.CodeMirror-info,.CodeMirror-lint-tooltip,.graphiql-dialog,.graphiql-container:is(button),.CodeMirror-info:is(button),.CodeMirror-lint-tooltip:is(button),.graphiql-dialog:is(button){color:hsla(var(--color-neutral),1);font-family:var(--font-family);font-size:var(--font-size-body);font-weight:var(----font-weight-regular);line-height:var(--line-height)}.graphiql-container input,.CodeMirror-info input,.CodeMirror-lint-tooltip input,.graphiql-dialog input{color:hsla(var(--color-neutral),1);font-family:var(--font-family);font-size:var(--font-size-caption)}.graphiql-container input::placeholder,.CodeMirror-info input::placeholder,.CodeMirror-lint-tooltip input::placeholder,.graphiql-dialog input::placeholder{color:hsla(var(--color-neutral),var(--alpha-secondary))}.graphiql-container a,.CodeMirror-info a,.CodeMirror-lint-tooltip a,.graphiql-dialog a{color:hsl(var(--color-primary))}.graphiql-container a:focus,.CodeMirror-info a:focus,.CodeMirror-lint-tooltip a:focus,.graphiql-dialog a:focus{outline:hsl(var(--color-primary)) auto 1px}.graphiql-un-styled,button.graphiql-un-styled{all:unset;border-radius:var(--border-radius-4);cursor:pointer}:is(.graphiql-un-styled,button.graphiql-un-styled):hover{background-color:hsla(var(--color-neutral),var(--alpha-background-light))}:is(.graphiql-un-styled,button.graphiql-un-styled):active{background-color:hsla(var(--color-neutral),var(--alpha-background-medium))}:is(.graphiql-un-styled,button.graphiql-un-styled):focus{outline:hsla(var(--color-neutral),var(--alpha-background-heavy)) auto 1px}.graphiql-button,button.graphiql-button{background-color:hsla(var(--color-neutral),var(--alpha-background-light));border:none;border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),1);cursor:pointer;font-size:var(--font-size-body);padding:var(--px-8) var(--px-12)}:is(.graphiql-button,button.graphiql-button):hover,:is(.graphiql-button,button.graphiql-button):active{background-color:hsla(var(--color-neutral),var(--alpha-background-medium))}:is(.graphiql-button,button.graphiql-button):focus{outline:hsla(var(--color-neutral),var(--alpha-background-heavy)) auto 1px}.graphiql-button-success:is(.graphiql-button,button.graphiql-button){background-color:hsla(var(--color-success),var(--alpha-background-heavy))}.graphiql-button-error:is(.graphiql-button,button.graphiql-button){background-color:hsla(var(--color-error),var(--alpha-background-heavy))}.graphiql-button-group{background-color:hsla(var(--color-neutral),var(--alpha-background-light));border-radius:calc(var(--border-radius-4) + var(--px-4));display:flex;padding:var(--px-4)}.graphiql-button-group>button.graphiql-button{background-color:transparent}.graphiql-button-group>button.graphiql-button:hover{background-color:hsla(var(--color-neutral),var(--alpha-background-light))}.graphiql-button-group>button.graphiql-button.active{background-color:hsl(var(--color-base));cursor:default}.graphiql-button-group>*+*{margin-left:var(--px-8)}.graphiql-dialog-overlay{position:fixed;inset:0;background-color:hsla(var(--color-neutral),var(--alpha-background-heavy));z-index:10}.graphiql-dialog{background-color:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-12);box-shadow:var(--popover-box-shadow);margin:0;max-height:80vh;max-width:80vw;overflow:auto;padding:0;width:unset;transform:translate(-50%,-50%);top:50%;left:50%;position:fixed;z-index:10}.graphiql-dialog-close>svg{color:hsla(var(--color-neutral),var(--alpha-secondary));display:block;height:var(--px-12);padding:var(--px-12);width:var(--px-12)}.graphiql-dropdown-content{background-color:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-8);box-shadow:var(--popover-box-shadow);font-size:inherit;max-width:250px;padding:var(--px-4);font-family:var(--font-family);color:hsl(var(--color-neutral));max-height:min(calc(var(--radix-dropdown-menu-content-available-height) - 10px),400px);overflow-y:scroll}.graphiql-dropdown-item{border-radius:var(--border-radius-4);font-size:inherit;margin:var(--px-4);overflow:hidden;padding:var(--px-6) var(--px-8);text-overflow:ellipsis;white-space:nowrap;outline:none;cursor:pointer;line-height:var(--line-height)}.graphiql-dropdown-item[data-selected],.graphiql-dropdown-item[data-current-nav],.graphiql-dropdown-item:hover{background-color:hsla(var(--color-neutral),var(--alpha-background-light));color:inherit}.graphiql-dropdown-item:not(:first-child){margin-top:0}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) blockquote{margin-left:0;margin-right:0;padding-left:var(--px-8)}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) code,:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) pre{border-radius:var(--border-radius-4);font-family:var(--font-family-mono);font-size:var(--font-size-inline-code)}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) code{padding:var(--px-2)}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) pre{overflow:auto;padding:var(--px-6) var(--px-8)}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) pre code{background-color:initial;border-radius:0;padding:0}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) ol,:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) ul{padding-left:var(--px-16)}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) ol{list-style-type:decimal}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) ul{list-style-type:disc}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) img{border-radius:var(--border-radius-4);max-height:120px;max-width:100%}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation)>:first-child{margin-top:0}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation)>:last-child{margin-bottom:0}:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description) a{color:hsl(var(--color-primary));text-decoration:none}:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description) a:hover{text-decoration:underline}:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description) blockquote{border-left:1.5px solid hsla(var(--color-neutral),var(--alpha-tertiary))}:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description) code,:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description) pre{background-color:hsla(var(--color-neutral),var(--alpha-background-light));color:hsla(var(--color-neutral),1)}:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description)>*{margin:var(--px-12) 0}:is(.graphiql-markdown-deprecation,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation) a{color:hsl(var(--color-warning));text-decoration:underline}:is(.graphiql-markdown-deprecation,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation) blockquote{border-left:1.5px solid hsl(var(--color-warning))}:is(.graphiql-markdown-deprecation,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation) code,:is(.graphiql-markdown-deprecation,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation) pre{background-color:hsla(var(--color-warning),var(--alpha-background-heavy))}:is(.graphiql-markdown-deprecation,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation)>*{margin:var(--px-8) 0}.graphiql-markdown-preview>:not(:first-child){display:none}.CodeMirror-hint-information-deprecation,.CodeMirror-info .info-deprecation{background-color:hsla(var(--color-warning),var(--alpha-background-light));border:1px solid hsl(var(--color-warning));border-radius:var(--border-radius-4);color:hsl(var(--color-warning));margin-top:var(--px-12);padding:var(--px-6) var(--px-8)}.CodeMirror-hint-information-deprecation-label,.CodeMirror-info .info-deprecation-label{font-size:var(--font-size-hint);font-weight:var(--font-weight-medium)}.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation-reason{margin-top:var(--px-6)}.graphiql-spinner{height:56px;margin:auto;margin-top:var(--px-16);width:56px}.graphiql-spinner:after{animation:rotation .8s linear 0s infinite;border:4px solid transparent;border-radius:100%;border-top:4px solid hsla(var(--color-neutral),var(--alpha-tertiary));content:"";display:inline-block;height:46px;vertical-align:middle;width:46px}@keyframes rotation{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.graphiql-tooltip{background:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-4);box-shadow:var(--popover-box-shadow);color:hsl(var(--color-neutral));font-size:inherit;padding:var(--px-4) var(--px-6);font-family:var(--font-family)}.graphiql-tabs{display:flex;align-items:center;overflow-x:auto;padding:var(--px-12)}.graphiql-tabs>:not(:first-child){margin-left:var(--px-12)}.graphiql-tab{align-items:stretch;border-radius:var(--border-radius-8);color:hsla(var(--color-neutral),var(--alpha-secondary));display:flex}.graphiql-tab>button.graphiql-tab-close{visibility:hidden}.graphiql-tab.graphiql-tab-active>button.graphiql-tab-close,.graphiql-tab:hover>button.graphiql-tab-close,.graphiql-tab:focus-within>button.graphiql-tab-close{visibility:unset}.graphiql-tab.graphiql-tab-active{background-color:hsla(var(--color-neutral),var(--alpha-background-heavy));color:hsla(var(--color-neutral),1)}button.graphiql-tab-button{padding:var(--px-4) 0 var(--px-4) var(--px-8)}button.graphiql-tab-close{align-items:center;display:flex;padding:var(--px-4) var(--px-8)}button.graphiql-tab-close>svg{height:var(--px-8);width:var(--px-8)}.graphiql-history-header{font-size:var(--font-size-h2);font-weight:var(--font-weight-medium);display:flex;justify-content:space-between;align-items:center}.graphiql-history-header button{font-size:var(--font-size-inline-code);padding:var(--px-6) var(--px-10)}.graphiql-history-items{margin:var(--px-16) 0 0;list-style:none;padding:0}.graphiql-history-item{border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),var(--alpha-secondary));display:flex;font-size:var(--font-size-inline-code);font-family:var(--font-family-mono);height:34px}.graphiql-history-item:hover{color:hsla(var(--color-neutral),1);background-color:hsla(var(--color-neutral),var(--alpha-background-light))}.graphiql-history-item:not(:first-child){margin-top:var(--px-4)}.graphiql-history-item.editable{background-color:hsla(var(--color-primary),var(--alpha-background-medium))}.graphiql-history-item.editable>input{background:transparent;border:none;flex:1;margin:0;outline:none;padding:0 var(--px-10);width:100%}.graphiql-history-item.editable>input::placeholder{color:hsla(var(--color-neutral),var(--alpha-secondary))}.graphiql-history-item.editable>button{color:hsl(var(--color-primary));padding:0 var(--px-10)}.graphiql-history-item.editable>button:active{background-color:hsla(var(--color-primary),var(--alpha-background-heavy))}.graphiql-history-item.editable>button:focus{outline:hsl(var(--color-primary)) auto 1px}.graphiql-history-item.editable>button>svg{display:block}button.graphiql-history-item-label{flex:1;padding:var(--px-8) var(--px-10);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}button.graphiql-history-item-action{align-items:center;color:hsla(var(--color-neutral),var(--alpha-secondary));display:flex;padding:var(--px-8) var(--px-6)}button.graphiql-history-item-action:hover{color:hsla(var(--color-neutral),1)}button.graphiql-history-item-action>svg{height:14px;width:14px}.graphiql-history-item-spacer{height:var(--px-16)}.graphiql-doc-explorer-default-value{color:hsl(var(--color-success))}a.graphiql-doc-explorer-type-name{color:hsl(var(--color-warning));text-decoration:none}a.graphiql-doc-explorer-type-name:hover{text-decoration:underline}a.graphiql-doc-explorer-type-name:focus{outline:hsl(var(--color-warning)) auto 1px}.graphiql-doc-explorer-argument>*+*{margin-top:var(--px-12)}.graphiql-doc-explorer-argument-name{color:hsl(var(--color-secondary))}.graphiql-doc-explorer-argument-deprecation{background-color:hsla(var(--color-warning),var(--alpha-background-light));border:1px solid hsl(var(--color-warning));border-radius:var(--border-radius-4);color:hsl(var(--color-warning));padding:var(--px-8)}.graphiql-doc-explorer-argument-deprecation-label{font-size:var(--font-size-hint);font-weight:var(--font-weight-medium)}.graphiql-doc-explorer-deprecation{background-color:hsla(var(--color-warning),var(--alpha-background-light));border:1px solid hsl(var(--color-warning));border-radius:var(--px-4);color:hsl(var(--color-warning));padding:var(--px-8)}.graphiql-doc-explorer-deprecation-label{font-size:var(--font-size-hint);font-weight:var(--font-weight-medium)}.graphiql-doc-explorer-directive{color:hsl(var(--color-secondary))}.graphiql-doc-explorer-section-title{align-items:center;display:flex;font-size:var(--font-size-hint);font-weight:var(--font-weight-medium);line-height:1}.graphiql-doc-explorer-section-title>svg{height:var(--px-16);margin-right:var(--px-8);width:var(--px-16)}.graphiql-doc-explorer-section-content{margin-left:var(--px-8);margin-top:var(--px-16)}.graphiql-doc-explorer-section-content>*+*{margin-top:var(--px-16)}.graphiql-doc-explorer-root-type{color:hsl(var(--color-info))}.graphiql-doc-explorer-search{color:hsla(var(--color-neutral),var(--alpha-secondary))}.graphiql-doc-explorer-search:not([data-state="idle"]){border:var(--popover-border);border-radius:var(--border-radius-4);box-shadow:var(--popover-box-shadow);color:hsla(var(--color-neutral),1)}.graphiql-doc-explorer-search:not([data-state="idle"]) .graphiql-doc-explorer-search-input{background:hsl(var(--color-base))}.graphiql-doc-explorer-search-input{align-items:center;background-color:hsla(var(--color-neutral),var(--alpha-background-light));border-radius:var(--border-radius-4);display:flex;padding:var(--px-8) var(--px-12)}.graphiql-doc-explorer-search [role=combobox]{border:none;background-color:transparent;margin-left:var(--px-4);width:100%}.graphiql-doc-explorer-search [role=combobox]:focus{outline:none}.graphiql-doc-explorer-search [role=listbox]{background-color:hsl(var(--color-base));border:none;border-bottom-left-radius:var(--border-radius-4);border-bottom-right-radius:var(--border-radius-4);border-top:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));max-height:400px;overflow-y:auto;margin:0;font-size:var(--font-size-body);padding:var(--px-4);position:relative}.graphiql-doc-explorer-search [role=option]{border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),var(--alpha-secondary));overflow-x:hidden;padding:var(--px-8) var(--px-12);text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.graphiql-doc-explorer-search [role=option][data-headlessui-state=active]{background-color:hsla(var(--color-neutral),var(--alpha-background-light))}.graphiql-doc-explorer-search [role=option]:hover{background-color:hsla(var(--color-neutral),var(--alpha-background-medium))}.graphiql-doc-explorer-search [role=option][data-headlessui-state=active]:hover{background-color:hsla(var(--color-neutral),var(--alpha-background-heavy))}:is(.graphiql-doc-explorer-search [role="option"])+:is(.graphiql-doc-explorer-search [role="option"]){margin-top:var(--px-4)}.graphiql-doc-explorer-search-type{color:hsl(var(--color-info))}.graphiql-doc-explorer-search-field{color:hsl(var(--color-warning))}.graphiql-doc-explorer-search-argument{color:hsl(var(--color-secondary))}.graphiql-doc-explorer-search-divider{color:hsla(var(--color-neutral),var(--alpha-secondary));font-size:var(--font-size-hint);font-weight:var(--font-weight-medium);margin-top:var(--px-8);padding:var(--px-8) var(--px-12)}.graphiql-doc-explorer-search-empty{color:hsla(var(--color-neutral),var(--alpha-secondary));padding:var(--px-8) var(--px-12)}a.graphiql-doc-explorer-field-name{color:hsl(var(--color-info));text-decoration:none}a.graphiql-doc-explorer-field-name:hover{text-decoration:underline}a.graphiql-doc-explorer-field-name:focus{outline:hsl(var(--color-info)) auto 1px}.graphiql-doc-explorer-item>:not(:first-child){margin-top:var(--px-12)}.graphiql-doc-explorer-argument-multiple{margin-left:var(--px-8)}.graphiql-doc-explorer-enum-value{color:hsl(var(--color-info))}.graphiql-doc-explorer-header{display:flex;justify-content:space-between;position:relative}.graphiql-doc-explorer-header:focus-within .graphiql-doc-explorer-title{visibility:hidden}.graphiql-doc-explorer-header:focus-within .graphiql-doc-explorer-back:not(:focus){color:transparent}.graphiql-doc-explorer-header-content{display:flex;flex-direction:column;min-width:0}.graphiql-doc-explorer-search{position:absolute;right:0;top:0}.graphiql-doc-explorer-search:focus-within{left:0}.graphiql-doc-explorer-search [role=combobox]{height:24px;width:4ch}.graphiql-doc-explorer-search [role=combobox]:focus{width:100%}a.graphiql-doc-explorer-back{align-items:center;color:hsla(var(--color-neutral),var(--alpha-secondary));display:flex;text-decoration:none}a.graphiql-doc-explorer-back:hover{text-decoration:underline}a.graphiql-doc-explorer-back:focus{outline:hsla(var(--color-neutral),var(--alpha-secondary)) auto 1px}a.graphiql-doc-explorer-back:focus+.graphiql-doc-explorer-title{visibility:unset}a.graphiql-doc-explorer-back>svg{height:var(--px-8);margin-right:var(--px-8);width:var(--px-8)}.graphiql-doc-explorer-title{font-weight:var(--font-weight-medium);font-size:var(--font-size-h2);overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.graphiql-doc-explorer-title:not(:first-child){font-size:var(--font-size-h3);margin-top:var(--px-8)}.graphiql-doc-explorer-content>*{color:hsla(var(--color-neutral),var(--alpha-secondary));margin-top:var(--px-20)}.graphiql-doc-explorer-error{background-color:hsla(var(--color-error),var(--alpha-background-heavy));border:1px solid hsl(var(--color-error));border-radius:var(--border-radius-8);color:hsl(var(--color-error));padding:var(--px-8) var(--px-12)}.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid black;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor .CodeMirror-line::selection,.cm-fat-cursor .CodeMirror-line>span::selection,.cm-fat-cursor .CodeMirror-line>span>span::selection{background:transparent}.cm-fat-cursor .CodeMirror-line::-moz-selection,.cm-fat-cursor .CodeMirror-line>span::-moz-selection,.cm-fat-cursor .CodeMirror-line>span>span::-moz-selection{background:transparent}.cm-fat-cursor{caret-color:transparent}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;inset:-50px 0 0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3,.cm-s-default .cm-type{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error,.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:white}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:none;position:relative;z-index:0}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-vscrollbar,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{position:absolute;z-index:6;display:none;outline:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:none!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:transparent;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;inset:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:none}.CodeMirror-scroll,.CodeMirror-sizer,.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors,.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:#ff06}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:""}span.CodeMirror-selectedtext{background:none}.graphiql-container .CodeMirror{height:100%;position:absolute;width:100%}.graphiql-container .CodeMirror{font-family:var(--font-family-mono)}.graphiql-container .CodeMirror,.graphiql-container .CodeMirror-gutters{background:none;background-color:var(--editor-background, hsl(var(--color-base)))}.graphiql-container .CodeMirror-linenumber{padding:0}.graphiql-container .CodeMirror-gutters{border:none}.cm-s-graphiql{color:hsla(var(--color-neutral),var(--alpha-tertiary))}.cm-s-graphiql .cm-keyword{color:hsl(var(--color-primary))}.cm-s-graphiql .cm-def{color:hsl(var(--color-tertiary))}.cm-s-graphiql .cm-punctuation{color:hsla(var(--color-neutral),var(--alpha-tertiary))}.cm-s-graphiql .cm-variable{color:hsl(var(--color-secondary))}.cm-s-graphiql .cm-atom{color:hsl(var(--color-tertiary))}.cm-s-graphiql .cm-number{color:hsl(var(--color-success))}.cm-s-graphiql .cm-string{color:hsl(var(--color-warning))}.cm-s-graphiql .cm-builtin{color:hsl(var(--color-success))}.cm-s-graphiql .cm-string-2{color:hsl(var(--color-secondary))}.cm-s-graphiql .cm-attribute,.cm-s-graphiql .cm-meta{color:hsl(var(--color-tertiary))}.cm-s-graphiql .cm-property{color:hsl(var(--color-info))}.cm-s-graphiql .cm-qualifier{color:hsl(var(--color-secondary))}.cm-s-graphiql .cm-comment{color:hsla(var(--color-neutral),var(--alpha-secondary))}.cm-s-graphiql .cm-ws{color:hsla(var(--color-neutral),var(--alpha-tertiary))}.cm-s-graphiql .cm-invalidchar{color:hsl(var(--color-error))}.cm-s-graphiql .CodeMirror-cursor{border-left:2px solid hsla(var(--color-neutral),var(--alpha-secondary))}.cm-s-graphiql .CodeMirror-linenumber{color:hsla(var(--color-neutral),var(--alpha-tertiary))}.graphiql-container div.CodeMirror span.CodeMirror-matchingbracket,.graphiql-container div.CodeMirror span.CodeMirror-nonmatchingbracket{color:hsl(var(--color-warning))}.graphiql-container .CodeMirror-selected,.graphiql-container .CodeMirror-focused .CodeMirror-selected{background:hsla(var(--color-neutral),var(--alpha-background-heavy))}.graphiql-container .CodeMirror-dialog{background:inherit;color:inherit;left:0;right:0;overflow:hidden;padding:var(--px-2) var(--px-6);position:absolute;z-index:6}.graphiql-container .CodeMirror-dialog-top{border-bottom:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));padding-bottom:var(--px-12);top:0}.graphiql-container .CodeMirror-dialog-bottom{border-top:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));bottom:0;padding-top:var(--px-12)}.graphiql-container .CodeMirror-search-hint{display:none}.graphiql-container .CodeMirror-dialog input{border:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));border-radius:var(--border-radius-4);padding:var(--px-4)}.graphiql-container .CodeMirror-dialog input:focus{outline:hsl(var(--color-primary)) solid 2px}.graphiql-container .cm-searching{background-color:hsla(var(--color-warning),var(--alpha-background-light));padding-bottom:1.5px;padding-top:.5px}.CodeMirror-foldmarker{color:#00f;text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.CodeMirror-foldgutter{width:.7em}.CodeMirror-foldgutter-open,.CodeMirror-foldgutter-folded{cursor:pointer}.CodeMirror-foldgutter-open:after{content:"\25be"}.CodeMirror-foldgutter-folded:after{content:"\25b8"}.CodeMirror-foldgutter{width:var(--px-12)}.CodeMirror-foldmarker{background-color:hsl(var(--color-info));border-radius:var(--border-radius-4);color:hsl(var(--color-base));font-family:inherit;margin:0 var(--px-4);padding:0 var(--px-8);text-shadow:none}.CodeMirror-foldgutter-open,.CodeMirror-foldgutter-folded{color:hsla(var(--color-neutral),var(--alpha-tertiary))}.CodeMirror-foldgutter-open:after,.CodeMirror-foldgutter-folded:after{margin:0 var(--px-2)}.graphiql-editor{height:100%;position:relative;width:100%}.graphiql-editor.hidden{left:-9999px;position:absolute;top:-9999px;visibility:hidden}.CodeMirror-lint-markers{width:16px}.CodeMirror-lint-tooltip{background-color:#ffd;border:1px solid black;border-radius:4px;color:#000;font-family:monospace;font-size:10pt;overflow:hidden;padding:2px 5px;position:fixed;white-space:pre;white-space:pre-wrap;z-index:100;max-width:600px;opacity:0;transition:opacity .4s;-moz-transition:opacity .4s;-webkit-transition:opacity .4s;-o-transition:opacity .4s;-ms-transition:opacity .4s}.CodeMirror-lint-mark{background-position:left bottom;background-repeat:repeat-x}.CodeMirror-lint-mark-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=)}.CodeMirror-lint-mark-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==)}.CodeMirror-lint-marker{background-position:center center;background-repeat:no-repeat;cursor:pointer;display:inline-block;height:16px;width:16px;vertical-align:middle;position:relative}.CodeMirror-lint-message{padding-left:18px;background-position:top left;background-repeat:no-repeat}.CodeMirror-lint-marker-warning,.CodeMirror-lint-message-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=)}.CodeMirror-lint-marker-error,.CodeMirror-lint-message-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=)}.CodeMirror-lint-marker-multiple{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC);background-repeat:no-repeat;background-position:right bottom;width:100%;height:100%}.CodeMirror-lint-line-error{background-color:#b74c5114}.CodeMirror-lint-line-warning{background-color:#ffd3001a}.CodeMirror-lint-mark-error,.CodeMirror-lint-mark-warning{background-repeat:repeat-x;background-size:10px 3px;background-position:0 95%}.cm-s-graphiql .CodeMirror-lint-mark-error{color:hsl(var(--color-error))}.CodeMirror-lint-mark-error{background-image:linear-gradient(45deg,transparent 65%,hsl(var(--color-error)) 80%,transparent 90%),linear-gradient(135deg,transparent 5%,hsl(var(--color-error)) 15%,transparent 25%),linear-gradient(135deg,transparent 45%,hsl(var(--color-error)) 55%,transparent 65%),linear-gradient(45deg,transparent 25%,hsl(var(--color-error)) 35%,transparent 50%)}.cm-s-graphiql .CodeMirror-lint-mark-warning{color:hsl(var(--color-warning))}.CodeMirror-lint-mark-warning{background-image:linear-gradient(45deg,transparent 65%,hsl(var(--color-warning)) 80%,transparent 90%),linear-gradient(135deg,transparent 5%,hsl(var(--color-warning)) 15%,transparent 25%),linear-gradient(135deg,transparent 45%,hsl(var(--color-warning)) 55%,transparent 65%),linear-gradient(45deg,transparent 25%,hsl(var(--color-warning)) 35%,transparent 50%)}.CodeMirror-lint-tooltip{background-color:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-8);box-shadow:var(--popover-box-shadow);font-size:var(--font-size-body);font-family:var(--font-family);max-width:600px;overflow:hidden;padding:var(--px-12)}.CodeMirror-lint-message-error,.CodeMirror-lint-message-warning{background-image:none;padding:0}.CodeMirror-lint-message-error{color:hsl(var(--color-error))}.CodeMirror-lint-message-warning{color:hsl(var(--color-warning))}.CodeMirror-hints{position:absolute;z-index:10;overflow:hidden;list-style:none;margin:0;padding:2px;-webkit-box-shadow:2px 3px 5px rgba(0,0,0,.2);-moz-box-shadow:2px 3px 5px rgba(0,0,0,.2);box-shadow:2px 3px 5px #0003;border-radius:3px;border:1px solid silver;background:white;font-size:90%;font-family:monospace;max-height:20em;overflow-y:auto}.CodeMirror-hint{margin:0;padding:0 4px;border-radius:2px;white-space:pre;color:#000;cursor:pointer}li.CodeMirror-hint-active{background:#08f;color:#fff}.CodeMirror-hints{background:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-8);box-shadow:var(--popover-box-shadow);display:grid;font-family:var(--font-family);font-size:var(--font-size-body);grid-template-columns:auto fit-content(300px);max-height:264px;padding:0}.CodeMirror-hint{border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),var(--alpha-secondary));grid-column:1 / 2;margin:var(--px-4);padding:var(--px-6) var(--px-8)!important}.CodeMirror-hint:not(:first-child){margin-top:0}li.CodeMirror-hint-active{background:hsla(var(--color-primary),var(--alpha-background-medium));color:hsl(var(--color-primary))}.CodeMirror-hint-information{border-left:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));grid-column:2 / 3;grid-row:1 / 99999;max-height:264px;overflow:auto;padding:var(--px-12)}.CodeMirror-hint-information-header{display:flex;align-items:baseline}.CodeMirror-hint-information-field-name{font-size:var(--font-size-h4);font-weight:var(--font-weight-medium)}.CodeMirror-hint-information-type-name-pill{border:1px solid hsla(var(--color-neutral),var(--alpha-tertiary));border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),var(--alpha-secondary));margin-left:var(--px-6);padding:var(--px-4)}.CodeMirror-hint-information-type-name{color:inherit;text-decoration:none}.CodeMirror-hint-information-type-name:hover{text-decoration:underline dotted}.CodeMirror-hint-information-description{color:hsla(var(--color-neutral),var(--alpha-secondary));margin-top:var(--px-12)}.CodeMirror-info{background-color:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-8);box-shadow:var(--popover-box-shadow);color:hsla(var(--color-neutral),1);max-height:300px;max-width:400px;opacity:0;overflow:auto;padding:var(--px-12);position:fixed;transition:opacity .15s;z-index:10}.CodeMirror-info a{color:inherit;text-decoration:none}.CodeMirror-info a:hover{text-decoration:underline dotted}.CodeMirror-info .CodeMirror-info-header{display:flex;align-items:baseline}.CodeMirror-info .CodeMirror-info-header>.type-name,.CodeMirror-info .CodeMirror-info-header>.field-name,.CodeMirror-info .CodeMirror-info-header>.arg-name,.CodeMirror-info .CodeMirror-info-header>.directive-name,.CodeMirror-info .CodeMirror-info-header>.enum-value{font-size:var(--font-size-h4);font-weight:var(--font-weight-medium)}.CodeMirror-info .type-name-pill{border:1px solid hsla(var(--color-neutral),var(--alpha-tertiary));border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),var(--alpha-secondary));margin-left:var(--px-6);padding:var(--px-4)}.CodeMirror-info .info-description{color:hsla(var(--color-neutral),var(--alpha-secondary));margin-top:var(--px-12);overflow:hidden}.CodeMirror-jump-token{text-decoration:underline dotted;cursor:pointer}.auto-inserted-leaf.cm-property{animation-duration:6s;animation-name:insertionFade;border-radius:var(--border-radius-4);padding:var(--px-2)}@keyframes insertionFade{0%,to{background-color:none}15%,85%{background-color:hsla(var(--color-warning),var(--alpha-background-light))}}button.graphiql-toolbar-button{display:flex;align-items:center;justify-content:center;height:var(--toolbar-width);width:var(--toolbar-width)}button.graphiql-toolbar-button.error{background:hsla(var(--color-error),var(--alpha-background-heavy))}.graphiql-execute-button-wrapper{position:relative}button.graphiql-execute-button{background-color:hsl(var(--color-primary));border:none;border-radius:var(--border-radius-8);cursor:pointer;height:var(--toolbar-width);padding:0;width:var(--toolbar-width)}button.graphiql-execute-button:hover{background-color:hsla(var(--color-primary),.9)}button.graphiql-execute-button:active{background-color:hsla(var(--color-primary),.8)}button.graphiql-execute-button:focus{outline:hsla(var(--color-primary),.8) auto 1px}button.graphiql-execute-button>svg{color:#fff;display:block;height:var(--px-16);margin:auto;width:var(--px-16)}button.graphiql-toolbar-menu{display:block;height:var(--toolbar-width);width:var(--toolbar-width)}.graphiql-container{background-color:hsl(var(--color-base));display:flex;height:100%;margin:0;overflow:hidden;width:100%}.graphiql-container .graphiql-sidebar{display:flex;flex-direction:column;justify-content:space-between;padding:var(--px-8);width:var(--sidebar-width)}.graphiql-container .graphiql-sidebar .graphiql-sidebar-section{display:flex;flex-direction:column;gap:var(--px-8)}.graphiql-container .graphiql-sidebar button{display:flex;align-items:center;justify-content:center;color:hsla(var(--color-neutral),var(--alpha-secondary));height:calc(var(--sidebar-width) - (2 * var(--px-8)));width:calc(var(--sidebar-width) - (2 * var(--px-8)))}.graphiql-container .graphiql-sidebar button.active{color:hsla(var(--color-neutral),1)}.graphiql-container .graphiql-sidebar button:not(:first-child){margin-top:var(--px-4)}.graphiql-container .graphiql-sidebar button>svg{height:var(--px-20);width:var(--px-20)}.graphiql-container .graphiql-main{display:flex;flex:1;min-width:0}.graphiql-container .graphiql-sessions{background-color:hsla(var(--color-neutral),var(--alpha-background-light));border-radius:calc(var(--border-radius-12) + var(--px-8));display:flex;flex-direction:column;flex:1;max-height:100%;margin:var(--px-16);margin-left:0;min-width:0}.graphiql-container .graphiql-session-header{align-items:center;display:flex;justify-content:space-between;height:var(--session-header-height)}button.graphiql-tab-add{height:100%;padding:var(--px-4)}button.graphiql-tab-add>svg{color:hsla(var(--color-neutral),var(--alpha-secondary));display:block;height:var(--px-16);width:var(--px-16)}.graphiql-container .graphiql-session-header-right{align-items:center;display:flex}.graphiql-container .graphiql-logo{color:hsla(var(--color-neutral),var(--alpha-secondary));font-size:var(--font-size-h4);font-weight:var(--font-weight-medium);padding:var(--px-12) var(--px-16)}.graphiql-container .graphiql-logo .graphiql-logo-link{color:hsla(var(--color-neutral),var(--alpha-secondary));text-decoration:none}.graphiql-container .graphiql-session{display:flex;flex:1;padding:0 var(--px-8) var(--px-8)}.graphiql-container .graphiql-editors{background-color:hsl(var(--color-base));border-radius:calc(var(--border-radius-12));box-shadow:var(--popover-box-shadow);display:flex;flex:1;flex-direction:column}.graphiql-container .graphiql-editors.full-height{margin-top:calc(var(--px-8) - var(--session-header-height))}.graphiql-container .graphiql-query-editor{border-bottom:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));padding:var(--px-16);column-gap:var(--px-16);display:flex;width:100%}.graphiql-container .graphiql-toolbar{width:var(--toolbar-width)}.graphiql-container .graphiql-toolbar>*+*{margin-top:var(--px-8)}.graphiql-toolbar-icon{color:hsla(var(--color-neutral),var(--alpha-tertiary));display:block;height:calc(var(--toolbar-width) - (var(--px-8) * 2));width:calc(var(--toolbar-width) - (var(--px-8) * 2))}.graphiql-container .graphiql-editor-tools{cursor:row-resize;display:flex;width:100%;column-gap:var(--px-8);padding:var(--px-8)}.graphiql-container .graphiql-editor-tools button{color:hsla(var(--color-neutral),var(--alpha-secondary))}.graphiql-container .graphiql-editor-tools button.active{color:hsla(var(--color-neutral),1)}.graphiql-container .graphiql-editor-tools>button:not(.graphiql-toggle-editor-tools){padding:var(--px-8) var(--px-12)}.graphiql-container .graphiql-editor-tools .graphiql-toggle-editor-tools{margin-left:auto}.graphiql-container .graphiql-editor-tool{flex:1;padding:var(--px-16)}.graphiql-container .graphiql-toolbar,.graphiql-container .graphiql-editor-tools,.graphiql-container .graphiql-editor-tool{position:relative}.graphiql-container .graphiql-response{--editor-background: transparent;display:flex;width:100%;flex-direction:column}.graphiql-container .graphiql-response .result-window{position:relative;flex:1}.graphiql-container .graphiql-footer{border-top:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy))}.graphiql-container .graphiql-plugin{border-left:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));flex:1;overflow-y:auto;padding:var(--px-16)}.graphiql-horizontal-drag-bar{width:var(--px-12);cursor:col-resize}.graphiql-horizontal-drag-bar:hover:after{border:var(--px-2) solid hsla(var(--color-neutral),var(--alpha-background-heavy));border-radius:var(--border-radius-2);content:"";display:block;height:25%;margin:0 auto;position:relative;top:37.5%;width:0}.graphiql-container .graphiql-chevron-icon{color:hsla(var(--color-neutral),var(--alpha-tertiary));display:block;height:var(--px-12);margin:var(--px-12);width:var(--px-12)}.graphiql-spin{animation:spin .8s linear 0s infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.graphiql-dialog .graphiql-dialog-header{align-items:center;display:flex;justify-content:space-between;padding:var(--px-24)}.graphiql-dialog .graphiql-dialog-title{font-size:var(--font-size-h3);font-weight:var(--font-weight-medium);margin:0}.graphiql-dialog .graphiql-dialog-section{align-items:center;border-top:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));display:flex;justify-content:space-between;padding:var(--px-24)}.graphiql-dialog .graphiql-dialog-section>:not(:first-child){margin-left:var(--px-24)}.graphiql-dialog .graphiql-dialog-section-title{font-size:var(--font-size-h4);font-weight:var(--font-weight-medium)}.graphiql-dialog .graphiql-dialog-section-caption{color:hsla(var(--color-neutral),var(--alpha-secondary))}.graphiql-dialog .graphiql-warning-text{color:hsl(var(--color-warning));font-weight:var(--font-weight-medium)}.graphiql-dialog .graphiql-table{border-collapse:collapse;width:100%}.graphiql-dialog .graphiql-table :is(th,td){border:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));padding:var(--px-8) var(--px-12)}.graphiql-dialog .graphiql-key{background-color:hsla(var(--color-neutral),var(--alpha-background-medium));border-radius:var(--border-radius-4);padding:var(--px-4)}.graphiql-container svg{pointer-events:none}.docExplorerWrap{height:unset!important;min-width:unset!important;width:unset!important}.doc-explorer-title{font-size:var(--font-size-h2);font-weight:var(--font-weight-medium)}.doc-explorer-rhs{display:none}.graphiql-explorer-root{font-family:var(--font-family-mono)!important;font-size:var(--font-size-body)!important;padding:0!important}.graphiql-explorer-root>div:first-child{padding-left:var(--px-8);overflow:hidden!important}.graphiql-explorer-root input{background:hsl(var(--color-base))}.graphiql-explorer-root select{background-color:hsl(var(--color-base));border:1px solid hsla(var(--color-neutral),var(--alpha-secondary));border-radius:var(--border-radius-4);color:hsl(var(--color-neutral));margin:0 var(--px-4);padding:var(--px-4) var(--px-6)}
-/*!********************************************************************************************!*\
- !*** css ../../../node_modules/css-loader/dist/cjs.js!../../graphiql-react/dist/style.css ***!
- \********************************************************************************************/
-/*!*********************************************************************************************!*\
- !*** css ../../../node_modules/css-loader/dist/cjs.js!../../graphiql-react/font/roboto.css ***!
- \*********************************************************************************************/
-/*!************************************************************************************************!*\
- !*** css ../../../node_modules/css-loader/dist/cjs.js!../../graphiql-react/font/fira-code.css ***!
- \************************************************************************************************/
-/*!*********************************************************************************************************************!*\
- !*** css ../../../node_modules/css-loader/dist/cjs.js!../../../node_modules/postcss-loader/dist/cjs.js!./style.css ***!
- \*********************************************************************************************************************/
diff --git a/netbox/project-static/dist/graphiql.js b/netbox/project-static/dist/graphiql.js
deleted file mode 100644
index 1b6949d0288..00000000000
--- a/netbox/project-static/dist/graphiql.js
+++ /dev/null
@@ -1,346 +0,0 @@
-(()=>{var HB=Object.create;var U0=Object.defineProperty;var zB=Object.getOwnPropertyDescriptor;var WB=Object.getOwnPropertyNames;var YB=Object.getPrototypeOf,JB=Object.prototype.hasOwnProperty;var XB=e=>U0(e,"__esModule",{value:!0});var tx=(e=>typeof require!="undefined"?require:typeof Proxy!="undefined"?new Proxy(e,{get:(t,r)=>(typeof require!="undefined"?require:t)[r]}):e)(function(e){if(typeof require!="undefined")return require.apply(this,arguments);throw new Error('Dynamic require of "'+e+'" is not supported')});var G=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var ZB=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of WB(t))!JB.call(e,n)&&n!=="default"&&U0(e,n,{get:()=>t[n],enumerable:!(r=zB(t,n))||r.enumerable});return e},Ee=e=>ZB(XB(U0(e!=null?HB(YB(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var G0=G((Oie,nx)=>{"use strict";var rx=Object.getOwnPropertySymbols,$B=Object.prototype.hasOwnProperty,eK=Object.prototype.propertyIsEnumerable;function tK(e){if(e==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}function rK(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de",Object.getOwnPropertyNames(e)[0]==="5")return!1;for(var t={},r=0;r<10;r++)t["_"+String.fromCharCode(r)]=r;var n=Object.getOwnPropertyNames(t).map(function(o){return t[o]});if(n.join("")!=="0123456789")return!1;var i={};return"abcdefghijklmnopqrst".split("").forEach(function(o){i[o]=o}),Object.keys(Object.assign({},i)).join("")==="abcdefghijklmnopqrst"}catch(o){return!1}}nx.exports=rK()?Object.assign:function(e,t){for(var r,n=tK(e),i,o=1;o{"use strict";var Q0=G0(),ml=60103,ix=60106;Et.Fragment=60107;Et.StrictMode=60108;Et.Profiler=60114;var ax=60109,ox=60110,ux=60112;Et.Suspense=60113;var sx=60115,lx=60116;typeof Symbol=="function"&&Symbol.for&&(Mi=Symbol.for,ml=Mi("react.element"),ix=Mi("react.portal"),Et.Fragment=Mi("react.fragment"),Et.StrictMode=Mi("react.strict_mode"),Et.Profiler=Mi("react.profiler"),ax=Mi("react.provider"),ox=Mi("react.context"),ux=Mi("react.forward_ref"),Et.Suspense=Mi("react.suspense"),sx=Mi("react.memo"),lx=Mi("react.lazy"));var Mi,cx=typeof Symbol=="function"&&Symbol.iterator;function nK(e){return e===null||typeof e!="object"?null:(e=cx&&e[cx]||e["@@iterator"],typeof e=="function"?e:null)}function If(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,r=1;r{"use strict";Tx.exports=bx()});var wx=G(qt=>{"use strict";var bl,Af,Bh,Y0;typeof performance=="object"&&typeof performance.now=="function"?(_x=performance,qt.unstable_now=function(){return _x.now()}):(J0=Date,Ex=J0.now(),qt.unstable_now=function(){return J0.now()-Ex});var _x,J0,Ex;typeof window=="undefined"||typeof MessageChannel!="function"?(Tl=null,X0=null,Z0=function(){if(Tl!==null)try{var e=qt.unstable_now();Tl(!0,e),Tl=null}catch(t){throw setTimeout(Z0,0),t}},bl=function(e){Tl!==null?setTimeout(bl,0,e):(Tl=e,setTimeout(Z0,0))},Af=function(e,t){X0=setTimeout(e,t)},Bh=function(){clearTimeout(X0)},qt.unstable_shouldYield=function(){return!1},Y0=qt.unstable_forceFrameRate=function(){}):(Sx=window.setTimeout,kx=window.clearTimeout,typeof console!="undefined"&&(Ox=window.cancelAnimationFrame,typeof window.requestAnimationFrame!="function"&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),typeof Ox!="function"&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")),Rf=!1,jf=null,Kh=-1,$0=5,eb=0,qt.unstable_shouldYield=function(){return qt.unstable_now()>=eb},Y0=function(){},qt.unstable_forceFrameRate=function(e){0>e||125>>1,i=e[n];if(i!==void 0&&0Wh(s,r))d!==void 0&&0>Wh(d,s)?(e[n]=d,e[l]=r,n=l):(e[n]=s,e[o]=r,n=o);else if(d!==void 0&&0>Wh(d,r))e[n]=d,e[l]=r,n=l;else break e}}return t}return null}function Wh(e,t){var r=e.sortIndex-t.sortIndex;return r!==0?r:e.id-t.id}var La=[],Ho=[],sK=1,qi=null,An=3,Yh=!1,$u=!1,Pf=!1;function nb(e){for(var t=ha(Ho);t!==null;){if(t.callback===null)zh(Ho);else if(t.startTime<=e)zh(Ho),t.sortIndex=t.expirationTime,rb(La,t);else break;t=ha(Ho)}}function ib(e){if(Pf=!1,nb(e),!$u)if(ha(La)!==null)$u=!0,bl(ab);else{var t=ha(Ho);t!==null&&Af(ib,t.startTime-e)}}function ab(e,t){$u=!1,Pf&&(Pf=!1,Bh()),Yh=!0;var r=An;try{for(nb(t),qi=ha(La);qi!==null&&(!(qi.expirationTime>t)||e&&!qt.unstable_shouldYield());){var n=qi.callback;if(typeof n=="function"){qi.callback=null,An=qi.priorityLevel;var i=n(qi.expirationTime<=t);t=qt.unstable_now(),typeof i=="function"?qi.callback=i:qi===ha(La)&&zh(La),nb(t)}else zh(La);qi=ha(La)}if(qi!==null)var o=!0;else{var s=ha(Ho);s!==null&&Af(ib,s.startTime-t),o=!1}return o}finally{qi=null,An=r,Yh=!1}}var lK=Y0;qt.unstable_IdlePriority=5;qt.unstable_ImmediatePriority=1;qt.unstable_LowPriority=4;qt.unstable_NormalPriority=3;qt.unstable_Profiling=null;qt.unstable_UserBlockingPriority=2;qt.unstable_cancelCallback=function(e){e.callback=null};qt.unstable_continueExecution=function(){$u||Yh||($u=!0,bl(ab))};qt.unstable_getCurrentPriorityLevel=function(){return An};qt.unstable_getFirstCallbackNode=function(){return ha(La)};qt.unstable_next=function(e){switch(An){case 1:case 2:case 3:var t=3;break;default:t=An}var r=An;An=t;try{return e()}finally{An=r}};qt.unstable_pauseExecution=function(){};qt.unstable_requestPaint=lK;qt.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var r=An;An=e;try{return t()}finally{An=r}};qt.unstable_scheduleCallback=function(e,t,r){var n=qt.unstable_now();switch(typeof r=="object"&&r!==null?(r=r.delay,r=typeof r=="number"&&0n?(e.sortIndex=r,rb(Ho,e),ha(La)===null&&e===ha(Ho)&&(Pf?Bh():Pf=!0,Af(ib,r-n))):(e.sortIndex=i,rb(La,e),$u||Yh||($u=!0,bl(ab))),e};qt.unstable_wrapCallback=function(e){var t=An;return function(){var r=An;An=t;try{return e.apply(this,arguments)}finally{An=r}}}});var Dx=G((xie,Nx)=>{"use strict";Nx.exports=wx()});var h1=G(Ki=>{"use strict";var Jh=zt(),cr=G0(),ln=Dx();function ye(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,r=1;rt}return!1}function $n(e,t,r,n,i,o,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=n,this.attributeNamespace=i,this.mustUseProperty=r,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=s}var yn={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){yn[e]=new $n(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];yn[t]=new $n(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){yn[e]=new $n(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){yn[e]=new $n(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){yn[e]=new $n(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){yn[e]=new $n(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){yn[e]=new $n(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){yn[e]=new $n(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){yn[e]=new $n(e,5,!1,e.toLowerCase(),null,!1,!1)});var ob=/[\-:]([a-z])/g;function ub(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ob,ub);yn[t]=new $n(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ob,ub);yn[t]=new $n(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ob,ub);yn[t]=new $n(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){yn[e]=new $n(e,1,!1,e.toLowerCase(),null,!1,!1)});yn.xlinkHref=new $n("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){yn[e]=new $n(e,1,!1,e.toLowerCase(),null,!0,!0)});function sb(e,t,r,n){var i=yn.hasOwnProperty(t)?yn[t]:null,o=i!==null?i.type===0:n?!1:!(!(2l||i[s]!==o[l])return`
-`+i[s].replace(" at new "," at ");while(1<=s&&0<=l);break}}}finally{yb=!1,Error.prepareStackTrace=r}return(e=e?e.displayName||e.name:"")?Gf(e):""}function hK(e){switch(e.tag){case 5:return Gf(e.type);case 16:return Gf("Lazy");case 13:return Gf("Suspense");case 19:return Gf("SuspenseList");case 0:case 2:case 15:return e=ev(e.type,!1),e;case 11:return e=ev(e.type.render,!1),e;case 22:return e=ev(e.type._render,!1),e;case 1:return e=ev(e.type,!0),e;default:return""}}function El(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case zo:return"Fragment";case rs:return"Portal";case qf:return"Profiler";case lb:return"StrictMode";case Vf:return"Suspense";case Zh:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case fb:return(e.displayName||"Context")+".Consumer";case cb:return(e._context.displayName||"Context")+".Provider";case Xh:var t=e.render;return t=t.displayName||t.name||"",e.displayName||(t!==""?"ForwardRef("+t+")":"ForwardRef");case $h:return El(e.type);case pb:return El(e._render);case db:t=e._payload,e=e._init;try{return El(e(t))}catch(r){}}return null}function Wo(e){switch(typeof e){case"boolean":case"number":case"object":case"string":case"undefined":return e;default:return""}}function jx(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function vK(e){var t=jx(e)?"checked":"value",r=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),n=""+e[t];if(!e.hasOwnProperty(t)&&typeof r!="undefined"&&typeof r.get=="function"&&typeof r.set=="function"){var i=r.get,o=r.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(s){n=""+s,o.call(this,s)}}),Object.defineProperty(e,t,{enumerable:r.enumerable}),{getValue:function(){return n},setValue:function(s){n=""+s},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function tv(e){e._valueTracker||(e._valueTracker=vK(e))}function Px(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var r=t.getValue(),n="";return e&&(n=jx(e)?e.checked?"true":"false":e.value),e=n,e!==r?(t.setValue(e),!0):!1}function rv(e){if(e=e||(typeof document!="undefined"?document:void 0),typeof e=="undefined")return null;try{return e.activeElement||e.body}catch(t){return e.body}}function bb(e,t){var r=t.checked;return cr({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:r!=null?r:e._wrapperState.initialChecked})}function Fx(e,t){var r=t.defaultValue==null?"":t.defaultValue,n=t.checked!=null?t.checked:t.defaultChecked;r=Wo(t.value!=null?t.value:r),e._wrapperState={initialChecked:n,initialValue:r,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Mx(e,t){t=t.checked,t!=null&&sb(e,"checked",t,!1)}function Tb(e,t){Mx(e,t);var r=Wo(t.value),n=t.type;if(r!=null)n==="number"?(r===0&&e.value===""||e.value!=r)&&(e.value=""+r):e.value!==""+r&&(e.value=""+r);else if(n==="submit"||n==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?_b(e,t.type,r):t.hasOwnProperty("defaultValue")&&_b(e,t.type,Wo(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function qx(e,t,r){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var n=t.type;if(!(n!=="submit"&&n!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,r||t===e.value||(e.value=t),e.defaultValue=t}r=e.name,r!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,r!==""&&(e.name=r)}function _b(e,t,r){(t!=="number"||rv(e.ownerDocument)!==e)&&(r==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+r&&(e.defaultValue=""+r))}function gK(e){var t="";return Jh.Children.forEach(e,function(r){r!=null&&(t+=r)}),t}function Eb(e,t){return e=cr({children:void 0},t),(t=gK(t.children))&&(e.children=t),e}function Sl(e,t,r,n){if(e=e.options,t){t={};for(var i=0;i=r.length))throw Error(ye(93));r=r[0]}t=r}t==null&&(t=""),r=t}e._wrapperState={initialValue:Wo(r)}}function Ux(e,t){var r=Wo(t.value),n=Wo(t.defaultValue);r!=null&&(r=""+r,r!==e.value&&(e.value=r),t.defaultValue==null&&e.defaultValue!==r&&(e.defaultValue=r)),n!=null&&(e.defaultValue=""+n)}function Gx(e){var t=e.textContent;t===e._wrapperState.initialValue&&t!==""&&t!==null&&(e.value=t)}var kb={html:"http://www.w3.org/1999/xhtml",mathml:"http://www.w3.org/1998/Math/MathML",svg:"http://www.w3.org/2000/svg"};function Qx(e){switch(e){case"svg":return"http://www.w3.org/2000/svg";case"math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function Ob(e,t){return e==null||e==="http://www.w3.org/1999/xhtml"?Qx(t):e==="http://www.w3.org/2000/svg"&&t==="foreignObject"?"http://www.w3.org/1999/xhtml":e}var nv,Bx=function(e){return typeof MSApp!="undefined"&&MSApp.execUnsafeLocalFunction?function(t,r,n,i){MSApp.execUnsafeLocalFunction(function(){return e(t,r,n,i)})}:e}(function(e,t){if(e.namespaceURI!==kb.svg||"innerHTML"in e)e.innerHTML=t;else{for(nv=nv||document.createElement("div"),nv.innerHTML=""+t.valueOf().toString()+" ",t=nv.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Qf(e,t){if(t){var r=e.firstChild;if(r&&r===e.lastChild&&r.nodeType===3){r.nodeValue=t;return}}e.textContent=t}var Bf={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},mK=["Webkit","ms","Moz","O"];Object.keys(Bf).forEach(function(e){mK.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Bf[t]=Bf[e]})});function Kx(e,t,r){return t==null||typeof t=="boolean"||t===""?"":r||typeof t!="number"||t===0||Bf.hasOwnProperty(e)&&Bf[e]?(""+t).trim():t+"px"}function Hx(e,t){e=e.style;for(var r in t)if(t.hasOwnProperty(r)){var n=r.indexOf("--")===0,i=Kx(r,t[r],n);r==="float"&&(r="cssFloat"),n?e.setProperty(r,i):e[r]=i}}var yK=cr({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function wb(e,t){if(t){if(yK[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(ye(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(ye(60));if(!(typeof t.dangerouslySetInnerHTML=="object"&&"__html"in t.dangerouslySetInnerHTML))throw Error(ye(61))}if(t.style!=null&&typeof t.style!="object")throw Error(ye(62))}}function Nb(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}function Db(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var xb=null,kl=null,Ol=null;function zx(e){if(e=sd(e)){if(typeof xb!="function")throw Error(ye(280));var t=e.stateNode;t&&(t=kv(t),xb(e.stateNode,e.type,t))}}function Wx(e){kl?Ol?Ol.push(e):Ol=[e]:kl=e}function Yx(){if(kl){var e=kl,t=Ol;if(Ol=kl=null,zx(e),t)for(e=0;en?0:1<r;r++)t.push(e);return t}function cv(e,t,r){e.pendingLanes|=t;var n=t-1;e.suspendedLanes&=n,e.pingedLanes&=n,e=e.eventTimes,t=31-Zo(t),e[t]=r}var Zo=Math.clz32?Math.clz32:RK,IK=Math.log,AK=Math.LN2;function RK(e){return e===0?32:31-(IK(e)/AK|0)|0}var jK=ln.unstable_UserBlockingPriority,PK=ln.unstable_runWithPriority,fv=!0;function FK(e,t,r,n){ns||Lb();var i=Qb,o=ns;ns=!0;try{Jx(i,e,t,r,n)}finally{(ns=o)||Ab()}}function MK(e,t,r,n){PK(jK,Qb.bind(null,e,t,r,n))}function Qb(e,t,r,n){if(fv){var i;if((i=(t&4)==0)&&0=td),EC=String.fromCharCode(32),SC=!1;function kC(e,t){switch(e){case"keyup":return u3.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function OC(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Ll=!1;function l3(e,t){switch(e){case"compositionend":return OC(t);case"keypress":return t.which!==32?null:(SC=!0,EC);case"textInput":return e=t.data,e===EC&&SC?null:e;default:return null}}function c3(e,t){if(Ll)return e==="compositionend"||!Xb&&kC(e,t)?(e=gC(),dv=Kb=$o=null,Ll=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:r,offset:t-e};e=n}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=LC(r)}}function AC(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?AC(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function RC(){for(var e=window,t=rv();t instanceof e.HTMLIFrameElement;){try{var r=typeof t.contentWindow.location.href=="string"}catch(n){r=!1}if(r)e=t.contentWindow;else break;t=rv(e.document)}return t}function $b(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var T3=vo&&"documentMode"in document&&11>=document.documentMode,Il=null,eT=null,ad=null,tT=!1;function jC(e,t,r){var n=r.window===r?r.document:r.nodeType===9?r:r.ownerDocument;tT||Il==null||Il!==rv(n)||(n=Il,"selectionStart"in n&&$b(n)?n={start:n.selectionStart,end:n.selectionEnd}:(n=(n.ownerDocument&&n.ownerDocument.defaultView||window).getSelection(),n={anchorNode:n.anchorNode,anchorOffset:n.anchorOffset,focusNode:n.focusNode,focusOffset:n.focusOffset}),ad&&id(ad,n)||(ad=n,n=Tv(eT,"onSelect"),0Fl||(e.current=sT[Fl],sT[Fl]=null,Fl--)}function _r(e,t){Fl++,sT[Fl]=e.current,e.current=t}var ru={},Rn=tu(ru),ci=tu(!1),os=ru;function Ml(e,t){var r=e.type.contextTypes;if(!r)return ru;var n=e.stateNode;if(n&&n.__reactInternalMemoizedUnmaskedChildContext===t)return n.__reactInternalMemoizedMaskedChildContext;var i={},o;for(o in r)i[o]=t[o];return n&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function fi(e){return e=e.childContextTypes,e!=null}function Ov(){or(ci),or(Rn)}function JC(e,t,r){if(Rn.current!==ru)throw Error(ye(168));_r(Rn,t),_r(ci,r)}function XC(e,t,r){var n=e.stateNode;if(e=t.childContextTypes,typeof n.getChildContext!="function")return r;n=n.getChildContext();for(var i in n)if(!(i in e))throw Error(ye(108,El(t)||"Unknown",i));return cr({},r,n)}function wv(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||ru,os=Rn.current,_r(Rn,e),_r(ci,ci.current),!0}function ZC(e,t,r){var n=e.stateNode;if(!n)throw Error(ye(169));r?(e=XC(e,t,os),n.__reactInternalMemoizedMergedChildContext=e,or(ci),or(Rn),_r(Rn,e)):or(ci),_r(ci,r)}var lT=null,us=null,S3=ln.unstable_runWithPriority,cT=ln.unstable_scheduleCallback,fT=ln.unstable_cancelCallback,k3=ln.unstable_shouldYield,$C=ln.unstable_requestPaint,dT=ln.unstable_now,O3=ln.unstable_getCurrentPriorityLevel,Nv=ln.unstable_ImmediatePriority,eL=ln.unstable_UserBlockingPriority,tL=ln.unstable_NormalPriority,rL=ln.unstable_LowPriority,nL=ln.unstable_IdlePriority,pT={},w3=$C!==void 0?$C:function(){},go=null,Dv=null,hT=!1,iL=dT(),jn=1e4>iL?dT:function(){return dT()-iL};function ql(){switch(O3()){case Nv:return 99;case eL:return 98;case tL:return 97;case rL:return 96;case nL:return 95;default:throw Error(ye(332))}}function aL(e){switch(e){case 99:return Nv;case 98:return eL;case 97:return tL;case 96:return rL;case 95:return nL;default:throw Error(ye(332))}}function ss(e,t){return e=aL(e),S3(e,t)}function ld(e,t,r){return e=aL(e),cT(e,t,r)}function Aa(){if(Dv!==null){var e=Dv;Dv=null,fT(e)}oL()}function oL(){if(!hT&&go!==null){hT=!0;var e=0;try{var t=go;ss(99,function(){for(;eR?(M=O,O=null):M=O.sibling;var q=b(T,O,m[R],w);if(q===null){O===null&&(O=M);break}e&&O&&q.alternate===null&&t(T,O),S=o(q,S,R),L===null?x=q:L.sibling=q,L=q,O=M}if(R===m.length)return r(T,O),x;if(O===null){for(;RR?(M=O,O=null):M=O.sibling;var z=b(T,O,q.value,w);if(z===null){O===null&&(O=M);break}e&&O&&z.alternate===null&&t(T,O),S=o(z,S,R),L===null?x=z:L.sibling=z,L=z,O=M}if(q.done)return r(T,O),x;if(O===null){for(;!q.done;R++,q=m.next())q=y(T,q.value,w),q!==null&&(S=o(q,S,R),L===null?x=q:L.sibling=q,L=q);return x}for(O=n(T,O);!q.done;R++,q=m.next())q=D(O,T,R,q.value,w),q!==null&&(e&&q.alternate!==null&&O.delete(q.key===null?R:q.key),S=o(q,S,R),L===null?x=q:L.sibling=q,L=q);return e&&O.forEach(function(B){return t(T,B)}),x}return function(T,S,m,w){var x=typeof m=="object"&&m!==null&&m.type===zo&&m.key===null;x&&(m=m.props.children);var L=typeof m=="object"&&m!==null;if(L)switch(m.$$typeof){case Mf:e:{for(L=m.key,x=S;x!==null;){if(x.key===L){switch(x.tag){case 7:if(m.type===zo){r(T,x.sibling),S=i(x,m.props.children),S.return=T,T=S;break e}break;default:if(x.elementType===m.type){r(T,x.sibling),S=i(x,m.props),S.ref=fd(T,x,m),S.return=T,T=S;break e}}r(T,x);break}else t(T,x);x=x.sibling}m.type===zo?(S=Yl(m.props.children,T.mode,w,m.key),S.return=T,T=S):(w=$v(m.type,m.key,m.props,null,T.mode,w),w.ref=fd(T,S,m),w.return=T,T=w)}return s(T);case rs:e:{for(x=m.key;S!==null;){if(S.key===x)if(S.tag===4&&S.stateNode.containerInfo===m.containerInfo&&S.stateNode.implementation===m.implementation){r(T,S.sibling),S=i(S,m.children||[]),S.return=T,T=S;break e}else{r(T,S);break}else t(T,S);S=S.sibling}S=t_(m,T.mode,w),S.return=T,T=S}return s(T)}if(typeof m=="string"||typeof m=="number")return m=""+m,S!==null&&S.tag===6?(r(T,S.sibling),S=i(S,m),S.return=T,T=S):(r(T,S),S=e_(m,T.mode,w),S.return=T,T=S),s(T);if(Rv(m))return _(T,S,m,w);if(Uf(m))return k(T,S,m,w);if(L&&jv(T,m),typeof m=="undefined"&&!x)switch(T.tag){case 1:case 22:case 0:case 11:case 15:throw Error(ye(152,El(T.type)||"Component"))}return r(T,S)}}var Pv=vL(!0),gL=vL(!1),dd={},Ra=tu(dd),pd=tu(dd),hd=tu(dd);function ls(e){if(e===dd)throw Error(ye(174));return e}function bT(e,t){switch(_r(hd,t),_r(pd,e),_r(Ra,dd),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:Ob(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=Ob(t,e)}or(Ra),_r(Ra,t)}function Gl(){or(Ra),or(pd),or(hd)}function mL(e){ls(hd.current);var t=ls(Ra.current),r=Ob(t,e.type);t!==r&&(_r(pd,e),_r(Ra,r))}function TT(e){pd.current===e&&(or(Ra),or(pd))}var Er=tu(0);function Fv(e){for(var t=e;t!==null;){if(t.tag===13){var r=t.memoizedState;if(r!==null&&(r=r.dehydrated,r===null||r.data==="$?"||r.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if((t.flags&64)!=0)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var mo=null,ou=null,ja=!1;function yL(e,t){var r=Bi(5,null,null,0);r.elementType="DELETED",r.type="DELETED",r.stateNode=t,r.return=e,r.flags=8,e.lastEffect!==null?(e.lastEffect.nextEffect=r,e.lastEffect=r):e.firstEffect=e.lastEffect=r}function bL(e,t){switch(e.tag){case 5:var r=e.type;return t=t.nodeType!==1||r.toLowerCase()!==t.nodeName.toLowerCase()?null:t,t!==null?(e.stateNode=t,!0):!1;case 6:return t=e.pendingProps===""||t.nodeType!==3?null:t,t!==null?(e.stateNode=t,!0):!1;case 13:return!1;default:return!1}}function _T(e){if(ja){var t=ou;if(t){var r=t;if(!bL(e,t)){if(t=Rl(r.nextSibling),!t||!bL(e,t)){e.flags=e.flags&-1025|2,ja=!1,mo=e;return}yL(mo,r)}mo=e,ou=Rl(t.firstChild)}else e.flags=e.flags&-1025|2,ja=!1,mo=e}}function TL(e){for(e=e.return;e!==null&&e.tag!==5&&e.tag!==3&&e.tag!==13;)e=e.return;mo=e}function Mv(e){if(e!==mo)return!1;if(!ja)return TL(e),ja=!0,!1;var t=e.type;if(e.tag!==5||t!=="head"&&t!=="body"&&!aT(t,e.memoizedProps))for(t=ou;t;)yL(e,t),t=Rl(t.nextSibling);if(TL(e),e.tag===13){if(e=e.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(ye(317));e:{for(e=e.nextSibling,t=0;e;){if(e.nodeType===8){var r=e.data;if(r==="/$"){if(t===0){ou=Rl(e.nextSibling);break e}t--}else r!=="$"&&r!=="$!"&&r!=="$?"||t++}e=e.nextSibling}ou=null}}else ou=mo?Rl(e.stateNode.nextSibling):null;return!0}function ET(){ou=mo=null,ja=!1}var Ql=[];function ST(){for(var e=0;eo))throw Error(ye(301));o+=1,bn=Pn=null,t.updateQueue=null,vd.current=L3,e=r(n,i)}while(md)}if(vd.current=Qv,t=Pn!==null&&Pn.next!==null,gd=0,bn=Pn=Dr=null,qv=!1,t)throw Error(ye(300));return e}function cs(){var e={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return bn===null?Dr.memoizedState=bn=e:bn=bn.next=e,bn}function fs(){if(Pn===null){var e=Dr.alternate;e=e!==null?e.memoizedState:null}else e=Pn.next;var t=bn===null?Dr.memoizedState:bn.next;if(t!==null)bn=t,Pn=e;else{if(e===null)throw Error(ye(310));Pn=e,e={memoizedState:Pn.memoizedState,baseState:Pn.baseState,baseQueue:Pn.baseQueue,queue:Pn.queue,next:null},bn===null?Dr.memoizedState=bn=e:bn=bn.next=e}return bn}function Pa(e,t){return typeof t=="function"?t(e):t}function yd(e){var t=fs(),r=t.queue;if(r===null)throw Error(ye(311));r.lastRenderedReducer=e;var n=Pn,i=n.baseQueue,o=r.pending;if(o!==null){if(i!==null){var s=i.next;i.next=o.next,o.next=s}n.baseQueue=i=o,r.pending=null}if(i!==null){i=i.next,n=n.baseState;var l=s=o=null,d=i;do{var h=d.lane;if((gd&h)===h)l!==null&&(l=l.next={lane:0,action:d.action,eagerReducer:d.eagerReducer,eagerState:d.eagerState,next:null}),n=d.eagerReducer===e?d.eagerState:e(n,d.action);else{var v={lane:h,action:d.action,eagerReducer:d.eagerReducer,eagerState:d.eagerState,next:null};l===null?(s=l=v,o=n):l=l.next=v,Dr.lanes|=h,Ed|=h}d=d.next}while(d!==null&&d!==i);l===null?o=n:l.next=s,Vi(n,t.memoizedState)||(ga=!0),t.memoizedState=n,t.baseState=o,t.baseQueue=l,r.lastRenderedState=n}return[t.memoizedState,r.dispatch]}function bd(e){var t=fs(),r=t.queue;if(r===null)throw Error(ye(311));r.lastRenderedReducer=e;var n=r.dispatch,i=r.pending,o=t.memoizedState;if(i!==null){r.pending=null;var s=i=i.next;do o=e(o,s.action),s=s.next;while(s!==i);Vi(o,t.memoizedState)||(ga=!0),t.memoizedState=o,t.baseQueue===null&&(t.baseState=o),r.lastRenderedState=o}return[o,n]}function _L(e,t,r){var n=t._getVersion;n=n(t._source);var i=t._workInProgressVersionPrimary;if(i!==null?e=i===n:(e=e.mutableReadLanes,(e=(gd&e)===e)&&(t._workInProgressVersionPrimary=n,Ql.push(t))),e)return r(t._source);throw Ql.push(t),Error(ye(350))}function EL(e,t,r,n){var i=ei;if(i===null)throw Error(ye(349));var o=t._getVersion,s=o(t._source),l=vd.current,d=l.useState(function(){return _L(i,t,r)}),h=d[1],v=d[0];d=bn;var y=e.memoizedState,b=y.refs,D=b.getSnapshot,_=y.source;y=y.subscribe;var k=Dr;return e.memoizedState={refs:b,source:t,subscribe:n},l.useEffect(function(){b.getSnapshot=r,b.setSnapshot=h;var T=o(t._source);if(!Vi(s,T)){T=r(t._source),Vi(v,T)||(h(T),T=su(k),i.mutableReadLanes|=T&i.pendingLanes),T=i.mutableReadLanes,i.entangledLanes|=T;for(var S=i.entanglements,m=T;0r?98:r,function(){e(!0)}),ss(97<\/script>",e=e.removeChild(e.firstChild)):typeof n.is=="string"?e=s.createElement(r,{is:n.is}):(e=s.createElement(r),r==="select"&&(s=e,n.multiple?s.multiple=!0:n.size&&(s.size=n.size))):e=s.createElementNS(e,r),e[eu]=t,e[Sv]=n,QL(e,t,!1,!1),t.stateNode=e,s=Nb(r,n),r){case"dialog":ar("cancel",e),ar("close",e),i=n;break;case"iframe":case"object":case"embed":ar("load",e),i=n;break;case"video":case"audio":for(i=0;iKT&&(t.flags|=64,o=!0,_d(n,!1),t.lanes=33554432)}else{if(!o)if(e=Fv(s),e!==null){if(t.flags|=64,o=!0,r=e.updateQueue,r!==null&&(t.updateQueue=r,t.flags|=4),_d(n,!0),n.tail===null&&n.tailMode==="hidden"&&!s.alternate&&!ja)return t=t.lastEffect=n.lastEffect,t!==null&&(t.nextEffect=null),null}else 2*jn()-n.renderingStartTime>KT&&r!==1073741824&&(t.flags|=64,o=!0,_d(n,!1),t.lanes=33554432);n.isBackwards?(s.sibling=t.child,t.child=s):(r=n.last,r!==null?r.sibling=s:t.child=s,n.last=s)}return n.tail!==null?(r=n.tail,n.rendering=r,n.tail=r.sibling,n.lastEffect=t.lastEffect,n.renderingStartTime=jn(),r.sibling=null,t=Er.current,_r(Er,o?t&1|2:t&1),r):null;case 23:case 24:return XT(),e!==null&&e.memoizedState!==null!=(t.memoizedState!==null)&&n.mode!=="unstable-defer-without-hiding"&&(t.flags|=4),null}throw Error(ye(156,t.tag))}function R3(e){switch(e.tag){case 1:fi(e.type)&&Ov();var t=e.flags;return t&4096?(e.flags=t&-4097|64,e):null;case 3:if(Gl(),or(ci),or(Rn),ST(),t=e.flags,(t&64)!=0)throw Error(ye(285));return e.flags=t&-4097|64,e;case 5:return TT(e),null;case 13:return or(Er),t=e.flags,t&4096?(e.flags=t&-4097|64,e):null;case 19:return or(Er),null;case 4:return Gl(),null;case 10:return gT(e),null;case 23:case 24:return XT(),null;default:return null}}function jT(e,t){try{var r="",n=t;do r+=hK(n),n=n.return;while(n);var i=r}catch(o){i=`
-Error generating stack: `+o.message+`
-`+o.stack}return{value:e,source:t,stack:i}}function PT(e,t){try{console.error(t.value)}catch(r){setTimeout(function(){throw r})}}var j3=typeof WeakMap=="function"?WeakMap:Map;function HL(e,t,r){r=iu(-1,r),r.tag=3,r.payload={element:null};var n=t.value;return r.callback=function(){zv||(zv=!0,HT=n),PT(e,t)},r}function zL(e,t,r){r=iu(-1,r),r.tag=3;var n=e.type.getDerivedStateFromError;if(typeof n=="function"){var i=t.value;r.payload=function(){return PT(e,t),n(i)}}var o=e.stateNode;return o!==null&&typeof o.componentDidCatch=="function"&&(r.callback=function(){typeof n!="function"&&(Fa===null?Fa=new Set([this]):Fa.add(this),PT(e,t));var s=t.stack;this.componentDidCatch(t.value,{componentStack:s!==null?s:""})}),r}var P3=typeof WeakSet=="function"?WeakSet:Set;function WL(e){var t=e.ref;if(t!==null)if(typeof t=="function")try{t(null)}catch(r){fu(e,r)}else t.current=null}function F3(e,t){switch(t.tag){case 0:case 11:case 15:case 22:return;case 1:if(t.flags&256&&e!==null){var r=e.memoizedProps,n=e.memoizedState;e=t.stateNode,t=e.getSnapshotBeforeUpdate(t.elementType===t.type?r:va(t.type,r),n),e.__reactInternalSnapshotBeforeUpdate=t}return;case 3:t.flags&256&&oT(t.stateNode.containerInfo);return;case 5:case 6:case 4:case 17:return}throw Error(ye(163))}function M3(e,t,r){switch(r.tag){case 0:case 11:case 15:case 22:if(t=r.updateQueue,t=t!==null?t.lastEffect:null,t!==null){e=t=t.next;do{if((e.tag&3)==3){var n=e.create;e.destroy=n()}e=e.next}while(e!==t)}if(t=r.updateQueue,t=t!==null?t.lastEffect:null,t!==null){e=t=t.next;do{var i=e;n=i.next,i=i.tag,(i&4)!=0&&(i&1)!=0&&(l1(r,e),H3(r,e)),e=n}while(e!==t)}return;case 1:e=r.stateNode,r.flags&4&&(t===null?e.componentDidMount():(n=r.elementType===r.type?t.memoizedProps:va(r.type,t.memoizedProps),e.componentDidUpdate(n,t.memoizedState,e.__reactInternalSnapshotBeforeUpdate))),t=r.updateQueue,t!==null&&cL(r,t,e);return;case 3:if(t=r.updateQueue,t!==null){if(e=null,r.child!==null)switch(r.child.tag){case 5:e=r.child.stateNode;break;case 1:e=r.child.stateNode}cL(r,t,e)}return;case 5:e=r.stateNode,t===null&&r.flags&4&&KC(r.type,r.memoizedProps)&&e.focus();return;case 6:return;case 4:return;case 12:return;case 13:r.memoizedState===null&&(r=r.alternate,r!==null&&(r=r.memoizedState,r!==null&&(r=r.dehydrated,r!==null&&sC(r))));return;case 19:case 17:case 20:case 21:case 23:case 24:return}throw Error(ye(163))}function YL(e,t){for(var r=e;;){if(r.tag===5){var n=r.stateNode;if(t)n=n.style,typeof n.setProperty=="function"?n.setProperty("display","none","important"):n.display="none";else{n=r.stateNode;var i=r.memoizedProps.style;i=i!=null&&i.hasOwnProperty("display")?i.display:null,n.style.display=Kx("display",i)}}else if(r.tag===6)r.stateNode.nodeValue=t?"":r.memoizedProps;else if((r.tag!==23&&r.tag!==24||r.memoizedState===null||r===e)&&r.child!==null){r.child.return=r,r=r.child;continue}if(r===e)break;for(;r.sibling===null;){if(r.return===null||r.return===e)return;r=r.return}r.sibling.return=r.return,r=r.sibling}}function JL(e,t){if(us&&typeof us.onCommitFiberUnmount=="function")try{us.onCommitFiberUnmount(lT,t)}catch(o){}switch(t.tag){case 0:case 11:case 14:case 15:case 22:if(e=t.updateQueue,e!==null&&(e=e.lastEffect,e!==null)){var r=e=e.next;do{var n=r,i=n.destroy;if(n=n.tag,i!==void 0)if((n&4)!=0)l1(t,r);else{n=t;try{i()}catch(o){fu(n,o)}}r=r.next}while(r!==e)}break;case 1:if(WL(t),e=t.stateNode,typeof e.componentWillUnmount=="function")try{e.props=t.memoizedProps,e.state=t.memoizedState,e.componentWillUnmount()}catch(o){fu(t,o)}break;case 5:WL(t);break;case 4:e1(e,t)}}function XL(e){e.alternate=null,e.child=null,e.dependencies=null,e.firstEffect=null,e.lastEffect=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.return=null,e.updateQueue=null}function ZL(e){return e.tag===5||e.tag===3||e.tag===4}function $L(e){e:{for(var t=e.return;t!==null;){if(ZL(t))break e;t=t.return}throw Error(ye(160))}var r=t;switch(t=r.stateNode,r.tag){case 5:var n=!1;break;case 3:t=t.containerInfo,n=!0;break;case 4:t=t.containerInfo,n=!0;break;default:throw Error(ye(161))}r.flags&16&&(Qf(t,""),r.flags&=-17);e:t:for(r=e;;){for(;r.sibling===null;){if(r.return===null||ZL(r.return)){r=null;break e}r=r.return}for(r.sibling.return=r.return,r=r.sibling;r.tag!==5&&r.tag!==6&&r.tag!==18;){if(r.flags&2||r.child===null||r.tag===4)continue t;r.child.return=r,r=r.child}if(!(r.flags&2)){r=r.stateNode;break e}}n?FT(e,r,t):MT(e,r,t)}function FT(e,t,r){var n=e.tag,i=n===5||n===6;if(i)e=i?e.stateNode:e.stateNode.instance,t?r.nodeType===8?r.parentNode.insertBefore(e,t):r.insertBefore(e,t):(r.nodeType===8?(t=r.parentNode,t.insertBefore(e,r)):(t=r,t.appendChild(e)),r=r._reactRootContainer,r!=null||t.onclick!==null||(t.onclick=_v));else if(n!==4&&(e=e.child,e!==null))for(FT(e,t,r),e=e.sibling;e!==null;)FT(e,t,r),e=e.sibling}function MT(e,t,r){var n=e.tag,i=n===5||n===6;if(i)e=i?e.stateNode:e.stateNode.instance,t?r.insertBefore(e,t):r.appendChild(e);else if(n!==4&&(e=e.child,e!==null))for(MT(e,t,r),e=e.sibling;e!==null;)MT(e,t,r),e=e.sibling}function e1(e,t){for(var r=t,n=!1,i,o;;){if(!n){n=r.return;e:for(;;){if(n===null)throw Error(ye(160));switch(i=n.stateNode,n.tag){case 5:o=!1;break e;case 3:i=i.containerInfo,o=!0;break e;case 4:i=i.containerInfo,o=!0;break e}n=n.return}n=!0}if(r.tag===5||r.tag===6){e:for(var s=e,l=r,d=l;;)if(JL(s,d),d.child!==null&&d.tag!==4)d.child.return=d,d=d.child;else{if(d===l)break e;for(;d.sibling===null;){if(d.return===null||d.return===l)break e;d=d.return}d.sibling.return=d.return,d=d.sibling}o?(s=i,l=r.stateNode,s.nodeType===8?s.parentNode.removeChild(l):s.removeChild(l)):i.removeChild(r.stateNode)}else if(r.tag===4){if(r.child!==null){i=r.stateNode.containerInfo,o=!0,r.child.return=r,r=r.child;continue}}else if(JL(e,r),r.child!==null){r.child.return=r,r=r.child;continue}if(r===t)break;for(;r.sibling===null;){if(r.return===null||r.return===t)return;r=r.return,r.tag===4&&(n=!1)}r.sibling.return=r.return,r=r.sibling}}function qT(e,t){switch(t.tag){case 0:case 11:case 14:case 15:case 22:var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var n=r=r.next;do(n.tag&3)==3&&(e=n.destroy,n.destroy=void 0,e!==void 0&&e()),n=n.next;while(n!==r)}return;case 1:return;case 5:if(r=t.stateNode,r!=null){n=t.memoizedProps;var i=e!==null?e.memoizedProps:n;e=t.type;var o=t.updateQueue;if(t.updateQueue=null,o!==null){for(r[Sv]=n,e==="input"&&n.type==="radio"&&n.name!=null&&Mx(r,n),Nb(e,i),t=Nb(e,n),i=0;ii&&(i=s),r&=~o}if(r=i,r=jn()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*V3(r/1960))-r,10 component higher in the tree to provide a loading indicator or placeholder to display.`)}Tn!==5&&(Tn=2),d=jT(d,l),b=s;do{switch(b.tag){case 3:o=d,b.flags|=4096,t&=-t,b.lanes|=t;var L=HL(b,o,t);lL(b,L);break e;case 1:o=d;var O=b.type,R=b.stateNode;if((b.flags&64)==0&&(typeof O.getDerivedStateFromError=="function"||R!==null&&typeof R.componentDidCatch=="function"&&(Fa===null||!Fa.has(R)))){b.flags|=4096,t&=-t,b.lanes|=t;var M=zL(b,o,t);lL(b,M);break e}}b=b.return}while(b!==null)}s1(r)}catch(q){t=q,Jr===r&&r!==null&&(Jr=r=r.return);continue}break}while(1)}function o1(){var e=Kv.current;return Kv.current=Qv,e===null?Qv:e}function Nd(e,t){var r=tt;tt|=16;var n=o1();ei===e&&Fn===t||Wl(e,t);do try{G3();break}catch(i){a1(e,i)}while(1);if(vT(),tt=r,Kv.current=n,Jr!==null)throw Error(ye(261));return ei=null,Fn=0,Tn}function G3(){for(;Jr!==null;)u1(Jr)}function Q3(){for(;Jr!==null&&!k3();)u1(Jr)}function u1(e){var t=f1(e.alternate,e,ds);e.memoizedProps=e.pendingProps,t===null?s1(e):Jr=t,VT.current=null}function s1(e){var t=e;do{var r=t.alternate;if(e=t.return,(t.flags&2048)==0){if(r=A3(r,t,ds),r!==null){Jr=r;return}if(r=t,r.tag!==24&&r.tag!==23||r.memoizedState===null||(ds&1073741824)!=0||(r.mode&4)==0){for(var n=0,i=r.child;i!==null;)n|=i.lanes|i.childLanes,i=i.sibling;r.childLanes=n}e!==null&&(e.flags&2048)==0&&(e.firstEffect===null&&(e.firstEffect=t.firstEffect),t.lastEffect!==null&&(e.lastEffect!==null&&(e.lastEffect.nextEffect=t.firstEffect),e.lastEffect=t.lastEffect),1s&&(l=s,s=L,L=l),l=IC(m,L),o=IC(m,s),l&&o&&(x.rangeCount!==1||x.anchorNode!==l.node||x.anchorOffset!==l.offset||x.focusNode!==o.node||x.focusOffset!==o.offset)&&(w=w.createRange(),w.setStart(l.node,l.offset),x.removeAllRanges(),L>s?(x.addRange(w),x.extend(o.node,o.offset)):(w.setEnd(o.node,o.offset),x.addRange(w)))))),w=[],x=m;x=x.parentNode;)x.nodeType===1&&w.push({element:x,left:x.scrollLeft,top:x.scrollTop});for(typeof m.focus=="function"&&m.focus(),m=0;mjn()-BT?Wl(e,0):GT|=r),Qi(e,t)}function Y3(e,t){var r=e.stateNode;r!==null&&r.delete(t),t=0,t===0&&(t=e.mode,(t&2)==0?t=1:(t&4)==0?t=ql()===99?1:2:(To===0&&(To=Bl),t=xl(62914560&~To),t===0&&(t=4194304))),r=wi(),e=Xv(e,t),e!==null&&(cv(e,t,r),Qi(e,r))}var f1;f1=function(e,t,r){var n=t.lanes;if(e!==null)if(e.memoizedProps!==t.pendingProps||ci.current)ga=!0;else if((r&n)!=0)ga=(e.flags&16384)!=0;else{switch(ga=!1,t.tag){case 3:PL(t),ET();break;case 5:mL(t);break;case 1:fi(t.type)&&wv(t);break;case 4:bT(t,t.stateNode.containerInfo);break;case 10:n=t.memoizedProps.value;var i=t.type._context;_r(xv,i._currentValue),i._currentValue=n;break;case 13:if(t.memoizedState!==null)return(r&t.child.childLanes)!=0?FL(e,t,r):(_r(Er,Er.current&1),t=yo(e,t,r),t!==null?t.sibling:null);_r(Er,Er.current&1);break;case 19:if(n=(r&t.childLanes)!=0,(e.flags&64)!=0){if(n)return GL(e,t,r);t.flags|=64}if(i=t.memoizedState,i!==null&&(i.rendering=null,i.tail=null,i.lastEffect=null),_r(Er,Er.current),n)break;return null;case 23:case 24:return t.lanes=0,CT(e,t,r)}return yo(e,t,r)}else ga=!1;switch(t.lanes=0,t.tag){case 2:if(n=t.type,e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2),e=t.pendingProps,i=Ml(t,Rn.current),Ul(t,r),i=OT(null,t,n,e,i,r),t.flags|=1,typeof i=="object"&&i!==null&&typeof i.render=="function"&&i.$$typeof===void 0){if(t.tag=1,t.memoizedState=null,t.updateQueue=null,fi(n)){var o=!0;wv(t)}else o=!1;t.memoizedState=i.state!==null&&i.state!==void 0?i.state:null,mT(t);var s=n.getDerivedStateFromProps;typeof s=="function"&&Iv(t,n,s,e),i.updater=Av,t.stateNode=i,i._reactInternals=t,yT(t,n,e,r),t=IT(null,t,n,!0,o,r)}else t.tag=0,pi(null,t,i,r),t=t.child;return t;case 16:i=t.elementType;e:{switch(e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2),e=t.pendingProps,o=i._init,i=o(i._payload),t.type=i,o=t.tag=X3(i),e=va(i,e),o){case 0:t=LT(null,t,i,e,r);break e;case 1:t=jL(null,t,i,e,r);break e;case 11:t=LL(null,t,i,e,r);break e;case 14:t=IL(null,t,i,va(i.type,e),n,r);break e}throw Error(ye(306,i,""))}return t;case 0:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:va(n,i),LT(e,t,n,i,r);case 1:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:va(n,i),jL(e,t,n,i,r);case 3:if(PL(t),n=t.updateQueue,e===null||n===null)throw Error(ye(282));if(n=t.pendingProps,i=t.memoizedState,i=i!==null?i.element:null,sL(e,t),cd(t,n,null,r),n=t.memoizedState.element,n===i)ET(),t=yo(e,t,r);else{if(i=t.stateNode,(o=i.hydrate)&&(ou=Rl(t.stateNode.containerInfo.firstChild),mo=t,o=ja=!0),o){if(e=i.mutableSourceEagerHydrationData,e!=null)for(i=0;i{"use strict";function v1(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__=="undefined"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(v1)}catch(e){console.error(e)}}v1(),g1.exports=h1()});var m1=G(Jl=>{"use strict";Object.defineProperty(Jl,"__esModule",{value:!0});Jl.versionInfo=Jl.version=void 0;var iH="15.5.0";Jl.version=iH;var aH=Object.freeze({major:15,minor:5,patch:0,preReleaseTag:null});Jl.versionInfo=aH});var rg=G(o_=>{"use strict";Object.defineProperty(o_,"__esModule",{value:!0});o_.default=oH;function oH(e){return typeof(e==null?void 0:e.then)=="function"}});var Ma=G(u_=>{"use strict";Object.defineProperty(u_,"__esModule",{value:!0});u_.default=uH;function ng(e){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?ng=function(r){return typeof r}:ng=function(r){return r&&typeof Symbol=="function"&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},ng(e)}function uH(e){return ng(e)=="object"&&e!==null}});var qa=G(pu=>{"use strict";Object.defineProperty(pu,"__esModule",{value:!0});pu.SYMBOL_TO_STRING_TAG=pu.SYMBOL_ASYNC_ITERATOR=pu.SYMBOL_ITERATOR=void 0;var sH=typeof Symbol=="function"&&Symbol.iterator!=null?Symbol.iterator:"@@iterator";pu.SYMBOL_ITERATOR=sH;var lH=typeof Symbol=="function"&&Symbol.asyncIterator!=null?Symbol.asyncIterator:"@@asyncIterator";pu.SYMBOL_ASYNC_ITERATOR=lH;var cH=typeof Symbol=="function"&&Symbol.toStringTag!=null?Symbol.toStringTag:"@@toStringTag";pu.SYMBOL_TO_STRING_TAG=cH});var ig=G(s_=>{"use strict";Object.defineProperty(s_,"__esModule",{value:!0});s_.getLocation=fH;function fH(e,t){for(var r=/\r\n|[\n\r]/g,n=1,i=t+1,o;(o=r.exec(e.body))&&o.index{"use strict";Object.defineProperty(og,"__esModule",{value:!0});og.printLocation=pH;og.printSourceLocation=y1;var dH=ig();function pH(e){return y1(e.source,(0,dH.getLocation)(e.source,e.start))}function y1(e,t){var r=e.locationOffset.column-1,n=ag(r)+e.body,i=t.line-1,o=e.locationOffset.line-1,s=t.line+o,l=t.line===1?r:0,d=t.column+l,h="".concat(e.name,":").concat(s,":").concat(d,`
-`),v=n.split(/\r\n|[\n\r]/g),y=v[i];if(y.length>120){for(var b=Math.floor(d/80),D=d%80,_=[],k=0;k{"use strict";function ug(e){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?ug=function(r){return typeof r}:ug=function(r){return r&&typeof Symbol=="function"&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},ug(e)}Object.defineProperty(Rd,"__esModule",{value:!0});Rd.printError=O1;Rd.GraphQLError=void 0;var vH=mH(Ma()),gH=qa(),T1=ig(),_1=l_();function mH(e){return e&&e.__esModule?e:{default:e}}function yH(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function E1(e,t){for(var r=0;r{"use strict";Object.defineProperty(f_,"__esModule",{value:!0});f_.syntaxError=OH;var kH=Je();function OH(e,t,r){return new kH.GraphQLError("Syntax Error: ".concat(r),void 0,e,[t])}});var Jt=G(cg=>{"use strict";Object.defineProperty(cg,"__esModule",{value:!0});cg.Kind=void 0;var wH=Object.freeze({NAME:"Name",DOCUMENT:"Document",OPERATION_DEFINITION:"OperationDefinition",VARIABLE_DEFINITION:"VariableDefinition",SELECTION_SET:"SelectionSet",FIELD:"Field",ARGUMENT:"Argument",FRAGMENT_SPREAD:"FragmentSpread",INLINE_FRAGMENT:"InlineFragment",FRAGMENT_DEFINITION:"FragmentDefinition",VARIABLE:"Variable",INT:"IntValue",FLOAT:"FloatValue",STRING:"StringValue",BOOLEAN:"BooleanValue",NULL:"NullValue",ENUM:"EnumValue",LIST:"ListValue",OBJECT:"ObjectValue",OBJECT_FIELD:"ObjectField",DIRECTIVE:"Directive",NAMED_TYPE:"NamedType",LIST_TYPE:"ListType",NON_NULL_TYPE:"NonNullType",SCHEMA_DEFINITION:"SchemaDefinition",OPERATION_TYPE_DEFINITION:"OperationTypeDefinition",SCALAR_TYPE_DEFINITION:"ScalarTypeDefinition",OBJECT_TYPE_DEFINITION:"ObjectTypeDefinition",FIELD_DEFINITION:"FieldDefinition",INPUT_VALUE_DEFINITION:"InputValueDefinition",INTERFACE_TYPE_DEFINITION:"InterfaceTypeDefinition",UNION_TYPE_DEFINITION:"UnionTypeDefinition",ENUM_TYPE_DEFINITION:"EnumTypeDefinition",ENUM_VALUE_DEFINITION:"EnumValueDefinition",INPUT_OBJECT_TYPE_DEFINITION:"InputObjectTypeDefinition",DIRECTIVE_DEFINITION:"DirectiveDefinition",SCHEMA_EXTENSION:"SchemaExtension",SCALAR_TYPE_EXTENSION:"ScalarTypeExtension",OBJECT_TYPE_EXTENSION:"ObjectTypeExtension",INTERFACE_TYPE_EXTENSION:"InterfaceTypeExtension",UNION_TYPE_EXTENSION:"UnionTypeExtension",ENUM_TYPE_EXTENSION:"EnumTypeExtension",INPUT_OBJECT_TYPE_EXTENSION:"InputObjectTypeExtension"});cg.Kind=wH});var _n=G(d_=>{"use strict";Object.defineProperty(d_,"__esModule",{value:!0});d_.default=NH;function NH(e,t){var r=Boolean(e);if(!r)throw new Error(t!=null?t:"Unexpected invariant triggered.")}});var p_=G(fg=>{"use strict";Object.defineProperty(fg,"__esModule",{value:!0});fg.default=void 0;var DH=typeof Symbol=="function"&&typeof Symbol.for=="function"?Symbol.for("nodejs.util.inspect.custom"):void 0,xH=DH;fg.default=xH});var dg=G(h_=>{"use strict";Object.defineProperty(h_,"__esModule",{value:!0});h_.default=LH;var CH=N1(_n()),w1=N1(p_());function N1(e){return e&&e.__esModule?e:{default:e}}function LH(e){var t=e.prototype.toJSON;typeof t=="function"||(0,CH.default)(0),e.prototype.inspect=t,w1.default&&(e.prototype[w1.default]=t)}});var Xl=G(hs=>{"use strict";Object.defineProperty(hs,"__esModule",{value:!0});hs.isNode=AH;hs.Token=hs.Location=void 0;var D1=IH(dg());function IH(e){return e&&e.__esModule?e:{default:e}}var x1=function(){function e(r,n,i){this.start=r.start,this.end=n.end,this.startToken=r,this.endToken=n,this.source=i}var t=e.prototype;return t.toJSON=function(){return{start:this.start,end:this.end}},e}();hs.Location=x1;(0,D1.default)(x1);var C1=function(){function e(r,n,i,o,s,l,d){this.kind=r,this.start=n,this.end=i,this.line=o,this.column=s,this.value=d,this.prev=l,this.next=null}var t=e.prototype;return t.toJSON=function(){return{kind:this.kind,value:this.value,line:this.line,column:this.column}},e}();hs.Token=C1;(0,D1.default)(C1);function AH(e){return e!=null&&typeof e.kind=="string"}});var Zl=G(pg=>{"use strict";Object.defineProperty(pg,"__esModule",{value:!0});pg.TokenKind=void 0;var RH=Object.freeze({SOF:"",EOF:"",BANG:"!",DOLLAR:"$",AMP:"&",PAREN_L:"(",PAREN_R:")",SPREAD:"...",COLON:":",EQUALS:"=",AT:"@",BRACKET_L:"[",BRACKET_R:"]",BRACE_L:"{",PIPE:"|",BRACE_R:"}",NAME:"Name",INT:"Int",FLOAT:"Float",STRING:"String",BLOCK_STRING:"BlockString",COMMENT:"Comment"});pg.TokenKind=RH});var jt=G(v_=>{"use strict";Object.defineProperty(v_,"__esModule",{value:!0});v_.default=MH;var jH=PH(p_());function PH(e){return e&&e.__esModule?e:{default:e}}function hg(e){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?hg=function(r){return typeof r}:hg=function(r){return r&&typeof Symbol=="function"&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},hg(e)}var FH=10,L1=2;function MH(e){return vg(e,[])}function vg(e,t){switch(hg(e)){case"string":return JSON.stringify(e);case"function":return e.name?"[function ".concat(e.name,"]"):"[function]";case"object":return e===null?"null":qH(e,t);default:return String(e)}}function qH(e,t){if(t.indexOf(e)!==-1)return"[Circular]";var r=[].concat(t,[e]),n=GH(e);if(n!==void 0){var i=n.call(e);if(i!==e)return typeof i=="string"?i:vg(i,r)}else if(Array.isArray(e))return UH(e,r);return VH(e,r)}function VH(e,t){var r=Object.keys(e);if(r.length===0)return"{}";if(t.length>L1)return"["+QH(e)+"]";var n=r.map(function(i){var o=vg(e[i],t);return i+": "+o});return"{ "+n.join(", ")+" }"}function UH(e,t){if(e.length===0)return"[]";if(t.length>L1)return"[Array]";for(var r=Math.min(FH,e.length),n=e.length-r,i=[],o=0;o1&&i.push("... ".concat(n," more items")),"["+i.join(", ")+"]"}function GH(e){var t=e[String(jH.default)];if(typeof t=="function")return t;if(typeof e.inspect=="function")return e.inspect}function QH(e){var t=Object.prototype.toString.call(e).replace(/^\[object /,"").replace(/]$/,"");if(t==="Object"&&typeof e.constructor=="function"){var r=e.constructor.name;if(typeof r=="string"&&r!=="")return r}return t}});var Hi=G(g_=>{"use strict";Object.defineProperty(g_,"__esModule",{value:!0});g_.default=BH;function BH(e,t){var r=Boolean(e);if(!r)throw new Error(t)}});var jd=G(gg=>{"use strict";Object.defineProperty(gg,"__esModule",{value:!0});gg.default=void 0;var KH=function(t,r){return t instanceof r};gg.default=KH});var mg=G(Pd=>{"use strict";Object.defineProperty(Pd,"__esModule",{value:!0});Pd.isSource=JH;Pd.Source=void 0;var HH=qa(),zH=y_(jt()),m_=y_(Hi()),WH=y_(jd());function y_(e){return e&&e.__esModule?e:{default:e}}function I1(e,t){for(var r=0;r1&&arguments[1]!==void 0?arguments[1]:"GraphQL request",n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{line:1,column:1};typeof t=="string"||(0,m_.default)(0,"Body must be a string. Received: ".concat((0,zH.default)(t),".")),this.body=t,this.name=r,this.locationOffset=n,this.locationOffset.line>0||(0,m_.default)(0,"line in locationOffset is 1-indexed and must be positive."),this.locationOffset.column>0||(0,m_.default)(0,"column in locationOffset is 1-indexed and must be positive.")}return YH(e,[{key:HH.SYMBOL_TO_STRING_TAG,get:function(){return"Source"}}]),e}();Pd.Source=A1;function JH(e){return(0,WH.default)(e,A1)}});var $l=G(yg=>{"use strict";Object.defineProperty(yg,"__esModule",{value:!0});yg.DirectiveLocation=void 0;var XH=Object.freeze({QUERY:"QUERY",MUTATION:"MUTATION",SUBSCRIPTION:"SUBSCRIPTION",FIELD:"FIELD",FRAGMENT_DEFINITION:"FRAGMENT_DEFINITION",FRAGMENT_SPREAD:"FRAGMENT_SPREAD",INLINE_FRAGMENT:"INLINE_FRAGMENT",VARIABLE_DEFINITION:"VARIABLE_DEFINITION",SCHEMA:"SCHEMA",SCALAR:"SCALAR",OBJECT:"OBJECT",FIELD_DEFINITION:"FIELD_DEFINITION",ARGUMENT_DEFINITION:"ARGUMENT_DEFINITION",INTERFACE:"INTERFACE",UNION:"UNION",ENUM:"ENUM",ENUM_VALUE:"ENUM_VALUE",INPUT_OBJECT:"INPUT_OBJECT",INPUT_FIELD_DEFINITION:"INPUT_FIELD_DEFINITION"});yg.DirectiveLocation=XH});var ec=G(Fd=>{"use strict";Object.defineProperty(Fd,"__esModule",{value:!0});Fd.dedentBlockStringValue=ZH;Fd.getBlockStringIndentation=j1;Fd.printBlockString=$H;function ZH(e){var t=e.split(/\r\n|[\n\r]/g),r=j1(e);if(r!==0)for(var n=1;ni&&R1(t[o-1]);)--o;return t.slice(i,o).join(`
-`)}function R1(e){for(var t=0;t1&&arguments[1]!==void 0?arguments[1]:"",r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!1,n=e.indexOf(`
-`)===-1,i=e[0]===" "||e[0]===" ",o=e[e.length-1]==='"',s=e[e.length-1]==="\\",l=!n||o||s||r,d="";return l&&!(n&&i)&&(d+=`
-`+t),d+=t?e.replace(/\n/g,`
-`+t):e,l&&(d+=`
-`),'"""'+d.replace(/"""/g,'\\"""')+'"""'}});var Tg=G(Md=>{"use strict";Object.defineProperty(Md,"__esModule",{value:!0});Md.isPunctuatorTokenKind=rz;Md.Lexer=void 0;var Va=lg(),xr=Xl(),dt=Zl(),ez=ec(),tz=function(){function e(r){var n=new xr.Token(dt.TokenKind.SOF,0,0,0,0,null);this.source=r,this.lastToken=n,this.token=n,this.line=1,this.lineStart=0}var t=e.prototype;return t.advance=function(){this.lastToken=this.token;var n=this.token=this.lookahead();return n},t.lookahead=function(){var n=this.token;if(n.kind!==dt.TokenKind.EOF)do{var i;n=(i=n.next)!==null&&i!==void 0?i:n.next=nz(this,n)}while(n.kind===dt.TokenKind.COMMENT);return n},e}();Md.Lexer=tz;function rz(e){return e===dt.TokenKind.BANG||e===dt.TokenKind.DOLLAR||e===dt.TokenKind.AMP||e===dt.TokenKind.PAREN_L||e===dt.TokenKind.PAREN_R||e===dt.TokenKind.SPREAD||e===dt.TokenKind.COLON||e===dt.TokenKind.EQUALS||e===dt.TokenKind.AT||e===dt.TokenKind.BRACKET_L||e===dt.TokenKind.BRACKET_R||e===dt.TokenKind.BRACE_L||e===dt.TokenKind.PIPE||e===dt.TokenKind.BRACE_R}function vs(e){return isNaN(e)?dt.TokenKind.EOF:e<127?JSON.stringify(String.fromCharCode(e)):'"\\u'.concat(("00"+e.toString(16).toUpperCase()).slice(-4),'"')}function nz(e,t){for(var r=e.source,n=r.body,i=n.length,o=t.end;o31||s===9));return new xr.Token(dt.TokenKind.COMMENT,t,l,r,n,i,o.slice(t+1,l))}function oz(e,t,r,n,i,o){var s=e.body,l=r,d=t,h=!1;if(l===45&&(l=s.charCodeAt(++d)),l===48){if(l=s.charCodeAt(++d),l>=48&&l<=57)throw(0,Va.syntaxError)(e,d,"Invalid number, unexpected digit after 0: ".concat(vs(l),"."))}else d=b_(e,d,l),l=s.charCodeAt(d);if(l===46&&(h=!0,l=s.charCodeAt(++d),d=b_(e,d,l),l=s.charCodeAt(d)),(l===69||l===101)&&(h=!0,l=s.charCodeAt(++d),(l===43||l===45)&&(l=s.charCodeAt(++d)),d=b_(e,d,l),l=s.charCodeAt(d)),l===46||fz(l))throw(0,Va.syntaxError)(e,d,"Invalid number, expected digit but got: ".concat(vs(l),"."));return new xr.Token(h?dt.TokenKind.FLOAT:dt.TokenKind.INT,t,d,n,i,o,s.slice(t,d))}function b_(e,t,r){var n=e.body,i=t,o=r;if(o>=48&&o<=57){do o=n.charCodeAt(++i);while(o>=48&&o<=57);return i}throw(0,Va.syntaxError)(e,i,"Invalid number, expected digit but got: ".concat(vs(o),"."))}function uz(e,t,r,n,i){for(var o=e.body,s=t+1,l=s,d=0,h="";s=48&&e<=57?e-48:e>=65&&e<=70?e-55:e>=97&&e<=102?e-87:-1}function cz(e,t,r,n,i){for(var o=e.body,s=o.length,l=t+1,d=0;l!==s&&!isNaN(d=o.charCodeAt(l))&&(d===95||d>=48&&d<=57||d>=65&&d<=90||d>=97&&d<=122);)++l;return new xr.Token(dt.TokenKind.NAME,t,l,r,n,i,o.slice(t,l))}function fz(e){return e===95||e>=65&&e<=90||e>=97&&e<=122}});var tc=G(gs=>{"use strict";Object.defineProperty(gs,"__esModule",{value:!0});gs.parse=hz;gs.parseValue=vz;gs.parseType=gz;gs.Parser=void 0;var T_=lg(),$e=Jt(),dz=Xl(),De=Zl(),P1=mg(),pz=$l(),F1=Tg();function hz(e,t){var r=new _g(e,t);return r.parseDocument()}function vz(e,t){var r=new _g(e,t);r.expectToken(De.TokenKind.SOF);var n=r.parseValueLiteral(!1);return r.expectToken(De.TokenKind.EOF),n}function gz(e,t){var r=new _g(e,t);r.expectToken(De.TokenKind.SOF);var n=r.parseTypeReference();return r.expectToken(De.TokenKind.EOF),n}var _g=function(){function e(r,n){var i=(0,P1.isSource)(r)?r:new P1.Source(r);this._lexer=new F1.Lexer(i),this._options=n}var t=e.prototype;return t.parseName=function(){var n=this.expectToken(De.TokenKind.NAME);return{kind:$e.Kind.NAME,value:n.value,loc:this.loc(n)}},t.parseDocument=function(){var n=this._lexer.token;return{kind:$e.Kind.DOCUMENT,definitions:this.many(De.TokenKind.SOF,this.parseDefinition,De.TokenKind.EOF),loc:this.loc(n)}},t.parseDefinition=function(){if(this.peek(De.TokenKind.NAME))switch(this._lexer.token.value){case"query":case"mutation":case"subscription":return this.parseOperationDefinition();case"fragment":return this.parseFragmentDefinition();case"schema":case"scalar":case"type":case"interface":case"union":case"enum":case"input":case"directive":return this.parseTypeSystemDefinition();case"extend":return this.parseTypeSystemExtension()}else{if(this.peek(De.TokenKind.BRACE_L))return this.parseOperationDefinition();if(this.peekDescription())return this.parseTypeSystemDefinition()}throw this.unexpected()},t.parseOperationDefinition=function(){var n=this._lexer.token;if(this.peek(De.TokenKind.BRACE_L))return{kind:$e.Kind.OPERATION_DEFINITION,operation:"query",name:void 0,variableDefinitions:[],directives:[],selectionSet:this.parseSelectionSet(),loc:this.loc(n)};var i=this.parseOperationType(),o;return this.peek(De.TokenKind.NAME)&&(o=this.parseName()),{kind:$e.Kind.OPERATION_DEFINITION,operation:i,name:o,variableDefinitions:this.parseVariableDefinitions(),directives:this.parseDirectives(!1),selectionSet:this.parseSelectionSet(),loc:this.loc(n)}},t.parseOperationType=function(){var n=this.expectToken(De.TokenKind.NAME);switch(n.value){case"query":return"query";case"mutation":return"mutation";case"subscription":return"subscription"}throw this.unexpected(n)},t.parseVariableDefinitions=function(){return this.optionalMany(De.TokenKind.PAREN_L,this.parseVariableDefinition,De.TokenKind.PAREN_R)},t.parseVariableDefinition=function(){var n=this._lexer.token;return{kind:$e.Kind.VARIABLE_DEFINITION,variable:this.parseVariable(),type:(this.expectToken(De.TokenKind.COLON),this.parseTypeReference()),defaultValue:this.expectOptionalToken(De.TokenKind.EQUALS)?this.parseValueLiteral(!0):void 0,directives:this.parseDirectives(!0),loc:this.loc(n)}},t.parseVariable=function(){var n=this._lexer.token;return this.expectToken(De.TokenKind.DOLLAR),{kind:$e.Kind.VARIABLE,name:this.parseName(),loc:this.loc(n)}},t.parseSelectionSet=function(){var n=this._lexer.token;return{kind:$e.Kind.SELECTION_SET,selections:this.many(De.TokenKind.BRACE_L,this.parseSelection,De.TokenKind.BRACE_R),loc:this.loc(n)}},t.parseSelection=function(){return this.peek(De.TokenKind.SPREAD)?this.parseFragment():this.parseField()},t.parseField=function(){var n=this._lexer.token,i=this.parseName(),o,s;return this.expectOptionalToken(De.TokenKind.COLON)?(o=i,s=this.parseName()):s=i,{kind:$e.Kind.FIELD,alias:o,name:s,arguments:this.parseArguments(!1),directives:this.parseDirectives(!1),selectionSet:this.peek(De.TokenKind.BRACE_L)?this.parseSelectionSet():void 0,loc:this.loc(n)}},t.parseArguments=function(n){var i=n?this.parseConstArgument:this.parseArgument;return this.optionalMany(De.TokenKind.PAREN_L,i,De.TokenKind.PAREN_R)},t.parseArgument=function(){var n=this._lexer.token,i=this.parseName();return this.expectToken(De.TokenKind.COLON),{kind:$e.Kind.ARGUMENT,name:i,value:this.parseValueLiteral(!1),loc:this.loc(n)}},t.parseConstArgument=function(){var n=this._lexer.token;return{kind:$e.Kind.ARGUMENT,name:this.parseName(),value:(this.expectToken(De.TokenKind.COLON),this.parseValueLiteral(!0)),loc:this.loc(n)}},t.parseFragment=function(){var n=this._lexer.token;this.expectToken(De.TokenKind.SPREAD);var i=this.expectOptionalKeyword("on");return!i&&this.peek(De.TokenKind.NAME)?{kind:$e.Kind.FRAGMENT_SPREAD,name:this.parseFragmentName(),directives:this.parseDirectives(!1),loc:this.loc(n)}:{kind:$e.Kind.INLINE_FRAGMENT,typeCondition:i?this.parseNamedType():void 0,directives:this.parseDirectives(!1),selectionSet:this.parseSelectionSet(),loc:this.loc(n)}},t.parseFragmentDefinition=function(){var n,i=this._lexer.token;return this.expectKeyword("fragment"),((n=this._options)===null||n===void 0?void 0:n.experimentalFragmentVariables)===!0?{kind:$e.Kind.FRAGMENT_DEFINITION,name:this.parseFragmentName(),variableDefinitions:this.parseVariableDefinitions(),typeCondition:(this.expectKeyword("on"),this.parseNamedType()),directives:this.parseDirectives(!1),selectionSet:this.parseSelectionSet(),loc:this.loc(i)}:{kind:$e.Kind.FRAGMENT_DEFINITION,name:this.parseFragmentName(),typeCondition:(this.expectKeyword("on"),this.parseNamedType()),directives:this.parseDirectives(!1),selectionSet:this.parseSelectionSet(),loc:this.loc(i)}},t.parseFragmentName=function(){if(this._lexer.token.value==="on")throw this.unexpected();return this.parseName()},t.parseValueLiteral=function(n){var i=this._lexer.token;switch(i.kind){case De.TokenKind.BRACKET_L:return this.parseList(n);case De.TokenKind.BRACE_L:return this.parseObject(n);case De.TokenKind.INT:return this._lexer.advance(),{kind:$e.Kind.INT,value:i.value,loc:this.loc(i)};case De.TokenKind.FLOAT:return this._lexer.advance(),{kind:$e.Kind.FLOAT,value:i.value,loc:this.loc(i)};case De.TokenKind.STRING:case De.TokenKind.BLOCK_STRING:return this.parseStringLiteral();case De.TokenKind.NAME:switch(this._lexer.advance(),i.value){case"true":return{kind:$e.Kind.BOOLEAN,value:!0,loc:this.loc(i)};case"false":return{kind:$e.Kind.BOOLEAN,value:!1,loc:this.loc(i)};case"null":return{kind:$e.Kind.NULL,loc:this.loc(i)};default:return{kind:$e.Kind.ENUM,value:i.value,loc:this.loc(i)}}case De.TokenKind.DOLLAR:if(!n)return this.parseVariable();break}throw this.unexpected()},t.parseStringLiteral=function(){var n=this._lexer.token;return this._lexer.advance(),{kind:$e.Kind.STRING,value:n.value,block:n.kind===De.TokenKind.BLOCK_STRING,loc:this.loc(n)}},t.parseList=function(n){var i=this,o=this._lexer.token,s=function(){return i.parseValueLiteral(n)};return{kind:$e.Kind.LIST,values:this.any(De.TokenKind.BRACKET_L,s,De.TokenKind.BRACKET_R),loc:this.loc(o)}},t.parseObject=function(n){var i=this,o=this._lexer.token,s=function(){return i.parseObjectField(n)};return{kind:$e.Kind.OBJECT,fields:this.any(De.TokenKind.BRACE_L,s,De.TokenKind.BRACE_R),loc:this.loc(o)}},t.parseObjectField=function(n){var i=this._lexer.token,o=this.parseName();return this.expectToken(De.TokenKind.COLON),{kind:$e.Kind.OBJECT_FIELD,name:o,value:this.parseValueLiteral(n),loc:this.loc(i)}},t.parseDirectives=function(n){for(var i=[];this.peek(De.TokenKind.AT);)i.push(this.parseDirective(n));return i},t.parseDirective=function(n){var i=this._lexer.token;return this.expectToken(De.TokenKind.AT),{kind:$e.Kind.DIRECTIVE,name:this.parseName(),arguments:this.parseArguments(n),loc:this.loc(i)}},t.parseTypeReference=function(){var n=this._lexer.token,i;return this.expectOptionalToken(De.TokenKind.BRACKET_L)?(i=this.parseTypeReference(),this.expectToken(De.TokenKind.BRACKET_R),i={kind:$e.Kind.LIST_TYPE,type:i,loc:this.loc(n)}):i=this.parseNamedType(),this.expectOptionalToken(De.TokenKind.BANG)?{kind:$e.Kind.NON_NULL_TYPE,type:i,loc:this.loc(n)}:i},t.parseNamedType=function(){var n=this._lexer.token;return{kind:$e.Kind.NAMED_TYPE,name:this.parseName(),loc:this.loc(n)}},t.parseTypeSystemDefinition=function(){var n=this.peekDescription()?this._lexer.lookahead():this._lexer.token;if(n.kind===De.TokenKind.NAME)switch(n.value){case"schema":return this.parseSchemaDefinition();case"scalar":return this.parseScalarTypeDefinition();case"type":return this.parseObjectTypeDefinition();case"interface":return this.parseInterfaceTypeDefinition();case"union":return this.parseUnionTypeDefinition();case"enum":return this.parseEnumTypeDefinition();case"input":return this.parseInputObjectTypeDefinition();case"directive":return this.parseDirectiveDefinition()}throw this.unexpected(n)},t.peekDescription=function(){return this.peek(De.TokenKind.STRING)||this.peek(De.TokenKind.BLOCK_STRING)},t.parseDescription=function(){if(this.peekDescription())return this.parseStringLiteral()},t.parseSchemaDefinition=function(){var n=this._lexer.token,i=this.parseDescription();this.expectKeyword("schema");var o=this.parseDirectives(!0),s=this.many(De.TokenKind.BRACE_L,this.parseOperationTypeDefinition,De.TokenKind.BRACE_R);return{kind:$e.Kind.SCHEMA_DEFINITION,description:i,directives:o,operationTypes:s,loc:this.loc(n)}},t.parseOperationTypeDefinition=function(){var n=this._lexer.token,i=this.parseOperationType();this.expectToken(De.TokenKind.COLON);var o=this.parseNamedType();return{kind:$e.Kind.OPERATION_TYPE_DEFINITION,operation:i,type:o,loc:this.loc(n)}},t.parseScalarTypeDefinition=function(){var n=this._lexer.token,i=this.parseDescription();this.expectKeyword("scalar");var o=this.parseName(),s=this.parseDirectives(!0);return{kind:$e.Kind.SCALAR_TYPE_DEFINITION,description:i,name:o,directives:s,loc:this.loc(n)}},t.parseObjectTypeDefinition=function(){var n=this._lexer.token,i=this.parseDescription();this.expectKeyword("type");var o=this.parseName(),s=this.parseImplementsInterfaces(),l=this.parseDirectives(!0),d=this.parseFieldsDefinition();return{kind:$e.Kind.OBJECT_TYPE_DEFINITION,description:i,name:o,interfaces:s,directives:l,fields:d,loc:this.loc(n)}},t.parseImplementsInterfaces=function(){var n;if(!this.expectOptionalKeyword("implements"))return[];if(((n=this._options)===null||n===void 0?void 0:n.allowLegacySDLImplementsInterfaces)===!0){var i=[];this.expectOptionalToken(De.TokenKind.AMP);do i.push(this.parseNamedType());while(this.expectOptionalToken(De.TokenKind.AMP)||this.peek(De.TokenKind.NAME));return i}return this.delimitedMany(De.TokenKind.AMP,this.parseNamedType)},t.parseFieldsDefinition=function(){var n;return((n=this._options)===null||n===void 0?void 0:n.allowLegacySDLEmptyFields)===!0&&this.peek(De.TokenKind.BRACE_L)&&this._lexer.lookahead().kind===De.TokenKind.BRACE_R?(this._lexer.advance(),this._lexer.advance(),[]):this.optionalMany(De.TokenKind.BRACE_L,this.parseFieldDefinition,De.TokenKind.BRACE_R)},t.parseFieldDefinition=function(){var n=this._lexer.token,i=this.parseDescription(),o=this.parseName(),s=this.parseArgumentDefs();this.expectToken(De.TokenKind.COLON);var l=this.parseTypeReference(),d=this.parseDirectives(!0);return{kind:$e.Kind.FIELD_DEFINITION,description:i,name:o,arguments:s,type:l,directives:d,loc:this.loc(n)}},t.parseArgumentDefs=function(){return this.optionalMany(De.TokenKind.PAREN_L,this.parseInputValueDef,De.TokenKind.PAREN_R)},t.parseInputValueDef=function(){var n=this._lexer.token,i=this.parseDescription(),o=this.parseName();this.expectToken(De.TokenKind.COLON);var s=this.parseTypeReference(),l;this.expectOptionalToken(De.TokenKind.EQUALS)&&(l=this.parseValueLiteral(!0));var d=this.parseDirectives(!0);return{kind:$e.Kind.INPUT_VALUE_DEFINITION,description:i,name:o,type:s,defaultValue:l,directives:d,loc:this.loc(n)}},t.parseInterfaceTypeDefinition=function(){var n=this._lexer.token,i=this.parseDescription();this.expectKeyword("interface");var o=this.parseName(),s=this.parseImplementsInterfaces(),l=this.parseDirectives(!0),d=this.parseFieldsDefinition();return{kind:$e.Kind.INTERFACE_TYPE_DEFINITION,description:i,name:o,interfaces:s,directives:l,fields:d,loc:this.loc(n)}},t.parseUnionTypeDefinition=function(){var n=this._lexer.token,i=this.parseDescription();this.expectKeyword("union");var o=this.parseName(),s=this.parseDirectives(!0),l=this.parseUnionMemberTypes();return{kind:$e.Kind.UNION_TYPE_DEFINITION,description:i,name:o,directives:s,types:l,loc:this.loc(n)}},t.parseUnionMemberTypes=function(){return this.expectOptionalToken(De.TokenKind.EQUALS)?this.delimitedMany(De.TokenKind.PIPE,this.parseNamedType):[]},t.parseEnumTypeDefinition=function(){var n=this._lexer.token,i=this.parseDescription();this.expectKeyword("enum");var o=this.parseName(),s=this.parseDirectives(!0),l=this.parseEnumValuesDefinition();return{kind:$e.Kind.ENUM_TYPE_DEFINITION,description:i,name:o,directives:s,values:l,loc:this.loc(n)}},t.parseEnumValuesDefinition=function(){return this.optionalMany(De.TokenKind.BRACE_L,this.parseEnumValueDefinition,De.TokenKind.BRACE_R)},t.parseEnumValueDefinition=function(){var n=this._lexer.token,i=this.parseDescription(),o=this.parseName(),s=this.parseDirectives(!0);return{kind:$e.Kind.ENUM_VALUE_DEFINITION,description:i,name:o,directives:s,loc:this.loc(n)}},t.parseInputObjectTypeDefinition=function(){var n=this._lexer.token,i=this.parseDescription();this.expectKeyword("input");var o=this.parseName(),s=this.parseDirectives(!0),l=this.parseInputFieldsDefinition();return{kind:$e.Kind.INPUT_OBJECT_TYPE_DEFINITION,description:i,name:o,directives:s,fields:l,loc:this.loc(n)}},t.parseInputFieldsDefinition=function(){return this.optionalMany(De.TokenKind.BRACE_L,this.parseInputValueDef,De.TokenKind.BRACE_R)},t.parseTypeSystemExtension=function(){var n=this._lexer.lookahead();if(n.kind===De.TokenKind.NAME)switch(n.value){case"schema":return this.parseSchemaExtension();case"scalar":return this.parseScalarTypeExtension();case"type":return this.parseObjectTypeExtension();case"interface":return this.parseInterfaceTypeExtension();case"union":return this.parseUnionTypeExtension();case"enum":return this.parseEnumTypeExtension();case"input":return this.parseInputObjectTypeExtension()}throw this.unexpected(n)},t.parseSchemaExtension=function(){var n=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("schema");var i=this.parseDirectives(!0),o=this.optionalMany(De.TokenKind.BRACE_L,this.parseOperationTypeDefinition,De.TokenKind.BRACE_R);if(i.length===0&&o.length===0)throw this.unexpected();return{kind:$e.Kind.SCHEMA_EXTENSION,directives:i,operationTypes:o,loc:this.loc(n)}},t.parseScalarTypeExtension=function(){var n=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("scalar");var i=this.parseName(),o=this.parseDirectives(!0);if(o.length===0)throw this.unexpected();return{kind:$e.Kind.SCALAR_TYPE_EXTENSION,name:i,directives:o,loc:this.loc(n)}},t.parseObjectTypeExtension=function(){var n=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("type");var i=this.parseName(),o=this.parseImplementsInterfaces(),s=this.parseDirectives(!0),l=this.parseFieldsDefinition();if(o.length===0&&s.length===0&&l.length===0)throw this.unexpected();return{kind:$e.Kind.OBJECT_TYPE_EXTENSION,name:i,interfaces:o,directives:s,fields:l,loc:this.loc(n)}},t.parseInterfaceTypeExtension=function(){var n=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("interface");var i=this.parseName(),o=this.parseImplementsInterfaces(),s=this.parseDirectives(!0),l=this.parseFieldsDefinition();if(o.length===0&&s.length===0&&l.length===0)throw this.unexpected();return{kind:$e.Kind.INTERFACE_TYPE_EXTENSION,name:i,interfaces:o,directives:s,fields:l,loc:this.loc(n)}},t.parseUnionTypeExtension=function(){var n=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("union");var i=this.parseName(),o=this.parseDirectives(!0),s=this.parseUnionMemberTypes();if(o.length===0&&s.length===0)throw this.unexpected();return{kind:$e.Kind.UNION_TYPE_EXTENSION,name:i,directives:o,types:s,loc:this.loc(n)}},t.parseEnumTypeExtension=function(){var n=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("enum");var i=this.parseName(),o=this.parseDirectives(!0),s=this.parseEnumValuesDefinition();if(o.length===0&&s.length===0)throw this.unexpected();return{kind:$e.Kind.ENUM_TYPE_EXTENSION,name:i,directives:o,values:s,loc:this.loc(n)}},t.parseInputObjectTypeExtension=function(){var n=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("input");var i=this.parseName(),o=this.parseDirectives(!0),s=this.parseInputFieldsDefinition();if(o.length===0&&s.length===0)throw this.unexpected();return{kind:$e.Kind.INPUT_OBJECT_TYPE_EXTENSION,name:i,directives:o,fields:s,loc:this.loc(n)}},t.parseDirectiveDefinition=function(){var n=this._lexer.token,i=this.parseDescription();this.expectKeyword("directive"),this.expectToken(De.TokenKind.AT);var o=this.parseName(),s=this.parseArgumentDefs(),l=this.expectOptionalKeyword("repeatable");this.expectKeyword("on");var d=this.parseDirectiveLocations();return{kind:$e.Kind.DIRECTIVE_DEFINITION,description:i,name:o,arguments:s,repeatable:l,locations:d,loc:this.loc(n)}},t.parseDirectiveLocations=function(){return this.delimitedMany(De.TokenKind.PIPE,this.parseDirectiveLocation)},t.parseDirectiveLocation=function(){var n=this._lexer.token,i=this.parseName();if(pz.DirectiveLocation[i.value]!==void 0)return i;throw this.unexpected(n)},t.loc=function(n){var i;if(((i=this._options)===null||i===void 0?void 0:i.noLocation)!==!0)return new dz.Location(n,this._lexer.lastToken,this._lexer.source)},t.peek=function(n){return this._lexer.token.kind===n},t.expectToken=function(n){var i=this._lexer.token;if(i.kind===n)return this._lexer.advance(),i;throw(0,T_.syntaxError)(this._lexer.source,i.start,"Expected ".concat(M1(n),", found ").concat(__(i),"."))},t.expectOptionalToken=function(n){var i=this._lexer.token;if(i.kind===n)return this._lexer.advance(),i},t.expectKeyword=function(n){var i=this._lexer.token;if(i.kind===De.TokenKind.NAME&&i.value===n)this._lexer.advance();else throw(0,T_.syntaxError)(this._lexer.source,i.start,'Expected "'.concat(n,'", found ').concat(__(i),"."))},t.expectOptionalKeyword=function(n){var i=this._lexer.token;return i.kind===De.TokenKind.NAME&&i.value===n?(this._lexer.advance(),!0):!1},t.unexpected=function(n){var i=n!=null?n:this._lexer.token;return(0,T_.syntaxError)(this._lexer.source,i.start,"Unexpected ".concat(__(i),"."))},t.any=function(n,i,o){this.expectToken(n);for(var s=[];!this.expectOptionalToken(o);)s.push(i.call(this));return s},t.optionalMany=function(n,i,o){if(this.expectOptionalToken(n)){var s=[];do s.push(i.call(this));while(!this.expectOptionalToken(o));return s}return[]},t.many=function(n,i,o){this.expectToken(n);var s=[];do s.push(i.call(this));while(!this.expectOptionalToken(o));return s},t.delimitedMany=function(n,i){this.expectOptionalToken(n);var o=[];do o.push(i.call(this));while(this.expectOptionalToken(n));return o},e}();gs.Parser=_g;function __(e){var t=e.value;return M1(e.kind)+(t!=null?' "'.concat(t,'"'):"")}function M1(e){return(0,F1.isPunctuatorTokenKind)(e)?'"'.concat(e,'"'):e}});var hu=G(_o=>{"use strict";Object.defineProperty(_o,"__esModule",{value:!0});_o.visit=bz;_o.visitInParallel=Tz;_o.getVisitFn=Eg;_o.BREAK=_o.QueryDocumentKeys=void 0;var mz=yz(jt()),q1=Xl();function yz(e){return e&&e.__esModule?e:{default:e}}var V1={Name:[],Document:["definitions"],OperationDefinition:["name","variableDefinitions","directives","selectionSet"],VariableDefinition:["variable","type","defaultValue","directives"],Variable:["name"],SelectionSet:["selections"],Field:["alias","name","arguments","directives","selectionSet"],Argument:["name","value"],FragmentSpread:["name","directives"],InlineFragment:["typeCondition","directives","selectionSet"],FragmentDefinition:["name","variableDefinitions","typeCondition","directives","selectionSet"],IntValue:[],FloatValue:[],StringValue:[],BooleanValue:[],NullValue:[],EnumValue:[],ListValue:["values"],ObjectValue:["fields"],ObjectField:["name","value"],Directive:["name","arguments"],NamedType:["name"],ListType:["type"],NonNullType:["type"],SchemaDefinition:["description","directives","operationTypes"],OperationTypeDefinition:["type"],ScalarTypeDefinition:["description","name","directives"],ObjectTypeDefinition:["description","name","interfaces","directives","fields"],FieldDefinition:["description","name","arguments","type","directives"],InputValueDefinition:["description","name","type","defaultValue","directives"],InterfaceTypeDefinition:["description","name","interfaces","directives","fields"],UnionTypeDefinition:["description","name","directives","types"],EnumTypeDefinition:["description","name","directives","values"],EnumValueDefinition:["description","name","directives"],InputObjectTypeDefinition:["description","name","directives","fields"],DirectiveDefinition:["description","name","arguments","locations"],SchemaExtension:["directives","operationTypes"],ScalarTypeExtension:["name","directives"],ObjectTypeExtension:["name","interfaces","directives","fields"],InterfaceTypeExtension:["name","interfaces","directives","fields"],UnionTypeExtension:["name","directives","types"],EnumTypeExtension:["name","directives","values"],InputObjectTypeExtension:["name","directives","fields"]};_o.QueryDocumentKeys=V1;var rc=Object.freeze({});_o.BREAK=rc;function bz(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:V1,n=void 0,i=Array.isArray(e),o=[e],s=-1,l=[],d=void 0,h=void 0,v=void 0,y=[],b=[],D=e;do{s++;var _=s===o.length,k=_&&l.length!==0;if(_){if(h=b.length===0?void 0:y[y.length-1],d=v,v=b.pop(),k){if(i)d=d.slice();else{for(var T={},S=0,m=Object.keys(d);S{"use strict";Object.defineProperty(Sg,"__esModule",{value:!0});Sg.default=void 0;var _z=Array.prototype.find?function(e,t){return Array.prototype.find.call(e,t)}:function(e,t){for(var r=0;r{"use strict";Object.defineProperty(kg,"__esModule",{value:!0});kg.default=void 0;var Sz=Object.values||function(e){return Object.keys(e).map(function(t){return e[t]})},kz=Sz;kg.default=kz});var qd=G(E_=>{"use strict";Object.defineProperty(E_,"__esModule",{value:!0});E_.locatedError=Dz;var Oz=Nz(jt()),wz=Je();function Nz(e){return e&&e.__esModule?e:{default:e}}function Dz(e,t,r){var n,i=e instanceof Error?e:new Error("Unexpected error value: "+(0,Oz.default)(e));return Array.isArray(i.path)?i:new wz.GraphQLError(i.message,(n=i.nodes)!==null&&n!==void 0?n:t,i.source,i.positions,r,i)}});var S_=G(Og=>{"use strict";Object.defineProperty(Og,"__esModule",{value:!0});Og.assertValidName=Iz;Og.isValidNameError=G1;var xz=Cz(Hi()),U1=Je();function Cz(e){return e&&e.__esModule?e:{default:e}}var Lz=/^[_a-zA-Z][_a-zA-Z0-9]*$/;function Iz(e){var t=G1(e);if(t)throw t;return e}function G1(e){if(typeof e=="string"||(0,xz.default)(0,"Expected name to be a string."),e.length>1&&e[0]==="_"&&e[1]==="_")return new U1.GraphQLError('Name "'.concat(e,'" must not begin with "__", which is reserved by GraphQL introspection.'));if(!Lz.test(e))return new U1.GraphQLError('Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "'.concat(e,'" does not.'))}});var ic=G(wg=>{"use strict";Object.defineProperty(wg,"__esModule",{value:!0});wg.default=void 0;var Az=Object.entries||function(e){return Object.keys(e).map(function(t){return[t,e[t]]})},Rz=Az;wg.default=Rz});var vu=G(k_=>{"use strict";Object.defineProperty(k_,"__esModule",{value:!0});k_.default=jz;function jz(e,t){return e.reduce(function(r,n){return r[t(n)]=n,r},Object.create(null))}});var w_=G(O_=>{"use strict";Object.defineProperty(O_,"__esModule",{value:!0});O_.default=Mz;var Pz=Fz(ic());function Fz(e){return e&&e.__esModule?e:{default:e}}function Mz(e,t){for(var r=Object.create(null),n=0,i=(0,Pz.default)(e);n{"use strict";Object.defineProperty(N_,"__esModule",{value:!0});N_.default=Uz;var qz=Vz(ic());function Vz(e){return e&&e.__esModule?e:{default:e}}function Uz(e){if(Object.getPrototypeOf(e)===null)return e;for(var t=Object.create(null),r=0,n=(0,qz.default)(e);r{"use strict";Object.defineProperty(D_,"__esModule",{value:!0});D_.default=Gz;function Gz(e,t,r){return e.reduce(function(n,i){return n[t(i)]=r(i),n},Object.create(null))}});var gu=G(x_=>{"use strict";Object.defineProperty(x_,"__esModule",{value:!0});x_.default=Bz;var Qz=5;function Bz(e,t){var r=typeof e=="string"?[e,t]:[void 0,e],n=r[0],i=r[1],o=" Did you mean ";n&&(o+=n+" ");var s=i.map(function(h){return'"'.concat(h,'"')});switch(s.length){case 0:return"";case 1:return o+s[0]+"?";case 2:return o+s[0]+" or "+s[1]+"?"}var l=s.slice(0,Qz),d=l.pop();return o+l.join(", ")+", or "+d+"?"}});var Q1=G(C_=>{"use strict";Object.defineProperty(C_,"__esModule",{value:!0});C_.default=Kz;function Kz(e){return e}});var Ud=G(I_=>{"use strict";Object.defineProperty(I_,"__esModule",{value:!0});I_.default=Hz;function Hz(e,t){for(var r=0,n=0;r0);var l=0;do++n,l=l*10+o-L_,o=t.charCodeAt(n);while(Dg(o)&&l>0);if(sl)return 1}else{if(io)return 1;++r,++n}}return e.length-t.length}var L_=48,zz=57;function Dg(e){return!isNaN(e)&&L_<=e&&e<=zz}});var mu=G(A_=>{"use strict";Object.defineProperty(A_,"__esModule",{value:!0});A_.default=Jz;var Wz=Yz(Ud());function Yz(e){return e&&e.__esModule?e:{default:e}}function Jz(e,t){for(var r=Object.create(null),n=new Xz(e),i=Math.floor(e.length*.4)+1,o=0;oi)){for(var y=this._rows,b=0;b<=v;b++)y[0][b]=b;for(var D=1;D<=h;D++){for(var _=y[(D-1)%3],k=y[D%3],T=k[0]=D,S=1;S<=v;S++){var m=s[D-1]===l[S-1]?0:1,w=Math.min(_[S]+1,k[S-1]+1,_[S-1]+m);if(D>1&&S>1&&s[D-1]===l[S-2]&&s[D-2]===l[S-1]){var x=y[(D-2)%3][S-2];w=Math.min(w,x+1)}wi)return}var L=y[h%3][v];return L<=i?L:void 0}},e}();function B1(e){for(var t=e.length,r=new Array(t),n=0;n{"use strict";Object.defineProperty(R_,"__esModule",{value:!0});R_.print=eW;var Zz=hu(),$z=ec();function eW(e){return(0,Zz.visit)(e,{leave:rW})}var tW=80,rW={Name:function(t){return t.value},Variable:function(t){return"$"+t.name},Document:function(t){return je(t.definitions,`
-
-`)+`
-`},OperationDefinition:function(t){var r=t.operation,n=t.name,i=yr("(",je(t.variableDefinitions,", "),")"),o=je(t.directives," "),s=t.selectionSet;return!n&&!o&&!i&&r==="query"?s:je([r,je([n,i]),o,s]," ")},VariableDefinition:function(t){var r=t.variable,n=t.type,i=t.defaultValue,o=t.directives;return r+": "+n+yr(" = ",i)+yr(" ",je(o," "))},SelectionSet:function(t){var r=t.selections;return ya(r)},Field:function(t){var r=t.alias,n=t.name,i=t.arguments,o=t.directives,s=t.selectionSet,l=yr("",r,": ")+n,d=l+yr("(",je(i,", "),")");return d.length>tW&&(d=l+yr(`(
-`,xg(je(i,`
-`)),`
-)`)),je([d,je(o," "),s]," ")},Argument:function(t){var r=t.name,n=t.value;return r+": "+n},FragmentSpread:function(t){var r=t.name,n=t.directives;return"..."+r+yr(" ",je(n," "))},InlineFragment:function(t){var r=t.typeCondition,n=t.directives,i=t.selectionSet;return je(["...",yr("on ",r),je(n," "),i]," ")},FragmentDefinition:function(t){var r=t.name,n=t.typeCondition,i=t.variableDefinitions,o=t.directives,s=t.selectionSet;return"fragment ".concat(r).concat(yr("(",je(i,", "),")")," ")+"on ".concat(n," ").concat(yr("",je(o," ")," "))+s},IntValue:function(t){var r=t.value;return r},FloatValue:function(t){var r=t.value;return r},StringValue:function(t,r){var n=t.value,i=t.block;return i?(0,$z.printBlockString)(n,r==="description"?"":" "):JSON.stringify(n)},BooleanValue:function(t){var r=t.value;return r?"true":"false"},NullValue:function(){return"null"},EnumValue:function(t){var r=t.value;return r},ListValue:function(t){var r=t.values;return"["+je(r,", ")+"]"},ObjectValue:function(t){var r=t.fields;return"{"+je(r,", ")+"}"},ObjectField:function(t){var r=t.name,n=t.value;return r+": "+n},Directive:function(t){var r=t.name,n=t.arguments;return"@"+r+yr("(",je(n,", "),")")},NamedType:function(t){var r=t.name;return r},ListType:function(t){var r=t.type;return"["+r+"]"},NonNullType:function(t){var r=t.type;return r+"!"},SchemaDefinition:ma(function(e){var t=e.directives,r=e.operationTypes;return je(["schema",je(t," "),ya(r)]," ")}),OperationTypeDefinition:function(t){var r=t.operation,n=t.type;return r+": "+n},ScalarTypeDefinition:ma(function(e){var t=e.name,r=e.directives;return je(["scalar",t,je(r," ")]," ")}),ObjectTypeDefinition:ma(function(e){var t=e.name,r=e.interfaces,n=e.directives,i=e.fields;return je(["type",t,yr("implements ",je(r," & ")),je(n," "),ya(i)]," ")}),FieldDefinition:ma(function(e){var t=e.name,r=e.arguments,n=e.type,i=e.directives;return t+(K1(r)?yr(`(
-`,xg(je(r,`
-`)),`
-)`):yr("(",je(r,", "),")"))+": "+n+yr(" ",je(i," "))}),InputValueDefinition:ma(function(e){var t=e.name,r=e.type,n=e.defaultValue,i=e.directives;return je([t+": "+r,yr("= ",n),je(i," ")]," ")}),InterfaceTypeDefinition:ma(function(e){var t=e.name,r=e.interfaces,n=e.directives,i=e.fields;return je(["interface",t,yr("implements ",je(r," & ")),je(n," "),ya(i)]," ")}),UnionTypeDefinition:ma(function(e){var t=e.name,r=e.directives,n=e.types;return je(["union",t,je(r," "),n&&n.length!==0?"= "+je(n," | "):""]," ")}),EnumTypeDefinition:ma(function(e){var t=e.name,r=e.directives,n=e.values;return je(["enum",t,je(r," "),ya(n)]," ")}),EnumValueDefinition:ma(function(e){var t=e.name,r=e.directives;return je([t,je(r," ")]," ")}),InputObjectTypeDefinition:ma(function(e){var t=e.name,r=e.directives,n=e.fields;return je(["input",t,je(r," "),ya(n)]," ")}),DirectiveDefinition:ma(function(e){var t=e.name,r=e.arguments,n=e.repeatable,i=e.locations;return"directive @"+t+(K1(r)?yr(`(
-`,xg(je(r,`
-`)),`
-)`):yr("(",je(r,", "),")"))+(n?" repeatable":"")+" on "+je(i," | ")}),SchemaExtension:function(t){var r=t.directives,n=t.operationTypes;return je(["extend schema",je(r," "),ya(n)]," ")},ScalarTypeExtension:function(t){var r=t.name,n=t.directives;return je(["extend scalar",r,je(n," ")]," ")},ObjectTypeExtension:function(t){var r=t.name,n=t.interfaces,i=t.directives,o=t.fields;return je(["extend type",r,yr("implements ",je(n," & ")),je(i," "),ya(o)]," ")},InterfaceTypeExtension:function(t){var r=t.name,n=t.interfaces,i=t.directives,o=t.fields;return je(["extend interface",r,yr("implements ",je(n," & ")),je(i," "),ya(o)]," ")},UnionTypeExtension:function(t){var r=t.name,n=t.directives,i=t.types;return je(["extend union",r,je(n," "),i&&i.length!==0?"= "+je(i," | "):""]," ")},EnumTypeExtension:function(t){var r=t.name,n=t.directives,i=t.values;return je(["extend enum",r,je(n," "),ya(i)]," ")},InputObjectTypeExtension:function(t){var r=t.name,n=t.directives,i=t.fields;return je(["extend input",r,je(n," "),ya(i)]," ")}};function ma(e){return function(t){return je([t.description,e(t)],`
-`)}}function je(e){var t,r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"";return(t=e==null?void 0:e.filter(function(n){return n}).join(r))!==null&&t!==void 0?t:""}function ya(e){return yr(`{
-`,xg(je(e,`
-`)),`
-}`)}function yr(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:"";return t!=null&&t!==""?e+t+r:""}function xg(e){return yr(" ",e.replace(/\n/g,`
- `))}function nW(e){return e.indexOf(`
-`)!==-1}function K1(e){return e!=null&&e.some(nW)}});var M_=G(F_=>{"use strict";Object.defineProperty(F_,"__esModule",{value:!0});F_.valueFromASTUntyped=P_;var iW=j_(jt()),aW=j_(_n()),oW=j_(Vd()),Eo=Jt();function j_(e){return e&&e.__esModule?e:{default:e}}function P_(e,t){switch(e.kind){case Eo.Kind.NULL:return null;case Eo.Kind.INT:return parseInt(e.value,10);case Eo.Kind.FLOAT:return parseFloat(e.value);case Eo.Kind.STRING:case Eo.Kind.ENUM:case Eo.Kind.BOOLEAN:return e.value;case Eo.Kind.LIST:return e.values.map(function(r){return P_(r,t)});case Eo.Kind.OBJECT:return(0,oW.default)(e.fields,function(r){return r.name.value},function(r){return P_(r.value,t)});case Eo.Kind.VARIABLE:return t==null?void 0:t[e.name.value]}(0,aW.default)(0,"Unexpected value node: "+(0,iW.default)(e))}});var bt=G(Be=>{"use strict";Object.defineProperty(Be,"__esModule",{value:!0});Be.isType=q_;Be.assertType=X1;Be.isScalarType=ms;Be.assertScalarType=pW;Be.isObjectType=oc;Be.assertObjectType=hW;Be.isInterfaceType=ys;Be.assertInterfaceType=vW;Be.isUnionType=bs;Be.assertUnionType=gW;Be.isEnumType=Ts;Be.assertEnumType=mW;Be.isInputObjectType=Qd;Be.assertInputObjectType=yW;Be.isListType=Lg;Be.assertListType=bW;Be.isNonNullType=_u;Be.assertNonNullType=TW;Be.isInputType=V_;Be.assertInputType=_W;Be.isOutputType=U_;Be.assertOutputType=EW;Be.isLeafType=Z1;Be.assertLeafType=SW;Be.isCompositeType=$1;Be.assertCompositeType=kW;Be.isAbstractType=eI;Be.assertAbstractType=OW;Be.GraphQLList=Eu;Be.GraphQLNonNull=Su;Be.isWrappingType=Bd;Be.assertWrappingType=wW;Be.isNullableType=tI;Be.assertNullableType=rI;Be.getNullableType=NW;Be.isNamedType=nI;Be.assertNamedType=DW;Be.getNamedType=xW;Be.argsToArgsConfig=uI;Be.isRequiredArgument=CW;Be.isRequiredInputField=RW;Be.GraphQLInputObjectType=Be.GraphQLEnumType=Be.GraphQLUnionType=Be.GraphQLInterfaceType=Be.GraphQLObjectType=Be.GraphQLScalarType=void 0;var H1=Di(ic()),yu=qa(),ur=Di(jt()),uW=Di(vu()),Cg=Di(w_()),Ua=Di(Ng()),fr=Di(Hi()),z1=Di(Vd()),bu=Di(jd()),sW=Di(gu()),lW=Di(Ma()),W1=Di(Q1()),Tu=Di(dg()),cW=Di(mu()),Gd=Je(),fW=Jt(),Y1=hi(),dW=M_();function Di(e){return e&&e.__esModule?e:{default:e}}function J1(e,t){for(var r=0;r0?e:void 0}var G_=function(){function e(r){var n,i,o,s=(n=r.parseValue)!==null&&n!==void 0?n:W1.default;this.name=r.name,this.description=r.description,this.specifiedByUrl=r.specifiedByUrl,this.serialize=(i=r.serialize)!==null&&i!==void 0?i:W1.default,this.parseValue=s,this.parseLiteral=(o=r.parseLiteral)!==null&&o!==void 0?o:function(l,d){return s((0,dW.valueFromASTUntyped)(l,d))},this.extensions=r.extensions&&(0,Ua.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=uc(r.extensionASTNodes),typeof r.name=="string"||(0,fr.default)(0,"Must provide name."),r.specifiedByUrl==null||typeof r.specifiedByUrl=="string"||(0,fr.default)(0,"".concat(this.name,' must provide "specifiedByUrl" as a string, ')+"but got: ".concat((0,ur.default)(r.specifiedByUrl),".")),r.serialize==null||typeof r.serialize=="function"||(0,fr.default)(0,"".concat(this.name,' must provide "serialize" function. If this custom Scalar is also used as an input type, ensure "parseValue" and "parseLiteral" functions are also provided.')),r.parseLiteral&&(typeof r.parseValue=="function"&&typeof r.parseLiteral=="function"||(0,fr.default)(0,"".concat(this.name,' must provide both "parseValue" and "parseLiteral" functions.')))}var t=e.prototype;return t.toConfig=function(){var n;return{name:this.name,description:this.description,specifiedByUrl:this.specifiedByUrl,serialize:this.serialize,parseValue:this.parseValue,parseLiteral:this.parseLiteral,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:(n=this.extensionASTNodes)!==null&&n!==void 0?n:[]}},t.toString=function(){return this.name},t.toJSON=function(){return this.toString()},ac(e,[{key:yu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLScalarType"}}]),e}();Be.GraphQLScalarType=G_;(0,Tu.default)(G_);var Q_=function(){function e(r){this.name=r.name,this.description=r.description,this.isTypeOf=r.isTypeOf,this.extensions=r.extensions&&(0,Ua.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=uc(r.extensionASTNodes),this._fields=aI.bind(void 0,r),this._interfaces=iI.bind(void 0,r),typeof r.name=="string"||(0,fr.default)(0,"Must provide name."),r.isTypeOf==null||typeof r.isTypeOf=="function"||(0,fr.default)(0,"".concat(this.name,' must provide "isTypeOf" as a function, ')+"but got: ".concat((0,ur.default)(r.isTypeOf),"."))}var t=e.prototype;return t.getFields=function(){return typeof this._fields=="function"&&(this._fields=this._fields()),this._fields},t.getInterfaces=function(){return typeof this._interfaces=="function"&&(this._interfaces=this._interfaces()),this._interfaces},t.toConfig=function(){return{name:this.name,description:this.description,interfaces:this.getInterfaces(),fields:oI(this.getFields()),isTypeOf:this.isTypeOf,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:this.extensionASTNodes||[]}},t.toString=function(){return this.name},t.toJSON=function(){return this.toString()},ac(e,[{key:yu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLObjectType"}}]),e}();Be.GraphQLObjectType=Q_;(0,Tu.default)(Q_);function iI(e){var t,r=(t=Ig(e.interfaces))!==null&&t!==void 0?t:[];return Array.isArray(r)||(0,fr.default)(0,"".concat(e.name," interfaces must be an Array or a function which returns an Array.")),r}function aI(e){var t=Ig(e.fields);return sc(t)||(0,fr.default)(0,"".concat(e.name," fields must be an object with field names as keys or a function which returns such an object.")),(0,Cg.default)(t,function(r,n){var i;sc(r)||(0,fr.default)(0,"".concat(e.name,".").concat(n," field config must be an object.")),!("isDeprecated"in r)||(0,fr.default)(0,"".concat(e.name,".").concat(n,' should provide "deprecationReason" instead of "isDeprecated".')),r.resolve==null||typeof r.resolve=="function"||(0,fr.default)(0,"".concat(e.name,".").concat(n," field resolver must be a function if ")+"provided, but got: ".concat((0,ur.default)(r.resolve),"."));var o=(i=r.args)!==null&&i!==void 0?i:{};sc(o)||(0,fr.default)(0,"".concat(e.name,".").concat(n," args must be an object with argument names as keys."));var s=(0,H1.default)(o).map(function(l){var d=l[0],h=l[1];return{name:d,description:h.description,type:h.type,defaultValue:h.defaultValue,deprecationReason:h.deprecationReason,extensions:h.extensions&&(0,Ua.default)(h.extensions),astNode:h.astNode}});return{name:n,description:r.description,type:r.type,args:s,resolve:r.resolve,subscribe:r.subscribe,isDeprecated:r.deprecationReason!=null,deprecationReason:r.deprecationReason,extensions:r.extensions&&(0,Ua.default)(r.extensions),astNode:r.astNode}})}function sc(e){return(0,lW.default)(e)&&!Array.isArray(e)}function oI(e){return(0,Cg.default)(e,function(t){return{description:t.description,type:t.type,args:uI(t.args),resolve:t.resolve,subscribe:t.subscribe,deprecationReason:t.deprecationReason,extensions:t.extensions,astNode:t.astNode}})}function uI(e){return(0,z1.default)(e,function(t){return t.name},function(t){return{description:t.description,type:t.type,defaultValue:t.defaultValue,deprecationReason:t.deprecationReason,extensions:t.extensions,astNode:t.astNode}})}function CW(e){return _u(e.type)&&e.defaultValue===void 0}var B_=function(){function e(r){this.name=r.name,this.description=r.description,this.resolveType=r.resolveType,this.extensions=r.extensions&&(0,Ua.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=uc(r.extensionASTNodes),this._fields=aI.bind(void 0,r),this._interfaces=iI.bind(void 0,r),typeof r.name=="string"||(0,fr.default)(0,"Must provide name."),r.resolveType==null||typeof r.resolveType=="function"||(0,fr.default)(0,"".concat(this.name,' must provide "resolveType" as a function, ')+"but got: ".concat((0,ur.default)(r.resolveType),"."))}var t=e.prototype;return t.getFields=function(){return typeof this._fields=="function"&&(this._fields=this._fields()),this._fields},t.getInterfaces=function(){return typeof this._interfaces=="function"&&(this._interfaces=this._interfaces()),this._interfaces},t.toConfig=function(){var n;return{name:this.name,description:this.description,interfaces:this.getInterfaces(),fields:oI(this.getFields()),resolveType:this.resolveType,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:(n=this.extensionASTNodes)!==null&&n!==void 0?n:[]}},t.toString=function(){return this.name},t.toJSON=function(){return this.toString()},ac(e,[{key:yu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLInterfaceType"}}]),e}();Be.GraphQLInterfaceType=B_;(0,Tu.default)(B_);var K_=function(){function e(r){this.name=r.name,this.description=r.description,this.resolveType=r.resolveType,this.extensions=r.extensions&&(0,Ua.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=uc(r.extensionASTNodes),this._types=LW.bind(void 0,r),typeof r.name=="string"||(0,fr.default)(0,"Must provide name."),r.resolveType==null||typeof r.resolveType=="function"||(0,fr.default)(0,"".concat(this.name,' must provide "resolveType" as a function, ')+"but got: ".concat((0,ur.default)(r.resolveType),"."))}var t=e.prototype;return t.getTypes=function(){return typeof this._types=="function"&&(this._types=this._types()),this._types},t.toConfig=function(){var n;return{name:this.name,description:this.description,types:this.getTypes(),resolveType:this.resolveType,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:(n=this.extensionASTNodes)!==null&&n!==void 0?n:[]}},t.toString=function(){return this.name},t.toJSON=function(){return this.toString()},ac(e,[{key:yu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLUnionType"}}]),e}();Be.GraphQLUnionType=K_;(0,Tu.default)(K_);function LW(e){var t=Ig(e.types);return Array.isArray(t)||(0,fr.default)(0,"Must provide Array of types or a function which returns such an array for Union ".concat(e.name,".")),t}var H_=function(){function e(r){this.name=r.name,this.description=r.description,this.extensions=r.extensions&&(0,Ua.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=uc(r.extensionASTNodes),this._values=IW(this.name,r.values),this._valueLookup=new Map(this._values.map(function(n){return[n.value,n]})),this._nameLookup=(0,uW.default)(this._values,function(n){return n.name}),typeof r.name=="string"||(0,fr.default)(0,"Must provide name.")}var t=e.prototype;return t.getValues=function(){return this._values},t.getValue=function(n){return this._nameLookup[n]},t.serialize=function(n){var i=this._valueLookup.get(n);if(i===void 0)throw new Gd.GraphQLError('Enum "'.concat(this.name,'" cannot represent value: ').concat((0,ur.default)(n)));return i.name},t.parseValue=function(n){if(typeof n!="string"){var i=(0,ur.default)(n);throw new Gd.GraphQLError('Enum "'.concat(this.name,'" cannot represent non-string value: ').concat(i,".")+Ag(this,i))}var o=this.getValue(n);if(o==null)throw new Gd.GraphQLError('Value "'.concat(n,'" does not exist in "').concat(this.name,'" enum.')+Ag(this,n));return o.value},t.parseLiteral=function(n,i){if(n.kind!==fW.Kind.ENUM){var o=(0,Y1.print)(n);throw new Gd.GraphQLError('Enum "'.concat(this.name,'" cannot represent non-enum value: ').concat(o,".")+Ag(this,o),n)}var s=this.getValue(n.value);if(s==null){var l=(0,Y1.print)(n);throw new Gd.GraphQLError('Value "'.concat(l,'" does not exist in "').concat(this.name,'" enum.')+Ag(this,l),n)}return s.value},t.toConfig=function(){var n,i=(0,z1.default)(this.getValues(),function(o){return o.name},function(o){return{description:o.description,value:o.value,deprecationReason:o.deprecationReason,extensions:o.extensions,astNode:o.astNode}});return{name:this.name,description:this.description,values:i,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:(n=this.extensionASTNodes)!==null&&n!==void 0?n:[]}},t.toString=function(){return this.name},t.toJSON=function(){return this.toString()},ac(e,[{key:yu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLEnumType"}}]),e}();Be.GraphQLEnumType=H_;(0,Tu.default)(H_);function Ag(e,t){var r=e.getValues().map(function(i){return i.name}),n=(0,cW.default)(t,r);return(0,sW.default)("the enum value",n)}function IW(e,t){return sc(t)||(0,fr.default)(0,"".concat(e," values must be an object with value names as keys.")),(0,H1.default)(t).map(function(r){var n=r[0],i=r[1];return sc(i)||(0,fr.default)(0,"".concat(e,".").concat(n,' must refer to an object with a "value" key ')+"representing an internal value but got: ".concat((0,ur.default)(i),".")),!("isDeprecated"in i)||(0,fr.default)(0,"".concat(e,".").concat(n,' should provide "deprecationReason" instead of "isDeprecated".')),{name:n,description:i.description,value:i.value!==void 0?i.value:n,isDeprecated:i.deprecationReason!=null,deprecationReason:i.deprecationReason,extensions:i.extensions&&(0,Ua.default)(i.extensions),astNode:i.astNode}})}var z_=function(){function e(r){this.name=r.name,this.description=r.description,this.extensions=r.extensions&&(0,Ua.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=uc(r.extensionASTNodes),this._fields=AW.bind(void 0,r),typeof r.name=="string"||(0,fr.default)(0,"Must provide name.")}var t=e.prototype;return t.getFields=function(){return typeof this._fields=="function"&&(this._fields=this._fields()),this._fields},t.toConfig=function(){var n,i=(0,Cg.default)(this.getFields(),function(o){return{description:o.description,type:o.type,defaultValue:o.defaultValue,extensions:o.extensions,astNode:o.astNode}});return{name:this.name,description:this.description,fields:i,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:(n=this.extensionASTNodes)!==null&&n!==void 0?n:[]}},t.toString=function(){return this.name},t.toJSON=function(){return this.toString()},ac(e,[{key:yu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLInputObjectType"}}]),e}();Be.GraphQLInputObjectType=z_;(0,Tu.default)(z_);function AW(e){var t=Ig(e.fields);return sc(t)||(0,fr.default)(0,"".concat(e.name," fields must be an object with field names as keys or a function which returns such an object.")),(0,Cg.default)(t,function(r,n){return!("resolve"in r)||(0,fr.default)(0,"".concat(e.name,".").concat(n," field has a resolve property, but Input Types cannot define resolvers.")),{name:n,description:r.description,type:r.type,defaultValue:r.defaultValue,deprecationReason:r.deprecationReason,extensions:r.extensions&&(0,Ua.default)(r.extensions),astNode:r.astNode}})}function RW(e){return _u(e.type)&&e.defaultValue===void 0}});var Hd=G(Kd=>{"use strict";Object.defineProperty(Kd,"__esModule",{value:!0});Kd.isEqualType=W_;Kd.isTypeSubTypeOf=Rg;Kd.doTypesOverlap=jW;var Mn=bt();function W_(e,t){return e===t?!0:(0,Mn.isNonNullType)(e)&&(0,Mn.isNonNullType)(t)||(0,Mn.isListType)(e)&&(0,Mn.isListType)(t)?W_(e.ofType,t.ofType):!1}function Rg(e,t,r){return t===r?!0:(0,Mn.isNonNullType)(r)?(0,Mn.isNonNullType)(t)?Rg(e,t.ofType,r.ofType):!1:(0,Mn.isNonNullType)(t)?Rg(e,t.ofType,r):(0,Mn.isListType)(r)?(0,Mn.isListType)(t)?Rg(e,t.ofType,r.ofType):!1:(0,Mn.isListType)(t)?!1:(0,Mn.isAbstractType)(r)&&((0,Mn.isInterfaceType)(t)||(0,Mn.isObjectType)(t))&&e.isSubType(r,t)}function jW(e,t,r){return t===r?!0:(0,Mn.isAbstractType)(t)?(0,Mn.isAbstractType)(r)?e.getPossibleTypes(t).some(function(n){return e.isSubType(r,n)}):e.isSubType(t,r):(0,Mn.isAbstractType)(r)?e.isSubType(r,t):!1}});var Y_=G(jg=>{"use strict";Object.defineProperty(jg,"__esModule",{value:!0});jg.default=void 0;var PW=qa(),FW=Array.from||function(e,t,r){if(e==null)throw new TypeError("Array.from requires an array-like object - not null or undefined");var n=e[PW.SYMBOL_ITERATOR];if(typeof n=="function"){for(var i=n.call(e),o=[],s,l=0;!(s=i.next()).done;++l)if(o.push(t.call(r,s.value,l)),l>9999999)throw new TypeError("Near-infinite iteration.");return o}var d=e.length;if(typeof d=="number"&&d>=0&&d%1==0){for(var h=[],v=0;v{"use strict";Object.defineProperty(Pg,"__esModule",{value:!0});Pg.default=void 0;var qW=Number.isFinite||function(e){return typeof e=="number"&&isFinite(e)},VW=qW;Pg.default=VW});var Mg=G(X_=>{"use strict";Object.defineProperty(X_,"__esModule",{value:!0});X_.default=GW;var UW=qa();function Fg(e){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Fg=function(r){return typeof r}:Fg=function(r){return r&&typeof Symbol=="function"&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},Fg(e)}function GW(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:function(v){return v};if(e==null||Fg(e)!=="object")return null;if(Array.isArray(e))return e.map(t);var r=e[UW.SYMBOL_ITERATOR];if(typeof r=="function"){for(var n=r.call(e),i=[],o,s=0;!(o=n.next()).done;++s)i.push(t(o.value,s));return i}var l=e.length;if(typeof l=="number"&&l>=0&&l%1==0){for(var d=[],h=0;h{"use strict";Object.defineProperty(qg,"__esModule",{value:!0});qg.default=void 0;var QW=Number.isInteger||function(e){return typeof e=="number"&&isFinite(e)&&Math.floor(e)===e},BW=QW;qg.default=BW});var Ga=G(ti=>{"use strict";Object.defineProperty(ti,"__esModule",{value:!0});ti.isSpecifiedScalarType=t4;ti.specifiedScalarTypes=ti.GraphQLID=ti.GraphQLBoolean=ti.GraphQLString=ti.GraphQLFloat=ti.GraphQLInt=void 0;var Vg=Gg(J_()),Ug=Gg(sI()),ba=Gg(jt()),lI=Gg(Ma()),_s=Jt(),zd=hi(),cn=Je(),Wd=bt();function Gg(e){return e&&e.__esModule?e:{default:e}}var Z_=2147483647,$_=-2147483648;function KW(e){var t=Yd(e);if(typeof t=="boolean")return t?1:0;var r=t;if(typeof t=="string"&&t!==""&&(r=Number(t)),!(0,Ug.default)(r))throw new cn.GraphQLError("Int cannot represent non-integer value: ".concat((0,ba.default)(t)));if(r>Z_||r<$_)throw new cn.GraphQLError("Int cannot represent non 32-bit signed integer value: "+(0,ba.default)(t));return r}function HW(e){if(!(0,Ug.default)(e))throw new cn.GraphQLError("Int cannot represent non-integer value: ".concat((0,ba.default)(e)));if(e>Z_||e<$_)throw new cn.GraphQLError("Int cannot represent non 32-bit signed integer value: ".concat(e));return e}var cI=new Wd.GraphQLScalarType({name:"Int",description:"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.",serialize:KW,parseValue:HW,parseLiteral:function(t){if(t.kind!==_s.Kind.INT)throw new cn.GraphQLError("Int cannot represent non-integer value: ".concat((0,zd.print)(t)),t);var r=parseInt(t.value,10);if(r>Z_||r<$_)throw new cn.GraphQLError("Int cannot represent non 32-bit signed integer value: ".concat(t.value),t);return r}});ti.GraphQLInt=cI;function zW(e){var t=Yd(e);if(typeof t=="boolean")return t?1:0;var r=t;if(typeof t=="string"&&t!==""&&(r=Number(t)),!(0,Vg.default)(r))throw new cn.GraphQLError("Float cannot represent non numeric value: ".concat((0,ba.default)(t)));return r}function WW(e){if(!(0,Vg.default)(e))throw new cn.GraphQLError("Float cannot represent non numeric value: ".concat((0,ba.default)(e)));return e}var fI=new Wd.GraphQLScalarType({name:"Float",description:"The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).",serialize:zW,parseValue:WW,parseLiteral:function(t){if(t.kind!==_s.Kind.FLOAT&&t.kind!==_s.Kind.INT)throw new cn.GraphQLError("Float cannot represent non numeric value: ".concat((0,zd.print)(t)),t);return parseFloat(t.value)}});ti.GraphQLFloat=fI;function Yd(e){if((0,lI.default)(e)){if(typeof e.valueOf=="function"){var t=e.valueOf();if(!(0,lI.default)(t))return t}if(typeof e.toJSON=="function")return e.toJSON()}return e}function YW(e){var t=Yd(e);if(typeof t=="string")return t;if(typeof t=="boolean")return t?"true":"false";if((0,Vg.default)(t))return t.toString();throw new cn.GraphQLError("String cannot represent value: ".concat((0,ba.default)(e)))}function JW(e){if(typeof e!="string")throw new cn.GraphQLError("String cannot represent a non string value: ".concat((0,ba.default)(e)));return e}var dI=new Wd.GraphQLScalarType({name:"String",description:"The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",serialize:YW,parseValue:JW,parseLiteral:function(t){if(t.kind!==_s.Kind.STRING)throw new cn.GraphQLError("String cannot represent a non string value: ".concat((0,zd.print)(t)),t);return t.value}});ti.GraphQLString=dI;function XW(e){var t=Yd(e);if(typeof t=="boolean")return t;if((0,Vg.default)(t))return t!==0;throw new cn.GraphQLError("Boolean cannot represent a non boolean value: ".concat((0,ba.default)(t)))}function ZW(e){if(typeof e!="boolean")throw new cn.GraphQLError("Boolean cannot represent a non boolean value: ".concat((0,ba.default)(e)));return e}var pI=new Wd.GraphQLScalarType({name:"Boolean",description:"The `Boolean` scalar type represents `true` or `false`.",serialize:XW,parseValue:ZW,parseLiteral:function(t){if(t.kind!==_s.Kind.BOOLEAN)throw new cn.GraphQLError("Boolean cannot represent a non boolean value: ".concat((0,zd.print)(t)),t);return t.value}});ti.GraphQLBoolean=pI;function $W(e){var t=Yd(e);if(typeof t=="string")return t;if((0,Ug.default)(t))return String(t);throw new cn.GraphQLError("ID cannot represent value: ".concat((0,ba.default)(e)))}function e4(e){if(typeof e=="string")return e;if((0,Ug.default)(e))return e.toString();throw new cn.GraphQLError("ID cannot represent value: ".concat((0,ba.default)(e)))}var hI=new Wd.GraphQLScalarType({name:"ID",description:'The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.',serialize:$W,parseValue:e4,parseLiteral:function(t){if(t.kind!==_s.Kind.STRING&&t.kind!==_s.Kind.INT)throw new cn.GraphQLError("ID cannot represent a non-string and non-integer value: "+(0,zd.print)(t),t);return t.value}});ti.GraphQLID=hI;var vI=Object.freeze([dI,cI,fI,pI,hI]);ti.specifiedScalarTypes=vI;function t4(e){return vI.some(function(t){var r=t.name;return e.name===r})}});var Zd=G(eE=>{"use strict";Object.defineProperty(eE,"__esModule",{value:!0});eE.astFromValue=Xd;var r4=lc(J_()),n4=lc(Ni()),gI=lc(jt()),i4=lc(_n()),a4=lc(Ma()),o4=lc(Mg()),zi=Jt(),u4=Ga(),Jd=bt();function lc(e){return e&&e.__esModule?e:{default:e}}function Xd(e,t){if((0,Jd.isNonNullType)(t)){var r=Xd(e,t.ofType);return(r==null?void 0:r.kind)===zi.Kind.NULL?null:r}if(e===null)return{kind:zi.Kind.NULL};if(e===void 0)return null;if((0,Jd.isListType)(t)){var n=t.ofType,i=(0,o4.default)(e);if(i!=null){for(var o=[],s=0;s{"use strict";Object.defineProperty(Gt,"__esModule",{value:!0});Gt.isIntrospectionType=v4;Gt.introspectionTypes=Gt.TypeNameMetaFieldDef=Gt.TypeMetaFieldDef=Gt.SchemaMetaFieldDef=Gt.__TypeKind=Gt.TypeKind=Gt.__EnumValue=Gt.__InputValue=Gt.__Field=Gt.__Type=Gt.__DirectiveLocation=Gt.__Directive=Gt.__Schema=void 0;var tE=rE(Ni()),s4=rE(jt()),l4=rE(_n()),c4=hi(),Xr=$l(),f4=Zd(),$t=Ga(),Pe=bt();function rE(e){return e&&e.__esModule?e:{default:e}}var nE=new Pe.GraphQLObjectType({name:"__Schema",description:"A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.",fields:function(){return{description:{type:$t.GraphQLString,resolve:function(r){return r.description}},types:{description:"A list of all types supported by this server.",type:new Pe.GraphQLNonNull(new Pe.GraphQLList(new Pe.GraphQLNonNull(Wi))),resolve:function(r){return(0,tE.default)(r.getTypeMap())}},queryType:{description:"The type that query operations will be rooted at.",type:new Pe.GraphQLNonNull(Wi),resolve:function(r){return r.getQueryType()}},mutationType:{description:"If this server supports mutation, the type that mutation operations will be rooted at.",type:Wi,resolve:function(r){return r.getMutationType()}},subscriptionType:{description:"If this server support subscription, the type that subscription operations will be rooted at.",type:Wi,resolve:function(r){return r.getSubscriptionType()}},directives:{description:"A list of all directives supported by this server.",type:new Pe.GraphQLNonNull(new Pe.GraphQLList(new Pe.GraphQLNonNull(iE))),resolve:function(r){return r.getDirectives()}}}}});Gt.__Schema=nE;var iE=new Pe.GraphQLObjectType({name:"__Directive",description:`A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
-
-In some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.`,fields:function(){return{name:{type:new Pe.GraphQLNonNull($t.GraphQLString),resolve:function(r){return r.name}},description:{type:$t.GraphQLString,resolve:function(r){return r.description}},isRepeatable:{type:new Pe.GraphQLNonNull($t.GraphQLBoolean),resolve:function(r){return r.isRepeatable}},locations:{type:new Pe.GraphQLNonNull(new Pe.GraphQLList(new Pe.GraphQLNonNull(aE))),resolve:function(r){return r.locations}},args:{type:new Pe.GraphQLNonNull(new Pe.GraphQLList(new Pe.GraphQLNonNull($d))),resolve:function(r){return r.args}}}}});Gt.__Directive=iE;var aE=new Pe.GraphQLEnumType({name:"__DirectiveLocation",description:"A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.",values:{QUERY:{value:Xr.DirectiveLocation.QUERY,description:"Location adjacent to a query operation."},MUTATION:{value:Xr.DirectiveLocation.MUTATION,description:"Location adjacent to a mutation operation."},SUBSCRIPTION:{value:Xr.DirectiveLocation.SUBSCRIPTION,description:"Location adjacent to a subscription operation."},FIELD:{value:Xr.DirectiveLocation.FIELD,description:"Location adjacent to a field."},FRAGMENT_DEFINITION:{value:Xr.DirectiveLocation.FRAGMENT_DEFINITION,description:"Location adjacent to a fragment definition."},FRAGMENT_SPREAD:{value:Xr.DirectiveLocation.FRAGMENT_SPREAD,description:"Location adjacent to a fragment spread."},INLINE_FRAGMENT:{value:Xr.DirectiveLocation.INLINE_FRAGMENT,description:"Location adjacent to an inline fragment."},VARIABLE_DEFINITION:{value:Xr.DirectiveLocation.VARIABLE_DEFINITION,description:"Location adjacent to a variable definition."},SCHEMA:{value:Xr.DirectiveLocation.SCHEMA,description:"Location adjacent to a schema definition."},SCALAR:{value:Xr.DirectiveLocation.SCALAR,description:"Location adjacent to a scalar definition."},OBJECT:{value:Xr.DirectiveLocation.OBJECT,description:"Location adjacent to an object type definition."},FIELD_DEFINITION:{value:Xr.DirectiveLocation.FIELD_DEFINITION,description:"Location adjacent to a field definition."},ARGUMENT_DEFINITION:{value:Xr.DirectiveLocation.ARGUMENT_DEFINITION,description:"Location adjacent to an argument definition."},INTERFACE:{value:Xr.DirectiveLocation.INTERFACE,description:"Location adjacent to an interface definition."},UNION:{value:Xr.DirectiveLocation.UNION,description:"Location adjacent to a union definition."},ENUM:{value:Xr.DirectiveLocation.ENUM,description:"Location adjacent to an enum definition."},ENUM_VALUE:{value:Xr.DirectiveLocation.ENUM_VALUE,description:"Location adjacent to an enum value definition."},INPUT_OBJECT:{value:Xr.DirectiveLocation.INPUT_OBJECT,description:"Location adjacent to an input object type definition."},INPUT_FIELD_DEFINITION:{value:Xr.DirectiveLocation.INPUT_FIELD_DEFINITION,description:"Location adjacent to an input object field definition."}}});Gt.__DirectiveLocation=aE;var Wi=new Pe.GraphQLObjectType({name:"__Type",description:"The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByUrl`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.",fields:function(){return{kind:{type:new Pe.GraphQLNonNull(sE),resolve:function(r){if((0,Pe.isScalarType)(r))return En.SCALAR;if((0,Pe.isObjectType)(r))return En.OBJECT;if((0,Pe.isInterfaceType)(r))return En.INTERFACE;if((0,Pe.isUnionType)(r))return En.UNION;if((0,Pe.isEnumType)(r))return En.ENUM;if((0,Pe.isInputObjectType)(r))return En.INPUT_OBJECT;if((0,Pe.isListType)(r))return En.LIST;if((0,Pe.isNonNullType)(r))return En.NON_NULL;(0,l4.default)(0,'Unexpected type: "'.concat((0,s4.default)(r),'".'))}},name:{type:$t.GraphQLString,resolve:function(r){return r.name!==void 0?r.name:void 0}},description:{type:$t.GraphQLString,resolve:function(r){return r.description!==void 0?r.description:void 0}},specifiedByUrl:{type:$t.GraphQLString,resolve:function(r){return r.specifiedByUrl!==void 0?r.specifiedByUrl:void 0}},fields:{type:new Pe.GraphQLList(new Pe.GraphQLNonNull(oE)),args:{includeDeprecated:{type:$t.GraphQLBoolean,defaultValue:!1}},resolve:function(r,n){var i=n.includeDeprecated;if((0,Pe.isObjectType)(r)||(0,Pe.isInterfaceType)(r)){var o=(0,tE.default)(r.getFields());return i?o:o.filter(function(s){return s.deprecationReason==null})}}},interfaces:{type:new Pe.GraphQLList(new Pe.GraphQLNonNull(Wi)),resolve:function(r){if((0,Pe.isObjectType)(r)||(0,Pe.isInterfaceType)(r))return r.getInterfaces()}},possibleTypes:{type:new Pe.GraphQLList(new Pe.GraphQLNonNull(Wi)),resolve:function(r,n,i,o){var s=o.schema;if((0,Pe.isAbstractType)(r))return s.getPossibleTypes(r)}},enumValues:{type:new Pe.GraphQLList(new Pe.GraphQLNonNull(uE)),args:{includeDeprecated:{type:$t.GraphQLBoolean,defaultValue:!1}},resolve:function(r,n){var i=n.includeDeprecated;if((0,Pe.isEnumType)(r)){var o=r.getValues();return i?o:o.filter(function(s){return s.deprecationReason==null})}}},inputFields:{type:new Pe.GraphQLList(new Pe.GraphQLNonNull($d)),args:{includeDeprecated:{type:$t.GraphQLBoolean,defaultValue:!1}},resolve:function(r,n){var i=n.includeDeprecated;if((0,Pe.isInputObjectType)(r)){var o=(0,tE.default)(r.getFields());return i?o:o.filter(function(s){return s.deprecationReason==null})}}},ofType:{type:Wi,resolve:function(r){return r.ofType!==void 0?r.ofType:void 0}}}}});Gt.__Type=Wi;var oE=new Pe.GraphQLObjectType({name:"__Field",description:"Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.",fields:function(){return{name:{type:new Pe.GraphQLNonNull($t.GraphQLString),resolve:function(r){return r.name}},description:{type:$t.GraphQLString,resolve:function(r){return r.description}},args:{type:new Pe.GraphQLNonNull(new Pe.GraphQLList(new Pe.GraphQLNonNull($d))),args:{includeDeprecated:{type:$t.GraphQLBoolean,defaultValue:!1}},resolve:function(r,n){var i=n.includeDeprecated;return i?r.args:r.args.filter(function(o){return o.deprecationReason==null})}},type:{type:new Pe.GraphQLNonNull(Wi),resolve:function(r){return r.type}},isDeprecated:{type:new Pe.GraphQLNonNull($t.GraphQLBoolean),resolve:function(r){return r.deprecationReason!=null}},deprecationReason:{type:$t.GraphQLString,resolve:function(r){return r.deprecationReason}}}}});Gt.__Field=oE;var $d=new Pe.GraphQLObjectType({name:"__InputValue",description:"Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.",fields:function(){return{name:{type:new Pe.GraphQLNonNull($t.GraphQLString),resolve:function(r){return r.name}},description:{type:$t.GraphQLString,resolve:function(r){return r.description}},type:{type:new Pe.GraphQLNonNull(Wi),resolve:function(r){return r.type}},defaultValue:{type:$t.GraphQLString,description:"A GraphQL-formatted string representing the default value for this input value.",resolve:function(r){var n=r.type,i=r.defaultValue,o=(0,f4.astFromValue)(i,n);return o?(0,c4.print)(o):null}},isDeprecated:{type:new Pe.GraphQLNonNull($t.GraphQLBoolean),resolve:function(r){return r.deprecationReason!=null}},deprecationReason:{type:$t.GraphQLString,resolve:function(r){return r.deprecationReason}}}}});Gt.__InputValue=$d;var uE=new Pe.GraphQLObjectType({name:"__EnumValue",description:"One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.",fields:function(){return{name:{type:new Pe.GraphQLNonNull($t.GraphQLString),resolve:function(r){return r.name}},description:{type:$t.GraphQLString,resolve:function(r){return r.description}},isDeprecated:{type:new Pe.GraphQLNonNull($t.GraphQLBoolean),resolve:function(r){return r.deprecationReason!=null}},deprecationReason:{type:$t.GraphQLString,resolve:function(r){return r.deprecationReason}}}}});Gt.__EnumValue=uE;var En=Object.freeze({SCALAR:"SCALAR",OBJECT:"OBJECT",INTERFACE:"INTERFACE",UNION:"UNION",ENUM:"ENUM",INPUT_OBJECT:"INPUT_OBJECT",LIST:"LIST",NON_NULL:"NON_NULL"});Gt.TypeKind=En;var sE=new Pe.GraphQLEnumType({name:"__TypeKind",description:"An enum describing what kind of type a given `__Type` is.",values:{SCALAR:{value:En.SCALAR,description:"Indicates this type is a scalar."},OBJECT:{value:En.OBJECT,description:"Indicates this type is an object. `fields` and `interfaces` are valid fields."},INTERFACE:{value:En.INTERFACE,description:"Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields."},UNION:{value:En.UNION,description:"Indicates this type is a union. `possibleTypes` is a valid field."},ENUM:{value:En.ENUM,description:"Indicates this type is an enum. `enumValues` is a valid field."},INPUT_OBJECT:{value:En.INPUT_OBJECT,description:"Indicates this type is an input object. `inputFields` is a valid field."},LIST:{value:En.LIST,description:"Indicates this type is a list. `ofType` is a valid field."},NON_NULL:{value:En.NON_NULL,description:"Indicates this type is a non-null. `ofType` is a valid field."}}});Gt.__TypeKind=sE;var d4={name:"__schema",type:new Pe.GraphQLNonNull(nE),description:"Access the current type schema of this server.",args:[],resolve:function(t,r,n,i){var o=i.schema;return o},isDeprecated:!1,deprecationReason:void 0,extensions:void 0,astNode:void 0};Gt.SchemaMetaFieldDef=d4;var p4={name:"__type",type:Wi,description:"Request the type information of a single type.",args:[{name:"name",description:void 0,type:new Pe.GraphQLNonNull($t.GraphQLString),defaultValue:void 0,deprecationReason:void 0,extensions:void 0,astNode:void 0}],resolve:function(t,r,n,i){var o=r.name,s=i.schema;return s.getType(o)},isDeprecated:!1,deprecationReason:void 0,extensions:void 0,astNode:void 0};Gt.TypeMetaFieldDef=p4;var h4={name:"__typename",type:new Pe.GraphQLNonNull($t.GraphQLString),description:"The name of the current Object type at runtime.",args:[],resolve:function(t,r,n,i){var o=i.parentType;return o.name},isDeprecated:!1,deprecationReason:void 0,extensions:void 0,astNode:void 0};Gt.TypeNameMetaFieldDef=h4;var yI=Object.freeze([nE,iE,aE,Wi,oE,$d,uE,sE]);Gt.introspectionTypes=yI;function v4(e){return yI.some(function(t){var r=t.name;return e.name===r})}});var gi=G(Zr=>{"use strict";Object.defineProperty(Zr,"__esModule",{value:!0});Zr.isDirective=_I;Zr.assertDirective=S4;Zr.isSpecifiedDirective=k4;Zr.specifiedDirectives=Zr.GraphQLSpecifiedByDirective=Zr.GraphQLDeprecatedDirective=Zr.DEFAULT_DEPRECATION_REASON=Zr.GraphQLSkipDirective=Zr.GraphQLIncludeDirective=Zr.GraphQLDirective=void 0;var g4=Es(ic()),m4=qa(),y4=Es(jt()),bI=Es(Ng()),lE=Es(Hi()),b4=Es(jd()),T4=Es(Ma()),_4=Es(dg()),Ta=$l(),Qg=Ga(),Bg=bt();function Es(e){return e&&e.__esModule?e:{default:e}}function TI(e,t){for(var r=0;r{"use strict";Object.defineProperty(cc,"__esModule",{value:!0});cc.isSchema=CI;cc.assertSchema=A4;cc.GraphQLSchema=void 0;var O4=ku(nc()),w4=ku(Y_()),cE=ku(Ni()),N4=qa(),fE=ku(jt()),D4=ku(Ng()),Kg=ku(Hi()),x4=ku(jd()),C4=ku(Ma()),L4=vi(),DI=gi(),_a=bt();function ku(e){return e&&e.__esModule?e:{default:e}}function xI(e,t){for(var r=0;r{"use strict";Object.defineProperty(Hg,"__esModule",{value:!0});Hg.validateSchema=jI;Hg.assertValidSchema=q4;var II=dE(nc()),ep=dE(Ni()),qn=dE(jt()),R4=Je(),j4=qd(),P4=S_(),AI=Hd(),F4=ks(),M4=vi(),RI=gi(),Cr=bt();function dE(e){return e&&e.__esModule?e:{default:e}}function jI(e){if((0,F4.assertSchema)(e),e.__validationErrors)return e.__validationErrors;var t=new V4(e);U4(t),G4(t),Q4(t);var r=t.getErrors();return e.__validationErrors=r,r}function q4(e){var t=jI(e);if(t.length!==0)throw new Error(t.map(function(r){return r.message}).join(`
-
-`))}var V4=function(){function e(r){this._errors=[],this.schema=r}var t=e.prototype;return t.reportError=function(n,i){var o=Array.isArray(i)?i.filter(Boolean):i;this.addError(new R4.GraphQLError(n,o))},t.addError=function(n){this._errors.push(n)},t.getErrors=function(){return this._errors},e}();function U4(e){var t=e.schema,r=t.getQueryType();if(!r)e.reportError("Query root type must be provided.",t.astNode);else if(!(0,Cr.isObjectType)(r)){var n;e.reportError("Query root type must be Object type, it cannot be ".concat((0,qn.default)(r),"."),(n=pE(t,"query"))!==null&&n!==void 0?n:r.astNode)}var i=t.getMutationType();if(i&&!(0,Cr.isObjectType)(i)){var o;e.reportError("Mutation root type must be Object type if provided, it cannot be "+"".concat((0,qn.default)(i),"."),(o=pE(t,"mutation"))!==null&&o!==void 0?o:i.astNode)}var s=t.getSubscriptionType();if(s&&!(0,Cr.isObjectType)(s)){var l;e.reportError("Subscription root type must be Object type if provided, it cannot be "+"".concat((0,qn.default)(s),"."),(l=pE(t,"subscription"))!==null&&l!==void 0?l:s.astNode)}}function pE(e,t){for(var r=hE(e,function(o){return o.operationTypes}),n=0;n{"use strict";Object.defineProperty(yE,"__esModule",{value:!0});yE.typeFromAST=mE;var J4=VI(jt()),X4=VI(_n()),gE=Jt(),qI=bt();function VI(e){return e&&e.__esModule?e:{default:e}}function mE(e,t){var r;if(t.kind===gE.Kind.LIST_TYPE)return r=mE(e,t.type),r&&new qI.GraphQLList(r);if(t.kind===gE.Kind.NON_NULL_TYPE)return r=mE(e,t.type),r&&new qI.GraphQLNonNull(r);if(t.kind===gE.Kind.NAMED_TYPE)return e.getType(t.name.value);(0,X4.default)(0,"Unexpected type node: "+(0,J4.default)(t))}});var zg=G(np=>{"use strict";Object.defineProperty(np,"__esModule",{value:!0});np.visitWithTypeInfo=n5;np.TypeInfo=void 0;var Z4=e5(nc()),Sr=Jt(),$4=Xl(),UI=hu(),kr=bt(),dc=vi(),GI=Qa();function e5(e){return e&&e.__esModule?e:{default:e}}var t5=function(){function e(r,n,i){this._schema=r,this._typeStack=[],this._parentTypeStack=[],this._inputTypeStack=[],this._fieldDefStack=[],this._defaultValueStack=[],this._directive=null,this._argument=null,this._enumValue=null,this._getFieldDef=n!=null?n:r5,i&&((0,kr.isInputType)(i)&&this._inputTypeStack.push(i),(0,kr.isCompositeType)(i)&&this._parentTypeStack.push(i),(0,kr.isOutputType)(i)&&this._typeStack.push(i))}var t=e.prototype;return t.getType=function(){if(this._typeStack.length>0)return this._typeStack[this._typeStack.length-1]},t.getParentType=function(){if(this._parentTypeStack.length>0)return this._parentTypeStack[this._parentTypeStack.length-1]},t.getInputType=function(){if(this._inputTypeStack.length>0)return this._inputTypeStack[this._inputTypeStack.length-1]},t.getParentInputType=function(){if(this._inputTypeStack.length>1)return this._inputTypeStack[this._inputTypeStack.length-2]},t.getFieldDef=function(){if(this._fieldDefStack.length>0)return this._fieldDefStack[this._fieldDefStack.length-1]},t.getDefaultValue=function(){if(this._defaultValueStack.length>0)return this._defaultValueStack[this._defaultValueStack.length-1]},t.getDirective=function(){return this._directive},t.getArgument=function(){return this._argument},t.getEnumValue=function(){return this._enumValue},t.enter=function(n){var i=this._schema;switch(n.kind){case Sr.Kind.SELECTION_SET:{var o=(0,kr.getNamedType)(this.getType());this._parentTypeStack.push((0,kr.isCompositeType)(o)?o:void 0);break}case Sr.Kind.FIELD:{var s=this.getParentType(),l,d;s&&(l=this._getFieldDef(i,s,n),l&&(d=l.type)),this._fieldDefStack.push(l),this._typeStack.push((0,kr.isOutputType)(d)?d:void 0);break}case Sr.Kind.DIRECTIVE:this._directive=i.getDirective(n.name.value);break;case Sr.Kind.OPERATION_DEFINITION:{var h;switch(n.operation){case"query":h=i.getQueryType();break;case"mutation":h=i.getMutationType();break;case"subscription":h=i.getSubscriptionType();break}this._typeStack.push((0,kr.isObjectType)(h)?h:void 0);break}case Sr.Kind.INLINE_FRAGMENT:case Sr.Kind.FRAGMENT_DEFINITION:{var v=n.typeCondition,y=v?(0,GI.typeFromAST)(i,v):(0,kr.getNamedType)(this.getType());this._typeStack.push((0,kr.isOutputType)(y)?y:void 0);break}case Sr.Kind.VARIABLE_DEFINITION:{var b=(0,GI.typeFromAST)(i,n.type);this._inputTypeStack.push((0,kr.isInputType)(b)?b:void 0);break}case Sr.Kind.ARGUMENT:{var D,_,k,T=(D=this.getDirective())!==null&&D!==void 0?D:this.getFieldDef();T&&(_=(0,Z4.default)(T.args,function(M){return M.name===n.name.value}),_&&(k=_.type)),this._argument=_,this._defaultValueStack.push(_?_.defaultValue:void 0),this._inputTypeStack.push((0,kr.isInputType)(k)?k:void 0);break}case Sr.Kind.LIST:{var S=(0,kr.getNullableType)(this.getInputType()),m=(0,kr.isListType)(S)?S.ofType:S;this._defaultValueStack.push(void 0),this._inputTypeStack.push((0,kr.isInputType)(m)?m:void 0);break}case Sr.Kind.OBJECT_FIELD:{var w=(0,kr.getNamedType)(this.getInputType()),x,L;(0,kr.isInputObjectType)(w)&&(L=w.getFields()[n.name.value],L&&(x=L.type)),this._defaultValueStack.push(L?L.defaultValue:void 0),this._inputTypeStack.push((0,kr.isInputType)(x)?x:void 0);break}case Sr.Kind.ENUM:{var O=(0,kr.getNamedType)(this.getInputType()),R;(0,kr.isEnumType)(O)&&(R=O.getValue(n.value)),this._enumValue=R;break}}},t.leave=function(n){switch(n.kind){case Sr.Kind.SELECTION_SET:this._parentTypeStack.pop();break;case Sr.Kind.FIELD:this._fieldDefStack.pop(),this._typeStack.pop();break;case Sr.Kind.DIRECTIVE:this._directive=null;break;case Sr.Kind.OPERATION_DEFINITION:case Sr.Kind.INLINE_FRAGMENT:case Sr.Kind.FRAGMENT_DEFINITION:this._typeStack.pop();break;case Sr.Kind.VARIABLE_DEFINITION:this._inputTypeStack.pop();break;case Sr.Kind.ARGUMENT:this._argument=null,this._defaultValueStack.pop(),this._inputTypeStack.pop();break;case Sr.Kind.LIST:case Sr.Kind.OBJECT_FIELD:this._defaultValueStack.pop(),this._inputTypeStack.pop();break;case Sr.Kind.ENUM:this._enumValue=null;break}},e}();np.TypeInfo=t5;function r5(e,t,r){var n=r.name.value;if(n===dc.SchemaMetaFieldDef.name&&e.getQueryType()===t)return dc.SchemaMetaFieldDef;if(n===dc.TypeMetaFieldDef.name&&e.getQueryType()===t)return dc.TypeMetaFieldDef;if(n===dc.TypeNameMetaFieldDef.name&&(0,kr.isCompositeType)(t))return dc.TypeNameMetaFieldDef;if((0,kr.isObjectType)(t)||(0,kr.isInterfaceType)(t))return t.getFields()[n]}function n5(e,t){return{enter:function(n){e.enter(n);var i=(0,UI.getVisitFn)(t,n.kind,!1);if(i){var o=i.apply(t,arguments);return o!==void 0&&(e.leave(n),(0,$4.isNode)(o)&&e.enter(o)),o}},leave:function(n){var i=(0,UI.getVisitFn)(t,n.kind,!0),o;return i&&(o=i.apply(t,arguments)),e.leave(n),o}}}});var ws=G(Sa=>{"use strict";Object.defineProperty(Sa,"__esModule",{value:!0});Sa.isDefinitionNode=i5;Sa.isExecutableDefinitionNode=QI;Sa.isSelectionNode=a5;Sa.isValueNode=o5;Sa.isTypeNode=u5;Sa.isTypeSystemDefinitionNode=BI;Sa.isTypeDefinitionNode=KI;Sa.isTypeSystemExtensionNode=HI;Sa.isTypeExtensionNode=zI;var Dt=Jt();function i5(e){return QI(e)||BI(e)||HI(e)}function QI(e){return e.kind===Dt.Kind.OPERATION_DEFINITION||e.kind===Dt.Kind.FRAGMENT_DEFINITION}function a5(e){return e.kind===Dt.Kind.FIELD||e.kind===Dt.Kind.FRAGMENT_SPREAD||e.kind===Dt.Kind.INLINE_FRAGMENT}function o5(e){return e.kind===Dt.Kind.VARIABLE||e.kind===Dt.Kind.INT||e.kind===Dt.Kind.FLOAT||e.kind===Dt.Kind.STRING||e.kind===Dt.Kind.BOOLEAN||e.kind===Dt.Kind.NULL||e.kind===Dt.Kind.ENUM||e.kind===Dt.Kind.LIST||e.kind===Dt.Kind.OBJECT}function u5(e){return e.kind===Dt.Kind.NAMED_TYPE||e.kind===Dt.Kind.LIST_TYPE||e.kind===Dt.Kind.NON_NULL_TYPE}function BI(e){return e.kind===Dt.Kind.SCHEMA_DEFINITION||KI(e)||e.kind===Dt.Kind.DIRECTIVE_DEFINITION}function KI(e){return e.kind===Dt.Kind.SCALAR_TYPE_DEFINITION||e.kind===Dt.Kind.OBJECT_TYPE_DEFINITION||e.kind===Dt.Kind.INTERFACE_TYPE_DEFINITION||e.kind===Dt.Kind.UNION_TYPE_DEFINITION||e.kind===Dt.Kind.ENUM_TYPE_DEFINITION||e.kind===Dt.Kind.INPUT_OBJECT_TYPE_DEFINITION}function HI(e){return e.kind===Dt.Kind.SCHEMA_EXTENSION||zI(e)}function zI(e){return e.kind===Dt.Kind.SCALAR_TYPE_EXTENSION||e.kind===Dt.Kind.OBJECT_TYPE_EXTENSION||e.kind===Dt.Kind.INTERFACE_TYPE_EXTENSION||e.kind===Dt.Kind.UNION_TYPE_EXTENSION||e.kind===Dt.Kind.ENUM_TYPE_EXTENSION||e.kind===Dt.Kind.INPUT_OBJECT_TYPE_EXTENSION}});var TE=G(bE=>{"use strict";Object.defineProperty(bE,"__esModule",{value:!0});bE.ExecutableDefinitionsRule=c5;var s5=Je(),WI=Jt(),l5=ws();function c5(e){return{Document:function(r){for(var n=0,i=r.definitions;n{"use strict";Object.defineProperty(_E,"__esModule",{value:!0});_E.UniqueOperationNamesRule=d5;var f5=Je();function d5(e){var t=Object.create(null);return{OperationDefinition:function(n){var i=n.name;return i&&(t[i.value]?e.reportError(new f5.GraphQLError('There can be only one operation named "'.concat(i.value,'".'),[t[i.value],i])):t[i.value]=i),!1},FragmentDefinition:function(){return!1}}}});var kE=G(SE=>{"use strict";Object.defineProperty(SE,"__esModule",{value:!0});SE.LoneAnonymousOperationRule=v5;var p5=Je(),h5=Jt();function v5(e){var t=0;return{Document:function(n){t=n.definitions.filter(function(i){return i.kind===h5.Kind.OPERATION_DEFINITION}).length},OperationDefinition:function(n){!n.name&&t>1&&e.reportError(new p5.GraphQLError("This anonymous operation must be the only defined operation.",n))}}}});var wE=G(OE=>{"use strict";Object.defineProperty(OE,"__esModule",{value:!0});OE.SingleFieldSubscriptionsRule=m5;var g5=Je();function m5(e){return{OperationDefinition:function(r){r.operation==="subscription"&&r.selectionSet.selections.length!==1&&e.reportError(new g5.GraphQLError(r.name?'Subscription "'.concat(r.name.value,'" must select only one top level field.'):"Anonymous Subscription must select only one top level field.",r.selectionSet.selections.slice(1)))}}}});var xE=G(DE=>{"use strict";Object.defineProperty(DE,"__esModule",{value:!0});DE.KnownTypeNamesRule=S5;var y5=YI(gu()),b5=YI(mu()),T5=Je(),NE=ws(),_5=Ga(),E5=vi();function YI(e){return e&&e.__esModule?e:{default:e}}function S5(e){for(var t=e.getSchema(),r=t?t.getTypeMap():Object.create(null),n=Object.create(null),i=0,o=e.getDocument().definitions;i{"use strict";Object.defineProperty(CE,"__esModule",{value:!0});CE.FragmentsOnCompositeTypesRule=w5;var XI=Je(),ZI=hi(),$I=bt(),eA=Qa();function w5(e){return{InlineFragment:function(r){var n=r.typeCondition;if(n){var i=(0,eA.typeFromAST)(e.getSchema(),n);if(i&&!(0,$I.isCompositeType)(i)){var o=(0,ZI.print)(n);e.reportError(new XI.GraphQLError('Fragment cannot condition on non composite type "'.concat(o,'".'),n))}}},FragmentDefinition:function(r){var n=(0,eA.typeFromAST)(e.getSchema(),r.typeCondition);if(n&&!(0,$I.isCompositeType)(n)){var i=(0,ZI.print)(r.typeCondition);e.reportError(new XI.GraphQLError('Fragment "'.concat(r.name.value,'" cannot condition on non composite type "').concat(i,'".'),r.typeCondition))}}}}});var AE=G(IE=>{"use strict";Object.defineProperty(IE,"__esModule",{value:!0});IE.VariablesAreInputTypesRule=L5;var N5=Je(),D5=hi(),x5=bt(),C5=Qa();function L5(e){return{VariableDefinition:function(r){var n=(0,C5.typeFromAST)(e.getSchema(),r.type);if(n&&!(0,x5.isInputType)(n)){var i=r.variable.name.value,o=(0,D5.print)(r.type);e.reportError(new N5.GraphQLError('Variable "$'.concat(i,'" cannot be non-input type "').concat(o,'".'),r.type))}}}}});var jE=G(RE=>{"use strict";Object.defineProperty(RE,"__esModule",{value:!0});RE.ScalarLeafsRule=A5;var tA=I5(jt()),rA=Je(),nA=bt();function I5(e){return e&&e.__esModule?e:{default:e}}function A5(e){return{Field:function(r){var n=e.getType(),i=r.selectionSet;if(n){if((0,nA.isLeafType)((0,nA.getNamedType)(n))){if(i){var o=r.name.value,s=(0,tA.default)(n);e.reportError(new rA.GraphQLError('Field "'.concat(o,'" must not have a selection since type "').concat(s,'" has no subfields.'),i))}}else if(!i){var l=r.name.value,d=(0,tA.default)(n);e.reportError(new rA.GraphQLError('Field "'.concat(l,'" of type "').concat(d,'" must have a selection of subfields. Did you mean "').concat(l,' { ... }"?'),r))}}}}}});var FE=G(PE=>{"use strict";Object.defineProperty(PE,"__esModule",{value:!0});PE.FieldsOnCorrectTypeRule=M5;var R5=Wg(Y_()),iA=Wg(gu()),j5=Wg(mu()),P5=Wg(Ud()),F5=Je(),ip=bt();function Wg(e){return e&&e.__esModule?e:{default:e}}function M5(e){return{Field:function(r){var n=e.getParentType();if(n){var i=e.getFieldDef();if(!i){var o=e.getSchema(),s=r.name.value,l=(0,iA.default)("to use an inline fragment on",q5(o,n,s));l===""&&(l=(0,iA.default)(V5(n,s))),e.reportError(new F5.GraphQLError('Cannot query field "'.concat(s,'" on type "').concat(n.name,'".')+l,r))}}}}}function q5(e,t,r){if(!(0,ip.isAbstractType)(t))return[];for(var n=new Set,i=Object.create(null),o=0,s=e.getPossibleTypes(t);o{"use strict";Object.defineProperty(ME,"__esModule",{value:!0});ME.UniqueFragmentNamesRule=G5;var U5=Je();function G5(e){var t=Object.create(null);return{OperationDefinition:function(){return!1},FragmentDefinition:function(n){var i=n.name.value;return t[i]?e.reportError(new U5.GraphQLError('There can be only one fragment named "'.concat(i,'".'),[t[i],n.name])):t[i]=n.name,!1}}}});var UE=G(VE=>{"use strict";Object.defineProperty(VE,"__esModule",{value:!0});VE.KnownFragmentNamesRule=B5;var Q5=Je();function B5(e){return{FragmentSpread:function(r){var n=r.name.value,i=e.getFragment(n);i||e.reportError(new Q5.GraphQLError('Unknown fragment "'.concat(n,'".'),r.name))}}}});var QE=G(GE=>{"use strict";Object.defineProperty(GE,"__esModule",{value:!0});GE.NoUnusedFragmentsRule=H5;var K5=Je();function H5(e){var t=[],r=[];return{OperationDefinition:function(i){return t.push(i),!1},FragmentDefinition:function(i){return r.push(i),!1},Document:{leave:function(){for(var i=Object.create(null),o=0;o{"use strict";Object.defineProperty(KE,"__esModule",{value:!0});KE.PossibleFragmentSpreadsRule=Y5;var Yg=W5(jt()),aA=Je(),BE=bt(),z5=Qa(),oA=Hd();function W5(e){return e&&e.__esModule?e:{default:e}}function Y5(e){return{InlineFragment:function(r){var n=e.getType(),i=e.getParentType();if((0,BE.isCompositeType)(n)&&(0,BE.isCompositeType)(i)&&!(0,oA.doTypesOverlap)(e.getSchema(),n,i)){var o=(0,Yg.default)(i),s=(0,Yg.default)(n);e.reportError(new aA.GraphQLError('Fragment cannot be spread here as objects of type "'.concat(o,'" can never be of type "').concat(s,'".'),r))}},FragmentSpread:function(r){var n=r.name.value,i=J5(e,n),o=e.getParentType();if(i&&o&&!(0,oA.doTypesOverlap)(e.getSchema(),i,o)){var s=(0,Yg.default)(o),l=(0,Yg.default)(i);e.reportError(new aA.GraphQLError('Fragment "'.concat(n,'" cannot be spread here as objects of type "').concat(s,'" can never be of type "').concat(l,'".'),r))}}}}function J5(e,t){var r=e.getFragment(t);if(r){var n=(0,z5.typeFromAST)(e.getSchema(),r.typeCondition);if((0,BE.isCompositeType)(n))return n}}});var WE=G(zE=>{"use strict";Object.defineProperty(zE,"__esModule",{value:!0});zE.NoFragmentCyclesRule=Z5;var X5=Je();function Z5(e){var t=Object.create(null),r=[],n=Object.create(null);return{OperationDefinition:function(){return!1},FragmentDefinition:function(s){return i(s),!1}};function i(o){if(!t[o.name.value]){var s=o.name.value;t[s]=!0;var l=e.getFragmentSpreads(o.selectionSet);if(l.length!==0){n[s]=r.length;for(var d=0;d{"use strict";Object.defineProperty(YE,"__esModule",{value:!0});YE.UniqueVariableNamesRule=e6;var $5=Je();function e6(e){var t=Object.create(null);return{OperationDefinition:function(){t=Object.create(null)},VariableDefinition:function(n){var i=n.variable.name.value;t[i]?e.reportError(new $5.GraphQLError('There can be only one variable named "$'.concat(i,'".'),[t[i],n.variable.name])):t[i]=n.variable.name}}}});var ZE=G(XE=>{"use strict";Object.defineProperty(XE,"__esModule",{value:!0});XE.NoUndefinedVariablesRule=r6;var t6=Je();function r6(e){var t=Object.create(null);return{OperationDefinition:{enter:function(){t=Object.create(null)},leave:function(n){for(var i=e.getRecursiveVariableUsages(n),o=0;o{"use strict";Object.defineProperty($E,"__esModule",{value:!0});$E.NoUnusedVariablesRule=i6;var n6=Je();function i6(e){var t=[];return{OperationDefinition:{enter:function(){t=[]},leave:function(n){for(var i=Object.create(null),o=e.getRecursiveVariableUsages(n),s=0;s{"use strict";Object.defineProperty(tS,"__esModule",{value:!0});tS.KnownDirectivesRule=u6;var a6=lA(jt()),uA=lA(_n()),sA=Je(),sr=Jt(),$r=$l(),o6=gi();function lA(e){return e&&e.__esModule?e:{default:e}}function u6(e){for(var t=Object.create(null),r=e.getSchema(),n=r?r.getDirectives():o6.specifiedDirectives,i=0;i{"use strict";Object.defineProperty(iS,"__esModule",{value:!0});iS.UniqueDirectivesPerLocationRule=d6;var c6=Je(),nS=Jt(),cA=ws(),f6=gi();function d6(e){for(var t=Object.create(null),r=e.getSchema(),n=r?r.getDirectives():f6.specifiedDirectives,i=0;i{"use strict";Object.defineProperty(Jg,"__esModule",{value:!0});Jg.KnownArgumentNamesRule=g6;Jg.KnownArgumentNamesOnDirectivesRule=mA;var fA=hA(gu()),dA=hA(mu()),pA=Je(),p6=Jt(),h6=gi();function hA(e){return e&&e.__esModule?e:{default:e}}function vA(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function gA(e){for(var t=1;t{"use strict";Object.defineProperty(uS,"__esModule",{value:!0});uS.UniqueArgumentNamesRule=y6;var m6=Je();function y6(e){var t=Object.create(null);return{Field:function(){t=Object.create(null)},Directive:function(){t=Object.create(null)},Argument:function(n){var i=n.name.value;return t[i]?e.reportError(new m6.GraphQLError('There can be only one argument named "'.concat(i,'".'),[t[i],n.name])):t[i]=n.name,!1}}}});var cS=G(lS=>{"use strict";Object.defineProperty(lS,"__esModule",{value:!0});lS.ValuesOfCorrectTypeRule=S6;var b6=op(Ni()),T6=op(vu()),ap=op(jt()),_6=op(gu()),E6=op(mu()),Ns=Je(),Xg=hi(),Ba=bt();function op(e){return e&&e.__esModule?e:{default:e}}function S6(e){return{ListValue:function(r){var n=(0,Ba.getNullableType)(e.getParentInputType());if(!(0,Ba.isListType)(n))return Ds(e,r),!1},ObjectValue:function(r){var n=(0,Ba.getNamedType)(e.getInputType());if(!(0,Ba.isInputObjectType)(n))return Ds(e,r),!1;for(var i=(0,T6.default)(r.fields,function(v){return v.name.value}),o=0,s=(0,b6.default)(n.getFields());o{"use strict";Object.defineProperty($g,"__esModule",{value:!0});$g.ProvidedRequiredArgumentsRule=N6;$g.ProvidedRequiredArgumentsOnDirectivesRule=kA;var yA=_A(jt()),Zg=_A(vu()),bA=Je(),TA=Jt(),k6=hi(),O6=gi(),fS=bt();function _A(e){return e&&e.__esModule?e:{default:e}}function EA(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function SA(e){for(var t=1;t{"use strict";Object.defineProperty(pS,"__esModule",{value:!0});pS.VariablesInAllowedPositionRule=A6;var OA=I6(jt()),x6=Je(),C6=Jt(),wA=bt(),L6=Qa(),NA=Hd();function I6(e){return e&&e.__esModule?e:{default:e}}function A6(e){var t=Object.create(null);return{OperationDefinition:{enter:function(){t=Object.create(null)},leave:function(n){for(var i=e.getRecursiveVariableUsages(n),o=0;o{"use strict";Object.defineProperty(TS,"__esModule",{value:!0});TS.OverlappingFieldsCanBeMergedRule=M6;var j6=gS(nc()),P6=gS(ic()),DA=gS(jt()),F6=Je(),vS=Jt(),xA=hi(),mi=bt(),CA=Qa();function gS(e){return e&&e.__esModule?e:{default:e}}function LA(e){return Array.isArray(e)?e.map(function(t){var r=t[0],n=t[1];return'subfields "'.concat(r,'" conflict because ')+LA(n)}).join(" and "):e}function M6(e){var t=new K6,r=new Map;return{SelectionSet:function(i){for(var o=q6(e,r,t,e.getParentType(),i),s=0;s1)for(var v=0;v0)return[[t,e.map(function(i){var o=i[0];return o})],e.reduce(function(i,o){var s=o[1];return i.concat(s)},[r]),e.reduce(function(i,o){var s=o[2];return i.concat(s)},[n])]}var K6=function(){function e(){this._data=Object.create(null)}var t=e.prototype;return t.has=function(n,i,o){var s=this._data[n],l=s&&s[i];return l===void 0?!1:o===!1?l===!1:!0},t.add=function(n,i,o){this._pairSetAdd(n,i,o),this._pairSetAdd(i,n,o)},t._pairSetAdd=function(n,i,o){var s=this._data[n];s||(s=Object.create(null),this._data[n]=s),s[i]=o},e}()});var SS=G(ES=>{"use strict";Object.defineProperty(ES,"__esModule",{value:!0});ES.UniqueInputFieldNamesRule=z6;var H6=Je();function z6(e){var t=[],r=Object.create(null);return{ObjectValue:{enter:function(){t.push(r),r=Object.create(null)},leave:function(){r=t.pop()}},ObjectField:function(i){var o=i.name.value;r[o]?e.reportError(new H6.GraphQLError('There can be only one input field named "'.concat(o,'".'),[r[o],i.name])):r[o]=i.name}}}});var OS=G(kS=>{"use strict";Object.defineProperty(kS,"__esModule",{value:!0});kS.LoneSchemaDefinitionRule=W6;var RA=Je();function W6(e){var t,r,n,i=e.getSchema(),o=(t=(r=(n=i==null?void 0:i.astNode)!==null&&n!==void 0?n:i==null?void 0:i.getQueryType())!==null&&r!==void 0?r:i==null?void 0:i.getMutationType())!==null&&t!==void 0?t:i==null?void 0:i.getSubscriptionType(),s=0;return{SchemaDefinition:function(d){if(o){e.reportError(new RA.GraphQLError("Cannot define a new schema within a schema extension.",d));return}s>0&&e.reportError(new RA.GraphQLError("Must provide only one schema definition.",d)),++s}}}});var NS=G(wS=>{"use strict";Object.defineProperty(wS,"__esModule",{value:!0});wS.UniqueOperationTypesRule=Y6;var jA=Je();function Y6(e){var t=e.getSchema(),r=Object.create(null),n=t?{query:t.getQueryType(),mutation:t.getMutationType(),subscription:t.getSubscriptionType()}:{};return{SchemaDefinition:i,SchemaExtension:i};function i(o){for(var s,l=(s=o.operationTypes)!==null&&s!==void 0?s:[],d=0;d{"use strict";Object.defineProperty(DS,"__esModule",{value:!0});DS.UniqueTypeNamesRule=J6;var PA=Je();function J6(e){var t=Object.create(null),r=e.getSchema();return{ScalarTypeDefinition:n,ObjectTypeDefinition:n,InterfaceTypeDefinition:n,UnionTypeDefinition:n,EnumTypeDefinition:n,InputObjectTypeDefinition:n};function n(i){var o=i.name.value;if(r!=null&&r.getType(o)){e.reportError(new PA.GraphQLError('Type "'.concat(o,'" already exists in the schema. It cannot also be defined in this type definition.'),i.name));return}return t[o]?e.reportError(new PA.GraphQLError('There can be only one type named "'.concat(o,'".'),[t[o],i.name])):t[o]=i.name,!1}}});var LS=G(CS=>{"use strict";Object.defineProperty(CS,"__esModule",{value:!0});CS.UniqueEnumValueNamesRule=Z6;var FA=Je(),X6=bt();function Z6(e){var t=e.getSchema(),r=t?t.getTypeMap():Object.create(null),n=Object.create(null);return{EnumTypeDefinition:i,EnumTypeExtension:i};function i(o){var s,l=o.name.value;n[l]||(n[l]=Object.create(null));for(var d=(s=o.values)!==null&&s!==void 0?s:[],h=n[l],v=0;v{"use strict";Object.defineProperty(AS,"__esModule",{value:!0});AS.UniqueFieldDefinitionNamesRule=$6;var MA=Je(),IS=bt();function $6(e){var t=e.getSchema(),r=t?t.getTypeMap():Object.create(null),n=Object.create(null);return{InputObjectTypeDefinition:i,InputObjectTypeExtension:i,InterfaceTypeDefinition:i,InterfaceTypeExtension:i,ObjectTypeDefinition:i,ObjectTypeExtension:i};function i(o){var s,l=o.name.value;n[l]||(n[l]=Object.create(null));for(var d=(s=o.fields)!==null&&s!==void 0?s:[],h=n[l],v=0;v{"use strict";Object.defineProperty(jS,"__esModule",{value:!0});jS.UniqueDirectiveNamesRule=t9;var qA=Je();function t9(e){var t=Object.create(null),r=e.getSchema();return{DirectiveDefinition:function(i){var o=i.name.value;if(r!=null&&r.getDirective(o)){e.reportError(new qA.GraphQLError('Directive "@'.concat(o,'" already exists in the schema. It cannot be redefined.'),i.name));return}return t[o]?e.reportError(new qA.GraphQLError('There can be only one directive named "@'.concat(o,'".'),[t[o],i.name])):t[o]=i.name,!1}}}});var MS=G(FS=>{"use strict";Object.defineProperty(FS,"__esModule",{value:!0});FS.PossibleTypeExtensionsRule=a9;var VA=nm(jt()),UA=nm(_n()),r9=nm(gu()),n9=nm(mu()),GA=Je(),dr=Jt(),i9=ws(),pc=bt(),Ou;function nm(e){return e&&e.__esModule?e:{default:e}}function hc(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function a9(e){for(var t=e.getSchema(),r=Object.create(null),n=0,i=e.getDocument().definitions;n{"use strict";Object.defineProperty(vc,"__esModule",{value:!0});vc.specifiedSDLRules=vc.specifiedRules=void 0;var l9=TE(),c9=EE(),f9=kE(),d9=wE(),QA=xE(),p9=LE(),h9=AE(),v9=jE(),g9=FE(),m9=qE(),y9=UE(),b9=QE(),T9=HE(),_9=WE(),E9=JE(),S9=ZE(),k9=eS(),BA=rS(),KA=aS(),HA=oS(),zA=sS(),O9=cS(),WA=dS(),w9=hS(),N9=_S(),YA=SS(),D9=OS(),x9=NS(),C9=xS(),L9=LS(),I9=RS(),A9=PS(),R9=MS(),j9=Object.freeze([l9.ExecutableDefinitionsRule,c9.UniqueOperationNamesRule,f9.LoneAnonymousOperationRule,d9.SingleFieldSubscriptionsRule,QA.KnownTypeNamesRule,p9.FragmentsOnCompositeTypesRule,h9.VariablesAreInputTypesRule,v9.ScalarLeafsRule,g9.FieldsOnCorrectTypeRule,m9.UniqueFragmentNamesRule,y9.KnownFragmentNamesRule,b9.NoUnusedFragmentsRule,T9.PossibleFragmentSpreadsRule,_9.NoFragmentCyclesRule,E9.UniqueVariableNamesRule,S9.NoUndefinedVariablesRule,k9.NoUnusedVariablesRule,BA.KnownDirectivesRule,KA.UniqueDirectivesPerLocationRule,HA.KnownArgumentNamesRule,zA.UniqueArgumentNamesRule,O9.ValuesOfCorrectTypeRule,WA.ProvidedRequiredArgumentsRule,w9.VariablesInAllowedPositionRule,N9.OverlappingFieldsCanBeMergedRule,YA.UniqueInputFieldNamesRule]);vc.specifiedRules=j9;var P9=Object.freeze([D9.LoneSchemaDefinitionRule,x9.UniqueOperationTypesRule,C9.UniqueTypeNamesRule,L9.UniqueEnumValueNamesRule,I9.UniqueFieldDefinitionNamesRule,A9.UniqueDirectiveNamesRule,QA.KnownTypeNamesRule,BA.KnownDirectivesRule,KA.UniqueDirectivesPerLocationRule,R9.PossibleTypeExtensionsRule,HA.KnownArgumentNamesOnDirectivesRule,zA.UniqueArgumentNamesRule,YA.UniqueInputFieldNamesRule,WA.ProvidedRequiredArgumentsOnDirectivesRule]);vc.specifiedSDLRules=P9});var US=G(wu=>{"use strict";Object.defineProperty(wu,"__esModule",{value:!0});wu.ValidationContext=wu.SDLValidationContext=wu.ASTValidationContext=void 0;var JA=Jt(),F9=hu(),XA=zg();function ZA(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,e.__proto__=t}var VS=function(){function e(r,n){this._ast=r,this._fragments=void 0,this._fragmentSpreads=new Map,this._recursivelyReferencedFragments=new Map,this._onError=n}var t=e.prototype;return t.reportError=function(n){this._onError(n)},t.getDocument=function(){return this._ast},t.getFragment=function(n){var i=this._fragments;return i||(this._fragments=i=this.getDocument().definitions.reduce(function(o,s){return s.kind===JA.Kind.FRAGMENT_DEFINITION&&(o[s.name.value]=s),o},Object.create(null))),i[n]},t.getFragmentSpreads=function(n){var i=this._fragmentSpreads.get(n);if(!i){i=[];for(var o=[n];o.length!==0;)for(var s=o.pop(),l=0,d=s.selections;l{"use strict";Object.defineProperty(gc,"__esModule",{value:!0});gc.validate=B9;gc.validateSDL=GS;gc.assertValidSDL=K9;gc.assertValidSDLExtension=H9;var V9=Q9(Hi()),U9=Je(),im=hu(),G9=rp(),$A=zg(),eR=qS(),tR=US();function Q9(e){return e&&e.__esModule?e:{default:e}}function B9(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:eR.specifiedRules,n=arguments.length>3&&arguments[3]!==void 0?arguments[3]:new $A.TypeInfo(e),i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:{maxErrors:void 0};t||(0,V9.default)(0,"Must provide document."),(0,G9.assertValidSchema)(e);var o=Object.freeze({}),s=[],l=new tR.ValidationContext(e,t,n,function(h){if(i.maxErrors!=null&&s.length>=i.maxErrors)throw s.push(new U9.GraphQLError("Too many validation errors, error limit reached. Validation aborted.")),o;s.push(h)}),d=(0,im.visitInParallel)(r.map(function(h){return h(l)}));try{(0,im.visit)(t,(0,$A.visitWithTypeInfo)(n,d))}catch(h){if(h!==o)throw h}return s}function GS(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:eR.specifiedSDLRules,n=[],i=new tR.SDLValidationContext(e,t,function(s){n.push(s)}),o=r.map(function(s){return s(i)});return(0,im.visit)(e,(0,im.visitInParallel)(o)),n}function K9(e){var t=GS(e);if(t.length!==0)throw new Error(t.map(function(r){return r.message}).join(`
-
-`))}function H9(e,t){var r=GS(e,t);if(r.length!==0)throw new Error(r.map(function(n){return n.message}).join(`
-
-`))}});var rR=G(QS=>{"use strict";Object.defineProperty(QS,"__esModule",{value:!0});QS.default=z9;function z9(e){var t;return function(n,i,o){t||(t=new WeakMap);var s=t.get(n),l;if(s){if(l=s.get(i),l){var d=l.get(o);if(d!==void 0)return d}}else s=new WeakMap,t.set(n,s);l||(l=new WeakMap,s.set(i,l));var h=e(n,i,o);return l.set(o,h),h}}});var nR=G(BS=>{"use strict";Object.defineProperty(BS,"__esModule",{value:!0});BS.default=J9;var W9=Y9(rg());function Y9(e){return e&&e.__esModule?e:{default:e}}function J9(e,t,r){return e.reduce(function(n,i){return(0,W9.default)(n)?n.then(function(o){return t(o,i)}):t(n,i)},r)}});var iR=G(KS=>{"use strict";Object.defineProperty(KS,"__esModule",{value:!0});KS.default=X9;function X9(e){var t=Object.keys(e),r=t.map(function(n){return e[n]});return Promise.all(r).then(function(n){return n.reduce(function(i,o,s){return i[t[s]]=o,i},Object.create(null))})}});var up=G(am=>{"use strict";Object.defineProperty(am,"__esModule",{value:!0});am.addPath=Z9;am.pathToArray=$9;function Z9(e,t,r){return{prev:e,key:t,typename:r}}function $9(e){for(var t=[],r=e;r;)t.push(r.key),r=r.prev;return t.reverse()}});var um=G(HS=>{"use strict";Object.defineProperty(HS,"__esModule",{value:!0});HS.getOperationRootType=e8;var om=Je();function e8(e,t){if(t.operation==="query"){var r=e.getQueryType();if(!r)throw new om.GraphQLError("Schema does not define the required query root type.",t);return r}if(t.operation==="mutation"){var n=e.getMutationType();if(!n)throw new om.GraphQLError("Schema is not configured for mutations.",t);return n}if(t.operation==="subscription"){var i=e.getSubscriptionType();if(!i)throw new om.GraphQLError("Schema is not configured for subscriptions.",t);return i}throw new om.GraphQLError("Can only have query, mutation and subscription operations.",t)}});var WS=G(zS=>{"use strict";Object.defineProperty(zS,"__esModule",{value:!0});zS.default=t8;function t8(e){return e.map(function(t){return typeof t=="number"?"["+t.toString()+"]":"."+t}).join("")}});var lp=G(YS=>{"use strict";Object.defineProperty(YS,"__esModule",{value:!0});YS.valueFromAST=sp;var r8=sm(Ni()),n8=sm(vu()),i8=sm(jt()),a8=sm(_n()),yc=Jt(),xs=bt();function sm(e){return e&&e.__esModule?e:{default:e}}function sp(e,t,r){if(!!e){if(e.kind===yc.Kind.VARIABLE){var n=e.name.value;if(r==null||r[n]===void 0)return;var i=r[n];return i===null&&(0,xs.isNonNullType)(t)?void 0:i}if((0,xs.isNonNullType)(t))return e.kind===yc.Kind.NULL?void 0:sp(e,t.ofType,r);if(e.kind===yc.Kind.NULL)return null;if((0,xs.isListType)(t)){var o=t.ofType;if(e.kind===yc.Kind.LIST){for(var s=[],l=0,d=e.values;l{"use strict";Object.defineProperty(JS,"__esModule",{value:!0});JS.coerceInputValue=p8;var o8=Nu(Ni()),lm=Nu(jt()),u8=Nu(_n()),s8=Nu(gu()),l8=Nu(Ma()),c8=Nu(Mg()),f8=Nu(mu()),d8=Nu(WS()),So=up(),Cs=Je(),cp=bt();function Nu(e){return e&&e.__esModule?e:{default:e}}function p8(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:h8;return fp(e,t,r)}function h8(e,t,r){var n="Invalid value "+(0,lm.default)(t);throw e.length>0&&(n+=' at "value'.concat((0,d8.default)(e),'"')),r.message=n+": "+r.message,r}function fp(e,t,r,n){if((0,cp.isNonNullType)(t)){if(e!=null)return fp(e,t.ofType,r,n);r((0,So.pathToArray)(n),e,new Cs.GraphQLError('Expected non-nullable type "'.concat((0,lm.default)(t),'" not to be null.')));return}if(e==null)return null;if((0,cp.isListType)(t)){var i=t.ofType,o=(0,c8.default)(e,function(m,w){var x=(0,So.addPath)(n,w,void 0);return fp(m,i,r,x)});return o!=null?o:[fp(e,i,r,n)]}if((0,cp.isInputObjectType)(t)){if(!(0,l8.default)(e)){r((0,So.pathToArray)(n),e,new Cs.GraphQLError('Expected type "'.concat(t.name,'" to be an object.')));return}for(var s={},l=t.getFields(),d=0,h=(0,o8.default)(l);d{"use strict";Object.defineProperty(dp,"__esModule",{value:!0});dp.getVariableValues=T8;dp.getArgumentValues=lR;dp.getDirectiveValues=E8;var v8=cm(nc()),g8=cm(vu()),bc=cm(jt()),m8=cm(WS()),ko=Je(),oR=Jt(),uR=hi(),Tc=bt(),y8=Qa(),sR=lp(),b8=XS();function cm(e){return e&&e.__esModule?e:{default:e}}function T8(e,t,r,n){var i=[],o=n==null?void 0:n.maxErrors;try{var s=_8(e,t,r,function(l){if(o!=null&&i.length>=o)throw new ko.GraphQLError("Too many errors processing variables, error limit reached. Execution aborted.");i.push(l)});if(i.length===0)return{coerced:s}}catch(l){i.push(l)}return{errors:i}}function _8(e,t,r,n){for(var i={},o=function(h){var v=t[h],y=v.variable.name.value,b=(0,y8.typeFromAST)(e,v.type);if(!(0,Tc.isInputType)(b)){var D=(0,uR.print)(v.type);return n(new ko.GraphQLError('Variable "$'.concat(y,'" expected value of type "').concat(D,'" which cannot be used as an input type.'),v.type)),"continue"}if(!cR(r,y)){if(v.defaultValue)i[y]=(0,sR.valueFromAST)(v.defaultValue,b);else if((0,Tc.isNonNullType)(b)){var _=(0,bc.default)(b);n(new ko.GraphQLError('Variable "$'.concat(y,'" of required type "').concat(_,'" was not provided.'),v))}return"continue"}var k=r[y];if(k===null&&(0,Tc.isNonNullType)(b)){var T=(0,bc.default)(b);return n(new ko.GraphQLError('Variable "$'.concat(y,'" of non-null type "').concat(T,'" must not be null.'),v)),"continue"}i[y]=(0,b8.coerceInputValue)(k,b,function(S,m,w){var x='Variable "$'.concat(y,'" got invalid value ')+(0,bc.default)(m);S.length>0&&(x+=' at "'.concat(y).concat((0,m8.default)(S),'"')),n(new ko.GraphQLError(x+"; "+w.message,v,void 0,void 0,void 0,w.originalError))})},s=0;s{"use strict";Object.defineProperty(xi,"__esModule",{value:!0});xi.execute=L8;xi.executeSync=I8;xi.assertValidExecutionArguments=hR;xi.buildExecutionContext=vR;xi.collectFields=vp;xi.buildResolveInfo=bR;xi.getFieldDef=OR;xi.defaultFieldResolver=xi.defaultTypeResolver=void 0;var _c=wo(jt()),S8=wo(rR()),k8=wo(_n()),fR=wo(Hi()),Yi=wo(rg()),ZS=wo(Ma()),O8=wo(Mg()),w8=wo(nR()),N8=wo(iR()),Ls=up(),Ka=Je(),fm=qd(),hp=Jt(),D8=rp(),Ec=vi(),dR=gi(),Oo=bt(),x8=Qa(),C8=um(),dm=pp();function wo(e){return e&&e.__esModule?e:{default:e}}function L8(e,t,r,n,i,o,s,l){return arguments.length===1?$S(e):$S({schema:e,document:t,rootValue:r,contextValue:n,variableValues:i,operationName:o,fieldResolver:s,typeResolver:l})}function I8(e){var t=$S(e);if((0,Yi.default)(t))throw new Error("GraphQL execution failed to complete synchronously.");return t}function $S(e){var t=e.schema,r=e.document,n=e.rootValue,i=e.contextValue,o=e.variableValues,s=e.operationName,l=e.fieldResolver,d=e.typeResolver;hR(t,r,o);var h=vR(t,r,n,i,o,s,l,d);if(Array.isArray(h))return{errors:h};var v=A8(h,h.operation,n);return pR(h,v)}function pR(e,t){return(0,Yi.default)(t)?t.then(function(r){return pR(e,r)}):e.errors.length===0?{data:t}:{errors:e.errors,data:t}}function hR(e,t,r){t||(0,fR.default)(0,"Must provide document."),(0,D8.assertValidSchema)(e),r==null||(0,ZS.default)(r)||(0,fR.default)(0,"Variables must be provided as an Object where each property is a variable value. Perhaps look to see if an unparsed JSON string was provided.")}function vR(e,t,r,n,i,o,s,l){for(var d,h,v,y=Object.create(null),b=0,D=t.definitions;b{"use strict";Object.defineProperty(vm,"__esModule",{value:!0});vm.graphql=z8;vm.graphqlSync=W8;var U8=H8(rg()),G8=tc(),Q8=mc(),B8=rp(),K8=mp();function H8(e){return e&&e.__esModule?e:{default:e}}function z8(e,t,r,n,i,o,s,l){var d=arguments;return new Promise(function(h){return h(d.length===1?hm(e):hm({schema:e,source:t,rootValue:r,contextValue:n,variableValues:i,operationName:o,fieldResolver:s,typeResolver:l}))})}function W8(e,t,r,n,i,o,s,l){var d=arguments.length===1?hm(e):hm({schema:e,source:t,rootValue:r,contextValue:n,variableValues:i,operationName:o,fieldResolver:s,typeResolver:l});if((0,U8.default)(d))throw new Error("GraphQL execution failed to complete synchronously.");return d}function hm(e){var t=e.schema,r=e.source,n=e.rootValue,i=e.contextValue,o=e.variableValues,s=e.operationName,l=e.fieldResolver,d=e.typeResolver,h=(0,B8.validateSchema)(t);if(h.length>0)return{errors:h};var v;try{v=(0,G8.parse)(r)}catch(b){return{errors:[b]}}var y=(0,Q8.validate)(t,v);return y.length>0?{errors:y}:(0,K8.execute)({schema:t,document:v,rootValue:n,contextValue:i,variableValues:o,operationName:s,fieldResolver:l,typeResolver:d})}});var DR=G(Se=>{"use strict";Object.defineProperty(Se,"__esModule",{value:!0});Object.defineProperty(Se,"isSchema",{enumerable:!0,get:function(){return rk.isSchema}});Object.defineProperty(Se,"assertSchema",{enumerable:!0,get:function(){return rk.assertSchema}});Object.defineProperty(Se,"GraphQLSchema",{enumerable:!0,get:function(){return rk.GraphQLSchema}});Object.defineProperty(Se,"isType",{enumerable:!0,get:function(){return rt.isType}});Object.defineProperty(Se,"isScalarType",{enumerable:!0,get:function(){return rt.isScalarType}});Object.defineProperty(Se,"isObjectType",{enumerable:!0,get:function(){return rt.isObjectType}});Object.defineProperty(Se,"isInterfaceType",{enumerable:!0,get:function(){return rt.isInterfaceType}});Object.defineProperty(Se,"isUnionType",{enumerable:!0,get:function(){return rt.isUnionType}});Object.defineProperty(Se,"isEnumType",{enumerable:!0,get:function(){return rt.isEnumType}});Object.defineProperty(Se,"isInputObjectType",{enumerable:!0,get:function(){return rt.isInputObjectType}});Object.defineProperty(Se,"isListType",{enumerable:!0,get:function(){return rt.isListType}});Object.defineProperty(Se,"isNonNullType",{enumerable:!0,get:function(){return rt.isNonNullType}});Object.defineProperty(Se,"isInputType",{enumerable:!0,get:function(){return rt.isInputType}});Object.defineProperty(Se,"isOutputType",{enumerable:!0,get:function(){return rt.isOutputType}});Object.defineProperty(Se,"isLeafType",{enumerable:!0,get:function(){return rt.isLeafType}});Object.defineProperty(Se,"isCompositeType",{enumerable:!0,get:function(){return rt.isCompositeType}});Object.defineProperty(Se,"isAbstractType",{enumerable:!0,get:function(){return rt.isAbstractType}});Object.defineProperty(Se,"isWrappingType",{enumerable:!0,get:function(){return rt.isWrappingType}});Object.defineProperty(Se,"isNullableType",{enumerable:!0,get:function(){return rt.isNullableType}});Object.defineProperty(Se,"isNamedType",{enumerable:!0,get:function(){return rt.isNamedType}});Object.defineProperty(Se,"isRequiredArgument",{enumerable:!0,get:function(){return rt.isRequiredArgument}});Object.defineProperty(Se,"isRequiredInputField",{enumerable:!0,get:function(){return rt.isRequiredInputField}});Object.defineProperty(Se,"assertType",{enumerable:!0,get:function(){return rt.assertType}});Object.defineProperty(Se,"assertScalarType",{enumerable:!0,get:function(){return rt.assertScalarType}});Object.defineProperty(Se,"assertObjectType",{enumerable:!0,get:function(){return rt.assertObjectType}});Object.defineProperty(Se,"assertInterfaceType",{enumerable:!0,get:function(){return rt.assertInterfaceType}});Object.defineProperty(Se,"assertUnionType",{enumerable:!0,get:function(){return rt.assertUnionType}});Object.defineProperty(Se,"assertEnumType",{enumerable:!0,get:function(){return rt.assertEnumType}});Object.defineProperty(Se,"assertInputObjectType",{enumerable:!0,get:function(){return rt.assertInputObjectType}});Object.defineProperty(Se,"assertListType",{enumerable:!0,get:function(){return rt.assertListType}});Object.defineProperty(Se,"assertNonNullType",{enumerable:!0,get:function(){return rt.assertNonNullType}});Object.defineProperty(Se,"assertInputType",{enumerable:!0,get:function(){return rt.assertInputType}});Object.defineProperty(Se,"assertOutputType",{enumerable:!0,get:function(){return rt.assertOutputType}});Object.defineProperty(Se,"assertLeafType",{enumerable:!0,get:function(){return rt.assertLeafType}});Object.defineProperty(Se,"assertCompositeType",{enumerable:!0,get:function(){return rt.assertCompositeType}});Object.defineProperty(Se,"assertAbstractType",{enumerable:!0,get:function(){return rt.assertAbstractType}});Object.defineProperty(Se,"assertWrappingType",{enumerable:!0,get:function(){return rt.assertWrappingType}});Object.defineProperty(Se,"assertNullableType",{enumerable:!0,get:function(){return rt.assertNullableType}});Object.defineProperty(Se,"assertNamedType",{enumerable:!0,get:function(){return rt.assertNamedType}});Object.defineProperty(Se,"getNullableType",{enumerable:!0,get:function(){return rt.getNullableType}});Object.defineProperty(Se,"getNamedType",{enumerable:!0,get:function(){return rt.getNamedType}});Object.defineProperty(Se,"GraphQLScalarType",{enumerable:!0,get:function(){return rt.GraphQLScalarType}});Object.defineProperty(Se,"GraphQLObjectType",{enumerable:!0,get:function(){return rt.GraphQLObjectType}});Object.defineProperty(Se,"GraphQLInterfaceType",{enumerable:!0,get:function(){return rt.GraphQLInterfaceType}});Object.defineProperty(Se,"GraphQLUnionType",{enumerable:!0,get:function(){return rt.GraphQLUnionType}});Object.defineProperty(Se,"GraphQLEnumType",{enumerable:!0,get:function(){return rt.GraphQLEnumType}});Object.defineProperty(Se,"GraphQLInputObjectType",{enumerable:!0,get:function(){return rt.GraphQLInputObjectType}});Object.defineProperty(Se,"GraphQLList",{enumerable:!0,get:function(){return rt.GraphQLList}});Object.defineProperty(Se,"GraphQLNonNull",{enumerable:!0,get:function(){return rt.GraphQLNonNull}});Object.defineProperty(Se,"isDirective",{enumerable:!0,get:function(){return Ha.isDirective}});Object.defineProperty(Se,"assertDirective",{enumerable:!0,get:function(){return Ha.assertDirective}});Object.defineProperty(Se,"GraphQLDirective",{enumerable:!0,get:function(){return Ha.GraphQLDirective}});Object.defineProperty(Se,"isSpecifiedDirective",{enumerable:!0,get:function(){return Ha.isSpecifiedDirective}});Object.defineProperty(Se,"specifiedDirectives",{enumerable:!0,get:function(){return Ha.specifiedDirectives}});Object.defineProperty(Se,"GraphQLIncludeDirective",{enumerable:!0,get:function(){return Ha.GraphQLIncludeDirective}});Object.defineProperty(Se,"GraphQLSkipDirective",{enumerable:!0,get:function(){return Ha.GraphQLSkipDirective}});Object.defineProperty(Se,"GraphQLDeprecatedDirective",{enumerable:!0,get:function(){return Ha.GraphQLDeprecatedDirective}});Object.defineProperty(Se,"GraphQLSpecifiedByDirective",{enumerable:!0,get:function(){return Ha.GraphQLSpecifiedByDirective}});Object.defineProperty(Se,"DEFAULT_DEPRECATION_REASON",{enumerable:!0,get:function(){return Ha.DEFAULT_DEPRECATION_REASON}});Object.defineProperty(Se,"isSpecifiedScalarType",{enumerable:!0,get:function(){return Is.isSpecifiedScalarType}});Object.defineProperty(Se,"specifiedScalarTypes",{enumerable:!0,get:function(){return Is.specifiedScalarTypes}});Object.defineProperty(Se,"GraphQLInt",{enumerable:!0,get:function(){return Is.GraphQLInt}});Object.defineProperty(Se,"GraphQLFloat",{enumerable:!0,get:function(){return Is.GraphQLFloat}});Object.defineProperty(Se,"GraphQLString",{enumerable:!0,get:function(){return Is.GraphQLString}});Object.defineProperty(Se,"GraphQLBoolean",{enumerable:!0,get:function(){return Is.GraphQLBoolean}});Object.defineProperty(Se,"GraphQLID",{enumerable:!0,get:function(){return Is.GraphQLID}});Object.defineProperty(Se,"isIntrospectionType",{enumerable:!0,get:function(){return yi.isIntrospectionType}});Object.defineProperty(Se,"introspectionTypes",{enumerable:!0,get:function(){return yi.introspectionTypes}});Object.defineProperty(Se,"__Schema",{enumerable:!0,get:function(){return yi.__Schema}});Object.defineProperty(Se,"__Directive",{enumerable:!0,get:function(){return yi.__Directive}});Object.defineProperty(Se,"__DirectiveLocation",{enumerable:!0,get:function(){return yi.__DirectiveLocation}});Object.defineProperty(Se,"__Type",{enumerable:!0,get:function(){return yi.__Type}});Object.defineProperty(Se,"__Field",{enumerable:!0,get:function(){return yi.__Field}});Object.defineProperty(Se,"__InputValue",{enumerable:!0,get:function(){return yi.__InputValue}});Object.defineProperty(Se,"__EnumValue",{enumerable:!0,get:function(){return yi.__EnumValue}});Object.defineProperty(Se,"__TypeKind",{enumerable:!0,get:function(){return yi.__TypeKind}});Object.defineProperty(Se,"TypeKind",{enumerable:!0,get:function(){return yi.TypeKind}});Object.defineProperty(Se,"SchemaMetaFieldDef",{enumerable:!0,get:function(){return yi.SchemaMetaFieldDef}});Object.defineProperty(Se,"TypeMetaFieldDef",{enumerable:!0,get:function(){return yi.TypeMetaFieldDef}});Object.defineProperty(Se,"TypeNameMetaFieldDef",{enumerable:!0,get:function(){return yi.TypeNameMetaFieldDef}});Object.defineProperty(Se,"validateSchema",{enumerable:!0,get:function(){return NR.validateSchema}});Object.defineProperty(Se,"assertValidSchema",{enumerable:!0,get:function(){return NR.assertValidSchema}});var rk=ks(),rt=bt(),Ha=gi(),Is=Ga(),yi=vi(),NR=rp()});var LR=G(Qt=>{"use strict";Object.defineProperty(Qt,"__esModule",{value:!0});Object.defineProperty(Qt,"Source",{enumerable:!0,get:function(){return Y8.Source}});Object.defineProperty(Qt,"getLocation",{enumerable:!0,get:function(){return J8.getLocation}});Object.defineProperty(Qt,"printLocation",{enumerable:!0,get:function(){return xR.printLocation}});Object.defineProperty(Qt,"printSourceLocation",{enumerable:!0,get:function(){return xR.printSourceLocation}});Object.defineProperty(Qt,"Kind",{enumerable:!0,get:function(){return X8.Kind}});Object.defineProperty(Qt,"TokenKind",{enumerable:!0,get:function(){return Z8.TokenKind}});Object.defineProperty(Qt,"Lexer",{enumerable:!0,get:function(){return $8.Lexer}});Object.defineProperty(Qt,"parse",{enumerable:!0,get:function(){return nk.parse}});Object.defineProperty(Qt,"parseValue",{enumerable:!0,get:function(){return nk.parseValue}});Object.defineProperty(Qt,"parseType",{enumerable:!0,get:function(){return nk.parseType}});Object.defineProperty(Qt,"print",{enumerable:!0,get:function(){return eY.print}});Object.defineProperty(Qt,"visit",{enumerable:!0,get:function(){return gm.visit}});Object.defineProperty(Qt,"visitInParallel",{enumerable:!0,get:function(){return gm.visitInParallel}});Object.defineProperty(Qt,"getVisitFn",{enumerable:!0,get:function(){return gm.getVisitFn}});Object.defineProperty(Qt,"BREAK",{enumerable:!0,get:function(){return gm.BREAK}});Object.defineProperty(Qt,"Location",{enumerable:!0,get:function(){return CR.Location}});Object.defineProperty(Qt,"Token",{enumerable:!0,get:function(){return CR.Token}});Object.defineProperty(Qt,"isDefinitionNode",{enumerable:!0,get:function(){return No.isDefinitionNode}});Object.defineProperty(Qt,"isExecutableDefinitionNode",{enumerable:!0,get:function(){return No.isExecutableDefinitionNode}});Object.defineProperty(Qt,"isSelectionNode",{enumerable:!0,get:function(){return No.isSelectionNode}});Object.defineProperty(Qt,"isValueNode",{enumerable:!0,get:function(){return No.isValueNode}});Object.defineProperty(Qt,"isTypeNode",{enumerable:!0,get:function(){return No.isTypeNode}});Object.defineProperty(Qt,"isTypeSystemDefinitionNode",{enumerable:!0,get:function(){return No.isTypeSystemDefinitionNode}});Object.defineProperty(Qt,"isTypeDefinitionNode",{enumerable:!0,get:function(){return No.isTypeDefinitionNode}});Object.defineProperty(Qt,"isTypeSystemExtensionNode",{enumerable:!0,get:function(){return No.isTypeSystemExtensionNode}});Object.defineProperty(Qt,"isTypeExtensionNode",{enumerable:!0,get:function(){return No.isTypeExtensionNode}});Object.defineProperty(Qt,"DirectiveLocation",{enumerable:!0,get:function(){return tY.DirectiveLocation}});var Y8=mg(),J8=ig(),xR=l_(),X8=Jt(),Z8=Zl(),$8=Tg(),nk=tc(),eY=hi(),gm=hu(),CR=Xl(),No=ws(),tY=$l()});var IR=G(Du=>{"use strict";Object.defineProperty(Du,"__esModule",{value:!0});Object.defineProperty(Du,"responsePathAsArray",{enumerable:!0,get:function(){return rY.pathToArray}});Object.defineProperty(Du,"execute",{enumerable:!0,get:function(){return mm.execute}});Object.defineProperty(Du,"executeSync",{enumerable:!0,get:function(){return mm.executeSync}});Object.defineProperty(Du,"defaultFieldResolver",{enumerable:!0,get:function(){return mm.defaultFieldResolver}});Object.defineProperty(Du,"defaultTypeResolver",{enumerable:!0,get:function(){return mm.defaultTypeResolver}});Object.defineProperty(Du,"getDirectiveValues",{enumerable:!0,get:function(){return nY.getDirectiveValues}});var rY=up(),mm=mp(),nY=pp()});var AR=G(ik=>{"use strict";Object.defineProperty(ik,"__esModule",{value:!0});ik.default=aY;var iY=qa();function aY(e){return typeof(e==null?void 0:e[iY.SYMBOL_ASYNC_ITERATOR])=="function"}});var FR=G(ak=>{"use strict";Object.defineProperty(ak,"__esModule",{value:!0});ak.default=uY;var RR=qa();function oY(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function uY(e,t,r){var n=e[RR.SYMBOL_ASYNC_ITERATOR],i=n.call(e),o,s;typeof i.return=="function"&&(o=i.return,s=function(y){var b=function(){return Promise.reject(y)};return o.call(i).then(b,b)});function l(v){return v.done?v:jR(v.value,t).then(PR,s)}var d;if(r){var h=r;d=function(y){return jR(y,h).then(PR,s)}}return oY({next:function(){return i.next().then(l,d)},return:function(){return o?o.call(i).then(l,d):Promise.resolve({value:void 0,done:!0})},throw:function(y){return typeof i.throw=="function"?i.throw(y).then(l,d):Promise.reject(y).catch(s)}},RR.SYMBOL_ASYNC_ITERATOR,function(){return this})}function jR(e,t){return new Promise(function(r){return r(t(e))})}function PR(e){return{value:e,done:!1}}});var BR=G(ym=>{"use strict";Object.defineProperty(ym,"__esModule",{value:!0});ym.subscribe=dY;ym.createSourceEventStream=QR;var sY=uk(jt()),MR=uk(AR()),ok=up(),qR=Je(),VR=qd(),lY=pp(),Sc=mp(),cY=um(),fY=uk(FR());function uk(e){return e&&e.__esModule?e:{default:e}}function dY(e,t,r,n,i,o,s,l){return arguments.length===1?GR(e):GR({schema:e,document:t,rootValue:r,contextValue:n,variableValues:i,operationName:o,fieldResolver:s,subscribeFieldResolver:l})}function UR(e){if(e instanceof qR.GraphQLError)return{errors:[e]};throw e}function GR(e){var t=e.schema,r=e.document,n=e.rootValue,i=e.contextValue,o=e.variableValues,s=e.operationName,l=e.fieldResolver,d=e.subscribeFieldResolver,h=QR(t,r,n,i,o,s,d),v=function(b){return(0,Sc.execute)({schema:t,document:r,rootValue:b,contextValue:i,variableValues:o,operationName:s,fieldResolver:l})};return h.then(function(y){return(0,MR.default)(y)?(0,fY.default)(y,v,UR):y})}function QR(e,t,r,n,i,o,s){return(0,Sc.assertValidExecutionArguments)(e,t,i),new Promise(function(l){var d=(0,Sc.buildExecutionContext)(e,t,r,n,i,o,s);l(Array.isArray(d)?{errors:d}:pY(d))}).catch(UR)}function pY(e){var t=e.schema,r=e.operation,n=e.variableValues,i=e.rootValue,o=(0,cY.getOperationRootType)(t,r),s=(0,Sc.collectFields)(e,o,r.selectionSet,Object.create(null),Object.create(null)),l=Object.keys(s),d=l[0],h=s[d],v=h[0],y=v.name.value,b=(0,Sc.getFieldDef)(t,o,y);if(!b)throw new qR.GraphQLError('The subscription field "'.concat(y,'" is not defined.'),h);var D=(0,ok.addPath)(void 0,d,o.name),_=(0,Sc.buildResolveInfo)(e,b,h,o,D);return new Promise(function(k){var T,S=(0,lY.getArgumentValues)(b,h[0],n),m=e.contextValue,w=(T=b.subscribe)!==null&&T!==void 0?T:e.fieldResolver;k(w(i,S,m,_))}).then(function(k){if(k instanceof Error)throw(0,VR.locatedError)(k,h,(0,ok.pathToArray)(D));if(!(0,MR.default)(k))throw new Error("Subscription field must return Async Iterable. "+"Received: ".concat((0,sY.default)(k),"."));return k},function(k){throw(0,VR.locatedError)(k,h,(0,ok.pathToArray)(D))})}});var HR=G(bm=>{"use strict";Object.defineProperty(bm,"__esModule",{value:!0});Object.defineProperty(bm,"subscribe",{enumerable:!0,get:function(){return KR.subscribe}});Object.defineProperty(bm,"createSourceEventStream",{enumerable:!0,get:function(){return KR.createSourceEventStream}});var KR=BR()});var fk=G(ck=>{"use strict";Object.defineProperty(ck,"__esModule",{value:!0});ck.NoDeprecatedCustomRule=vY;var sk=hY(_n()),yp=Je(),lk=bt();function hY(e){return e&&e.__esModule?e:{default:e}}function vY(e){return{Field:function(r){var n=e.getFieldDef(),i=n==null?void 0:n.deprecationReason;if(n&&i!=null){var o=e.getParentType();o!=null||(0,sk.default)(0),e.reportError(new yp.GraphQLError("The field ".concat(o.name,".").concat(n.name," is deprecated. ").concat(i),r))}},Argument:function(r){var n=e.getArgument(),i=n==null?void 0:n.deprecationReason;if(n&&i!=null){var o=e.getDirective();if(o!=null)e.reportError(new yp.GraphQLError('Directive "@'.concat(o.name,'" argument "').concat(n.name,'" is deprecated. ').concat(i),r));else{var s=e.getParentType(),l=e.getFieldDef();s!=null&&l!=null||(0,sk.default)(0),e.reportError(new yp.GraphQLError('Field "'.concat(s.name,".").concat(l.name,'" argument "').concat(n.name,'" is deprecated. ').concat(i),r))}}},ObjectField:function(r){var n=(0,lk.getNamedType)(e.getParentInputType());if((0,lk.isInputObjectType)(n)){var i=n.getFields()[r.name.value],o=i==null?void 0:i.deprecationReason;o!=null&&e.reportError(new yp.GraphQLError("The input field ".concat(n.name,".").concat(i.name," is deprecated. ").concat(o),r))}},EnumValue:function(r){var n=e.getEnumValue(),i=n==null?void 0:n.deprecationReason;if(n&&i!=null){var o=(0,lk.getNamedType)(e.getInputType());o!=null||(0,sk.default)(0),e.reportError(new yp.GraphQLError('The enum value "'.concat(o.name,".").concat(n.name,'" is deprecated. ').concat(i),r))}}}}});var zR=G(dk=>{"use strict";Object.defineProperty(dk,"__esModule",{value:!0});dk.NoSchemaIntrospectionCustomRule=bY;var gY=Je(),mY=bt(),yY=vi();function bY(e){return{Field:function(r){var n=(0,mY.getNamedType)(e.getType());n&&(0,yY.isIntrospectionType)(n)&&e.reportError(new gY.GraphQLError('GraphQL introspection has been disabled, but the requested query contained the field "'.concat(r.name.value,'".'),r))}}}});var WR=G(ft=>{"use strict";Object.defineProperty(ft,"__esModule",{value:!0});Object.defineProperty(ft,"validate",{enumerable:!0,get:function(){return TY.validate}});Object.defineProperty(ft,"ValidationContext",{enumerable:!0,get:function(){return _Y.ValidationContext}});Object.defineProperty(ft,"specifiedRules",{enumerable:!0,get:function(){return EY.specifiedRules}});Object.defineProperty(ft,"ExecutableDefinitionsRule",{enumerable:!0,get:function(){return SY.ExecutableDefinitionsRule}});Object.defineProperty(ft,"FieldsOnCorrectTypeRule",{enumerable:!0,get:function(){return kY.FieldsOnCorrectTypeRule}});Object.defineProperty(ft,"FragmentsOnCompositeTypesRule",{enumerable:!0,get:function(){return OY.FragmentsOnCompositeTypesRule}});Object.defineProperty(ft,"KnownArgumentNamesRule",{enumerable:!0,get:function(){return wY.KnownArgumentNamesRule}});Object.defineProperty(ft,"KnownDirectivesRule",{enumerable:!0,get:function(){return NY.KnownDirectivesRule}});Object.defineProperty(ft,"KnownFragmentNamesRule",{enumerable:!0,get:function(){return DY.KnownFragmentNamesRule}});Object.defineProperty(ft,"KnownTypeNamesRule",{enumerable:!0,get:function(){return xY.KnownTypeNamesRule}});Object.defineProperty(ft,"LoneAnonymousOperationRule",{enumerable:!0,get:function(){return CY.LoneAnonymousOperationRule}});Object.defineProperty(ft,"NoFragmentCyclesRule",{enumerable:!0,get:function(){return LY.NoFragmentCyclesRule}});Object.defineProperty(ft,"NoUndefinedVariablesRule",{enumerable:!0,get:function(){return IY.NoUndefinedVariablesRule}});Object.defineProperty(ft,"NoUnusedFragmentsRule",{enumerable:!0,get:function(){return AY.NoUnusedFragmentsRule}});Object.defineProperty(ft,"NoUnusedVariablesRule",{enumerable:!0,get:function(){return RY.NoUnusedVariablesRule}});Object.defineProperty(ft,"OverlappingFieldsCanBeMergedRule",{enumerable:!0,get:function(){return jY.OverlappingFieldsCanBeMergedRule}});Object.defineProperty(ft,"PossibleFragmentSpreadsRule",{enumerable:!0,get:function(){return PY.PossibleFragmentSpreadsRule}});Object.defineProperty(ft,"ProvidedRequiredArgumentsRule",{enumerable:!0,get:function(){return FY.ProvidedRequiredArgumentsRule}});Object.defineProperty(ft,"ScalarLeafsRule",{enumerable:!0,get:function(){return MY.ScalarLeafsRule}});Object.defineProperty(ft,"SingleFieldSubscriptionsRule",{enumerable:!0,get:function(){return qY.SingleFieldSubscriptionsRule}});Object.defineProperty(ft,"UniqueArgumentNamesRule",{enumerable:!0,get:function(){return VY.UniqueArgumentNamesRule}});Object.defineProperty(ft,"UniqueDirectivesPerLocationRule",{enumerable:!0,get:function(){return UY.UniqueDirectivesPerLocationRule}});Object.defineProperty(ft,"UniqueFragmentNamesRule",{enumerable:!0,get:function(){return GY.UniqueFragmentNamesRule}});Object.defineProperty(ft,"UniqueInputFieldNamesRule",{enumerable:!0,get:function(){return QY.UniqueInputFieldNamesRule}});Object.defineProperty(ft,"UniqueOperationNamesRule",{enumerable:!0,get:function(){return BY.UniqueOperationNamesRule}});Object.defineProperty(ft,"UniqueVariableNamesRule",{enumerable:!0,get:function(){return KY.UniqueVariableNamesRule}});Object.defineProperty(ft,"ValuesOfCorrectTypeRule",{enumerable:!0,get:function(){return HY.ValuesOfCorrectTypeRule}});Object.defineProperty(ft,"VariablesAreInputTypesRule",{enumerable:!0,get:function(){return zY.VariablesAreInputTypesRule}});Object.defineProperty(ft,"VariablesInAllowedPositionRule",{enumerable:!0,get:function(){return WY.VariablesInAllowedPositionRule}});Object.defineProperty(ft,"LoneSchemaDefinitionRule",{enumerable:!0,get:function(){return YY.LoneSchemaDefinitionRule}});Object.defineProperty(ft,"UniqueOperationTypesRule",{enumerable:!0,get:function(){return JY.UniqueOperationTypesRule}});Object.defineProperty(ft,"UniqueTypeNamesRule",{enumerable:!0,get:function(){return XY.UniqueTypeNamesRule}});Object.defineProperty(ft,"UniqueEnumValueNamesRule",{enumerable:!0,get:function(){return ZY.UniqueEnumValueNamesRule}});Object.defineProperty(ft,"UniqueFieldDefinitionNamesRule",{enumerable:!0,get:function(){return $Y.UniqueFieldDefinitionNamesRule}});Object.defineProperty(ft,"UniqueDirectiveNamesRule",{enumerable:!0,get:function(){return e7.UniqueDirectiveNamesRule}});Object.defineProperty(ft,"PossibleTypeExtensionsRule",{enumerable:!0,get:function(){return t7.PossibleTypeExtensionsRule}});Object.defineProperty(ft,"NoDeprecatedCustomRule",{enumerable:!0,get:function(){return r7.NoDeprecatedCustomRule}});Object.defineProperty(ft,"NoSchemaIntrospectionCustomRule",{enumerable:!0,get:function(){return n7.NoSchemaIntrospectionCustomRule}});var TY=mc(),_Y=US(),EY=qS(),SY=TE(),kY=FE(),OY=LE(),wY=oS(),NY=rS(),DY=UE(),xY=xE(),CY=kE(),LY=WE(),IY=ZE(),AY=QE(),RY=eS(),jY=_S(),PY=HE(),FY=dS(),MY=jE(),qY=wE(),VY=sS(),UY=aS(),GY=qE(),QY=SS(),BY=EE(),KY=JE(),HY=cS(),zY=AE(),WY=hS(),YY=OS(),JY=NS(),XY=xS(),ZY=LS(),$Y=RS(),e7=PS(),t7=MS(),r7=fk(),n7=zR()});var YR=G(pk=>{"use strict";Object.defineProperty(pk,"__esModule",{value:!0});pk.formatError=o7;var i7=a7(Hi());function a7(e){return e&&e.__esModule?e:{default:e}}function o7(e){var t;e||(0,i7.default)(0,"Received null or undefined error.");var r=(t=e.message)!==null&&t!==void 0?t:"An unknown error occurred.",n=e.locations,i=e.path,o=e.extensions;return o?{message:r,locations:n,path:i,extensions:o}:{message:r,locations:n,path:i}}});var XR=G(As=>{"use strict";Object.defineProperty(As,"__esModule",{value:!0});Object.defineProperty(As,"GraphQLError",{enumerable:!0,get:function(){return JR.GraphQLError}});Object.defineProperty(As,"printError",{enumerable:!0,get:function(){return JR.printError}});Object.defineProperty(As,"syntaxError",{enumerable:!0,get:function(){return u7.syntaxError}});Object.defineProperty(As,"locatedError",{enumerable:!0,get:function(){return s7.locatedError}});Object.defineProperty(As,"formatError",{enumerable:!0,get:function(){return l7.formatError}});var JR=Je(),u7=lg(),s7=qd(),l7=YR()});var vk=G(hk=>{"use strict";Object.defineProperty(hk,"__esModule",{value:!0});hk.getIntrospectionQuery=d7;function ZR(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function c7(e){for(var t=1;t{"use strict";Object.defineProperty(gk,"__esModule",{value:!0});gk.getOperationAST=h7;var p7=Jt();function h7(e,t){for(var r=null,n=0,i=e.definitions;n{"use strict";Object.defineProperty(yk,"__esModule",{value:!0});yk.introspectionFromSchema=E7;var v7=b7(_n()),g7=tc(),m7=mp(),y7=vk();function b7(e){return e&&e.__esModule?e:{default:e}}function $R(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function T7(e){for(var t=1;t{"use strict";Object.defineProperty(bk,"__esModule",{value:!0});bk.buildClientSchema=C7;var S7=bp(Ni()),Ci=bp(jt()),k7=bp(Hi()),Tm=bp(Vd()),tj=bp(Ma()),O7=tc(),w7=ks(),N7=gi(),D7=Ga(),za=vi(),Li=bt(),x7=lp();function bp(e){return e&&e.__esModule?e:{default:e}}function C7(e,t){(0,tj.default)(e)&&(0,tj.default)(e.__schema)||(0,k7.default)(0,'Invalid or incomplete introspection result. Ensure that you are passing "data" property of introspection response and no "errors" was returned alongside: '.concat((0,Ci.default)(e),"."));for(var r=e.__schema,n=(0,Tm.default)(r.types,function(Q){return Q.name},function(Q){return k(Q)}),i=0,o=[].concat(D7.specifiedScalarTypes,za.introspectionTypes);i{"use strict";Object.defineProperty(_p,"__esModule",{value:!0});_p.extendSchema=M7;_p.extendSchemaImpl=dj;_p.getDescription=Rs;var L7=kc(Ni()),I7=kc(vu()),nj=kc(jt()),Tp=kc(w_()),ij=kc(_n()),A7=kc(Hi()),Ji=Jt(),R7=Zl(),j7=ec(),aj=ws(),P7=mc(),oj=pp(),uj=ks(),sj=Ga(),lj=vi(),_m=gi(),pr=bt(),cj=lp();function kc(e){return e&&e.__esModule?e:{default:e}}function fj(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function xt(e){for(var t=1;t0?r.reverse().join(`
-`):void 0}}});var mj=G(Sm=>{"use strict";Object.defineProperty(Sm,"__esModule",{value:!0});Sm.buildASTSchema=gj;Sm.buildSchema=z7;var V7=H7(Hi()),U7=Jt(),G7=tc(),Q7=mc(),B7=ks(),vj=gi(),K7=Tk();function H7(e){return e&&e.__esModule?e:{default:e}}function gj(e,t){e!=null&&e.kind===U7.Kind.DOCUMENT||(0,V7.default)(0,"Must provide valid Document AST."),(t==null?void 0:t.assumeValid)!==!0&&(t==null?void 0:t.assumeValidSDL)!==!0&&(0,Q7.assertValidSDL)(e);var r={description:void 0,types:[],directives:[],extensions:void 0,extensionASTNodes:[],assumeValid:!1},n=(0,K7.extendSchemaImpl)(r,e,t);if(n.astNode==null)for(var i=0,o=n.types;i{"use strict";Object.defineProperty(Sk,"__esModule",{value:!0});Sk.lexicographicSortSchema=nJ;var W7=Ep(Ni()),Y7=Ep(jt()),J7=Ep(_n()),X7=Ep(Vd()),Z7=Ep(Ud()),$7=ks(),eJ=gi(),tJ=vi(),ri=bt();function Ep(e){return e&&e.__esModule?e:{default:e}}function yj(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Mr(e){for(var t=1;t{"use strict";Object.defineProperty(Sp,"__esModule",{value:!0});Sp.printSchema=oJ;Sp.printIntrospectionSchema=uJ;Sp.printType=Sj;var kk=xk(Ni()),iJ=xk(jt()),Tj=xk(_n()),Ok=hi(),aJ=ec(),_j=vi(),wk=Ga(),Nk=gi(),Oc=bt(),Dk=Zd();function xk(e){return e&&e.__esModule?e:{default:e}}function oJ(e,t){return Ej(e,function(r){return!(0,Nk.isSpecifiedDirective)(r)},sJ,t)}function uJ(e,t){return Ej(e,Nk.isSpecifiedDirective,_j.isIntrospectionType,t)}function sJ(e){return!(0,wk.isSpecifiedScalarType)(e)&&!(0,_j.isIntrospectionType)(e)}function Ej(e,t,r,n){var i=e.getDirectives().filter(t),o=(0,kk.default)(e.getTypeMap()).filter(r);return[lJ(e)].concat(i.map(function(s){return mJ(s,n)}),o.map(function(s){return Sj(s,n)})).filter(Boolean).join(`
-
-`)+`
-`}function lJ(e){if(!(e.description==null&&cJ(e))){var t=[],r=e.getQueryType();r&&t.push(" query: ".concat(r.name));var n=e.getMutationType();n&&t.push(" mutation: ".concat(n.name));var i=e.getSubscriptionType();return i&&t.push(" subscription: ".concat(i.name)),Xi({},e)+`schema {
-`.concat(t.join(`
-`),`
-}`)}}function cJ(e){var t=e.getQueryType();if(t&&t.name!=="Query")return!1;var r=e.getMutationType();if(r&&r.name!=="Mutation")return!1;var n=e.getSubscriptionType();return!(n&&n.name!=="Subscription")}function Sj(e,t){if((0,Oc.isScalarType)(e))return fJ(e,t);if((0,Oc.isObjectType)(e))return dJ(e,t);if((0,Oc.isInterfaceType)(e))return pJ(e,t);if((0,Oc.isUnionType)(e))return hJ(e,t);if((0,Oc.isEnumType)(e))return vJ(e,t);if((0,Oc.isInputObjectType)(e))return gJ(e,t);(0,Tj.default)(0,"Unexpected type: "+(0,iJ.default)(e))}function fJ(e,t){return Xi(t,e)+"scalar ".concat(e.name)+yJ(e)}function kj(e){var t=e.getInterfaces();return t.length?" implements "+t.map(function(r){return r.name}).join(" & "):""}function dJ(e,t){return Xi(t,e)+"type ".concat(e.name)+kj(e)+Oj(t,e)}function pJ(e,t){return Xi(t,e)+"interface ".concat(e.name)+kj(e)+Oj(t,e)}function hJ(e,t){var r=e.getTypes(),n=r.length?" = "+r.join(" | "):"";return Xi(t,e)+"union "+e.name+n}function vJ(e,t){var r=e.getValues().map(function(n,i){return Xi(t,n," ",!i)+" "+n.name+Ik(n.deprecationReason)});return Xi(t,e)+"enum ".concat(e.name)+Ck(r)}function gJ(e,t){var r=(0,kk.default)(e.getFields()).map(function(n,i){return Xi(t,n," ",!i)+" "+Lk(n)});return Xi(t,e)+"input ".concat(e.name)+Ck(r)}function Oj(e,t){var r=(0,kk.default)(t.getFields()).map(function(n,i){return Xi(e,n," ",!i)+" "+n.name+wj(e,n.args," ")+": "+String(n.type)+Ik(n.deprecationReason)});return Ck(r)}function Ck(e){return e.length!==0?` {
-`+e.join(`
-`)+`
-}`:""}function wj(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:"";return t.length===0?"":t.every(function(n){return!n.description})?"("+t.map(Lk).join(", ")+")":`(
-`+t.map(function(n,i){return Xi(e,n," "+r,!i)+" "+r+Lk(n)}).join(`
-`)+`
-`+r+")"}function Lk(e){var t=(0,Dk.astFromValue)(e.defaultValue,e.type),r=e.name+": "+String(e.type);return t&&(r+=" = ".concat((0,Ok.print)(t))),r+Ik(e.deprecationReason)}function mJ(e,t){return Xi(t,e)+"directive @"+e.name+wj(t,e.args)+(e.isRepeatable?" repeatable":"")+" on "+e.locations.join(" | ")}function Ik(e){if(e==null)return"";var t=(0,Dk.astFromValue)(e,wk.GraphQLString);return t&&e!==Nk.DEFAULT_DEPRECATION_REASON?" @deprecated(reason: "+(0,Ok.print)(t)+")":" @deprecated"}function yJ(e){if(e.specifiedByUrl==null)return"";var t=e.specifiedByUrl,r=(0,Dk.astFromValue)(t,wk.GraphQLString);return r||(0,Tj.default)(0,"Unexpected null value returned from `astFromValue` for specifiedByUrl")," @specifiedBy(url: "+(0,Ok.print)(r)+")"}function Xi(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:"",n=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!0,i=t.description;if(i==null)return"";if((e==null?void 0:e.commentDescriptions)===!0)return bJ(i,r,n);var o=i.length>70,s=(0,aJ.printBlockString)(i,"",o),l=r&&!n?`
-`+r:r;return l+s.replace(/\n/g,`
-`+r)+`
-`}function bJ(e,t,r){var n=t&&!r?`
-`:"",i=e.split(`
-`).map(function(o){return t+(o!==""?"# "+o:"#")}).join(`
-`);return n+i+`
-`}});var Dj=G(Ak=>{"use strict";Object.defineProperty(Ak,"__esModule",{value:!0});Ak.concatAST=TJ;function TJ(e){for(var t=[],r=0;r{"use strict";Object.defineProperty(Rk,"__esModule",{value:!0});Rk.separateOperations=EJ;var Om=Jt(),_J=hu();function EJ(e){for(var t=[],r=Object.create(null),n=0,i=e.definitions;n{"use strict";Object.defineProperty(Pk,"__esModule",{value:!0});Pk.stripIgnoredCharacters=SJ;var Ij=mg(),jk=Zl(),Aj=Tg(),Rj=ec();function SJ(e){for(var t=(0,Ij.isSource)(e)?e:new Ij.Source(e),r=t.body,n=new Aj.Lexer(t),i="",o=!1;n.advance().kind!==jk.TokenKind.EOF;){var s=n.token,l=s.kind,d=!(0,Aj.isPunctuatorTokenKind)(s.kind);o&&(d||s.kind===jk.TokenKind.SPREAD)&&(i+=" ");var h=r.slice(s.start,s.end);l===jk.TokenKind.BLOCK_STRING?i+=kJ(h):i+=h,o=d}return i}function kJ(e){var t=e.slice(3,-3),r=(0,Rj.dedentBlockStringValue)(t);(0,Rj.getBlockStringIndentation)(r)>0&&(r=`
-`+r);var n=r[r.length-1],i=n==='"'&&r.slice(-4)!=='\\"""';return(i||n==="\\")&&(r+=`
-`),'"""'+r+'"""'}});var Kj=G(xu=>{"use strict";Object.defineProperty(xu,"__esModule",{value:!0});xu.findBreakingChanges=IJ;xu.findDangerousChanges=AJ;xu.DangerousChangeType=xu.BreakingChangeType=void 0;var wc=kp(Ni()),Pj=kp(vu()),OJ=kp(jt()),Fj=kp(_n()),wJ=kp(Ud()),NJ=hi(),DJ=hu(),xJ=Ga(),Ct=bt(),CJ=Zd();function kp(e){return e&&e.__esModule?e:{default:e}}function Mj(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function qj(e){for(var t=1;t{"use strict";Object.defineProperty(Fk,"__esModule",{value:!0});Fk.findDeprecatedUsages=GJ;var VJ=mc(),UJ=fk();function GJ(e,t){return(0,VJ.validate)(e,t,[UJ.NoDeprecatedCustomRule])}});var Xj=G(yt=>{"use strict";Object.defineProperty(yt,"__esModule",{value:!0});Object.defineProperty(yt,"getIntrospectionQuery",{enumerable:!0,get:function(){return QJ.getIntrospectionQuery}});Object.defineProperty(yt,"getOperationAST",{enumerable:!0,get:function(){return BJ.getOperationAST}});Object.defineProperty(yt,"getOperationRootType",{enumerable:!0,get:function(){return KJ.getOperationRootType}});Object.defineProperty(yt,"introspectionFromSchema",{enumerable:!0,get:function(){return HJ.introspectionFromSchema}});Object.defineProperty(yt,"buildClientSchema",{enumerable:!0,get:function(){return zJ.buildClientSchema}});Object.defineProperty(yt,"buildASTSchema",{enumerable:!0,get:function(){return zj.buildASTSchema}});Object.defineProperty(yt,"buildSchema",{enumerable:!0,get:function(){return zj.buildSchema}});Object.defineProperty(yt,"extendSchema",{enumerable:!0,get:function(){return Wj.extendSchema}});Object.defineProperty(yt,"getDescription",{enumerable:!0,get:function(){return Wj.getDescription}});Object.defineProperty(yt,"lexicographicSortSchema",{enumerable:!0,get:function(){return WJ.lexicographicSortSchema}});Object.defineProperty(yt,"printSchema",{enumerable:!0,get:function(){return Mk.printSchema}});Object.defineProperty(yt,"printType",{enumerable:!0,get:function(){return Mk.printType}});Object.defineProperty(yt,"printIntrospectionSchema",{enumerable:!0,get:function(){return Mk.printIntrospectionSchema}});Object.defineProperty(yt,"typeFromAST",{enumerable:!0,get:function(){return YJ.typeFromAST}});Object.defineProperty(yt,"valueFromAST",{enumerable:!0,get:function(){return JJ.valueFromAST}});Object.defineProperty(yt,"valueFromASTUntyped",{enumerable:!0,get:function(){return XJ.valueFromASTUntyped}});Object.defineProperty(yt,"astFromValue",{enumerable:!0,get:function(){return ZJ.astFromValue}});Object.defineProperty(yt,"TypeInfo",{enumerable:!0,get:function(){return Yj.TypeInfo}});Object.defineProperty(yt,"visitWithTypeInfo",{enumerable:!0,get:function(){return Yj.visitWithTypeInfo}});Object.defineProperty(yt,"coerceInputValue",{enumerable:!0,get:function(){return $J.coerceInputValue}});Object.defineProperty(yt,"concatAST",{enumerable:!0,get:function(){return eX.concatAST}});Object.defineProperty(yt,"separateOperations",{enumerable:!0,get:function(){return tX.separateOperations}});Object.defineProperty(yt,"stripIgnoredCharacters",{enumerable:!0,get:function(){return rX.stripIgnoredCharacters}});Object.defineProperty(yt,"isEqualType",{enumerable:!0,get:function(){return qk.isEqualType}});Object.defineProperty(yt,"isTypeSubTypeOf",{enumerable:!0,get:function(){return qk.isTypeSubTypeOf}});Object.defineProperty(yt,"doTypesOverlap",{enumerable:!0,get:function(){return qk.doTypesOverlap}});Object.defineProperty(yt,"assertValidName",{enumerable:!0,get:function(){return Jj.assertValidName}});Object.defineProperty(yt,"isValidNameError",{enumerable:!0,get:function(){return Jj.isValidNameError}});Object.defineProperty(yt,"BreakingChangeType",{enumerable:!0,get:function(){return wm.BreakingChangeType}});Object.defineProperty(yt,"DangerousChangeType",{enumerable:!0,get:function(){return wm.DangerousChangeType}});Object.defineProperty(yt,"findBreakingChanges",{enumerable:!0,get:function(){return wm.findBreakingChanges}});Object.defineProperty(yt,"findDangerousChanges",{enumerable:!0,get:function(){return wm.findDangerousChanges}});Object.defineProperty(yt,"findDeprecatedUsages",{enumerable:!0,get:function(){return nX.findDeprecatedUsages}});var QJ=vk(),BJ=mk(),KJ=um(),HJ=ej(),zJ=rj(),zj=mj(),Wj=Tk(),WJ=bj(),Mk=Nj(),YJ=Qa(),JJ=lp(),XJ=M_(),ZJ=Zd(),Yj=zg(),$J=XS(),eX=Dj(),tX=Lj(),rX=jj(),qk=Hd(),Jj=S_(),wm=Kj(),nX=Hj()});var ht=G(Z=>{"use strict";Object.defineProperty(Z,"__esModule",{value:!0});Object.defineProperty(Z,"version",{enumerable:!0,get:function(){return Zj.version}});Object.defineProperty(Z,"versionInfo",{enumerable:!0,get:function(){return Zj.versionInfo}});Object.defineProperty(Z,"graphql",{enumerable:!0,get:function(){return $j.graphql}});Object.defineProperty(Z,"graphqlSync",{enumerable:!0,get:function(){return $j.graphqlSync}});Object.defineProperty(Z,"GraphQLSchema",{enumerable:!0,get:function(){return Oe.GraphQLSchema}});Object.defineProperty(Z,"GraphQLDirective",{enumerable:!0,get:function(){return Oe.GraphQLDirective}});Object.defineProperty(Z,"GraphQLScalarType",{enumerable:!0,get:function(){return Oe.GraphQLScalarType}});Object.defineProperty(Z,"GraphQLObjectType",{enumerable:!0,get:function(){return Oe.GraphQLObjectType}});Object.defineProperty(Z,"GraphQLInterfaceType",{enumerable:!0,get:function(){return Oe.GraphQLInterfaceType}});Object.defineProperty(Z,"GraphQLUnionType",{enumerable:!0,get:function(){return Oe.GraphQLUnionType}});Object.defineProperty(Z,"GraphQLEnumType",{enumerable:!0,get:function(){return Oe.GraphQLEnumType}});Object.defineProperty(Z,"GraphQLInputObjectType",{enumerable:!0,get:function(){return Oe.GraphQLInputObjectType}});Object.defineProperty(Z,"GraphQLList",{enumerable:!0,get:function(){return Oe.GraphQLList}});Object.defineProperty(Z,"GraphQLNonNull",{enumerable:!0,get:function(){return Oe.GraphQLNonNull}});Object.defineProperty(Z,"specifiedScalarTypes",{enumerable:!0,get:function(){return Oe.specifiedScalarTypes}});Object.defineProperty(Z,"GraphQLInt",{enumerable:!0,get:function(){return Oe.GraphQLInt}});Object.defineProperty(Z,"GraphQLFloat",{enumerable:!0,get:function(){return Oe.GraphQLFloat}});Object.defineProperty(Z,"GraphQLString",{enumerable:!0,get:function(){return Oe.GraphQLString}});Object.defineProperty(Z,"GraphQLBoolean",{enumerable:!0,get:function(){return Oe.GraphQLBoolean}});Object.defineProperty(Z,"GraphQLID",{enumerable:!0,get:function(){return Oe.GraphQLID}});Object.defineProperty(Z,"specifiedDirectives",{enumerable:!0,get:function(){return Oe.specifiedDirectives}});Object.defineProperty(Z,"GraphQLIncludeDirective",{enumerable:!0,get:function(){return Oe.GraphQLIncludeDirective}});Object.defineProperty(Z,"GraphQLSkipDirective",{enumerable:!0,get:function(){return Oe.GraphQLSkipDirective}});Object.defineProperty(Z,"GraphQLDeprecatedDirective",{enumerable:!0,get:function(){return Oe.GraphQLDeprecatedDirective}});Object.defineProperty(Z,"GraphQLSpecifiedByDirective",{enumerable:!0,get:function(){return Oe.GraphQLSpecifiedByDirective}});Object.defineProperty(Z,"TypeKind",{enumerable:!0,get:function(){return Oe.TypeKind}});Object.defineProperty(Z,"DEFAULT_DEPRECATION_REASON",{enumerable:!0,get:function(){return Oe.DEFAULT_DEPRECATION_REASON}});Object.defineProperty(Z,"introspectionTypes",{enumerable:!0,get:function(){return Oe.introspectionTypes}});Object.defineProperty(Z,"__Schema",{enumerable:!0,get:function(){return Oe.__Schema}});Object.defineProperty(Z,"__Directive",{enumerable:!0,get:function(){return Oe.__Directive}});Object.defineProperty(Z,"__DirectiveLocation",{enumerable:!0,get:function(){return Oe.__DirectiveLocation}});Object.defineProperty(Z,"__Type",{enumerable:!0,get:function(){return Oe.__Type}});Object.defineProperty(Z,"__Field",{enumerable:!0,get:function(){return Oe.__Field}});Object.defineProperty(Z,"__InputValue",{enumerable:!0,get:function(){return Oe.__InputValue}});Object.defineProperty(Z,"__EnumValue",{enumerable:!0,get:function(){return Oe.__EnumValue}});Object.defineProperty(Z,"__TypeKind",{enumerable:!0,get:function(){return Oe.__TypeKind}});Object.defineProperty(Z,"SchemaMetaFieldDef",{enumerable:!0,get:function(){return Oe.SchemaMetaFieldDef}});Object.defineProperty(Z,"TypeMetaFieldDef",{enumerable:!0,get:function(){return Oe.TypeMetaFieldDef}});Object.defineProperty(Z,"TypeNameMetaFieldDef",{enumerable:!0,get:function(){return Oe.TypeNameMetaFieldDef}});Object.defineProperty(Z,"isSchema",{enumerable:!0,get:function(){return Oe.isSchema}});Object.defineProperty(Z,"isDirective",{enumerable:!0,get:function(){return Oe.isDirective}});Object.defineProperty(Z,"isType",{enumerable:!0,get:function(){return Oe.isType}});Object.defineProperty(Z,"isScalarType",{enumerable:!0,get:function(){return Oe.isScalarType}});Object.defineProperty(Z,"isObjectType",{enumerable:!0,get:function(){return Oe.isObjectType}});Object.defineProperty(Z,"isInterfaceType",{enumerable:!0,get:function(){return Oe.isInterfaceType}});Object.defineProperty(Z,"isUnionType",{enumerable:!0,get:function(){return Oe.isUnionType}});Object.defineProperty(Z,"isEnumType",{enumerable:!0,get:function(){return Oe.isEnumType}});Object.defineProperty(Z,"isInputObjectType",{enumerable:!0,get:function(){return Oe.isInputObjectType}});Object.defineProperty(Z,"isListType",{enumerable:!0,get:function(){return Oe.isListType}});Object.defineProperty(Z,"isNonNullType",{enumerable:!0,get:function(){return Oe.isNonNullType}});Object.defineProperty(Z,"isInputType",{enumerable:!0,get:function(){return Oe.isInputType}});Object.defineProperty(Z,"isOutputType",{enumerable:!0,get:function(){return Oe.isOutputType}});Object.defineProperty(Z,"isLeafType",{enumerable:!0,get:function(){return Oe.isLeafType}});Object.defineProperty(Z,"isCompositeType",{enumerable:!0,get:function(){return Oe.isCompositeType}});Object.defineProperty(Z,"isAbstractType",{enumerable:!0,get:function(){return Oe.isAbstractType}});Object.defineProperty(Z,"isWrappingType",{enumerable:!0,get:function(){return Oe.isWrappingType}});Object.defineProperty(Z,"isNullableType",{enumerable:!0,get:function(){return Oe.isNullableType}});Object.defineProperty(Z,"isNamedType",{enumerable:!0,get:function(){return Oe.isNamedType}});Object.defineProperty(Z,"isRequiredArgument",{enumerable:!0,get:function(){return Oe.isRequiredArgument}});Object.defineProperty(Z,"isRequiredInputField",{enumerable:!0,get:function(){return Oe.isRequiredInputField}});Object.defineProperty(Z,"isSpecifiedScalarType",{enumerable:!0,get:function(){return Oe.isSpecifiedScalarType}});Object.defineProperty(Z,"isIntrospectionType",{enumerable:!0,get:function(){return Oe.isIntrospectionType}});Object.defineProperty(Z,"isSpecifiedDirective",{enumerable:!0,get:function(){return Oe.isSpecifiedDirective}});Object.defineProperty(Z,"assertSchema",{enumerable:!0,get:function(){return Oe.assertSchema}});Object.defineProperty(Z,"assertDirective",{enumerable:!0,get:function(){return Oe.assertDirective}});Object.defineProperty(Z,"assertType",{enumerable:!0,get:function(){return Oe.assertType}});Object.defineProperty(Z,"assertScalarType",{enumerable:!0,get:function(){return Oe.assertScalarType}});Object.defineProperty(Z,"assertObjectType",{enumerable:!0,get:function(){return Oe.assertObjectType}});Object.defineProperty(Z,"assertInterfaceType",{enumerable:!0,get:function(){return Oe.assertInterfaceType}});Object.defineProperty(Z,"assertUnionType",{enumerable:!0,get:function(){return Oe.assertUnionType}});Object.defineProperty(Z,"assertEnumType",{enumerable:!0,get:function(){return Oe.assertEnumType}});Object.defineProperty(Z,"assertInputObjectType",{enumerable:!0,get:function(){return Oe.assertInputObjectType}});Object.defineProperty(Z,"assertListType",{enumerable:!0,get:function(){return Oe.assertListType}});Object.defineProperty(Z,"assertNonNullType",{enumerable:!0,get:function(){return Oe.assertNonNullType}});Object.defineProperty(Z,"assertInputType",{enumerable:!0,get:function(){return Oe.assertInputType}});Object.defineProperty(Z,"assertOutputType",{enumerable:!0,get:function(){return Oe.assertOutputType}});Object.defineProperty(Z,"assertLeafType",{enumerable:!0,get:function(){return Oe.assertLeafType}});Object.defineProperty(Z,"assertCompositeType",{enumerable:!0,get:function(){return Oe.assertCompositeType}});Object.defineProperty(Z,"assertAbstractType",{enumerable:!0,get:function(){return Oe.assertAbstractType}});Object.defineProperty(Z,"assertWrappingType",{enumerable:!0,get:function(){return Oe.assertWrappingType}});Object.defineProperty(Z,"assertNullableType",{enumerable:!0,get:function(){return Oe.assertNullableType}});Object.defineProperty(Z,"assertNamedType",{enumerable:!0,get:function(){return Oe.assertNamedType}});Object.defineProperty(Z,"getNullableType",{enumerable:!0,get:function(){return Oe.getNullableType}});Object.defineProperty(Z,"getNamedType",{enumerable:!0,get:function(){return Oe.getNamedType}});Object.defineProperty(Z,"validateSchema",{enumerable:!0,get:function(){return Oe.validateSchema}});Object.defineProperty(Z,"assertValidSchema",{enumerable:!0,get:function(){return Oe.assertValidSchema}});Object.defineProperty(Z,"Token",{enumerable:!0,get:function(){return Xt.Token}});Object.defineProperty(Z,"Source",{enumerable:!0,get:function(){return Xt.Source}});Object.defineProperty(Z,"Location",{enumerable:!0,get:function(){return Xt.Location}});Object.defineProperty(Z,"getLocation",{enumerable:!0,get:function(){return Xt.getLocation}});Object.defineProperty(Z,"printLocation",{enumerable:!0,get:function(){return Xt.printLocation}});Object.defineProperty(Z,"printSourceLocation",{enumerable:!0,get:function(){return Xt.printSourceLocation}});Object.defineProperty(Z,"Lexer",{enumerable:!0,get:function(){return Xt.Lexer}});Object.defineProperty(Z,"TokenKind",{enumerable:!0,get:function(){return Xt.TokenKind}});Object.defineProperty(Z,"parse",{enumerable:!0,get:function(){return Xt.parse}});Object.defineProperty(Z,"parseValue",{enumerable:!0,get:function(){return Xt.parseValue}});Object.defineProperty(Z,"parseType",{enumerable:!0,get:function(){return Xt.parseType}});Object.defineProperty(Z,"print",{enumerable:!0,get:function(){return Xt.print}});Object.defineProperty(Z,"visit",{enumerable:!0,get:function(){return Xt.visit}});Object.defineProperty(Z,"visitInParallel",{enumerable:!0,get:function(){return Xt.visitInParallel}});Object.defineProperty(Z,"getVisitFn",{enumerable:!0,get:function(){return Xt.getVisitFn}});Object.defineProperty(Z,"BREAK",{enumerable:!0,get:function(){return Xt.BREAK}});Object.defineProperty(Z,"Kind",{enumerable:!0,get:function(){return Xt.Kind}});Object.defineProperty(Z,"DirectiveLocation",{enumerable:!0,get:function(){return Xt.DirectiveLocation}});Object.defineProperty(Z,"isDefinitionNode",{enumerable:!0,get:function(){return Xt.isDefinitionNode}});Object.defineProperty(Z,"isExecutableDefinitionNode",{enumerable:!0,get:function(){return Xt.isExecutableDefinitionNode}});Object.defineProperty(Z,"isSelectionNode",{enumerable:!0,get:function(){return Xt.isSelectionNode}});Object.defineProperty(Z,"isValueNode",{enumerable:!0,get:function(){return Xt.isValueNode}});Object.defineProperty(Z,"isTypeNode",{enumerable:!0,get:function(){return Xt.isTypeNode}});Object.defineProperty(Z,"isTypeSystemDefinitionNode",{enumerable:!0,get:function(){return Xt.isTypeSystemDefinitionNode}});Object.defineProperty(Z,"isTypeDefinitionNode",{enumerable:!0,get:function(){return Xt.isTypeDefinitionNode}});Object.defineProperty(Z,"isTypeSystemExtensionNode",{enumerable:!0,get:function(){return Xt.isTypeSystemExtensionNode}});Object.defineProperty(Z,"isTypeExtensionNode",{enumerable:!0,get:function(){return Xt.isTypeExtensionNode}});Object.defineProperty(Z,"execute",{enumerable:!0,get:function(){return Nc.execute}});Object.defineProperty(Z,"executeSync",{enumerable:!0,get:function(){return Nc.executeSync}});Object.defineProperty(Z,"defaultFieldResolver",{enumerable:!0,get:function(){return Nc.defaultFieldResolver}});Object.defineProperty(Z,"defaultTypeResolver",{enumerable:!0,get:function(){return Nc.defaultTypeResolver}});Object.defineProperty(Z,"responsePathAsArray",{enumerable:!0,get:function(){return Nc.responsePathAsArray}});Object.defineProperty(Z,"getDirectiveValues",{enumerable:!0,get:function(){return Nc.getDirectiveValues}});Object.defineProperty(Z,"subscribe",{enumerable:!0,get:function(){return eP.subscribe}});Object.defineProperty(Z,"createSourceEventStream",{enumerable:!0,get:function(){return eP.createSourceEventStream}});Object.defineProperty(Z,"validate",{enumerable:!0,get:function(){return pt.validate}});Object.defineProperty(Z,"ValidationContext",{enumerable:!0,get:function(){return pt.ValidationContext}});Object.defineProperty(Z,"specifiedRules",{enumerable:!0,get:function(){return pt.specifiedRules}});Object.defineProperty(Z,"ExecutableDefinitionsRule",{enumerable:!0,get:function(){return pt.ExecutableDefinitionsRule}});Object.defineProperty(Z,"FieldsOnCorrectTypeRule",{enumerable:!0,get:function(){return pt.FieldsOnCorrectTypeRule}});Object.defineProperty(Z,"FragmentsOnCompositeTypesRule",{enumerable:!0,get:function(){return pt.FragmentsOnCompositeTypesRule}});Object.defineProperty(Z,"KnownArgumentNamesRule",{enumerable:!0,get:function(){return pt.KnownArgumentNamesRule}});Object.defineProperty(Z,"KnownDirectivesRule",{enumerable:!0,get:function(){return pt.KnownDirectivesRule}});Object.defineProperty(Z,"KnownFragmentNamesRule",{enumerable:!0,get:function(){return pt.KnownFragmentNamesRule}});Object.defineProperty(Z,"KnownTypeNamesRule",{enumerable:!0,get:function(){return pt.KnownTypeNamesRule}});Object.defineProperty(Z,"LoneAnonymousOperationRule",{enumerable:!0,get:function(){return pt.LoneAnonymousOperationRule}});Object.defineProperty(Z,"NoFragmentCyclesRule",{enumerable:!0,get:function(){return pt.NoFragmentCyclesRule}});Object.defineProperty(Z,"NoUndefinedVariablesRule",{enumerable:!0,get:function(){return pt.NoUndefinedVariablesRule}});Object.defineProperty(Z,"NoUnusedFragmentsRule",{enumerable:!0,get:function(){return pt.NoUnusedFragmentsRule}});Object.defineProperty(Z,"NoUnusedVariablesRule",{enumerable:!0,get:function(){return pt.NoUnusedVariablesRule}});Object.defineProperty(Z,"OverlappingFieldsCanBeMergedRule",{enumerable:!0,get:function(){return pt.OverlappingFieldsCanBeMergedRule}});Object.defineProperty(Z,"PossibleFragmentSpreadsRule",{enumerable:!0,get:function(){return pt.PossibleFragmentSpreadsRule}});Object.defineProperty(Z,"ProvidedRequiredArgumentsRule",{enumerable:!0,get:function(){return pt.ProvidedRequiredArgumentsRule}});Object.defineProperty(Z,"ScalarLeafsRule",{enumerable:!0,get:function(){return pt.ScalarLeafsRule}});Object.defineProperty(Z,"SingleFieldSubscriptionsRule",{enumerable:!0,get:function(){return pt.SingleFieldSubscriptionsRule}});Object.defineProperty(Z,"UniqueArgumentNamesRule",{enumerable:!0,get:function(){return pt.UniqueArgumentNamesRule}});Object.defineProperty(Z,"UniqueDirectivesPerLocationRule",{enumerable:!0,get:function(){return pt.UniqueDirectivesPerLocationRule}});Object.defineProperty(Z,"UniqueFragmentNamesRule",{enumerable:!0,get:function(){return pt.UniqueFragmentNamesRule}});Object.defineProperty(Z,"UniqueInputFieldNamesRule",{enumerable:!0,get:function(){return pt.UniqueInputFieldNamesRule}});Object.defineProperty(Z,"UniqueOperationNamesRule",{enumerable:!0,get:function(){return pt.UniqueOperationNamesRule}});Object.defineProperty(Z,"UniqueVariableNamesRule",{enumerable:!0,get:function(){return pt.UniqueVariableNamesRule}});Object.defineProperty(Z,"ValuesOfCorrectTypeRule",{enumerable:!0,get:function(){return pt.ValuesOfCorrectTypeRule}});Object.defineProperty(Z,"VariablesAreInputTypesRule",{enumerable:!0,get:function(){return pt.VariablesAreInputTypesRule}});Object.defineProperty(Z,"VariablesInAllowedPositionRule",{enumerable:!0,get:function(){return pt.VariablesInAllowedPositionRule}});Object.defineProperty(Z,"LoneSchemaDefinitionRule",{enumerable:!0,get:function(){return pt.LoneSchemaDefinitionRule}});Object.defineProperty(Z,"UniqueOperationTypesRule",{enumerable:!0,get:function(){return pt.UniqueOperationTypesRule}});Object.defineProperty(Z,"UniqueTypeNamesRule",{enumerable:!0,get:function(){return pt.UniqueTypeNamesRule}});Object.defineProperty(Z,"UniqueEnumValueNamesRule",{enumerable:!0,get:function(){return pt.UniqueEnumValueNamesRule}});Object.defineProperty(Z,"UniqueFieldDefinitionNamesRule",{enumerable:!0,get:function(){return pt.UniqueFieldDefinitionNamesRule}});Object.defineProperty(Z,"UniqueDirectiveNamesRule",{enumerable:!0,get:function(){return pt.UniqueDirectiveNamesRule}});Object.defineProperty(Z,"PossibleTypeExtensionsRule",{enumerable:!0,get:function(){return pt.PossibleTypeExtensionsRule}});Object.defineProperty(Z,"NoDeprecatedCustomRule",{enumerable:!0,get:function(){return pt.NoDeprecatedCustomRule}});Object.defineProperty(Z,"NoSchemaIntrospectionCustomRule",{enumerable:!0,get:function(){return pt.NoSchemaIntrospectionCustomRule}});Object.defineProperty(Z,"GraphQLError",{enumerable:!0,get:function(){return Np.GraphQLError}});Object.defineProperty(Z,"syntaxError",{enumerable:!0,get:function(){return Np.syntaxError}});Object.defineProperty(Z,"locatedError",{enumerable:!0,get:function(){return Np.locatedError}});Object.defineProperty(Z,"printError",{enumerable:!0,get:function(){return Np.printError}});Object.defineProperty(Z,"formatError",{enumerable:!0,get:function(){return Np.formatError}});Object.defineProperty(Z,"getIntrospectionQuery",{enumerable:!0,get:function(){return St.getIntrospectionQuery}});Object.defineProperty(Z,"getOperationAST",{enumerable:!0,get:function(){return St.getOperationAST}});Object.defineProperty(Z,"getOperationRootType",{enumerable:!0,get:function(){return St.getOperationRootType}});Object.defineProperty(Z,"introspectionFromSchema",{enumerable:!0,get:function(){return St.introspectionFromSchema}});Object.defineProperty(Z,"buildClientSchema",{enumerable:!0,get:function(){return St.buildClientSchema}});Object.defineProperty(Z,"buildASTSchema",{enumerable:!0,get:function(){return St.buildASTSchema}});Object.defineProperty(Z,"buildSchema",{enumerable:!0,get:function(){return St.buildSchema}});Object.defineProperty(Z,"getDescription",{enumerable:!0,get:function(){return St.getDescription}});Object.defineProperty(Z,"extendSchema",{enumerable:!0,get:function(){return St.extendSchema}});Object.defineProperty(Z,"lexicographicSortSchema",{enumerable:!0,get:function(){return St.lexicographicSortSchema}});Object.defineProperty(Z,"printSchema",{enumerable:!0,get:function(){return St.printSchema}});Object.defineProperty(Z,"printType",{enumerable:!0,get:function(){return St.printType}});Object.defineProperty(Z,"printIntrospectionSchema",{enumerable:!0,get:function(){return St.printIntrospectionSchema}});Object.defineProperty(Z,"typeFromAST",{enumerable:!0,get:function(){return St.typeFromAST}});Object.defineProperty(Z,"valueFromAST",{enumerable:!0,get:function(){return St.valueFromAST}});Object.defineProperty(Z,"valueFromASTUntyped",{enumerable:!0,get:function(){return St.valueFromASTUntyped}});Object.defineProperty(Z,"astFromValue",{enumerable:!0,get:function(){return St.astFromValue}});Object.defineProperty(Z,"TypeInfo",{enumerable:!0,get:function(){return St.TypeInfo}});Object.defineProperty(Z,"visitWithTypeInfo",{enumerable:!0,get:function(){return St.visitWithTypeInfo}});Object.defineProperty(Z,"coerceInputValue",{enumerable:!0,get:function(){return St.coerceInputValue}});Object.defineProperty(Z,"concatAST",{enumerable:!0,get:function(){return St.concatAST}});Object.defineProperty(Z,"separateOperations",{enumerable:!0,get:function(){return St.separateOperations}});Object.defineProperty(Z,"stripIgnoredCharacters",{enumerable:!0,get:function(){return St.stripIgnoredCharacters}});Object.defineProperty(Z,"isEqualType",{enumerable:!0,get:function(){return St.isEqualType}});Object.defineProperty(Z,"isTypeSubTypeOf",{enumerable:!0,get:function(){return St.isTypeSubTypeOf}});Object.defineProperty(Z,"doTypesOverlap",{enumerable:!0,get:function(){return St.doTypesOverlap}});Object.defineProperty(Z,"assertValidName",{enumerable:!0,get:function(){return St.assertValidName}});Object.defineProperty(Z,"isValidNameError",{enumerable:!0,get:function(){return St.isValidNameError}});Object.defineProperty(Z,"BreakingChangeType",{enumerable:!0,get:function(){return St.BreakingChangeType}});Object.defineProperty(Z,"DangerousChangeType",{enumerable:!0,get:function(){return St.DangerousChangeType}});Object.defineProperty(Z,"findBreakingChanges",{enumerable:!0,get:function(){return St.findBreakingChanges}});Object.defineProperty(Z,"findDangerousChanges",{enumerable:!0,get:function(){return St.findDangerousChanges}});Object.defineProperty(Z,"findDeprecatedUsages",{enumerable:!0,get:function(){return St.findDeprecatedUsages}});var Zj=m1(),$j=wR(),Oe=DR(),Xt=LR(),Nc=IR(),eP=HR(),pt=WR(),Np=XR(),St=Xj()});var rP=G((Xoe,tP)=>{tP.exports=function(){var e=document.getSelection();if(!e.rangeCount)return function(){};for(var t=document.activeElement,r=[],n=0;n{"use strict";var iX=rP(),nP={"text/plain":"Text","text/html":"Url",default:"Text"},aX="Copy to clipboard: #{key}, Enter";function oX(e){var t=(/mac os x/i.test(navigator.userAgent)?"\u2318":"Ctrl")+"+C";return e.replace(/#{\s*key\s*}/g,t)}function uX(e,t){var r,n,i,o,s,l,d=!1;t||(t={}),r=t.debug||!1;try{i=iX(),o=document.createRange(),s=document.getSelection(),l=document.createElement("span"),l.textContent=e,l.style.all="unset",l.style.position="fixed",l.style.top=0,l.style.clip="rect(0, 0, 0, 0)",l.style.whiteSpace="pre",l.style.webkitUserSelect="text",l.style.MozUserSelect="text",l.style.msUserSelect="text",l.style.userSelect="text",l.addEventListener("copy",function(v){if(v.stopPropagation(),t.format)if(v.preventDefault(),typeof v.clipboardData=="undefined"){r&&console.warn("unable to use e.clipboardData"),r&&console.warn("trying IE specific stuff"),window.clipboardData.clearData();var y=nP[t.format]||nP.default;window.clipboardData.setData(y,e)}else v.clipboardData.clearData(),v.clipboardData.setData(t.format,e);t.onCopy&&(v.preventDefault(),t.onCopy(v.clipboardData))}),document.body.appendChild(l),o.selectNodeContents(l),s.addRange(o);var h=document.execCommand("copy");if(!h)throw new Error("copy command was unsuccessful");d=!0}catch(v){r&&console.error("unable to copy using execCommand: ",v),r&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(t.format||"text",e),t.onCopy&&t.onCopy(window.clipboardData),d=!0}catch(y){r&&console.error("unable to copy using clipboardData: ",y),r&&console.error("falling back to prompt"),n=oX("message"in t?t.message:aX),window.prompt(n,e)}}finally{s&&(typeof s.removeRange=="function"?s.removeRange(o):s.removeAllRanges()),l&&document.body.removeChild(l),i()}return d}iP.exports=uX});var Xk=G((Oue,Fm)=>{"use strict";function aF(e,t){if(e!=null)return e;var r=new Error(t!==void 0?t:"Got unexpected "+e);throw r.framesToPop=1,r}Fm.exports=aF;Fm.exports.default=aF;Object.defineProperty(Fm.exports,"__esModule",{value:!0})});var pF=G((Nse,xX)=>{xX.exports={Aacute:"\xC1",aacute:"\xE1",Abreve:"\u0102",abreve:"\u0103",ac:"\u223E",acd:"\u223F",acE:"\u223E\u0333",Acirc:"\xC2",acirc:"\xE2",acute:"\xB4",Acy:"\u0410",acy:"\u0430",AElig:"\xC6",aelig:"\xE6",af:"\u2061",Afr:"\u{1D504}",afr:"\u{1D51E}",Agrave:"\xC0",agrave:"\xE0",alefsym:"\u2135",aleph:"\u2135",Alpha:"\u0391",alpha:"\u03B1",Amacr:"\u0100",amacr:"\u0101",amalg:"\u2A3F",amp:"&",AMP:"&",andand:"\u2A55",And:"\u2A53",and:"\u2227",andd:"\u2A5C",andslope:"\u2A58",andv:"\u2A5A",ang:"\u2220",ange:"\u29A4",angle:"\u2220",angmsdaa:"\u29A8",angmsdab:"\u29A9",angmsdac:"\u29AA",angmsdad:"\u29AB",angmsdae:"\u29AC",angmsdaf:"\u29AD",angmsdag:"\u29AE",angmsdah:"\u29AF",angmsd:"\u2221",angrt:"\u221F",angrtvb:"\u22BE",angrtvbd:"\u299D",angsph:"\u2222",angst:"\xC5",angzarr:"\u237C",Aogon:"\u0104",aogon:"\u0105",Aopf:"\u{1D538}",aopf:"\u{1D552}",apacir:"\u2A6F",ap:"\u2248",apE:"\u2A70",ape:"\u224A",apid:"\u224B",apos:"'",ApplyFunction:"\u2061",approx:"\u2248",approxeq:"\u224A",Aring:"\xC5",aring:"\xE5",Ascr:"\u{1D49C}",ascr:"\u{1D4B6}",Assign:"\u2254",ast:"*",asymp:"\u2248",asympeq:"\u224D",Atilde:"\xC3",atilde:"\xE3",Auml:"\xC4",auml:"\xE4",awconint:"\u2233",awint:"\u2A11",backcong:"\u224C",backepsilon:"\u03F6",backprime:"\u2035",backsim:"\u223D",backsimeq:"\u22CD",Backslash:"\u2216",Barv:"\u2AE7",barvee:"\u22BD",barwed:"\u2305",Barwed:"\u2306",barwedge:"\u2305",bbrk:"\u23B5",bbrktbrk:"\u23B6",bcong:"\u224C",Bcy:"\u0411",bcy:"\u0431",bdquo:"\u201E",becaus:"\u2235",because:"\u2235",Because:"\u2235",bemptyv:"\u29B0",bepsi:"\u03F6",bernou:"\u212C",Bernoullis:"\u212C",Beta:"\u0392",beta:"\u03B2",beth:"\u2136",between:"\u226C",Bfr:"\u{1D505}",bfr:"\u{1D51F}",bigcap:"\u22C2",bigcirc:"\u25EF",bigcup:"\u22C3",bigodot:"\u2A00",bigoplus:"\u2A01",bigotimes:"\u2A02",bigsqcup:"\u2A06",bigstar:"\u2605",bigtriangledown:"\u25BD",bigtriangleup:"\u25B3",biguplus:"\u2A04",bigvee:"\u22C1",bigwedge:"\u22C0",bkarow:"\u290D",blacklozenge:"\u29EB",blacksquare:"\u25AA",blacktriangle:"\u25B4",blacktriangledown:"\u25BE",blacktriangleleft:"\u25C2",blacktriangleright:"\u25B8",blank:"\u2423",blk12:"\u2592",blk14:"\u2591",blk34:"\u2593",block:"\u2588",bne:"=\u20E5",bnequiv:"\u2261\u20E5",bNot:"\u2AED",bnot:"\u2310",Bopf:"\u{1D539}",bopf:"\u{1D553}",bot:"\u22A5",bottom:"\u22A5",bowtie:"\u22C8",boxbox:"\u29C9",boxdl:"\u2510",boxdL:"\u2555",boxDl:"\u2556",boxDL:"\u2557",boxdr:"\u250C",boxdR:"\u2552",boxDr:"\u2553",boxDR:"\u2554",boxh:"\u2500",boxH:"\u2550",boxhd:"\u252C",boxHd:"\u2564",boxhD:"\u2565",boxHD:"\u2566",boxhu:"\u2534",boxHu:"\u2567",boxhU:"\u2568",boxHU:"\u2569",boxminus:"\u229F",boxplus:"\u229E",boxtimes:"\u22A0",boxul:"\u2518",boxuL:"\u255B",boxUl:"\u255C",boxUL:"\u255D",boxur:"\u2514",boxuR:"\u2558",boxUr:"\u2559",boxUR:"\u255A",boxv:"\u2502",boxV:"\u2551",boxvh:"\u253C",boxvH:"\u256A",boxVh:"\u256B",boxVH:"\u256C",boxvl:"\u2524",boxvL:"\u2561",boxVl:"\u2562",boxVL:"\u2563",boxvr:"\u251C",boxvR:"\u255E",boxVr:"\u255F",boxVR:"\u2560",bprime:"\u2035",breve:"\u02D8",Breve:"\u02D8",brvbar:"\xA6",bscr:"\u{1D4B7}",Bscr:"\u212C",bsemi:"\u204F",bsim:"\u223D",bsime:"\u22CD",bsolb:"\u29C5",bsol:"\\",bsolhsub:"\u27C8",bull:"\u2022",bullet:"\u2022",bump:"\u224E",bumpE:"\u2AAE",bumpe:"\u224F",Bumpeq:"\u224E",bumpeq:"\u224F",Cacute:"\u0106",cacute:"\u0107",capand:"\u2A44",capbrcup:"\u2A49",capcap:"\u2A4B",cap:"\u2229",Cap:"\u22D2",capcup:"\u2A47",capdot:"\u2A40",CapitalDifferentialD:"\u2145",caps:"\u2229\uFE00",caret:"\u2041",caron:"\u02C7",Cayleys:"\u212D",ccaps:"\u2A4D",Ccaron:"\u010C",ccaron:"\u010D",Ccedil:"\xC7",ccedil:"\xE7",Ccirc:"\u0108",ccirc:"\u0109",Cconint:"\u2230",ccups:"\u2A4C",ccupssm:"\u2A50",Cdot:"\u010A",cdot:"\u010B",cedil:"\xB8",Cedilla:"\xB8",cemptyv:"\u29B2",cent:"\xA2",centerdot:"\xB7",CenterDot:"\xB7",cfr:"\u{1D520}",Cfr:"\u212D",CHcy:"\u0427",chcy:"\u0447",check:"\u2713",checkmark:"\u2713",Chi:"\u03A7",chi:"\u03C7",circ:"\u02C6",circeq:"\u2257",circlearrowleft:"\u21BA",circlearrowright:"\u21BB",circledast:"\u229B",circledcirc:"\u229A",circleddash:"\u229D",CircleDot:"\u2299",circledR:"\xAE",circledS:"\u24C8",CircleMinus:"\u2296",CirclePlus:"\u2295",CircleTimes:"\u2297",cir:"\u25CB",cirE:"\u29C3",cire:"\u2257",cirfnint:"\u2A10",cirmid:"\u2AEF",cirscir:"\u29C2",ClockwiseContourIntegral:"\u2232",CloseCurlyDoubleQuote:"\u201D",CloseCurlyQuote:"\u2019",clubs:"\u2663",clubsuit:"\u2663",colon:":",Colon:"\u2237",Colone:"\u2A74",colone:"\u2254",coloneq:"\u2254",comma:",",commat:"@",comp:"\u2201",compfn:"\u2218",complement:"\u2201",complexes:"\u2102",cong:"\u2245",congdot:"\u2A6D",Congruent:"\u2261",conint:"\u222E",Conint:"\u222F",ContourIntegral:"\u222E",copf:"\u{1D554}",Copf:"\u2102",coprod:"\u2210",Coproduct:"\u2210",copy:"\xA9",COPY:"\xA9",copysr:"\u2117",CounterClockwiseContourIntegral:"\u2233",crarr:"\u21B5",cross:"\u2717",Cross:"\u2A2F",Cscr:"\u{1D49E}",cscr:"\u{1D4B8}",csub:"\u2ACF",csube:"\u2AD1",csup:"\u2AD0",csupe:"\u2AD2",ctdot:"\u22EF",cudarrl:"\u2938",cudarrr:"\u2935",cuepr:"\u22DE",cuesc:"\u22DF",cularr:"\u21B6",cularrp:"\u293D",cupbrcap:"\u2A48",cupcap:"\u2A46",CupCap:"\u224D",cup:"\u222A",Cup:"\u22D3",cupcup:"\u2A4A",cupdot:"\u228D",cupor:"\u2A45",cups:"\u222A\uFE00",curarr:"\u21B7",curarrm:"\u293C",curlyeqprec:"\u22DE",curlyeqsucc:"\u22DF",curlyvee:"\u22CE",curlywedge:"\u22CF",curren:"\xA4",curvearrowleft:"\u21B6",curvearrowright:"\u21B7",cuvee:"\u22CE",cuwed:"\u22CF",cwconint:"\u2232",cwint:"\u2231",cylcty:"\u232D",dagger:"\u2020",Dagger:"\u2021",daleth:"\u2138",darr:"\u2193",Darr:"\u21A1",dArr:"\u21D3",dash:"\u2010",Dashv:"\u2AE4",dashv:"\u22A3",dbkarow:"\u290F",dblac:"\u02DD",Dcaron:"\u010E",dcaron:"\u010F",Dcy:"\u0414",dcy:"\u0434",ddagger:"\u2021",ddarr:"\u21CA",DD:"\u2145",dd:"\u2146",DDotrahd:"\u2911",ddotseq:"\u2A77",deg:"\xB0",Del:"\u2207",Delta:"\u0394",delta:"\u03B4",demptyv:"\u29B1",dfisht:"\u297F",Dfr:"\u{1D507}",dfr:"\u{1D521}",dHar:"\u2965",dharl:"\u21C3",dharr:"\u21C2",DiacriticalAcute:"\xB4",DiacriticalDot:"\u02D9",DiacriticalDoubleAcute:"\u02DD",DiacriticalGrave:"`",DiacriticalTilde:"\u02DC",diam:"\u22C4",diamond:"\u22C4",Diamond:"\u22C4",diamondsuit:"\u2666",diams:"\u2666",die:"\xA8",DifferentialD:"\u2146",digamma:"\u03DD",disin:"\u22F2",div:"\xF7",divide:"\xF7",divideontimes:"\u22C7",divonx:"\u22C7",DJcy:"\u0402",djcy:"\u0452",dlcorn:"\u231E",dlcrop:"\u230D",dollar:"$",Dopf:"\u{1D53B}",dopf:"\u{1D555}",Dot:"\xA8",dot:"\u02D9",DotDot:"\u20DC",doteq:"\u2250",doteqdot:"\u2251",DotEqual:"\u2250",dotminus:"\u2238",dotplus:"\u2214",dotsquare:"\u22A1",doublebarwedge:"\u2306",DoubleContourIntegral:"\u222F",DoubleDot:"\xA8",DoubleDownArrow:"\u21D3",DoubleLeftArrow:"\u21D0",DoubleLeftRightArrow:"\u21D4",DoubleLeftTee:"\u2AE4",DoubleLongLeftArrow:"\u27F8",DoubleLongLeftRightArrow:"\u27FA",DoubleLongRightArrow:"\u27F9",DoubleRightArrow:"\u21D2",DoubleRightTee:"\u22A8",DoubleUpArrow:"\u21D1",DoubleUpDownArrow:"\u21D5",DoubleVerticalBar:"\u2225",DownArrowBar:"\u2913",downarrow:"\u2193",DownArrow:"\u2193",Downarrow:"\u21D3",DownArrowUpArrow:"\u21F5",DownBreve:"\u0311",downdownarrows:"\u21CA",downharpoonleft:"\u21C3",downharpoonright:"\u21C2",DownLeftRightVector:"\u2950",DownLeftTeeVector:"\u295E",DownLeftVectorBar:"\u2956",DownLeftVector:"\u21BD",DownRightTeeVector:"\u295F",DownRightVectorBar:"\u2957",DownRightVector:"\u21C1",DownTeeArrow:"\u21A7",DownTee:"\u22A4",drbkarow:"\u2910",drcorn:"\u231F",drcrop:"\u230C",Dscr:"\u{1D49F}",dscr:"\u{1D4B9}",DScy:"\u0405",dscy:"\u0455",dsol:"\u29F6",Dstrok:"\u0110",dstrok:"\u0111",dtdot:"\u22F1",dtri:"\u25BF",dtrif:"\u25BE",duarr:"\u21F5",duhar:"\u296F",dwangle:"\u29A6",DZcy:"\u040F",dzcy:"\u045F",dzigrarr:"\u27FF",Eacute:"\xC9",eacute:"\xE9",easter:"\u2A6E",Ecaron:"\u011A",ecaron:"\u011B",Ecirc:"\xCA",ecirc:"\xEA",ecir:"\u2256",ecolon:"\u2255",Ecy:"\u042D",ecy:"\u044D",eDDot:"\u2A77",Edot:"\u0116",edot:"\u0117",eDot:"\u2251",ee:"\u2147",efDot:"\u2252",Efr:"\u{1D508}",efr:"\u{1D522}",eg:"\u2A9A",Egrave:"\xC8",egrave:"\xE8",egs:"\u2A96",egsdot:"\u2A98",el:"\u2A99",Element:"\u2208",elinters:"\u23E7",ell:"\u2113",els:"\u2A95",elsdot:"\u2A97",Emacr:"\u0112",emacr:"\u0113",empty:"\u2205",emptyset:"\u2205",EmptySmallSquare:"\u25FB",emptyv:"\u2205",EmptyVerySmallSquare:"\u25AB",emsp13:"\u2004",emsp14:"\u2005",emsp:"\u2003",ENG:"\u014A",eng:"\u014B",ensp:"\u2002",Eogon:"\u0118",eogon:"\u0119",Eopf:"\u{1D53C}",eopf:"\u{1D556}",epar:"\u22D5",eparsl:"\u29E3",eplus:"\u2A71",epsi:"\u03B5",Epsilon:"\u0395",epsilon:"\u03B5",epsiv:"\u03F5",eqcirc:"\u2256",eqcolon:"\u2255",eqsim:"\u2242",eqslantgtr:"\u2A96",eqslantless:"\u2A95",Equal:"\u2A75",equals:"=",EqualTilde:"\u2242",equest:"\u225F",Equilibrium:"\u21CC",equiv:"\u2261",equivDD:"\u2A78",eqvparsl:"\u29E5",erarr:"\u2971",erDot:"\u2253",escr:"\u212F",Escr:"\u2130",esdot:"\u2250",Esim:"\u2A73",esim:"\u2242",Eta:"\u0397",eta:"\u03B7",ETH:"\xD0",eth:"\xF0",Euml:"\xCB",euml:"\xEB",euro:"\u20AC",excl:"!",exist:"\u2203",Exists:"\u2203",expectation:"\u2130",exponentiale:"\u2147",ExponentialE:"\u2147",fallingdotseq:"\u2252",Fcy:"\u0424",fcy:"\u0444",female:"\u2640",ffilig:"\uFB03",fflig:"\uFB00",ffllig:"\uFB04",Ffr:"\u{1D509}",ffr:"\u{1D523}",filig:"\uFB01",FilledSmallSquare:"\u25FC",FilledVerySmallSquare:"\u25AA",fjlig:"fj",flat:"\u266D",fllig:"\uFB02",fltns:"\u25B1",fnof:"\u0192",Fopf:"\u{1D53D}",fopf:"\u{1D557}",forall:"\u2200",ForAll:"\u2200",fork:"\u22D4",forkv:"\u2AD9",Fouriertrf:"\u2131",fpartint:"\u2A0D",frac12:"\xBD",frac13:"\u2153",frac14:"\xBC",frac15:"\u2155",frac16:"\u2159",frac18:"\u215B",frac23:"\u2154",frac25:"\u2156",frac34:"\xBE",frac35:"\u2157",frac38:"\u215C",frac45:"\u2158",frac56:"\u215A",frac58:"\u215D",frac78:"\u215E",frasl:"\u2044",frown:"\u2322",fscr:"\u{1D4BB}",Fscr:"\u2131",gacute:"\u01F5",Gamma:"\u0393",gamma:"\u03B3",Gammad:"\u03DC",gammad:"\u03DD",gap:"\u2A86",Gbreve:"\u011E",gbreve:"\u011F",Gcedil:"\u0122",Gcirc:"\u011C",gcirc:"\u011D",Gcy:"\u0413",gcy:"\u0433",Gdot:"\u0120",gdot:"\u0121",ge:"\u2265",gE:"\u2267",gEl:"\u2A8C",gel:"\u22DB",geq:"\u2265",geqq:"\u2267",geqslant:"\u2A7E",gescc:"\u2AA9",ges:"\u2A7E",gesdot:"\u2A80",gesdoto:"\u2A82",gesdotol:"\u2A84",gesl:"\u22DB\uFE00",gesles:"\u2A94",Gfr:"\u{1D50A}",gfr:"\u{1D524}",gg:"\u226B",Gg:"\u22D9",ggg:"\u22D9",gimel:"\u2137",GJcy:"\u0403",gjcy:"\u0453",gla:"\u2AA5",gl:"\u2277",glE:"\u2A92",glj:"\u2AA4",gnap:"\u2A8A",gnapprox:"\u2A8A",gne:"\u2A88",gnE:"\u2269",gneq:"\u2A88",gneqq:"\u2269",gnsim:"\u22E7",Gopf:"\u{1D53E}",gopf:"\u{1D558}",grave:"`",GreaterEqual:"\u2265",GreaterEqualLess:"\u22DB",GreaterFullEqual:"\u2267",GreaterGreater:"\u2AA2",GreaterLess:"\u2277",GreaterSlantEqual:"\u2A7E",GreaterTilde:"\u2273",Gscr:"\u{1D4A2}",gscr:"\u210A",gsim:"\u2273",gsime:"\u2A8E",gsiml:"\u2A90",gtcc:"\u2AA7",gtcir:"\u2A7A",gt:">",GT:">",Gt:"\u226B",gtdot:"\u22D7",gtlPar:"\u2995",gtquest:"\u2A7C",gtrapprox:"\u2A86",gtrarr:"\u2978",gtrdot:"\u22D7",gtreqless:"\u22DB",gtreqqless:"\u2A8C",gtrless:"\u2277",gtrsim:"\u2273",gvertneqq:"\u2269\uFE00",gvnE:"\u2269\uFE00",Hacek:"\u02C7",hairsp:"\u200A",half:"\xBD",hamilt:"\u210B",HARDcy:"\u042A",hardcy:"\u044A",harrcir:"\u2948",harr:"\u2194",hArr:"\u21D4",harrw:"\u21AD",Hat:"^",hbar:"\u210F",Hcirc:"\u0124",hcirc:"\u0125",hearts:"\u2665",heartsuit:"\u2665",hellip:"\u2026",hercon:"\u22B9",hfr:"\u{1D525}",Hfr:"\u210C",HilbertSpace:"\u210B",hksearow:"\u2925",hkswarow:"\u2926",hoarr:"\u21FF",homtht:"\u223B",hookleftarrow:"\u21A9",hookrightarrow:"\u21AA",hopf:"\u{1D559}",Hopf:"\u210D",horbar:"\u2015",HorizontalLine:"\u2500",hscr:"\u{1D4BD}",Hscr:"\u210B",hslash:"\u210F",Hstrok:"\u0126",hstrok:"\u0127",HumpDownHump:"\u224E",HumpEqual:"\u224F",hybull:"\u2043",hyphen:"\u2010",Iacute:"\xCD",iacute:"\xED",ic:"\u2063",Icirc:"\xCE",icirc:"\xEE",Icy:"\u0418",icy:"\u0438",Idot:"\u0130",IEcy:"\u0415",iecy:"\u0435",iexcl:"\xA1",iff:"\u21D4",ifr:"\u{1D526}",Ifr:"\u2111",Igrave:"\xCC",igrave:"\xEC",ii:"\u2148",iiiint:"\u2A0C",iiint:"\u222D",iinfin:"\u29DC",iiota:"\u2129",IJlig:"\u0132",ijlig:"\u0133",Imacr:"\u012A",imacr:"\u012B",image:"\u2111",ImaginaryI:"\u2148",imagline:"\u2110",imagpart:"\u2111",imath:"\u0131",Im:"\u2111",imof:"\u22B7",imped:"\u01B5",Implies:"\u21D2",incare:"\u2105",in:"\u2208",infin:"\u221E",infintie:"\u29DD",inodot:"\u0131",intcal:"\u22BA",int:"\u222B",Int:"\u222C",integers:"\u2124",Integral:"\u222B",intercal:"\u22BA",Intersection:"\u22C2",intlarhk:"\u2A17",intprod:"\u2A3C",InvisibleComma:"\u2063",InvisibleTimes:"\u2062",IOcy:"\u0401",iocy:"\u0451",Iogon:"\u012E",iogon:"\u012F",Iopf:"\u{1D540}",iopf:"\u{1D55A}",Iota:"\u0399",iota:"\u03B9",iprod:"\u2A3C",iquest:"\xBF",iscr:"\u{1D4BE}",Iscr:"\u2110",isin:"\u2208",isindot:"\u22F5",isinE:"\u22F9",isins:"\u22F4",isinsv:"\u22F3",isinv:"\u2208",it:"\u2062",Itilde:"\u0128",itilde:"\u0129",Iukcy:"\u0406",iukcy:"\u0456",Iuml:"\xCF",iuml:"\xEF",Jcirc:"\u0134",jcirc:"\u0135",Jcy:"\u0419",jcy:"\u0439",Jfr:"\u{1D50D}",jfr:"\u{1D527}",jmath:"\u0237",Jopf:"\u{1D541}",jopf:"\u{1D55B}",Jscr:"\u{1D4A5}",jscr:"\u{1D4BF}",Jsercy:"\u0408",jsercy:"\u0458",Jukcy:"\u0404",jukcy:"\u0454",Kappa:"\u039A",kappa:"\u03BA",kappav:"\u03F0",Kcedil:"\u0136",kcedil:"\u0137",Kcy:"\u041A",kcy:"\u043A",Kfr:"\u{1D50E}",kfr:"\u{1D528}",kgreen:"\u0138",KHcy:"\u0425",khcy:"\u0445",KJcy:"\u040C",kjcy:"\u045C",Kopf:"\u{1D542}",kopf:"\u{1D55C}",Kscr:"\u{1D4A6}",kscr:"\u{1D4C0}",lAarr:"\u21DA",Lacute:"\u0139",lacute:"\u013A",laemptyv:"\u29B4",lagran:"\u2112",Lambda:"\u039B",lambda:"\u03BB",lang:"\u27E8",Lang:"\u27EA",langd:"\u2991",langle:"\u27E8",lap:"\u2A85",Laplacetrf:"\u2112",laquo:"\xAB",larrb:"\u21E4",larrbfs:"\u291F",larr:"\u2190",Larr:"\u219E",lArr:"\u21D0",larrfs:"\u291D",larrhk:"\u21A9",larrlp:"\u21AB",larrpl:"\u2939",larrsim:"\u2973",larrtl:"\u21A2",latail:"\u2919",lAtail:"\u291B",lat:"\u2AAB",late:"\u2AAD",lates:"\u2AAD\uFE00",lbarr:"\u290C",lBarr:"\u290E",lbbrk:"\u2772",lbrace:"{",lbrack:"[",lbrke:"\u298B",lbrksld:"\u298F",lbrkslu:"\u298D",Lcaron:"\u013D",lcaron:"\u013E",Lcedil:"\u013B",lcedil:"\u013C",lceil:"\u2308",lcub:"{",Lcy:"\u041B",lcy:"\u043B",ldca:"\u2936",ldquo:"\u201C",ldquor:"\u201E",ldrdhar:"\u2967",ldrushar:"\u294B",ldsh:"\u21B2",le:"\u2264",lE:"\u2266",LeftAngleBracket:"\u27E8",LeftArrowBar:"\u21E4",leftarrow:"\u2190",LeftArrow:"\u2190",Leftarrow:"\u21D0",LeftArrowRightArrow:"\u21C6",leftarrowtail:"\u21A2",LeftCeiling:"\u2308",LeftDoubleBracket:"\u27E6",LeftDownTeeVector:"\u2961",LeftDownVectorBar:"\u2959",LeftDownVector:"\u21C3",LeftFloor:"\u230A",leftharpoondown:"\u21BD",leftharpoonup:"\u21BC",leftleftarrows:"\u21C7",leftrightarrow:"\u2194",LeftRightArrow:"\u2194",Leftrightarrow:"\u21D4",leftrightarrows:"\u21C6",leftrightharpoons:"\u21CB",leftrightsquigarrow:"\u21AD",LeftRightVector:"\u294E",LeftTeeArrow:"\u21A4",LeftTee:"\u22A3",LeftTeeVector:"\u295A",leftthreetimes:"\u22CB",LeftTriangleBar:"\u29CF",LeftTriangle:"\u22B2",LeftTriangleEqual:"\u22B4",LeftUpDownVector:"\u2951",LeftUpTeeVector:"\u2960",LeftUpVectorBar:"\u2958",LeftUpVector:"\u21BF",LeftVectorBar:"\u2952",LeftVector:"\u21BC",lEg:"\u2A8B",leg:"\u22DA",leq:"\u2264",leqq:"\u2266",leqslant:"\u2A7D",lescc:"\u2AA8",les:"\u2A7D",lesdot:"\u2A7F",lesdoto:"\u2A81",lesdotor:"\u2A83",lesg:"\u22DA\uFE00",lesges:"\u2A93",lessapprox:"\u2A85",lessdot:"\u22D6",lesseqgtr:"\u22DA",lesseqqgtr:"\u2A8B",LessEqualGreater:"\u22DA",LessFullEqual:"\u2266",LessGreater:"\u2276",lessgtr:"\u2276",LessLess:"\u2AA1",lesssim:"\u2272",LessSlantEqual:"\u2A7D",LessTilde:"\u2272",lfisht:"\u297C",lfloor:"\u230A",Lfr:"\u{1D50F}",lfr:"\u{1D529}",lg:"\u2276",lgE:"\u2A91",lHar:"\u2962",lhard:"\u21BD",lharu:"\u21BC",lharul:"\u296A",lhblk:"\u2584",LJcy:"\u0409",ljcy:"\u0459",llarr:"\u21C7",ll:"\u226A",Ll:"\u22D8",llcorner:"\u231E",Lleftarrow:"\u21DA",llhard:"\u296B",lltri:"\u25FA",Lmidot:"\u013F",lmidot:"\u0140",lmoustache:"\u23B0",lmoust:"\u23B0",lnap:"\u2A89",lnapprox:"\u2A89",lne:"\u2A87",lnE:"\u2268",lneq:"\u2A87",lneqq:"\u2268",lnsim:"\u22E6",loang:"\u27EC",loarr:"\u21FD",lobrk:"\u27E6",longleftarrow:"\u27F5",LongLeftArrow:"\u27F5",Longleftarrow:"\u27F8",longleftrightarrow:"\u27F7",LongLeftRightArrow:"\u27F7",Longleftrightarrow:"\u27FA",longmapsto:"\u27FC",longrightarrow:"\u27F6",LongRightArrow:"\u27F6",Longrightarrow:"\u27F9",looparrowleft:"\u21AB",looparrowright:"\u21AC",lopar:"\u2985",Lopf:"\u{1D543}",lopf:"\u{1D55D}",loplus:"\u2A2D",lotimes:"\u2A34",lowast:"\u2217",lowbar:"_",LowerLeftArrow:"\u2199",LowerRightArrow:"\u2198",loz:"\u25CA",lozenge:"\u25CA",lozf:"\u29EB",lpar:"(",lparlt:"\u2993",lrarr:"\u21C6",lrcorner:"\u231F",lrhar:"\u21CB",lrhard:"\u296D",lrm:"\u200E",lrtri:"\u22BF",lsaquo:"\u2039",lscr:"\u{1D4C1}",Lscr:"\u2112",lsh:"\u21B0",Lsh:"\u21B0",lsim:"\u2272",lsime:"\u2A8D",lsimg:"\u2A8F",lsqb:"[",lsquo:"\u2018",lsquor:"\u201A",Lstrok:"\u0141",lstrok:"\u0142",ltcc:"\u2AA6",ltcir:"\u2A79",lt:"<",LT:"<",Lt:"\u226A",ltdot:"\u22D6",lthree:"\u22CB",ltimes:"\u22C9",ltlarr:"\u2976",ltquest:"\u2A7B",ltri:"\u25C3",ltrie:"\u22B4",ltrif:"\u25C2",ltrPar:"\u2996",lurdshar:"\u294A",luruhar:"\u2966",lvertneqq:"\u2268\uFE00",lvnE:"\u2268\uFE00",macr:"\xAF",male:"\u2642",malt:"\u2720",maltese:"\u2720",Map:"\u2905",map:"\u21A6",mapsto:"\u21A6",mapstodown:"\u21A7",mapstoleft:"\u21A4",mapstoup:"\u21A5",marker:"\u25AE",mcomma:"\u2A29",Mcy:"\u041C",mcy:"\u043C",mdash:"\u2014",mDDot:"\u223A",measuredangle:"\u2221",MediumSpace:"\u205F",Mellintrf:"\u2133",Mfr:"\u{1D510}",mfr:"\u{1D52A}",mho:"\u2127",micro:"\xB5",midast:"*",midcir:"\u2AF0",mid:"\u2223",middot:"\xB7",minusb:"\u229F",minus:"\u2212",minusd:"\u2238",minusdu:"\u2A2A",MinusPlus:"\u2213",mlcp:"\u2ADB",mldr:"\u2026",mnplus:"\u2213",models:"\u22A7",Mopf:"\u{1D544}",mopf:"\u{1D55E}",mp:"\u2213",mscr:"\u{1D4C2}",Mscr:"\u2133",mstpos:"\u223E",Mu:"\u039C",mu:"\u03BC",multimap:"\u22B8",mumap:"\u22B8",nabla:"\u2207",Nacute:"\u0143",nacute:"\u0144",nang:"\u2220\u20D2",nap:"\u2249",napE:"\u2A70\u0338",napid:"\u224B\u0338",napos:"\u0149",napprox:"\u2249",natural:"\u266E",naturals:"\u2115",natur:"\u266E",nbsp:"\xA0",nbump:"\u224E\u0338",nbumpe:"\u224F\u0338",ncap:"\u2A43",Ncaron:"\u0147",ncaron:"\u0148",Ncedil:"\u0145",ncedil:"\u0146",ncong:"\u2247",ncongdot:"\u2A6D\u0338",ncup:"\u2A42",Ncy:"\u041D",ncy:"\u043D",ndash:"\u2013",nearhk:"\u2924",nearr:"\u2197",neArr:"\u21D7",nearrow:"\u2197",ne:"\u2260",nedot:"\u2250\u0338",NegativeMediumSpace:"\u200B",NegativeThickSpace:"\u200B",NegativeThinSpace:"\u200B",NegativeVeryThinSpace:"\u200B",nequiv:"\u2262",nesear:"\u2928",nesim:"\u2242\u0338",NestedGreaterGreater:"\u226B",NestedLessLess:"\u226A",NewLine:`
-`,nexist:"\u2204",nexists:"\u2204",Nfr:"\u{1D511}",nfr:"\u{1D52B}",ngE:"\u2267\u0338",nge:"\u2271",ngeq:"\u2271",ngeqq:"\u2267\u0338",ngeqslant:"\u2A7E\u0338",nges:"\u2A7E\u0338",nGg:"\u22D9\u0338",ngsim:"\u2275",nGt:"\u226B\u20D2",ngt:"\u226F",ngtr:"\u226F",nGtv:"\u226B\u0338",nharr:"\u21AE",nhArr:"\u21CE",nhpar:"\u2AF2",ni:"\u220B",nis:"\u22FC",nisd:"\u22FA",niv:"\u220B",NJcy:"\u040A",njcy:"\u045A",nlarr:"\u219A",nlArr:"\u21CD",nldr:"\u2025",nlE:"\u2266\u0338",nle:"\u2270",nleftarrow:"\u219A",nLeftarrow:"\u21CD",nleftrightarrow:"\u21AE",nLeftrightarrow:"\u21CE",nleq:"\u2270",nleqq:"\u2266\u0338",nleqslant:"\u2A7D\u0338",nles:"\u2A7D\u0338",nless:"\u226E",nLl:"\u22D8\u0338",nlsim:"\u2274",nLt:"\u226A\u20D2",nlt:"\u226E",nltri:"\u22EA",nltrie:"\u22EC",nLtv:"\u226A\u0338",nmid:"\u2224",NoBreak:"\u2060",NonBreakingSpace:"\xA0",nopf:"\u{1D55F}",Nopf:"\u2115",Not:"\u2AEC",not:"\xAC",NotCongruent:"\u2262",NotCupCap:"\u226D",NotDoubleVerticalBar:"\u2226",NotElement:"\u2209",NotEqual:"\u2260",NotEqualTilde:"\u2242\u0338",NotExists:"\u2204",NotGreater:"\u226F",NotGreaterEqual:"\u2271",NotGreaterFullEqual:"\u2267\u0338",NotGreaterGreater:"\u226B\u0338",NotGreaterLess:"\u2279",NotGreaterSlantEqual:"\u2A7E\u0338",NotGreaterTilde:"\u2275",NotHumpDownHump:"\u224E\u0338",NotHumpEqual:"\u224F\u0338",notin:"\u2209",notindot:"\u22F5\u0338",notinE:"\u22F9\u0338",notinva:"\u2209",notinvb:"\u22F7",notinvc:"\u22F6",NotLeftTriangleBar:"\u29CF\u0338",NotLeftTriangle:"\u22EA",NotLeftTriangleEqual:"\u22EC",NotLess:"\u226E",NotLessEqual:"\u2270",NotLessGreater:"\u2278",NotLessLess:"\u226A\u0338",NotLessSlantEqual:"\u2A7D\u0338",NotLessTilde:"\u2274",NotNestedGreaterGreater:"\u2AA2\u0338",NotNestedLessLess:"\u2AA1\u0338",notni:"\u220C",notniva:"\u220C",notnivb:"\u22FE",notnivc:"\u22FD",NotPrecedes:"\u2280",NotPrecedesEqual:"\u2AAF\u0338",NotPrecedesSlantEqual:"\u22E0",NotReverseElement:"\u220C",NotRightTriangleBar:"\u29D0\u0338",NotRightTriangle:"\u22EB",NotRightTriangleEqual:"\u22ED",NotSquareSubset:"\u228F\u0338",NotSquareSubsetEqual:"\u22E2",NotSquareSuperset:"\u2290\u0338",NotSquareSupersetEqual:"\u22E3",NotSubset:"\u2282\u20D2",NotSubsetEqual:"\u2288",NotSucceeds:"\u2281",NotSucceedsEqual:"\u2AB0\u0338",NotSucceedsSlantEqual:"\u22E1",NotSucceedsTilde:"\u227F\u0338",NotSuperset:"\u2283\u20D2",NotSupersetEqual:"\u2289",NotTilde:"\u2241",NotTildeEqual:"\u2244",NotTildeFullEqual:"\u2247",NotTildeTilde:"\u2249",NotVerticalBar:"\u2224",nparallel:"\u2226",npar:"\u2226",nparsl:"\u2AFD\u20E5",npart:"\u2202\u0338",npolint:"\u2A14",npr:"\u2280",nprcue:"\u22E0",nprec:"\u2280",npreceq:"\u2AAF\u0338",npre:"\u2AAF\u0338",nrarrc:"\u2933\u0338",nrarr:"\u219B",nrArr:"\u21CF",nrarrw:"\u219D\u0338",nrightarrow:"\u219B",nRightarrow:"\u21CF",nrtri:"\u22EB",nrtrie:"\u22ED",nsc:"\u2281",nsccue:"\u22E1",nsce:"\u2AB0\u0338",Nscr:"\u{1D4A9}",nscr:"\u{1D4C3}",nshortmid:"\u2224",nshortparallel:"\u2226",nsim:"\u2241",nsime:"\u2244",nsimeq:"\u2244",nsmid:"\u2224",nspar:"\u2226",nsqsube:"\u22E2",nsqsupe:"\u22E3",nsub:"\u2284",nsubE:"\u2AC5\u0338",nsube:"\u2288",nsubset:"\u2282\u20D2",nsubseteq:"\u2288",nsubseteqq:"\u2AC5\u0338",nsucc:"\u2281",nsucceq:"\u2AB0\u0338",nsup:"\u2285",nsupE:"\u2AC6\u0338",nsupe:"\u2289",nsupset:"\u2283\u20D2",nsupseteq:"\u2289",nsupseteqq:"\u2AC6\u0338",ntgl:"\u2279",Ntilde:"\xD1",ntilde:"\xF1",ntlg:"\u2278",ntriangleleft:"\u22EA",ntrianglelefteq:"\u22EC",ntriangleright:"\u22EB",ntrianglerighteq:"\u22ED",Nu:"\u039D",nu:"\u03BD",num:"#",numero:"\u2116",numsp:"\u2007",nvap:"\u224D\u20D2",nvdash:"\u22AC",nvDash:"\u22AD",nVdash:"\u22AE",nVDash:"\u22AF",nvge:"\u2265\u20D2",nvgt:">\u20D2",nvHarr:"\u2904",nvinfin:"\u29DE",nvlArr:"\u2902",nvle:"\u2264\u20D2",nvlt:"<\u20D2",nvltrie:"\u22B4\u20D2",nvrArr:"\u2903",nvrtrie:"\u22B5\u20D2",nvsim:"\u223C\u20D2",nwarhk:"\u2923",nwarr:"\u2196",nwArr:"\u21D6",nwarrow:"\u2196",nwnear:"\u2927",Oacute:"\xD3",oacute:"\xF3",oast:"\u229B",Ocirc:"\xD4",ocirc:"\xF4",ocir:"\u229A",Ocy:"\u041E",ocy:"\u043E",odash:"\u229D",Odblac:"\u0150",odblac:"\u0151",odiv:"\u2A38",odot:"\u2299",odsold:"\u29BC",OElig:"\u0152",oelig:"\u0153",ofcir:"\u29BF",Ofr:"\u{1D512}",ofr:"\u{1D52C}",ogon:"\u02DB",Ograve:"\xD2",ograve:"\xF2",ogt:"\u29C1",ohbar:"\u29B5",ohm:"\u03A9",oint:"\u222E",olarr:"\u21BA",olcir:"\u29BE",olcross:"\u29BB",oline:"\u203E",olt:"\u29C0",Omacr:"\u014C",omacr:"\u014D",Omega:"\u03A9",omega:"\u03C9",Omicron:"\u039F",omicron:"\u03BF",omid:"\u29B6",ominus:"\u2296",Oopf:"\u{1D546}",oopf:"\u{1D560}",opar:"\u29B7",OpenCurlyDoubleQuote:"\u201C",OpenCurlyQuote:"\u2018",operp:"\u29B9",oplus:"\u2295",orarr:"\u21BB",Or:"\u2A54",or:"\u2228",ord:"\u2A5D",order:"\u2134",orderof:"\u2134",ordf:"\xAA",ordm:"\xBA",origof:"\u22B6",oror:"\u2A56",orslope:"\u2A57",orv:"\u2A5B",oS:"\u24C8",Oscr:"\u{1D4AA}",oscr:"\u2134",Oslash:"\xD8",oslash:"\xF8",osol:"\u2298",Otilde:"\xD5",otilde:"\xF5",otimesas:"\u2A36",Otimes:"\u2A37",otimes:"\u2297",Ouml:"\xD6",ouml:"\xF6",ovbar:"\u233D",OverBar:"\u203E",OverBrace:"\u23DE",OverBracket:"\u23B4",OverParenthesis:"\u23DC",para:"\xB6",parallel:"\u2225",par:"\u2225",parsim:"\u2AF3",parsl:"\u2AFD",part:"\u2202",PartialD:"\u2202",Pcy:"\u041F",pcy:"\u043F",percnt:"%",period:".",permil:"\u2030",perp:"\u22A5",pertenk:"\u2031",Pfr:"\u{1D513}",pfr:"\u{1D52D}",Phi:"\u03A6",phi:"\u03C6",phiv:"\u03D5",phmmat:"\u2133",phone:"\u260E",Pi:"\u03A0",pi:"\u03C0",pitchfork:"\u22D4",piv:"\u03D6",planck:"\u210F",planckh:"\u210E",plankv:"\u210F",plusacir:"\u2A23",plusb:"\u229E",pluscir:"\u2A22",plus:"+",plusdo:"\u2214",plusdu:"\u2A25",pluse:"\u2A72",PlusMinus:"\xB1",plusmn:"\xB1",plussim:"\u2A26",plustwo:"\u2A27",pm:"\xB1",Poincareplane:"\u210C",pointint:"\u2A15",popf:"\u{1D561}",Popf:"\u2119",pound:"\xA3",prap:"\u2AB7",Pr:"\u2ABB",pr:"\u227A",prcue:"\u227C",precapprox:"\u2AB7",prec:"\u227A",preccurlyeq:"\u227C",Precedes:"\u227A",PrecedesEqual:"\u2AAF",PrecedesSlantEqual:"\u227C",PrecedesTilde:"\u227E",preceq:"\u2AAF",precnapprox:"\u2AB9",precneqq:"\u2AB5",precnsim:"\u22E8",pre:"\u2AAF",prE:"\u2AB3",precsim:"\u227E",prime:"\u2032",Prime:"\u2033",primes:"\u2119",prnap:"\u2AB9",prnE:"\u2AB5",prnsim:"\u22E8",prod:"\u220F",Product:"\u220F",profalar:"\u232E",profline:"\u2312",profsurf:"\u2313",prop:"\u221D",Proportional:"\u221D",Proportion:"\u2237",propto:"\u221D",prsim:"\u227E",prurel:"\u22B0",Pscr:"\u{1D4AB}",pscr:"\u{1D4C5}",Psi:"\u03A8",psi:"\u03C8",puncsp:"\u2008",Qfr:"\u{1D514}",qfr:"\u{1D52E}",qint:"\u2A0C",qopf:"\u{1D562}",Qopf:"\u211A",qprime:"\u2057",Qscr:"\u{1D4AC}",qscr:"\u{1D4C6}",quaternions:"\u210D",quatint:"\u2A16",quest:"?",questeq:"\u225F",quot:'"',QUOT:'"',rAarr:"\u21DB",race:"\u223D\u0331",Racute:"\u0154",racute:"\u0155",radic:"\u221A",raemptyv:"\u29B3",rang:"\u27E9",Rang:"\u27EB",rangd:"\u2992",range:"\u29A5",rangle:"\u27E9",raquo:"\xBB",rarrap:"\u2975",rarrb:"\u21E5",rarrbfs:"\u2920",rarrc:"\u2933",rarr:"\u2192",Rarr:"\u21A0",rArr:"\u21D2",rarrfs:"\u291E",rarrhk:"\u21AA",rarrlp:"\u21AC",rarrpl:"\u2945",rarrsim:"\u2974",Rarrtl:"\u2916",rarrtl:"\u21A3",rarrw:"\u219D",ratail:"\u291A",rAtail:"\u291C",ratio:"\u2236",rationals:"\u211A",rbarr:"\u290D",rBarr:"\u290F",RBarr:"\u2910",rbbrk:"\u2773",rbrace:"}",rbrack:"]",rbrke:"\u298C",rbrksld:"\u298E",rbrkslu:"\u2990",Rcaron:"\u0158",rcaron:"\u0159",Rcedil:"\u0156",rcedil:"\u0157",rceil:"\u2309",rcub:"}",Rcy:"\u0420",rcy:"\u0440",rdca:"\u2937",rdldhar:"\u2969",rdquo:"\u201D",rdquor:"\u201D",rdsh:"\u21B3",real:"\u211C",realine:"\u211B",realpart:"\u211C",reals:"\u211D",Re:"\u211C",rect:"\u25AD",reg:"\xAE",REG:"\xAE",ReverseElement:"\u220B",ReverseEquilibrium:"\u21CB",ReverseUpEquilibrium:"\u296F",rfisht:"\u297D",rfloor:"\u230B",rfr:"\u{1D52F}",Rfr:"\u211C",rHar:"\u2964",rhard:"\u21C1",rharu:"\u21C0",rharul:"\u296C",Rho:"\u03A1",rho:"\u03C1",rhov:"\u03F1",RightAngleBracket:"\u27E9",RightArrowBar:"\u21E5",rightarrow:"\u2192",RightArrow:"\u2192",Rightarrow:"\u21D2",RightArrowLeftArrow:"\u21C4",rightarrowtail:"\u21A3",RightCeiling:"\u2309",RightDoubleBracket:"\u27E7",RightDownTeeVector:"\u295D",RightDownVectorBar:"\u2955",RightDownVector:"\u21C2",RightFloor:"\u230B",rightharpoondown:"\u21C1",rightharpoonup:"\u21C0",rightleftarrows:"\u21C4",rightleftharpoons:"\u21CC",rightrightarrows:"\u21C9",rightsquigarrow:"\u219D",RightTeeArrow:"\u21A6",RightTee:"\u22A2",RightTeeVector:"\u295B",rightthreetimes:"\u22CC",RightTriangleBar:"\u29D0",RightTriangle:"\u22B3",RightTriangleEqual:"\u22B5",RightUpDownVector:"\u294F",RightUpTeeVector:"\u295C",RightUpVectorBar:"\u2954",RightUpVector:"\u21BE",RightVectorBar:"\u2953",RightVector:"\u21C0",ring:"\u02DA",risingdotseq:"\u2253",rlarr:"\u21C4",rlhar:"\u21CC",rlm:"\u200F",rmoustache:"\u23B1",rmoust:"\u23B1",rnmid:"\u2AEE",roang:"\u27ED",roarr:"\u21FE",robrk:"\u27E7",ropar:"\u2986",ropf:"\u{1D563}",Ropf:"\u211D",roplus:"\u2A2E",rotimes:"\u2A35",RoundImplies:"\u2970",rpar:")",rpargt:"\u2994",rppolint:"\u2A12",rrarr:"\u21C9",Rrightarrow:"\u21DB",rsaquo:"\u203A",rscr:"\u{1D4C7}",Rscr:"\u211B",rsh:"\u21B1",Rsh:"\u21B1",rsqb:"]",rsquo:"\u2019",rsquor:"\u2019",rthree:"\u22CC",rtimes:"\u22CA",rtri:"\u25B9",rtrie:"\u22B5",rtrif:"\u25B8",rtriltri:"\u29CE",RuleDelayed:"\u29F4",ruluhar:"\u2968",rx:"\u211E",Sacute:"\u015A",sacute:"\u015B",sbquo:"\u201A",scap:"\u2AB8",Scaron:"\u0160",scaron:"\u0161",Sc:"\u2ABC",sc:"\u227B",sccue:"\u227D",sce:"\u2AB0",scE:"\u2AB4",Scedil:"\u015E",scedil:"\u015F",Scirc:"\u015C",scirc:"\u015D",scnap:"\u2ABA",scnE:"\u2AB6",scnsim:"\u22E9",scpolint:"\u2A13",scsim:"\u227F",Scy:"\u0421",scy:"\u0441",sdotb:"\u22A1",sdot:"\u22C5",sdote:"\u2A66",searhk:"\u2925",searr:"\u2198",seArr:"\u21D8",searrow:"\u2198",sect:"\xA7",semi:";",seswar:"\u2929",setminus:"\u2216",setmn:"\u2216",sext:"\u2736",Sfr:"\u{1D516}",sfr:"\u{1D530}",sfrown:"\u2322",sharp:"\u266F",SHCHcy:"\u0429",shchcy:"\u0449",SHcy:"\u0428",shcy:"\u0448",ShortDownArrow:"\u2193",ShortLeftArrow:"\u2190",shortmid:"\u2223",shortparallel:"\u2225",ShortRightArrow:"\u2192",ShortUpArrow:"\u2191",shy:"\xAD",Sigma:"\u03A3",sigma:"\u03C3",sigmaf:"\u03C2",sigmav:"\u03C2",sim:"\u223C",simdot:"\u2A6A",sime:"\u2243",simeq:"\u2243",simg:"\u2A9E",simgE:"\u2AA0",siml:"\u2A9D",simlE:"\u2A9F",simne:"\u2246",simplus:"\u2A24",simrarr:"\u2972",slarr:"\u2190",SmallCircle:"\u2218",smallsetminus:"\u2216",smashp:"\u2A33",smeparsl:"\u29E4",smid:"\u2223",smile:"\u2323",smt:"\u2AAA",smte:"\u2AAC",smtes:"\u2AAC\uFE00",SOFTcy:"\u042C",softcy:"\u044C",solbar:"\u233F",solb:"\u29C4",sol:"/",Sopf:"\u{1D54A}",sopf:"\u{1D564}",spades:"\u2660",spadesuit:"\u2660",spar:"\u2225",sqcap:"\u2293",sqcaps:"\u2293\uFE00",sqcup:"\u2294",sqcups:"\u2294\uFE00",Sqrt:"\u221A",sqsub:"\u228F",sqsube:"\u2291",sqsubset:"\u228F",sqsubseteq:"\u2291",sqsup:"\u2290",sqsupe:"\u2292",sqsupset:"\u2290",sqsupseteq:"\u2292",square:"\u25A1",Square:"\u25A1",SquareIntersection:"\u2293",SquareSubset:"\u228F",SquareSubsetEqual:"\u2291",SquareSuperset:"\u2290",SquareSupersetEqual:"\u2292",SquareUnion:"\u2294",squarf:"\u25AA",squ:"\u25A1",squf:"\u25AA",srarr:"\u2192",Sscr:"\u{1D4AE}",sscr:"\u{1D4C8}",ssetmn:"\u2216",ssmile:"\u2323",sstarf:"\u22C6",Star:"\u22C6",star:"\u2606",starf:"\u2605",straightepsilon:"\u03F5",straightphi:"\u03D5",strns:"\xAF",sub:"\u2282",Sub:"\u22D0",subdot:"\u2ABD",subE:"\u2AC5",sube:"\u2286",subedot:"\u2AC3",submult:"\u2AC1",subnE:"\u2ACB",subne:"\u228A",subplus:"\u2ABF",subrarr:"\u2979",subset:"\u2282",Subset:"\u22D0",subseteq:"\u2286",subseteqq:"\u2AC5",SubsetEqual:"\u2286",subsetneq:"\u228A",subsetneqq:"\u2ACB",subsim:"\u2AC7",subsub:"\u2AD5",subsup:"\u2AD3",succapprox:"\u2AB8",succ:"\u227B",succcurlyeq:"\u227D",Succeeds:"\u227B",SucceedsEqual:"\u2AB0",SucceedsSlantEqual:"\u227D",SucceedsTilde:"\u227F",succeq:"\u2AB0",succnapprox:"\u2ABA",succneqq:"\u2AB6",succnsim:"\u22E9",succsim:"\u227F",SuchThat:"\u220B",sum:"\u2211",Sum:"\u2211",sung:"\u266A",sup1:"\xB9",sup2:"\xB2",sup3:"\xB3",sup:"\u2283",Sup:"\u22D1",supdot:"\u2ABE",supdsub:"\u2AD8",supE:"\u2AC6",supe:"\u2287",supedot:"\u2AC4",Superset:"\u2283",SupersetEqual:"\u2287",suphsol:"\u27C9",suphsub:"\u2AD7",suplarr:"\u297B",supmult:"\u2AC2",supnE:"\u2ACC",supne:"\u228B",supplus:"\u2AC0",supset:"\u2283",Supset:"\u22D1",supseteq:"\u2287",supseteqq:"\u2AC6",supsetneq:"\u228B",supsetneqq:"\u2ACC",supsim:"\u2AC8",supsub:"\u2AD4",supsup:"\u2AD6",swarhk:"\u2926",swarr:"\u2199",swArr:"\u21D9",swarrow:"\u2199",swnwar:"\u292A",szlig:"\xDF",Tab:" ",target:"\u2316",Tau:"\u03A4",tau:"\u03C4",tbrk:"\u23B4",Tcaron:"\u0164",tcaron:"\u0165",Tcedil:"\u0162",tcedil:"\u0163",Tcy:"\u0422",tcy:"\u0442",tdot:"\u20DB",telrec:"\u2315",Tfr:"\u{1D517}",tfr:"\u{1D531}",there4:"\u2234",therefore:"\u2234",Therefore:"\u2234",Theta:"\u0398",theta:"\u03B8",thetasym:"\u03D1",thetav:"\u03D1",thickapprox:"\u2248",thicksim:"\u223C",ThickSpace:"\u205F\u200A",ThinSpace:"\u2009",thinsp:"\u2009",thkap:"\u2248",thksim:"\u223C",THORN:"\xDE",thorn:"\xFE",tilde:"\u02DC",Tilde:"\u223C",TildeEqual:"\u2243",TildeFullEqual:"\u2245",TildeTilde:"\u2248",timesbar:"\u2A31",timesb:"\u22A0",times:"\xD7",timesd:"\u2A30",tint:"\u222D",toea:"\u2928",topbot:"\u2336",topcir:"\u2AF1",top:"\u22A4",Topf:"\u{1D54B}",topf:"\u{1D565}",topfork:"\u2ADA",tosa:"\u2929",tprime:"\u2034",trade:"\u2122",TRADE:"\u2122",triangle:"\u25B5",triangledown:"\u25BF",triangleleft:"\u25C3",trianglelefteq:"\u22B4",triangleq:"\u225C",triangleright:"\u25B9",trianglerighteq:"\u22B5",tridot:"\u25EC",trie:"\u225C",triminus:"\u2A3A",TripleDot:"\u20DB",triplus:"\u2A39",trisb:"\u29CD",tritime:"\u2A3B",trpezium:"\u23E2",Tscr:"\u{1D4AF}",tscr:"\u{1D4C9}",TScy:"\u0426",tscy:"\u0446",TSHcy:"\u040B",tshcy:"\u045B",Tstrok:"\u0166",tstrok:"\u0167",twixt:"\u226C",twoheadleftarrow:"\u219E",twoheadrightarrow:"\u21A0",Uacute:"\xDA",uacute:"\xFA",uarr:"\u2191",Uarr:"\u219F",uArr:"\u21D1",Uarrocir:"\u2949",Ubrcy:"\u040E",ubrcy:"\u045E",Ubreve:"\u016C",ubreve:"\u016D",Ucirc:"\xDB",ucirc:"\xFB",Ucy:"\u0423",ucy:"\u0443",udarr:"\u21C5",Udblac:"\u0170",udblac:"\u0171",udhar:"\u296E",ufisht:"\u297E",Ufr:"\u{1D518}",ufr:"\u{1D532}",Ugrave:"\xD9",ugrave:"\xF9",uHar:"\u2963",uharl:"\u21BF",uharr:"\u21BE",uhblk:"\u2580",ulcorn:"\u231C",ulcorner:"\u231C",ulcrop:"\u230F",ultri:"\u25F8",Umacr:"\u016A",umacr:"\u016B",uml:"\xA8",UnderBar:"_",UnderBrace:"\u23DF",UnderBracket:"\u23B5",UnderParenthesis:"\u23DD",Union:"\u22C3",UnionPlus:"\u228E",Uogon:"\u0172",uogon:"\u0173",Uopf:"\u{1D54C}",uopf:"\u{1D566}",UpArrowBar:"\u2912",uparrow:"\u2191",UpArrow:"\u2191",Uparrow:"\u21D1",UpArrowDownArrow:"\u21C5",updownarrow:"\u2195",UpDownArrow:"\u2195",Updownarrow:"\u21D5",UpEquilibrium:"\u296E",upharpoonleft:"\u21BF",upharpoonright:"\u21BE",uplus:"\u228E",UpperLeftArrow:"\u2196",UpperRightArrow:"\u2197",upsi:"\u03C5",Upsi:"\u03D2",upsih:"\u03D2",Upsilon:"\u03A5",upsilon:"\u03C5",UpTeeArrow:"\u21A5",UpTee:"\u22A5",upuparrows:"\u21C8",urcorn:"\u231D",urcorner:"\u231D",urcrop:"\u230E",Uring:"\u016E",uring:"\u016F",urtri:"\u25F9",Uscr:"\u{1D4B0}",uscr:"\u{1D4CA}",utdot:"\u22F0",Utilde:"\u0168",utilde:"\u0169",utri:"\u25B5",utrif:"\u25B4",uuarr:"\u21C8",Uuml:"\xDC",uuml:"\xFC",uwangle:"\u29A7",vangrt:"\u299C",varepsilon:"\u03F5",varkappa:"\u03F0",varnothing:"\u2205",varphi:"\u03D5",varpi:"\u03D6",varpropto:"\u221D",varr:"\u2195",vArr:"\u21D5",varrho:"\u03F1",varsigma:"\u03C2",varsubsetneq:"\u228A\uFE00",varsubsetneqq:"\u2ACB\uFE00",varsupsetneq:"\u228B\uFE00",varsupsetneqq:"\u2ACC\uFE00",vartheta:"\u03D1",vartriangleleft:"\u22B2",vartriangleright:"\u22B3",vBar:"\u2AE8",Vbar:"\u2AEB",vBarv:"\u2AE9",Vcy:"\u0412",vcy:"\u0432",vdash:"\u22A2",vDash:"\u22A8",Vdash:"\u22A9",VDash:"\u22AB",Vdashl:"\u2AE6",veebar:"\u22BB",vee:"\u2228",Vee:"\u22C1",veeeq:"\u225A",vellip:"\u22EE",verbar:"|",Verbar:"\u2016",vert:"|",Vert:"\u2016",VerticalBar:"\u2223",VerticalLine:"|",VerticalSeparator:"\u2758",VerticalTilde:"\u2240",VeryThinSpace:"\u200A",Vfr:"\u{1D519}",vfr:"\u{1D533}",vltri:"\u22B2",vnsub:"\u2282\u20D2",vnsup:"\u2283\u20D2",Vopf:"\u{1D54D}",vopf:"\u{1D567}",vprop:"\u221D",vrtri:"\u22B3",Vscr:"\u{1D4B1}",vscr:"\u{1D4CB}",vsubnE:"\u2ACB\uFE00",vsubne:"\u228A\uFE00",vsupnE:"\u2ACC\uFE00",vsupne:"\u228B\uFE00",Vvdash:"\u22AA",vzigzag:"\u299A",Wcirc:"\u0174",wcirc:"\u0175",wedbar:"\u2A5F",wedge:"\u2227",Wedge:"\u22C0",wedgeq:"\u2259",weierp:"\u2118",Wfr:"\u{1D51A}",wfr:"\u{1D534}",Wopf:"\u{1D54E}",wopf:"\u{1D568}",wp:"\u2118",wr:"\u2240",wreath:"\u2240",Wscr:"\u{1D4B2}",wscr:"\u{1D4CC}",xcap:"\u22C2",xcirc:"\u25EF",xcup:"\u22C3",xdtri:"\u25BD",Xfr:"\u{1D51B}",xfr:"\u{1D535}",xharr:"\u27F7",xhArr:"\u27FA",Xi:"\u039E",xi:"\u03BE",xlarr:"\u27F5",xlArr:"\u27F8",xmap:"\u27FC",xnis:"\u22FB",xodot:"\u2A00",Xopf:"\u{1D54F}",xopf:"\u{1D569}",xoplus:"\u2A01",xotime:"\u2A02",xrarr:"\u27F6",xrArr:"\u27F9",Xscr:"\u{1D4B3}",xscr:"\u{1D4CD}",xsqcup:"\u2A06",xuplus:"\u2A04",xutri:"\u25B3",xvee:"\u22C1",xwedge:"\u22C0",Yacute:"\xDD",yacute:"\xFD",YAcy:"\u042F",yacy:"\u044F",Ycirc:"\u0176",ycirc:"\u0177",Ycy:"\u042B",ycy:"\u044B",yen:"\xA5",Yfr:"\u{1D51C}",yfr:"\u{1D536}",YIcy:"\u0407",yicy:"\u0457",Yopf:"\u{1D550}",yopf:"\u{1D56A}",Yscr:"\u{1D4B4}",yscr:"\u{1D4CE}",YUcy:"\u042E",yucy:"\u044E",yuml:"\xFF",Yuml:"\u0178",Zacute:"\u0179",zacute:"\u017A",Zcaron:"\u017D",zcaron:"\u017E",Zcy:"\u0417",zcy:"\u0437",Zdot:"\u017B",zdot:"\u017C",zeetrf:"\u2128",ZeroWidthSpace:"\u200B",Zeta:"\u0396",zeta:"\u03B6",zfr:"\u{1D537}",Zfr:"\u2128",ZHcy:"\u0416",zhcy:"\u0436",zigrarr:"\u21DD",zopf:"\u{1D56B}",Zopf:"\u2124",Zscr:"\u{1D4B5}",zscr:"\u{1D4CF}",zwj:"\u200D",zwnj:"\u200C"}});var oO=G((Dse,hF)=>{"use strict";hF.exports=pF()});var Gm=G((xse,vF)=>{vF.exports=/[!-#%-\*,-\/:;\?@\[-\]_\{\}\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4E\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD803[\uDF55-\uDF59]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDF3C-\uDF3E]|\uD806[\uDC3B\uDE3F-\uDE46\uDE9A-\uDE9C\uDE9E-\uDEA2]|\uD807[\uDC41-\uDC45\uDC70\uDC71\uDEF7\uDEF8]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD81B[\uDE97-\uDE9A]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]/});var yF=G((Cse,mF)=>{"use strict";var gF={};function CX(e){var t,r,n=gF[e];if(n)return n;for(n=gF[e]=[],t=0;t<128;t++)r=String.fromCharCode(t),/^[0-9a-z]$/i.test(r)?n.push(r):n.push("%"+("0"+t.toString(16).toUpperCase()).slice(-2));for(t=0;t=55296&&o<=57343){if(o>=55296&&o<=56319&&n+1=56320&&s<=57343)){d+=encodeURIComponent(e[n]+e[n+1]),n++;continue}d+="%EF%BF%BD";continue}d+=encodeURIComponent(e[n])}return d}Qm.defaultChars=";/?:@&=+$,-_.!~*'()#";Qm.componentChars="-_.!~*'()";mF.exports=Qm});var _F=G((Lse,TF)=>{"use strict";var bF={};function LX(e){var t,r,n=bF[e];if(n)return n;for(n=bF[e]=[],t=0;t<128;t++)r=String.fromCharCode(t),n.push(r);for(t=0;t=55296&&v<=57343?y+="\uFFFD\uFFFD\uFFFD":y+=String.fromCharCode(v),i+=6;continue}if((s&248)==240&&i+91114111?y+="\uFFFD\uFFFD\uFFFD\uFFFD":(v-=65536,y+=String.fromCharCode(55296+(v>>10),56320+(v&1023))),i+=9;continue}y+="\uFFFD"}return y})}Bm.defaultChars=";/?:@&=+$,#";Bm.componentChars="";TF.exports=Bm});var SF=G((Ise,EF)=>{"use strict";EF.exports=function(t){var r="";return r+=t.protocol||"",r+=t.slashes?"//":"",r+=t.auth?t.auth+"@":"",t.hostname&&t.hostname.indexOf(":")!==-1?r+="["+t.hostname+"]":r+=t.hostname||"",r+=t.port?":"+t.port:"",r+=t.pathname||"",r+=t.search||"",r+=t.hash||"",r}});var CF=G((Ase,xF)=>{"use strict";function Km(){this.protocol=null,this.slashes=null,this.auth=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.pathname=null}var IX=/^([a-z0-9.+-]+:)/i,AX=/:[0-9]*$/,RX=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,jX=["<",">",'"',"`"," ","\r",`
-`," "],PX=["{","}","|","\\","^","`"].concat(jX),FX=["'"].concat(PX),kF=["%","/","?",";","#"].concat(FX),OF=["/","?","#"],MX=255,wF=/^[+a-z0-9A-Z_-]{0,63}$/,qX=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,NF={javascript:!0,"javascript:":!0},DF={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0};function VX(e,t){if(e&&e instanceof Km)return e;var r=new Km;return r.parse(e,t),r}Km.prototype.parse=function(e,t){var r,n,i,o,s,l=e;if(l=l.trim(),!t&&e.split("#").length===1){var d=RX.exec(l);if(d)return this.pathname=d[1],d[2]&&(this.search=d[2]),this}var h=IX.exec(l);if(h&&(h=h[0],i=h.toLowerCase(),this.protocol=h,l=l.substr(h.length)),(t||h||l.match(/^\/\/[^@\/]+@[^@\/]+/))&&(s=l.substr(0,2)==="//",s&&!(h&&NF[h])&&(l=l.substr(2),this.slashes=!0)),!NF[h]&&(s||h&&!DF[h])){var v=-1;for(r=0;r127?S+="x":S+=T[m];if(!S.match(wF)){var x=k.slice(0,r),L=k.slice(r+1),O=T.match(qX);O&&(x.push(O[1]),L.unshift(O[2])),L.length&&(l=L.join(".")+l),this.hostname=x.join(".");break}}}}this.hostname.length>MX&&(this.hostname=""),_&&(this.hostname=this.hostname.substr(1,this.hostname.length-2))}var R=l.indexOf("#");R!==-1&&(this.hash=l.substr(R),l=l.slice(0,R));var M=l.indexOf("?");return M!==-1&&(this.search=l.substr(M),l=l.slice(0,M)),l&&(this.pathname=l),DF[i]&&this.hostname&&!this.pathname&&(this.pathname=""),this};Km.prototype.parseHost=function(e){var t=AX.exec(e);t&&(t=t[0],t!==":"&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)};xF.exports=VX});var uO=G((Rse,Rp)=>{"use strict";Rp.exports.encode=yF();Rp.exports.decode=_F();Rp.exports.format=SF();Rp.exports.parse=CF()});var sO=G((jse,LF)=>{LF.exports=/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/});var lO=G((Pse,IF)=>{IF.exports=/[\0-\x1F\x7F-\x9F]/});var RF=G((Fse,AF)=>{AF.exports=/[\xAD\u0600-\u0605\u061C\u06DD\u070F\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/});var cO=G((Mse,jF)=>{jF.exports=/[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/});var PF=G(Ic=>{"use strict";Ic.Any=sO();Ic.Cc=lO();Ic.Cf=RF();Ic.P=Gm();Ic.Z=cO()});var Pt=G(en=>{"use strict";function UX(e){return Object.prototype.toString.call(e)}function GX(e){return UX(e)==="[object String]"}var QX=Object.prototype.hasOwnProperty;function FF(e,t){return QX.call(e,t)}function BX(e){var t=Array.prototype.slice.call(arguments,1);return t.forEach(function(r){if(!!r){if(typeof r!="object")throw new TypeError(r+"must be object");Object.keys(r).forEach(function(n){e[n]=r[n]})}}),e}function KX(e,t,r){return[].concat(e.slice(0,t),r,e.slice(t+1))}function MF(e){return!(e>=55296&&e<=57343||e>=64976&&e<=65007||(e&65535)==65535||(e&65535)==65534||e>=0&&e<=8||e===11||e>=14&&e<=31||e>=127&&e<=159||e>1114111)}function qF(e){if(e>65535){e-=65536;var t=55296+(e>>10),r=56320+(e&1023);return String.fromCharCode(t,r)}return String.fromCharCode(e)}var VF=/\\([!"#$%&'()*+,\-.\/:;<=>?@[\\\]^_`{|}~])/g,HX=/&([a-z#][a-z0-9]{1,31});/gi,zX=new RegExp(VF.source+"|"+HX.source,"gi"),WX=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i,UF=oO();function YX(e,t){var r=0;return FF(UF,t)?UF[t]:t.charCodeAt(0)===35&&WX.test(t)&&(r=t[1].toLowerCase()==="x"?parseInt(t.slice(2),16):parseInt(t.slice(1),10),MF(r))?qF(r):e}function JX(e){return e.indexOf("\\")<0?e:e.replace(VF,"$1")}function XX(e){return e.indexOf("\\")<0&&e.indexOf("&")<0?e:e.replace(zX,function(t,r,n){return r||YX(t,n)})}var ZX=/[&<>"]/,$X=/[&<>"]/g,eZ={"&":"&","<":"<",">":">",'"':"""};function tZ(e){return eZ[e]}function rZ(e){return ZX.test(e)?e.replace($X,tZ):e}var nZ=/[.?*+^$[\]\\(){}|-]/g;function iZ(e){return e.replace(nZ,"\\$&")}function aZ(e){switch(e){case 9:case 32:return!0}return!1}function oZ(e){if(e>=8192&&e<=8202)return!0;switch(e){case 9:case 10:case 11:case 12:case 13:case 32:case 160:case 5760:case 8239:case 8287:case 12288:return!0}return!1}var uZ=Gm();function sZ(e){return uZ.test(e)}function lZ(e){switch(e){case 33:case 34:case 35:case 36:case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:case 47:case 58:case 59:case 60:case 61:case 62:case 63:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 124:case 125:case 126:return!0;default:return!1}}function cZ(e){return e=e.trim().replace(/\s+/g," "),"\u1E9E".toLowerCase()==="\u1E7E"&&(e=e.replace(/ẞ/g,"\xDF")),e.toLowerCase().toUpperCase()}en.lib={};en.lib.mdurl=uO();en.lib.ucmicro=PF();en.assign=BX;en.isString=GX;en.has=FF;en.unescapeMd=JX;en.unescapeAll=XX;en.isValidEntityCode=MF;en.fromCodePoint=qF;en.escapeHtml=rZ;en.arrayReplaceAt=KX;en.isSpace=aZ;en.isWhiteSpace=oZ;en.isMdAsciiPunct=lZ;en.isPunctChar=sZ;en.escapeRE=iZ;en.normalizeReference=cZ});var QF=G((Use,GF)=>{"use strict";GF.exports=function(t,r,n){var i,o,s,l,d=-1,h=t.posMax,v=t.pos;for(t.pos=r+1,i=1;t.pos{"use strict";var BF=Pt().unescapeAll;KF.exports=function(t,r,n){var i,o,s=0,l=r,d={ok:!1,pos:0,lines:0,str:""};if(t.charCodeAt(r)===60){for(r++;r32))return d;if(i===41){if(o===0)break;o--}r++}return l===r||o!==0||(d.str=BF(t.slice(l,r)),d.lines=s,d.pos=r,d.ok=!0),d}});var WF=G((Qse,zF)=>{"use strict";var fZ=Pt().unescapeAll;zF.exports=function(t,r,n){var i,o,s=0,l=r,d={ok:!1,pos:0,lines:0,str:""};if(r>=n||(o=t.charCodeAt(r),o!==34&&o!==39&&o!==40))return d;for(r++,o===40&&(o=41);r{"use strict";Hm.parseLinkLabel=QF();Hm.parseLinkDestination=HF();Hm.parseLinkTitle=WF()});var XF=G((Kse,JF)=>{"use strict";var dZ=Pt().assign,pZ=Pt().unescapeAll,Fs=Pt().escapeHtml,Wa={};Wa.code_inline=function(e,t,r,n,i){var o=e[t];return""+Fs(e[t].content)+""};Wa.code_block=function(e,t,r,n,i){var o=e[t];return""+Fs(e[t].content)+`
-`};Wa.fence=function(e,t,r,n,i){var o=e[t],s=o.info?pZ(o.info).trim():"",l="",d="",h,v,y,b,D;return s&&(y=s.split(/(\s+)/g),l=y[0],d=y.slice(2).join("")),r.highlight?h=r.highlight(o.content,l,d)||Fs(o.content):h=Fs(o.content),h.indexOf(""+h+`
-`):""+h+`
-`};Wa.image=function(e,t,r,n,i){var o=e[t];return o.attrs[o.attrIndex("alt")][1]=i.renderInlineAsText(o.children,r,n),i.renderToken(e,t,r)};Wa.hardbreak=function(e,t,r){return r.xhtmlOut?`
-`:`
-`};Wa.softbreak=function(e,t,r){return r.breaks?r.xhtmlOut?`
-`:`
-`:`
-`};Wa.text=function(e,t){return Fs(e[t].content)};Wa.html_block=function(e,t){return e[t].content};Wa.html_inline=function(e,t){return e[t].content};function Ac(){this.rules=dZ({},Wa)}Ac.prototype.renderAttrs=function(t){var r,n,i;if(!t.attrs)return"";for(i="",r=0,n=t.attrs.length;r
-`:">",o)};Ac.prototype.renderInline=function(e,t,r){for(var n,i="",o=this.rules,s=0,l=e.length;s{"use strict";function Oa(){this.__rules__=[],this.__cache__=null}Oa.prototype.__find__=function(e){for(var t=0;t{"use strict";var hZ=/\r\n?|\n/g,vZ=/\0/g;$F.exports=function(t){var r;r=t.src.replace(hZ,`
-`),r=r.replace(vZ,"\uFFFD"),t.src=r}});var rM=G((Wse,tM)=>{"use strict";tM.exports=function(t){var r;t.inlineMode?(r=new t.Token("inline","",0),r.content=t.src,r.map=[0,1],r.children=[],t.tokens.push(r)):t.md.block.parse(t.src,t.md,t.env,t.tokens)}});var iM=G((Yse,nM)=>{"use strict";nM.exports=function(t){var r=t.tokens,n,i,o;for(i=0,o=r.length;i{"use strict";var gZ=Pt().arrayReplaceAt;function mZ(e){return/^\s]/i.test(e)}function yZ(e){return/^<\/a\s*>/i.test(e)}aM.exports=function(t){var r,n,i,o,s,l,d,h,v,y,b,D,_,k,T,S,m=t.tokens,w;if(!!t.md.options.linkify){for(n=0,i=m.length;n=0;r--){if(l=o[r],l.type==="link_close"){for(r--;o[r].level!==l.level&&o[r].type!=="link_open";)r--;continue}if(l.type==="html_inline"&&(mZ(l.content)&&_>0&&_--,yZ(l.content)&&_++),!(_>0)&&l.type==="text"&&t.md.linkify.test(l.content)){for(v=l.content,w=t.md.linkify.match(v),d=[],D=l.level,b=0,h=0;hb&&(s=new t.Token("text","",0),s.content=v.slice(b,y),s.level=D,d.push(s)),s=new t.Token("link_open","a",1),s.attrs=[["href",T]],s.level=D++,s.markup="linkify",s.info="auto",d.push(s),s=new t.Token("text","",0),s.content=S,s.level=D,d.push(s),s=new t.Token("link_close","a",-1),s.level=--D,s.markup="linkify",s.info="auto",d.push(s),b=w[h].lastIndex);b{"use strict";var uM=/\+-|\.\.|\?\?\?\?|!!!!|,,|--/,bZ=/\((c|tm|r|p)\)/i,TZ=/\((c|tm|r|p)\)/ig,_Z={c:"\xA9",r:"\xAE",p:"\xA7",tm:"\u2122"};function EZ(e,t){return _Z[t.toLowerCase()]}function SZ(e){var t,r,n=0;for(t=e.length-1;t>=0;t--)r=e[t],r.type==="text"&&!n&&(r.content=r.content.replace(TZ,EZ)),r.type==="link_open"&&r.info==="auto"&&n--,r.type==="link_close"&&r.info==="auto"&&n++}function kZ(e){var t,r,n=0;for(t=e.length-1;t>=0;t--)r=e[t],r.type==="text"&&!n&&uM.test(r.content)&&(r.content=r.content.replace(/\+-/g,"\xB1").replace(/\.{2,}/g,"\u2026").replace(/([?!])…/g,"$1..").replace(/([?!]){4,}/g,"$1$1$1").replace(/,{2,}/g,",").replace(/(^|[^-])---(?=[^-]|$)/mg,"$1\u2014").replace(/(^|\s)--(?=\s|$)/mg,"$1\u2013").replace(/(^|[^-\s])--(?=[^-\s]|$)/mg,"$1\u2013")),r.type==="link_open"&&r.info==="auto"&&n--,r.type==="link_close"&&r.info==="auto"&&n++}sM.exports=function(t){var r;if(!!t.md.options.typographer)for(r=t.tokens.length-1;r>=0;r--)t.tokens[r].type==="inline"&&(bZ.test(t.tokens[r].content)&&SZ(t.tokens[r].children),uM.test(t.tokens[r].content)&&kZ(t.tokens[r].children))}});var gM=G((Zse,vM)=>{"use strict";var cM=Pt().isWhiteSpace,fM=Pt().isPunctChar,dM=Pt().isMdAsciiPunct,OZ=/['"]/,pM=/['"]/g,hM="\u2019";function Wm(e,t,r){return e.substr(0,t)+r+e.substr(t+1)}function wZ(e,t){var r,n,i,o,s,l,d,h,v,y,b,D,_,k,T,S,m,w,x,L,O;for(x=[],r=0;r=0&&!(x[m].level<=d);m--);if(x.length=m+1,n.type!=="text")continue;i=n.content,s=0,l=i.length;e:for(;s=0)v=i.charCodeAt(o.index-1);else for(m=r-1;m>=0&&!(e[m].type==="softbreak"||e[m].type==="hardbreak");m--)if(!!e[m].content){v=e[m].content.charCodeAt(e[m].content.length-1);break}if(y=32,s=48&&v<=57&&(S=T=!1),T&&S&&(T=b,S=D),!T&&!S){w&&(n.content=Wm(n.content,o.index,hM));continue}if(S){for(m=x.length-1;m>=0&&(h=x[m],!(x[m].level=0;r--)t.tokens[r].type!=="inline"||!OZ.test(t.tokens[r].content)||wZ(t.tokens[r].children,t)}});var Ym=G(($se,mM)=>{"use strict";function Rc(e,t,r){this.type=e,this.tag=t,this.attrs=null,this.map=null,this.nesting=r,this.level=0,this.children=null,this.content="",this.markup="",this.info="",this.meta=null,this.block=!1,this.hidden=!1}Rc.prototype.attrIndex=function(t){var r,n,i;if(!this.attrs)return-1;for(r=this.attrs,n=0,i=r.length;n=0&&(n=this.attrs[r][1]),n};Rc.prototype.attrJoin=function(t,r){var n=this.attrIndex(t);n<0?this.attrPush([t,r]):this.attrs[n][1]=this.attrs[n][1]+" "+r};mM.exports=Rc});var TM=G((ele,bM)=>{"use strict";var NZ=Ym();function yM(e,t,r){this.src=e,this.env=r,this.tokens=[],this.inlineMode=!1,this.md=t}yM.prototype.Token=NZ;bM.exports=yM});var EM=G((tle,_M)=>{"use strict";var DZ=zm(),fO=[["normalize",eM()],["block",rM()],["inline",iM()],["linkify",oM()],["replacements",lM()],["smartquotes",gM()]];function dO(){this.ruler=new DZ;for(var e=0;e{"use strict";var pO=Pt().isSpace;function hO(e,t){var r=e.bMarks[t]+e.tShift[t],n=e.eMarks[t];return e.src.substr(r,n-r)}function SM(e){var t=[],r=0,n=e.length,i,o=!1,s=0,l="";for(i=e.charCodeAt(r);rn||(v=r+1,t.sCount[v]=4||(l=t.bMarks[v]+t.tShift[v],l>=t.eMarks[v])||(L=t.src.charCodeAt(l++),L!==124&&L!==45&&L!==58)||l>=t.eMarks[v]||(O=t.src.charCodeAt(l++),O!==124&&O!==45&&O!==58&&!pO(O))||L===45&&pO(O))return!1;for(;l=4||(y=SM(s),y.length&&y[0]===""&&y.shift(),y.length&&y[y.length-1]===""&&y.pop(),b=y.length,b===0||b!==_.length))return!1;if(i)return!0;for(m=t.parentType,t.parentType="table",x=t.md.block.ruler.getRules("blockquote"),D=t.push("table_open","table",1),D.map=T=[r,0],D=t.push("thead_open","thead",1),D.map=[r,r+1],D=t.push("tr_open","tr",1),D.map=[r,r+1],d=0;d=4)break;for(y=SM(s),y.length&&y[0]===""&&y.shift(),y.length&&y[y.length-1]===""&&y.pop(),v===r+2&&(D=t.push("tbody_open","tbody",1),D.map=S=[r+2,0]),D=t.push("tr_open","tr",1),D.map=[v,v+1],d=0;d{"use strict";wM.exports=function(t,r,n){var i,o,s;if(t.sCount[r]-t.blkIndent<4)return!1;for(o=i=r+1;i=4){i++,o=i;continue}break}return t.line=o,s=t.push("code_block","code",0),s.content=t.getLines(r,o,4+t.blkIndent,!1)+`
-`,s.map=[r,t.line],!0}});var xM=G((ile,DM)=>{"use strict";DM.exports=function(t,r,n,i){var o,s,l,d,h,v,y,b=!1,D=t.bMarks[r]+t.tShift[r],_=t.eMarks[r];if(t.sCount[r]-t.blkIndent>=4||D+3>_||(o=t.src.charCodeAt(D),o!==126&&o!==96)||(h=D,D=t.skipChars(D,o),s=D-h,s<3)||(y=t.src.slice(h,D),l=t.src.slice(D,_),o===96&&l.indexOf(String.fromCharCode(o))>=0))return!1;if(i)return!0;for(d=r;d++,!(d>=n||(D=h=t.bMarks[d]+t.tShift[d],_=t.eMarks[d],D<_&&t.sCount[d]=4)&&(D=t.skipChars(D,o),!(D-h{"use strict";var CM=Pt().isSpace;LM.exports=function(t,r,n,i){var o,s,l,d,h,v,y,b,D,_,k,T,S,m,w,x,L,O,R,M,q=t.lineMax,z=t.bMarks[r]+t.tShift[r],B=t.eMarks[r];if(t.sCount[r]-t.blkIndent>=4||t.src.charCodeAt(z++)!==62)return!1;if(i)return!0;for(d=D=t.sCount[r]+1,t.src.charCodeAt(z)===32?(z++,d++,D++,o=!1,x=!0):t.src.charCodeAt(z)===9?(x=!0,(t.bsCount[r]+D)%4==3?(z++,d++,D++,o=!1):o=!0):x=!1,_=[t.bMarks[r]],t.bMarks[r]=z;z=B,m=[t.sCount[r]],t.sCount[r]=D-d,w=[t.tShift[r]],t.tShift[r]=z-t.bMarks[r],O=t.md.block.ruler.getRules("blockquote"),S=t.parentType,t.parentType="blockquote",b=r+1;b=B));b++){if(t.src.charCodeAt(z++)===62&&!M){for(d=D=t.sCount[b]+1,t.src.charCodeAt(z)===32?(z++,d++,D++,o=!1,x=!0):t.src.charCodeAt(z)===9?(x=!0,(t.bsCount[b]+D)%4==3?(z++,d++,D++,o=!1):o=!0):x=!1,_.push(t.bMarks[b]),t.bMarks[b]=z;z=B,k.push(t.bsCount[b]),t.bsCount[b]=t.sCount[b]+1+(x?1:0),m.push(t.sCount[b]),t.sCount[b]=D-d,w.push(t.tShift[b]),t.tShift[b]=z-t.bMarks[b];continue}if(v)break;for(L=!1,l=0,h=O.length;l",R.map=y=[r,0],t.md.block.tokenize(t,r,b),R=t.push("blockquote_close","blockquote",-1),R.markup=">",t.lineMax=q,t.parentType=S,y[1]=t.line,l=0;l{"use strict";var xZ=Pt().isSpace;AM.exports=function(t,r,n,i){var o,s,l,d,h=t.bMarks[r]+t.tShift[r],v=t.eMarks[r];if(t.sCount[r]-t.blkIndent>=4||(o=t.src.charCodeAt(h++),o!==42&&o!==45&&o!==95))return!1;for(s=1;h{"use strict";var jM=Pt().isSpace;function PM(e,t){var r,n,i,o;return n=e.bMarks[t]+e.tShift[t],i=e.eMarks[t],r=e.src.charCodeAt(n++),r!==42&&r!==45&&r!==43||n=o||(r=e.src.charCodeAt(i++),r<48||r>57))return-1;for(;;){if(i>=o)return-1;if(r=e.src.charCodeAt(i++),r>=48&&r<=57){if(i-n>=10)return-1;continue}if(r===41||r===46)break;return-1}return i=4||t.listIndent>=0&&t.sCount[r]-t.listIndent>=4&&t.sCount[r]=t.blkIndent&&(Fe=!0),(B=FM(t,r))>=0){if(y=!0,P=t.bMarks[r]+t.tShift[r],S=Number(t.src.slice(P,B-1)),Fe&&S!==1)return!1}else if((B=PM(t,r))>=0)y=!1;else return!1;if(Fe&&t.skipSpaces(B)>=t.eMarks[r])return!1;if(T=t.src.charCodeAt(B-1),i)return!0;for(k=t.tokens.length,y?(ge=t.push("ordered_list_open","ol",1),S!==1&&(ge.attrs=[["start",S]])):ge=t.push("bullet_list_open","ul",1),ge.map=_=[r,0],ge.markup=String.fromCharCode(T),w=r,Q=!1,xe=t.md.block.ruler.getRules("list"),O=t.parentType,t.parentType="list";w=m?h=1:h=x-v,h>4&&(h=1),d=v+h,ge=t.push("list_item_open","li",1),ge.markup=String.fromCharCode(T),ge.map=b=[r,0],y&&(ge.info=t.src.slice(P,B-1)),q=t.tight,M=t.tShift[r],R=t.sCount[r],L=t.listIndent,t.listIndent=t.blkIndent,t.blkIndent=d,t.tight=!0,t.tShift[r]=s-t.bMarks[r],t.sCount[r]=x,s>=m&&t.isEmpty(r+1)?t.line=Math.min(t.line+2,n):t.md.block.tokenize(t,r,n,!0),(!t.tight||Q)&&(Le=!1),Q=t.line-r>1&&t.isEmpty(t.line-1),t.blkIndent=t.listIndent,t.listIndent=L,t.tShift[r]=M,t.sCount[r]=R,t.tight=q,ge=t.push("list_item_close","li",-1),ge.markup=String.fromCharCode(T),w=r=t.line,b[1]=w,s=t.bMarks[r],w>=n||t.sCount[w]=4)break;for(he=!1,l=0,D=xe.length;l{"use strict";var LZ=Pt().normalizeReference,Jm=Pt().isSpace;VM.exports=function(t,r,n,i){var o,s,l,d,h,v,y,b,D,_,k,T,S,m,w,x,L=0,O=t.bMarks[r]+t.tShift[r],R=t.eMarks[r],M=r+1;if(t.sCount[r]-t.blkIndent>=4||t.src.charCodeAt(O)!==91)return!1;for(;++O3)&&!(t.sCount[M]<0)){for(m=!1,v=0,y=w.length;v{"use strict";GM.exports=["address","article","aside","base","basefont","blockquote","body","caption","center","col","colgroup","dd","details","dialog","dir","div","dl","dt","fieldset","figcaption","figure","footer","form","frame","frameset","h1","h2","h3","h4","h5","h6","head","header","hr","html","iframe","legend","li","link","main","menu","menuitem","nav","noframes","ol","optgroup","option","p","param","section","source","summary","table","tbody","td","tfoot","th","thead","title","tr","track","ul"]});var gO=G((cle,vO)=>{"use strict";var IZ="[a-zA-Z_:][a-zA-Z0-9:._-]*",AZ="[^\"'=<>`\\x00-\\x20]+",RZ="'[^']*'",jZ='"[^"]*"',PZ="(?:"+AZ+"|"+RZ+"|"+jZ+")",FZ="(?:\\s+"+IZ+"(?:\\s*=\\s*"+PZ+")?)",BM="<[A-Za-z][A-Za-z0-9\\-]*"+FZ+"*\\s*\\/?>",KM="<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>",MZ="|",qZ="<[?][\\s\\S]*?[?]>",VZ="]*>",UZ="",GZ=new RegExp("^(?:"+BM+"|"+KM+"|"+MZ+"|"+qZ+"|"+VZ+"|"+UZ+")"),QZ=new RegExp("^(?:"+BM+"|"+KM+")");vO.exports.HTML_TAG_RE=GZ;vO.exports.HTML_OPEN_CLOSE_TAG_RE=QZ});var zM=G((fle,HM)=>{"use strict";var BZ=QM(),KZ=gO().HTML_OPEN_CLOSE_TAG_RE,jc=[[/^<(script|pre|style|textarea)(?=(\s|>|$))/i,/<\/(script|pre|style|textarea)>/i,!0],[/^/,!0],[/^<\?/,/\?>/,!0],[/^/,!0],[/^/,!0],[new RegExp("^?("+BZ.join("|")+")(?=(\\s|/?>|$))","i"),/^$/,!0],[new RegExp(KZ.source+"\\s*$"),/^$/,!1]];HM.exports=function(t,r,n,i){var o,s,l,d,h=t.bMarks[r]+t.tShift[r],v=t.eMarks[r];if(t.sCount[r]-t.blkIndent>=4||!t.md.options.html||t.src.charCodeAt(h)!==60)return!1;for(d=t.src.slice(h,v),o=0;o{"use strict";var WM=Pt().isSpace;YM.exports=function(t,r,n,i){var o,s,l,d,h=t.bMarks[r]+t.tShift[r],v=t.eMarks[r];if(t.sCount[r]-t.blkIndent>=4||(o=t.src.charCodeAt(h),o!==35||h>=v))return!1;for(s=1,o=t.src.charCodeAt(++h);o===35&&h6||hh&&WM(t.src.charCodeAt(l-1))&&(v=l),t.line=r+1,d=t.push("heading_open","h"+String(s),1),d.markup="########".slice(0,s),d.map=[r,t.line],d=t.push("inline","",0),d.content=t.src.slice(h,v).trim(),d.map=[r,t.line],d.children=[],d=t.push("heading_close","h"+String(s),-1),d.markup="########".slice(0,s)),!0)}});var ZM=G((ple,XM)=>{"use strict";XM.exports=function(t,r,n){var i,o,s,l,d,h,v,y,b,D=r+1,_,k=t.md.block.ruler.getRules("paragraph");if(t.sCount[r]-t.blkIndent>=4)return!1;for(_=t.parentType,t.parentType="paragraph";D3)){if(t.sCount[D]>=t.blkIndent&&(h=t.bMarks[D]+t.tShift[D],v=t.eMarks[D],h=v)))){y=b===61?1:2;break}if(!(t.sCount[D]<0)){for(o=!1,s=0,l=k.length;s{"use strict";$M.exports=function(t,r){var n,i,o,s,l,d,h=r+1,v=t.md.block.ruler.getRules("paragraph"),y=t.lineMax;for(d=t.parentType,t.parentType="paragraph";h3)&&!(t.sCount[h]<0)){for(i=!1,o=0,s=v.length;o{"use strict";var tq=Ym(),Xm=Pt().isSpace;function Ya(e,t,r,n){var i,o,s,l,d,h,v,y;for(this.src=e,this.md=t,this.env=r,this.tokens=n,this.bMarks=[],this.eMarks=[],this.tShift=[],this.sCount=[],this.bsCount=[],this.blkIndent=0,this.line=0,this.lineMax=0,this.tight=!1,this.ddIndent=-1,this.listIndent=-1,this.parentType="root",this.level=0,this.result="",o=this.src,y=!1,s=l=h=v=0,d=o.length;l0&&this.level++,this.tokens.push(n),n};Ya.prototype.isEmpty=function(t){return this.bMarks[t]+this.tShift[t]>=this.eMarks[t]};Ya.prototype.skipEmptyLines=function(t){for(var r=this.lineMax;tr;)if(!Xm(this.src.charCodeAt(--t)))return t+1;return t};Ya.prototype.skipChars=function(t,r){for(var n=this.src.length;tn;)if(r!==this.src.charCodeAt(--t))return t+1;return t};Ya.prototype.getLines=function(t,r,n,i){var o,s,l,d,h,v,y,b=t;if(t>=r)return"";for(v=new Array(r-t),o=0;bn?v[o]=new Array(s-n+1).join(" ")+this.src.slice(d,h):v[o]=this.src.slice(d,h)}return v.join("")};Ya.prototype.Token=tq;rq.exports=Ya});var aq=G((gle,iq)=>{"use strict";var HZ=zm(),Zm=[["table",OM(),["paragraph","reference"]],["code",NM()],["fence",xM(),["paragraph","reference","blockquote","list"]],["blockquote",IM(),["paragraph","reference","blockquote","list"]],["hr",RM(),["paragraph","reference","blockquote","list"]],["list",qM(),["paragraph","reference","blockquote"]],["reference",UM()],["html_block",zM(),["paragraph","reference","blockquote"]],["heading",JM(),["paragraph","reference","blockquote"]],["lheading",ZM()],["paragraph",eq()]];function $m(){this.ruler=new HZ;for(var e=0;e=r||e.sCount[l]=h){e.line=r;break}for(i=0;i{"use strict";function zZ(e){switch(e){case 10:case 33:case 35:case 36:case 37:case 38:case 42:case 43:case 45:case 58:case 60:case 61:case 62:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 125:case 126:return!0;default:return!1}}oq.exports=function(t,r){for(var n=t.pos;n{"use strict";var WZ=Pt().isSpace;sq.exports=function(t,r){var n,i,o,s=t.pos;if(t.src.charCodeAt(s)!==10)return!1;if(n=t.pending.length-1,i=t.posMax,!r)if(n>=0&&t.pending.charCodeAt(n)===32)if(n>=1&&t.pending.charCodeAt(n-1)===32){for(o=n-1;o>=1&&t.pending.charCodeAt(o-1)===32;)o--;t.pending=t.pending.slice(0,o),t.push("hardbreak","br",0)}else t.pending=t.pending.slice(0,-1),t.push("softbreak","br",0);else t.push("softbreak","br",0);for(s++;s{"use strict";var YZ=Pt().isSpace,mO=[];for(yO=0;yO<256;yO++)mO.push(0);var yO;"\\!\"#$%&'()*+,./:;<=>?@[]^_`{|}~-".split("").forEach(function(e){mO[e.charCodeAt(0)]=1});cq.exports=function(t,r){var n,i=t.pos,o=t.posMax;if(t.src.charCodeAt(i)!==92)return!1;if(i++,i{"use strict";dq.exports=function(t,r){var n,i,o,s,l,d,h,v,y=t.pos,b=t.src.charCodeAt(y);if(b!==96)return!1;for(n=y,y++,i=t.posMax;y{"use strict";bO.exports.tokenize=function(t,r){var n,i,o,s,l,d=t.pos,h=t.src.charCodeAt(d);if(r||h!==126||(i=t.scanDelims(t.pos,!0),s=i.length,l=String.fromCharCode(h),s<2))return!1;for(s%2&&(o=t.push("text","",0),o.content=l,s--),n=0;n{"use strict";_O.exports.tokenize=function(t,r){var n,i,o,s=t.pos,l=t.src.charCodeAt(s);if(r||l!==95&&l!==42)return!1;for(i=t.scanDelims(t.pos,l===42),n=0;n=0;r--)n=t[r],!(n.marker!==95&&n.marker!==42)&&n.end!==-1&&(i=t[n.end],l=r>0&&t[r-1].end===n.end+1&&t[r-1].marker===n.marker&&t[r-1].token===n.token-1&&t[n.end+1].token===i.token+1,s=String.fromCharCode(n.marker),o=e.tokens[n.token],o.type=l?"strong_open":"em_open",o.tag=l?"strong":"em",o.nesting=1,o.markup=l?s+s:s,o.content="",o=e.tokens[i.token],o.type=l?"strong_close":"em_close",o.tag=l?"strong":"em",o.nesting=-1,o.markup=l?s+s:s,o.content="",l&&(e.tokens[t[r-1].token].content="",e.tokens[t[n.end+1].token].content="",r--))}_O.exports.postProcess=function(t){var r,n=t.tokens_meta,i=t.tokens_meta.length;for(vq(t,t.delimiters),r=0;r{"use strict";var JZ=Pt().normalizeReference,SO=Pt().isSpace;gq.exports=function(t,r){var n,i,o,s,l,d,h,v,y,b="",D="",_=t.pos,k=t.posMax,T=t.pos,S=!0;if(t.src.charCodeAt(t.pos)!==91||(l=t.pos+1,s=t.md.helpers.parseLinkLabel(t,t.pos,!0),s<0))return!1;if(d=s+1,d=k)return!1;if(T=d,h=t.md.helpers.parseLinkDestination(t.src,d,t.posMax),h.ok){for(b=t.md.normalizeLink(h.str),t.md.validateLink(b)?d=h.pos:b="",T=d;d=k||t.src.charCodeAt(d)!==41)&&(S=!0),d++}if(S){if(typeof t.env.references=="undefined")return!1;if(d=0?o=t.src.slice(T,d++):d=s+1):d=s+1,o||(o=t.src.slice(l,s)),v=t.env.references[JZ(o)],!v)return t.pos=_,!1;b=v.href,D=v.title}return r||(t.pos=l,t.posMax=s,y=t.push("link_open","a",1),y.attrs=n=[["href",b]],D&&n.push(["title",D]),t.md.inline.tokenize(t),y=t.push("link_close","a",-1)),t.pos=d,t.posMax=k,!0}});var bq=G((kle,yq)=>{"use strict";var XZ=Pt().normalizeReference,kO=Pt().isSpace;yq.exports=function(t,r){var n,i,o,s,l,d,h,v,y,b,D,_,k,T="",S=t.pos,m=t.posMax;if(t.src.charCodeAt(t.pos)!==33||t.src.charCodeAt(t.pos+1)!==91||(d=t.pos+2,l=t.md.helpers.parseLinkLabel(t,t.pos+1,!1),l<0))return!1;if(h=l+1,h=m)return!1;for(k=h,y=t.md.helpers.parseLinkDestination(t.src,h,t.posMax),y.ok&&(T=t.md.normalizeLink(y.str),t.md.validateLink(T)?h=y.pos:T=""),k=h;h=m||t.src.charCodeAt(h)!==41)return t.pos=S,!1;h++}else{if(typeof t.env.references=="undefined")return!1;if(h=0?s=t.src.slice(k,h++):h=l+1):h=l+1,s||(s=t.src.slice(d,l)),v=t.env.references[XZ(s)],!v)return t.pos=S,!1;T=v.href,b=v.title}return r||(o=t.src.slice(d,l),t.md.inline.parse(o,t.md,t.env,_=[]),D=t.push("image","img",0),D.attrs=n=[["src",T],["alt",""]],D.children=_,D.content=o,b&&n.push(["title",b])),t.pos=h,t.posMax=m,!0}});var _q=G((Ole,Tq)=>{"use strict";var ZZ=/^([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)$/,$Z=/^([a-zA-Z][a-zA-Z0-9+.\-]{1,31}):([^<>\x00-\x20]*)$/;Tq.exports=function(t,r){var n,i,o,s,l,d,h=t.pos;if(t.src.charCodeAt(h)!==60)return!1;for(l=t.pos,d=t.posMax;;){if(++h>=d||(s=t.src.charCodeAt(h),s===60))return!1;if(s===62)break}return n=t.src.slice(l+1,h),$Z.test(n)?(i=t.md.normalizeLink(n),t.md.validateLink(i)?(r||(o=t.push("link_open","a",1),o.attrs=[["href",i]],o.markup="autolink",o.info="auto",o=t.push("text","",0),o.content=t.md.normalizeLinkText(n),o=t.push("link_close","a",-1),o.markup="autolink",o.info="auto"),t.pos+=n.length+2,!0):!1):ZZ.test(n)?(i=t.md.normalizeLink("mailto:"+n),t.md.validateLink(i)?(r||(o=t.push("link_open","a",1),o.attrs=[["href",i]],o.markup="autolink",o.info="auto",o=t.push("text","",0),o.content=t.md.normalizeLinkText(n),o=t.push("link_close","a",-1),o.markup="autolink",o.info="auto"),t.pos+=n.length+2,!0):!1):!1}});var Sq=G((wle,Eq)=>{"use strict";var e$=gO().HTML_TAG_RE;function t$(e){var t=e|32;return t>=97&&t<=122}Eq.exports=function(t,r){var n,i,o,s,l=t.pos;return!t.md.options.html||(o=t.posMax,t.src.charCodeAt(l)!==60||l+2>=o)||(n=t.src.charCodeAt(l+1),n!==33&&n!==63&&n!==47&&!t$(n))||(i=t.src.slice(l).match(e$),!i)?!1:(r||(s=t.push("html_inline","",0),s.content=t.src.slice(l,l+i[0].length)),t.pos+=i[0].length,!0)}});var Nq=G((Nle,wq)=>{"use strict";var kq=oO(),r$=Pt().has,n$=Pt().isValidEntityCode,Oq=Pt().fromCodePoint,i$=/^((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i,a$=/^&([a-z][a-z0-9]{1,31});/i;wq.exports=function(t,r){var n,i,o,s=t.pos,l=t.posMax;if(t.src.charCodeAt(s)!==38)return!1;if(s+1{"use strict";function Dq(e,t){var r,n,i,o,s,l,d,h,v={},y=t.length;if(!!y){var b=0,D=-2,_=[];for(r=0;rs;n-=_[n]+1)if(o=t[n],o.marker===i.marker&&o.open&&o.end<0&&(d=!1,(o.close||i.open)&&(o.length+i.length)%3==0&&(o.length%3!=0||i.length%3!=0)&&(d=!0),!d)){h=n>0&&!t[n-1].open?_[n-1]+1:0,_[r]=r-n+h,_[n]=h,i.open=!1,o.end=r,o.close=!1,l=-1,D=-2;break}l!==-1&&(v[i.marker][(i.open?3:0)+(i.length||0)%3]=l)}}}xq.exports=function(t){var r,n=t.tokens_meta,i=t.tokens_meta.length;for(Dq(t,t.delimiters),r=0;r{"use strict";Lq.exports=function(t){var r,n,i=0,o=t.tokens,s=t.tokens.length;for(r=n=0;r0&&i++,o[r].type==="text"&&r+1{"use strict";var OO=Ym(),Aq=Pt().isWhiteSpace,Rq=Pt().isPunctChar,jq=Pt().isMdAsciiPunct;function jp(e,t,r,n){this.src=e,this.env=r,this.md=t,this.tokens=n,this.tokens_meta=Array(n.length),this.pos=0,this.posMax=this.src.length,this.level=0,this.pending="",this.pendingLevel=0,this.cache={},this.delimiters=[],this._prev_delimiters=[],this.backticks={},this.backticksScanned=!1}jp.prototype.pushPending=function(){var e=new OO("text","",0);return e.content=this.pending,e.level=this.pendingLevel,this.tokens.push(e),this.pending="",e};jp.prototype.push=function(e,t,r){this.pending&&this.pushPending();var n=new OO(e,t,r),i=null;return r<0&&(this.level--,this.delimiters=this._prev_delimiters.pop()),n.level=this.level,r>0&&(this.level++,this._prev_delimiters.push(this.delimiters),this.delimiters=[],i={delimiters:this.delimiters}),this.pendingLevel=this.level,this.tokens.push(n),this.tokens_meta.push(i),n};jp.prototype.scanDelims=function(e,t){var r=e,n,i,o,s,l,d,h,v,y,b=!0,D=!0,_=this.posMax,k=this.src.charCodeAt(e);for(n=e>0?this.src.charCodeAt(e-1):32;r<_&&this.src.charCodeAt(r)===k;)r++;return o=r-e,i=r<_?this.src.charCodeAt(r):32,h=jq(n)||Rq(String.fromCharCode(n)),y=jq(i)||Rq(String.fromCharCode(i)),d=Aq(n),v=Aq(i),v?b=!1:y&&(d||h||(b=!1)),d?D=!1:h&&(v||y||(D=!1)),t?(s=b,l=D):(s=b&&(!D||h),l=D&&(!b||y)),{can_open:s,can_close:l,length:o}};jp.prototype.Token=OO;Pq.exports=jp});var Vq=G((Lle,qq)=>{"use strict";var Mq=zm(),wO=[["text",uq()],["newline",lq()],["escape",fq()],["backticks",pq()],["strikethrough",TO().tokenize],["emphasis",EO().tokenize],["link",mq()],["image",bq()],["autolink",_q()],["html_inline",Sq()],["entity",Nq()]],NO=[["balance_pairs",Cq()],["strikethrough",TO().postProcess],["emphasis",EO().postProcess],["text_collapse",Iq()]];function Pp(){var e;for(this.ruler=new Mq,e=0;e=o)break;continue}e.pending+=e.src[e.pos++]}e.pending&&e.pushPending()};Pp.prototype.parse=function(e,t,r,n){var i,o,s,l=new this.State(e,t,r,n);for(this.tokenize(l),o=this.ruler2.getRules(""),s=o.length,i=0;i{"use strict";Uq.exports=function(e){var t={};t.src_Any=sO().source,t.src_Cc=lO().source,t.src_Z=cO().source,t.src_P=Gm().source,t.src_ZPCc=[t.src_Z,t.src_P,t.src_Cc].join("|"),t.src_ZCc=[t.src_Z,t.src_Cc].join("|");var r="[><\uFF5C]";return t.src_pseudo_letter="(?:(?!"+r+"|"+t.src_ZPCc+")"+t.src_Any+")",t.src_ip4="(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",t.src_auth="(?:(?:(?!"+t.src_ZCc+"|[@/\\[\\]()]).)+@)?",t.src_port="(?::(?:6(?:[0-4]\\d{3}|5(?:[0-4]\\d{2}|5(?:[0-2]\\d|3[0-5])))|[1-5]?\\d{1,4}))?",t.src_host_terminator="(?=$|"+r+"|"+t.src_ZPCc+")(?!-|_|:\\d|\\.-|\\.(?!$|"+t.src_ZPCc+"))",t.src_path="(?:[/?#](?:(?!"+t.src_ZCc+"|"+r+`|[()[\\]{}.,"'?!\\-;]).|\\[(?:(?!`+t.src_ZCc+"|\\]).)*\\]|\\((?:(?!"+t.src_ZCc+"|[)]).)*\\)|\\{(?:(?!"+t.src_ZCc+'|[}]).)*\\}|\\"(?:(?!'+t.src_ZCc+`|["]).)+\\"|\\'(?:(?!`+t.src_ZCc+"|[']).)+\\'|\\'(?="+t.src_pseudo_letter+"|[-]).|\\.{2,}[a-zA-Z0-9%/&]|\\.(?!"+t.src_ZCc+"|[.]).|"+(e&&e["---"]?"\\-(?!--(?:[^-]|$))(?:-*)|":"\\-+|")+",(?!"+t.src_ZCc+").|;(?!"+t.src_ZCc+").|\\!+(?!"+t.src_ZCc+"|[!]).|\\?(?!"+t.src_ZCc+"|[?]).)+|\\/)?",t.src_email_name='[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]*',t.src_xn="xn--[a-z0-9\\-]{1,59}",t.src_domain_root="(?:"+t.src_xn+"|"+t.src_pseudo_letter+"{1,63})",t.src_domain="(?:"+t.src_xn+"|(?:"+t.src_pseudo_letter+")|(?:"+t.src_pseudo_letter+"(?:-|"+t.src_pseudo_letter+"){0,61}"+t.src_pseudo_letter+"))",t.src_host="(?:(?:(?:(?:"+t.src_domain+")\\.)*"+t.src_domain+"))",t.tpl_host_fuzzy="(?:"+t.src_ip4+"|(?:(?:(?:"+t.src_domain+")\\.)+(?:%TLDS%)))",t.tpl_host_no_ip_fuzzy="(?:(?:(?:"+t.src_domain+")\\.)+(?:%TLDS%))",t.src_host_strict=t.src_host+t.src_host_terminator,t.tpl_host_fuzzy_strict=t.tpl_host_fuzzy+t.src_host_terminator,t.src_host_port_strict=t.src_host+t.src_port+t.src_host_terminator,t.tpl_host_port_fuzzy_strict=t.tpl_host_fuzzy+t.src_port+t.src_host_terminator,t.tpl_host_port_no_ip_fuzzy_strict=t.tpl_host_no_ip_fuzzy+t.src_port+t.src_host_terminator,t.tpl_host_fuzzy_test="localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:"+t.src_ZPCc+"|>|$))",t.tpl_email_fuzzy="(^|"+r+'|"|\\(|'+t.src_ZCc+")("+t.src_email_name+"@"+t.tpl_host_fuzzy_strict+")",t.tpl_link_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`|\uFF5C]|"+t.src_ZPCc+"))((?![$+<=>^`|\uFF5C])"+t.tpl_host_port_fuzzy_strict+t.src_path+")",t.tpl_link_no_ip_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`|\uFF5C]|"+t.src_ZPCc+"))((?![$+<=>^`|\uFF5C])"+t.tpl_host_port_no_ip_fuzzy_strict+t.src_path+")",t}});var Wq=G((Ale,zq)=>{"use strict";function DO(e){var t=Array.prototype.slice.call(arguments,1);return t.forEach(function(r){!r||Object.keys(r).forEach(function(n){e[n]=r[n]})}),e}function ey(e){return Object.prototype.toString.call(e)}function o$(e){return ey(e)==="[object String]"}function u$(e){return ey(e)==="[object Object]"}function s$(e){return ey(e)==="[object RegExp]"}function Qq(e){return ey(e)==="[object Function]"}function l$(e){return e.replace(/[.?*+^$[\]\\(){}|-]/g,"\\$&")}var Bq={fuzzyLink:!0,fuzzyEmail:!0,fuzzyIP:!1};function c$(e){return Object.keys(e||{}).reduce(function(t,r){return t||Bq.hasOwnProperty(r)},!1)}var f$={"http:":{validate:function(e,t,r){var n=e.slice(t);return r.re.http||(r.re.http=new RegExp("^\\/\\/"+r.re.src_auth+r.re.src_host_port_strict+r.re.src_path,"i")),r.re.http.test(n)?n.match(r.re.http)[0].length:0}},"https:":"http:","ftp:":"http:","//":{validate:function(e,t,r){var n=e.slice(t);return r.re.no_http||(r.re.no_http=new RegExp("^"+r.re.src_auth+"(?:localhost|(?:(?:"+r.re.src_domain+")\\.)+"+r.re.src_domain_root+")"+r.re.src_port+r.re.src_host_terminator+r.re.src_path,"i")),r.re.no_http.test(n)?t>=3&&e[t-3]===":"||t>=3&&e[t-3]==="/"?0:n.match(r.re.no_http)[0].length:0}},"mailto:":{validate:function(e,t,r){var n=e.slice(t);return r.re.mailto||(r.re.mailto=new RegExp("^"+r.re.src_email_name+"@"+r.re.src_host_strict,"i")),r.re.mailto.test(n)?n.match(r.re.mailto)[0].length:0}}},d$="a[cdefgilmnoqrstuwxz]|b[abdefghijmnorstvwyz]|c[acdfghiklmnoruvwxyz]|d[ejkmoz]|e[cegrstu]|f[ijkmor]|g[abdefghilmnpqrstuwy]|h[kmnrtu]|i[delmnoqrst]|j[emop]|k[eghimnprwyz]|l[abcikrstuvy]|m[acdeghklmnopqrstuvwxyz]|n[acefgilopruz]|om|p[aefghklmnrstwy]|qa|r[eosuw]|s[abcdeghijklmnortuvxyz]|t[cdfghjklmnortvwz]|u[agksyz]|v[aceginu]|w[fs]|y[et]|z[amw]",p$="biz|com|edu|gov|net|org|pro|web|xxx|aero|asia|coop|info|museum|name|shop|\u0440\u0444".split("|");function h$(e){e.__index__=-1,e.__text_cache__=""}function v$(e){return function(t,r){var n=t.slice(r);return e.test(n)?n.match(e)[0].length:0}}function Kq(){return function(e,t){t.normalize(e)}}function ty(e){var t=e.re=Gq()(e.__opts__),r=e.__tlds__.slice();e.onCompile(),e.__tlds_replaced__||r.push(d$),r.push(t.src_xn),t.src_tlds=r.join("|");function n(l){return l.replace("%TLDS%",t.src_tlds)}t.email_fuzzy=RegExp(n(t.tpl_email_fuzzy),"i"),t.link_fuzzy=RegExp(n(t.tpl_link_fuzzy),"i"),t.link_no_ip_fuzzy=RegExp(n(t.tpl_link_no_ip_fuzzy),"i"),t.host_fuzzy_test=RegExp(n(t.tpl_host_fuzzy_test),"i");var i=[];e.__compiled__={};function o(l,d){throw new Error('(LinkifyIt) Invalid schema "'+l+'": '+d)}Object.keys(e.__schemas__).forEach(function(l){var d=e.__schemas__[l];if(d!==null){var h={validate:null,link:null};if(e.__compiled__[l]=h,u$(d)){s$(d.validate)?h.validate=v$(d.validate):Qq(d.validate)?h.validate=d.validate:o(l,d),Qq(d.normalize)?h.normalize=d.normalize:d.normalize?o(l,d):h.normalize=Kq();return}if(o$(d)){i.push(l);return}o(l,d)}}),i.forEach(function(l){!e.__compiled__[e.__schemas__[l]]||(e.__compiled__[l].validate=e.__compiled__[e.__schemas__[l]].validate,e.__compiled__[l].normalize=e.__compiled__[e.__schemas__[l]].normalize)}),e.__compiled__[""]={validate:null,normalize:Kq()};var s=Object.keys(e.__compiled__).filter(function(l){return l.length>0&&e.__compiled__[l]}).map(l$).join("|");e.re.schema_test=RegExp("(^|(?!_)(?:[><\uFF5C]|"+t.src_ZPCc+"))("+s+")","i"),e.re.schema_search=RegExp("(^|(?!_)(?:[><\uFF5C]|"+t.src_ZPCc+"))("+s+")","ig"),e.re.pretest=RegExp("("+e.re.schema_test.source+")|("+e.re.host_fuzzy_test.source+")|@","i"),h$(e)}function g$(e,t){var r=e.__index__,n=e.__last_index__,i=e.__text_cache__.slice(r,n);this.schema=e.__schema__.toLowerCase(),this.index=r+t,this.lastIndex=n+t,this.raw=i,this.text=i,this.url=i}function Hq(e,t){var r=new g$(e,t);return e.__compiled__[r.schema].normalize(r,e),r}function Zi(e,t){if(!(this instanceof Zi))return new Zi(e,t);t||c$(e)&&(t=e,e={}),this.__opts__=DO({},Bq,t),this.__index__=-1,this.__last_index__=-1,this.__schema__="",this.__text_cache__="",this.__schemas__=DO({},f$,e),this.__compiled__={},this.__tlds__=p$,this.__tlds_replaced__=!1,this.re={},ty(this)}Zi.prototype.add=function(t,r){return this.__schemas__[t]=r,ty(this),this};Zi.prototype.set=function(t){return this.__opts__=DO(this.__opts__,t),this};Zi.prototype.test=function(t){if(this.__text_cache__=t,this.__index__=-1,!t.length)return!1;var r,n,i,o,s,l,d,h,v;if(this.re.schema_test.test(t)){for(d=this.re.schema_search,d.lastIndex=0;(r=d.exec(t))!==null;)if(o=this.testSchemaAt(t,r[2],d.lastIndex),o){this.__schema__=r[2],this.__index__=r.index+r[1].length,this.__last_index__=r.index+r[0].length+o;break}}return this.__opts__.fuzzyLink&&this.__compiled__["http:"]&&(h=t.search(this.re.host_fuzzy_test),h>=0&&(this.__index__<0||h=0&&(i=t.match(this.re.email_fuzzy))!==null&&(s=i.index+i[1].length,l=i.index+i[0].length,(this.__index__<0||sthis.__last_index__)&&(this.__schema__="mailto:",this.__index__=s,this.__last_index__=l))),this.__index__>=0};Zi.prototype.pretest=function(t){return this.re.pretest.test(t)};Zi.prototype.testSchemaAt=function(t,r,n){return this.__compiled__[r.toLowerCase()]?this.__compiled__[r.toLowerCase()].validate(t,n,this):0};Zi.prototype.match=function(t){var r=0,n=[];this.__index__>=0&&this.__text_cache__===t&&(n.push(Hq(this,r)),r=this.__last_index__);for(var i=r?t.slice(r):t;this.test(i);)n.push(Hq(this,r)),i=i.slice(this.__last_index__),r+=this.__last_index__;return n.length?n:null};Zi.prototype.tlds=function(t,r){return t=Array.isArray(t)?t:[t],r?(this.__tlds__=this.__tlds__.concat(t).sort().filter(function(n,i,o){return n!==o[i-1]}).reverse(),ty(this),this):(this.__tlds__=t.slice(),this.__tlds_replaced__=!0,ty(this),this)};Zi.prototype.normalize=function(t){t.schema||(t.url="http://"+t.url),t.schema==="mailto:"&&!/^mailto:/i.test(t.url)&&(t.url="mailto:"+t.url)};Zi.prototype.onCompile=function(){};zq.exports=Zi});var aV=G((Rle,iV)=>{"use strict";var Pc=2147483647,Ja=36,xO=1,Fp=26,m$=38,y$=700,Yq=72,Jq=128,Xq="-",b$=/^xn--/,T$=/[^\0-\x7E]/,_$=/[\x2E\u3002\uFF0E\uFF61]/g,E$={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},CO=Ja-xO,Xa=Math.floor,LO=String.fromCharCode;function Ms(e){throw new RangeError(E$[e])}function S$(e,t){let r=[],n=e.length;for(;n--;)r[n]=t(e[n]);return r}function Zq(e,t){let r=e.split("@"),n="";r.length>1&&(n=r[0]+"@",e=r[1]),e=e.replace(_$,".");let i=e.split("."),o=S$(i,t).join(".");return n+o}function $q(e){let t=[],r=0,n=e.length;for(;r=55296&&i<=56319&&rString.fromCodePoint(...e),O$=function(e){return e-48<10?e-22:e-65<26?e-65:e-97<26?e-97:Ja},eV=function(e,t){return e+22+75*(e<26)-((t!=0)<<5)},tV=function(e,t,r){let n=0;for(e=r?Xa(e/y$):e>>1,e+=Xa(e/t);e>CO*Fp>>1;n+=Ja)e=Xa(e/CO);return Xa(n+(CO+1)*e/(e+m$))},rV=function(e){let t=[],r=e.length,n=0,i=Jq,o=Yq,s=e.lastIndexOf(Xq);s<0&&(s=0);for(let l=0;l=128&&Ms("not-basic"),t.push(e.charCodeAt(l));for(let l=s>0?s+1:0;l=r&&Ms("invalid-input");let b=O$(e.charCodeAt(l++));(b>=Ja||b>Xa((Pc-n)/v))&&Ms("overflow"),n+=b*v;let D=y<=o?xO:y>=o+Fp?Fp:y-o;if(bXa(Pc/_)&&Ms("overflow"),v*=_}let h=t.length+1;o=tV(n-d,h,d==0),Xa(n/h)>Pc-i&&Ms("overflow"),i+=Xa(n/h),n%=h,t.splice(n++,0,i)}return String.fromCodePoint(...t)},nV=function(e){let t=[];e=$q(e);let r=e.length,n=Jq,i=0,o=Yq;for(let d of e)d<128&&t.push(LO(d));let s=t.length,l=s;for(s&&t.push(Xq);l=n&&vXa((Pc-i)/h)&&Ms("overflow"),i+=(d-n)*h,n=d;for(let v of e)if(vPc&&Ms("overflow"),v==n){let y=i;for(let b=Ja;;b+=Ja){let D=b<=o?xO:b>=o+Fp?Fp:b-o;if(y{"use strict";oV.exports={options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201C\u201D\u2018\u2019",highlight:null,maxNesting:100},components:{core:{},block:{},inline:{}}}});var lV=G((Ple,sV)=>{"use strict";sV.exports={options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201C\u201D\u2018\u2019",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline"]},block:{rules:["paragraph"]},inline:{rules:["text"],rules2:["balance_pairs","text_collapse"]}}}});var fV=G((Fle,cV)=>{"use strict";cV.exports={options:{html:!0,xhtmlOut:!0,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201C\u201D\u2018\u2019",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline"]},block:{rules:["blockquote","code","fence","heading","hr","html_block","lheading","list","reference","paragraph"]},inline:{rules:["autolink","backticks","emphasis","entity","escape","html_inline","image","link","newline","text"],rules2:["balance_pairs","emphasis","text_collapse"]}}}});var vV=G((Mle,hV)=>{"use strict";var Mp=Pt(),x$=YF(),C$=XF(),L$=EM(),I$=aq(),A$=Vq(),R$=Wq(),qs=uO(),dV=aV(),j$={default:uV(),zero:lV(),commonmark:fV()},P$=/^(vbscript|javascript|file|data):/,F$=/^data:image\/(gif|png|jpeg|webp);/;function M$(e){var t=e.trim().toLowerCase();return P$.test(t)?!!F$.test(t):!0}var pV=["http:","https:","mailto:"];function q$(e){var t=qs.parse(e,!0);if(t.hostname&&(!t.protocol||pV.indexOf(t.protocol)>=0))try{t.hostname=dV.toASCII(t.hostname)}catch(r){}return qs.encode(qs.format(t))}function V$(e){var t=qs.parse(e,!0);if(t.hostname&&(!t.protocol||pV.indexOf(t.protocol)>=0))try{t.hostname=dV.toUnicode(t.hostname)}catch(r){}return qs.decode(qs.format(t),qs.decode.defaultChars+"%")}function $i(e,t){if(!(this instanceof $i))return new $i(e,t);t||Mp.isString(e)||(t=e||{},e="default"),this.inline=new A$,this.block=new I$,this.core=new L$,this.renderer=new C$,this.linkify=new R$,this.validateLink=M$,this.normalizeLink=q$,this.normalizeLinkText=V$,this.utils=Mp,this.helpers=Mp.assign({},x$),this.options={},this.configure(e),t&&this.set(t)}$i.prototype.set=function(e){return Mp.assign(this.options,e),this};$i.prototype.configure=function(e){var t=this,r;if(Mp.isString(e)&&(r=e,e=j$[r],!e))throw new Error('Wrong `markdown-it` preset "'+r+'", check name');if(!e)throw new Error("Wrong `markdown-it` preset, can't be empty");return e.options&&t.set(e.options),e.components&&Object.keys(e.components).forEach(function(n){e.components[n].rules&&t[n].ruler.enableOnly(e.components[n].rules),e.components[n].rules2&&t[n].ruler2.enableOnly(e.components[n].rules2)}),this};$i.prototype.enable=function(e,t){var r=[];Array.isArray(e)||(e=[e]),["core","block","inline"].forEach(function(i){r=r.concat(this[i].ruler.enable(e,!0))},this),r=r.concat(this.inline.ruler2.enable(e,!0));var n=e.filter(function(i){return r.indexOf(i)<0});if(n.length&&!t)throw new Error("MarkdownIt. Failed to enable unknown rule(s): "+n);return this};$i.prototype.disable=function(e,t){var r=[];Array.isArray(e)||(e=[e]),["core","block","inline"].forEach(function(i){r=r.concat(this[i].ruler.disable(e,!0))},this),r=r.concat(this.inline.ruler2.disable(e,!0));var n=e.filter(function(i){return r.indexOf(i)<0});if(n.length&&!t)throw new Error("MarkdownIt. Failed to disable unknown rule(s): "+n);return this};$i.prototype.use=function(e){var t=[this].concat(Array.prototype.slice.call(arguments,1));return e.apply(e,t),this};$i.prototype.parse=function(e,t){if(typeof e!="string")throw new Error("Input data should be a String");var r=new this.core.State(e,this,t);return this.core.process(r),r.tokens};$i.prototype.render=function(e,t){return t=t||{},this.renderer.render(this.parse(e,t),this.options,t)};$i.prototype.parseInline=function(e,t){var r=new this.core.State(e,this,t);return r.inlineMode=!0,this.core.process(r),r.tokens};$i.prototype.renderInline=function(e,t){return t=t||{},this.renderer.render(this.parseInline(e,t),this.options,t)};hV.exports=$i});var ry=G((qle,gV)=>{"use strict";gV.exports=vV()});var bV=G((Ule,yV)=>{"use strict";var Q$=/["'&<>]/;yV.exports=B$;function B$(e){var t=""+e,r=Q$.exec(t);if(!r)return t;var n,i="",o=0,s=0;for(o=r.index;o{(function(e,t){typeof IO=="object"&&typeof AO!="undefined"?AO.exports=t():typeof define=="function"&&define.amd?define(t):(e=e||self,e.CodeMirror=t())})(IO,function(){"use strict";var e=navigator.userAgent,t=navigator.platform,r=/gecko\/\d/i.test(e),n=/MSIE \d/.test(e),i=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(e),o=/Edge\/(\d+)/.exec(e),s=n||i||o,l=s&&(n?document.documentMode||6:+(o||i)[1]),d=!o&&/WebKit\//.test(e),h=d&&/Qt\/\d+\.\d+/.test(e),v=!o&&/Chrome\/(\d+)/.exec(e),y=v&&+v[1],b=/Opera\//.test(e),D=/Apple Computer/.test(navigator.vendor),_=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(e),k=/PhantomJS/.test(e),T=D&&(/Mobile\/\w+/.test(e)||navigator.maxTouchPoints>2),S=/Android/.test(e),m=T||S||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(e),w=T||/Mac/.test(t),x=/\bCrOS\b/.test(e),L=/win/i.test(t),O=b&&e.match(/Version\/(\d*\.\d*)/);O&&(O=Number(O[1])),O&&O>=15&&(b=!1,d=!0);var R=w&&(h||b&&(O==null||O<12.11)),M=r||s&&l>=9;function q(a){return new RegExp("(^|\\s)"+a+"(?:$|\\s)\\s*")}var z=function(a,u){var f=a.className,c=q(u).exec(f);if(c){var p=f.slice(c.index+c[0].length);a.className=f.slice(0,c.index)+(p?c[1]+p:"")}};function B(a){for(var u=a.childNodes.length;u>0;--u)a.removeChild(a.firstChild);return a}function Q(a,u){return B(a).appendChild(u)}function P(a,u,f,c){var p=document.createElement(a);if(f&&(p.className=f),c&&(p.style.cssText=c),typeof u=="string")p.appendChild(document.createTextNode(u));else if(u)for(var g=0;g=u)return E+(u-g);E+=N-g,E+=f-E%f,g=N+1}}var ce=function(){this.id=null,this.f=null,this.time=0,this.handler=Ot(this.onTimeout,this)};ce.prototype.onTimeout=function(a){a.id=0,a.time<=+new Date?a.f():setTimeout(a.handler,a.time-+new Date)},ce.prototype.set=function(a,u){this.f=u;var f=+new Date+a;(!this.id||f=u)return c+Math.min(E,u-p);if(p+=g-c,p+=f-p%f,c=g+1,p>=u)return c}}var me=[""];function fe(a){for(;me.length<=a;)me.push(se(me)+" ");return me[a]}function se(a){return a[a.length-1]}function Ue(a,u){for(var f=[],c=0;c"\x80"&&(a.toUpperCase()!=a.toLowerCase()||Dn.test(a))}function dn(a,u){return u?u.source.indexOf("\\w")>-1&&Ei(a)?!0:u.test(a):Ei(a)}function Hn(a){for(var u in a)if(a.hasOwnProperty(u)&&a[u])return!1;return!0}var pn=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;function Pi(a){return a.charCodeAt(0)>=768&&pn.test(a)}function Qr(a,u,f){for(;(f<0?u>0:uf?-1:1;;){if(u==f)return u;var p=(u+f)/2,g=c<0?Math.ceil(p):Math.floor(p);if(g==u)return a(g)?u:f;a(g)?f=g:u=g+c}}function hn(a,u,f,c){if(!a)return c(u,f,"ltr",0);for(var p=!1,g=0;gu||u==f&&E.to==u)&&(c(Math.max(E.from,u),Math.min(E.to,f),E.level==1?"rtl":"ltr",g),p=!0)}p||c(u,f,"ltr")}var zn=null;function vr(a,u,f){var c;zn=null;for(var p=0;pu)return p;g.to==u&&(g.from!=g.to&&f=="before"?c=p:zn=p),g.from==u&&(g.from!=g.to&&f!="before"?c=p:zn=p)}return c!=null?c:zn}var Ro=function(){var a="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",u="nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111";function f(I){return I<=247?a.charAt(I):1424<=I&&I<=1524?"R":1536<=I&&I<=1785?u.charAt(I-1536):1774<=I&&I<=2220?"r":8192<=I&&I<=8203?"w":I==8204?"b":"L"}var c=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,p=/[stwN]/,g=/[LRr]/,E=/[Lb1n]/,N=/[1n]/;function C(I,U,K){this.level=I,this.from=U,this.to=K}return function(I,U){var K=U=="ltr"?"L":"R";if(I.length==0||U=="ltr"&&!c.test(I))return!1;for(var $=I.length,X=[],ae=0;ae<$;++ae)X.push(f(I.charCodeAt(ae)));for(var le=0,pe=K;le<$;++le){var be=X[le];be=="m"?X[le]=pe:pe=be}for(var Ne=0,Te=K;Ne<$;++Ne){var Ce=X[Ne];Ce=="1"&&Te=="r"?X[Ne]="n":g.test(Ce)&&(Te=Ce,Ce=="r"&&(X[Ne]="R"))}for(var Ge=1,qe=X[0];Ge<$-1;++Ge){var lt=X[Ge];lt=="+"&&qe=="1"&&X[Ge+1]=="1"?X[Ge]="1":lt==","&&qe==X[Ge+1]&&(qe=="1"||qe=="n")&&(X[Ge]=qe),qe=lt}for(var Ht=0;Ht<$;++Ht){var zr=X[Ht];if(zr==",")X[Ht]="N";else if(zr=="%"){var lr=void 0;for(lr=Ht+1;lr<$&&X[lr]=="%";++lr);for(var li=Ht&&X[Ht-1]=="!"||lr<$&&X[lr]=="1"?"1":"N",Jn=Ht;Jn-1&&(c[u]=p.slice(0,g).concat(p.slice(g+1)))}}}function Ft(a,u){var f=Gu(a,u);if(!!f.length)for(var c=Array.prototype.slice.call(arguments,2),p=0;p0}function un(a){a.prototype.on=function(u,f){_e(this,u,f)},a.prototype.off=function(u,f){Ar(this,u,f)}}function ee(a){a.preventDefault?a.preventDefault():a.returnValue=!1}function F(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0}function Y(a){return a.defaultPrevented!=null?a.defaultPrevented:a.returnValue==!1}function J(a){ee(a),F(a)}function V(a){return a.target||a.srcElement}function A(a){var u=a.which;return u==null&&(a.button&1?u=1:a.button&2?u=3:a.button&4&&(u=2)),w&&a.ctrlKey&&u==1&&(u=3),u}var re=function(){if(s&&l<9)return!1;var a=P("div");return"draggable"in a||"dragDrop"in a}(),ue;function Ze(a){if(ue==null){var u=P("span","\u200B");Q(a,P("span",[u,document.createTextNode("x")])),a.firstChild.offsetHeight!=0&&(ue=u.offsetWidth<=1&&u.offsetHeight>2&&!(s&&l<8))}var f=ue?P("span","\u200B"):P("span","\xA0",null,"display: inline-block; width: 1px; margin-right: -1px");return f.setAttribute("cm-text",""),f}var Ut;function Rt(a){if(Ut!=null)return Ut;var u=Q(a,document.createTextNode("A\u062EA")),f=xe(u,0,1).getBoundingClientRect(),c=xe(u,1,2).getBoundingClientRect();return B(a),!f||f.left==f.right?!1:Ut=c.right-f.right<3}var vn=`
-
-b`.split(/\n/).length!=3?function(a){for(var u=0,f=[],c=a.length;u<=c;){var p=a.indexOf(`
-`,u);p==-1&&(p=a.length);var g=a.slice(u,a.charAt(p-1)=="\r"?p-1:p),E=g.indexOf("\r");E!=-1?(f.push(g.slice(0,E)),u+=E+1):(f.push(g),u=p+1)}return f}:function(a){return a.split(/\r\n?|\n/)},Rr=window.getSelection?function(a){try{return a.selectionStart!=a.selectionEnd}catch(u){return!1}}:function(a){var u;try{u=a.ownerDocument.selection.createRange()}catch(f){}return!u||u.parentElement()!=a?!1:u.compareEndPoints("StartToEnd",u)!=0},jr=function(){var a=P("div");return"oncopy"in a?!0:(a.setAttribute("oncopy","return;"),typeof a.oncopy=="function")}(),et=null;function sa(a){if(et!=null)return et;var u=Q(a,P("span","x")),f=u.getBoundingClientRect(),c=xe(u,0,1).getBoundingClientRect();return et=Math.abs(f.left-c.left)>1}var Cn={},la={};function ch(a,u){arguments.length>2&&(u.dependencies=Array.prototype.slice.call(arguments,2)),Cn[a]=u}function Js(a,u){la[a]=u}function ui(a){if(typeof a=="string"&&la.hasOwnProperty(a))a=la[a];else if(a&&typeof a.name=="string"&&la.hasOwnProperty(a.name)){var u=la[a.name];typeof u=="string"&&(u={name:u}),a=st(u,a),a.name=u.name}else{if(typeof a=="string"&&/^[\w\-]+\/[\w\-]+\+xml$/.test(a))return ui("application/xml");if(typeof a=="string"&&/^[\w\-]+\/[\w\-]+\+json$/.test(a))return ui("application/json")}return typeof a=="string"?{name:a}:a||{name:"null"}}function io(a,u){u=ui(u);var f=Cn[u.name];if(!f)return io(a,"text/plain");var c=f(a,u);if(jo.hasOwnProperty(u.name)){var p=jo[u.name];for(var g in p)!p.hasOwnProperty(g)||(c.hasOwnProperty(g)&&(c["_"+g]=c[g]),c[g]=p[g])}if(c.name=u.name,u.helperType&&(c.helperType=u.helperType),u.modeProps)for(var E in u.modeProps)c[E]=u.modeProps[E];return c}var jo={};function fh(a,u){var f=jo.hasOwnProperty(a)?jo[a]:jo[a]={};Ie(u,f)}function ao(a,u){if(u===!0)return u;if(a.copyState)return a.copyState(u);var f={};for(var c in u){var p=u[c];p instanceof Array&&(p=p.concat([])),f[c]=p}return f}function Po(a,u){for(var f;a.innerMode&&(f=a.innerMode(u),!(!f||f.mode==a));)u=f.state,a=f.mode;return f||{mode:a,state:u}}function nf(a,u,f){return a.startState?a.startState(u,f):!0}var gr=function(a,u,f){this.pos=this.start=0,this.string=a,this.tabSize=u||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0,this.lineOracle=f};gr.prototype.eol=function(){return this.pos>=this.string.length},gr.prototype.sol=function(){return this.pos==this.lineStart},gr.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},gr.prototype.next=function(){if(this.posu},gr.prototype.eatSpace=function(){for(var a=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>a},gr.prototype.skipToEnd=function(){this.pos=this.string.length},gr.prototype.skipTo=function(a){var u=this.string.indexOf(a,this.pos);if(u>-1)return this.pos=u,!0},gr.prototype.backUp=function(a){this.pos-=a},gr.prototype.column=function(){return this.lastColumnPos0?null:(g&&u!==!1&&(this.pos+=g[0].length),g)}},gr.prototype.current=function(){return this.string.slice(this.start,this.pos)},gr.prototype.hideFirstChars=function(a,u){this.lineStart+=a;try{return u()}finally{this.lineStart-=a}},gr.prototype.lookAhead=function(a){var u=this.lineOracle;return u&&u.lookAhead(a)},gr.prototype.baseToken=function(){var a=this.lineOracle;return a&&a.baseToken(this.pos)};function Ae(a,u){if(u-=a.first,u<0||u>=a.size)throw new Error("There is no line "+(u+a.first)+" in the document.");for(var f=a;!f.lines;)for(var c=0;;++c){var p=f.children[c],g=p.chunkSize();if(u=a.first&&uf?W(f,Ae(a,f).text.length):zQ(u,Ae(a,u.line).text.length)}function zQ(a,u){var f=a.ch;return f==null||f>u?W(a.line,u):f<0?W(a.line,0):a}function nN(a,u){for(var f=[],c=0;cthis.maxLookAhead&&(this.maxLookAhead=a),u},Na.prototype.baseToken=function(a){if(!this.baseTokens)return null;for(;this.baseTokens[this.baseTokenPos]<=a;)this.baseTokenPos+=2;var u=this.baseTokens[this.baseTokenPos+1];return{type:u&&u.replace(/( |^)overlay .*/,""),size:this.baseTokens[this.baseTokenPos]-a}},Na.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},Na.fromSaved=function(a,u,f){return u instanceof dh?new Na(a,ao(a.mode,u.state),f,u.lookAhead):new Na(a,ao(a.mode,u),f)},Na.prototype.save=function(a){var u=a!==!1?ao(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new dh(u,this.maxLookAhead):u};function iN(a,u,f,c){var p=[a.state.modeGen],g={};cN(a,u.text,a.doc.mode,f,function(I,U){return p.push(I,U)},g,c);for(var E=f.state,N=function(I){f.baseTokens=p;var U=a.state.overlays[I],K=1,$=0;f.state=!0,cN(a,u.text,U.mode,f,function(X,ae){for(var le=K;$X&&p.splice(K,1,X,p[K+1],pe),K+=2,$=Math.min(X,pe)}if(!!ae)if(U.opaque)p.splice(le,K-le,X,"overlay "+ae),K=le+2;else for(;lea.options.maxHighlightLength&&ao(a.doc.mode,c.state),g=iN(a,u,c);p&&(c.state=p),u.stateAfter=c.save(!p),u.styles=g.styles,g.classes?u.styleClasses=g.classes:u.styleClasses&&(u.styleClasses=null),f===a.doc.highlightFrontier&&(a.doc.modeFrontier=Math.max(a.doc.modeFrontier,++a.doc.highlightFrontier))}return u.styles}function af(a,u,f){var c=a.doc,p=a.display;if(!c.mode.startState)return new Na(c,!0,u);var g=WQ(a,u,f),E=g>c.first&&Ae(c,g-1).stateAfter,N=E?Na.fromSaved(c,E,g):new Na(c,nf(c.mode),g);return c.iter(g,u,function(C){Wy(a,C.text,N);var I=N.line;C.stateAfter=I==u-1||I%5==0||I>=p.viewFrom&&Iu.start)return g}throw new Error("Mode "+a.name+" failed to advance stream.")}var uN=function(a,u,f){this.start=a.start,this.end=a.pos,this.string=a.current(),this.type=u||null,this.state=f};function sN(a,u,f,c){var p=a.doc,g=p.mode,E;u=Ye(p,u);var N=Ae(p,u.line),C=af(a,u.line,f),I=new gr(N.text,a.options.tabSize,C),U;for(c&&(U=[]);(c||I.posa.options.maxHighlightLength?(N=!1,E&&Wy(a,u,c,U.pos),U.pos=u.length,K=null):K=lN(Yy(f,U,c.state,$),g),$){var X=$[0].name;X&&(K="m-"+(K?X+" "+K:X))}if(!N||I!=K){for(;CE;--N){if(N<=g.first)return g.first;var C=Ae(g,N-1),I=C.stateAfter;if(I&&(!f||N+(I instanceof dh?I.lookAhead:0)<=g.modeFrontier))return N;var U=te(C.text,null,a.options.tabSize);(p==null||c>U)&&(p=N-1,c=U)}return p}function YQ(a,u){if(a.modeFrontier=Math.min(a.modeFrontier,u),!(a.highlightFrontierf;c--){var p=Ae(a,c).stateAfter;if(p&&(!(p instanceof dh)||c+p.lookAhead=u:g.to>u);(c||(c=[])).push(new ph(E,g.from,C?null:g.to))}}return c}function t2(a,u,f){var c;if(a)for(var p=0;p=u:g.to>u);if(N||g.from==u&&E.type=="bookmark"&&(!f||g.marker.insertLeft)){var C=g.from==null||(E.inclusiveLeft?g.from<=u:g.from0&&N)for(var Ce=0;Ce0)){var U=[C,1],K=ie(I.from,N.from),$=ie(I.to,N.to);(K<0||!E.inclusiveLeft&&!K)&&U.push({from:I.from,to:N.from}),($>0||!E.inclusiveRight&&!$)&&U.push({from:N.to,to:I.to}),p.splice.apply(p,U),C+=U.length-3}}return p}function pN(a){var u=a.markedSpans;if(!!u){for(var f=0;fu)&&(!c||Xy(c,g.marker)<0)&&(c=g.marker)}return c}function mN(a,u,f,c,p){var g=Ae(a,u),E=uo&&g.markedSpans;if(E)for(var N=0;N=0&&K<=0||U<=0&&K>=0)&&(U<=0&&(C.marker.inclusiveRight&&p.inclusiveLeft?ie(I.to,f)>=0:ie(I.to,f)>0)||U>=0&&(C.marker.inclusiveRight&&p.inclusiveLeft?ie(I.from,c)<=0:ie(I.from,c)<0)))return!0}}}function ca(a){for(var u;u=gN(a);)a=u.find(-1,!0).line;return a}function i2(a){for(var u;u=gh(a);)a=u.find(1,!0).line;return a}function a2(a){for(var u,f;u=gh(a);)a=u.find(1,!0).line,(f||(f=[])).push(a);return f}function Zy(a,u){var f=Ae(a,u),c=ca(f);return f==c?u:wt(c)}function yN(a,u){if(u>a.lastLine())return u;var f=Ae(a,u),c;if(!Fo(a,f))return u;for(;c=gh(f);)f=c.find(1,!0).line;return wt(f)+1}function Fo(a,u){var f=uo&&u.markedSpans;if(f){for(var c=void 0,p=0;pu.maxLineLength&&(u.maxLineLength=p,u.maxLine=c)})}var Zs=function(a,u,f){this.text=a,hN(this,u),this.height=f?f(this):1};Zs.prototype.lineNo=function(){return wt(this)},un(Zs);function o2(a,u,f,c){a.text=u,a.stateAfter&&(a.stateAfter=null),a.styles&&(a.styles=null),a.order!=null&&(a.order=null),pN(a),hN(a,f);var p=c?c(a):1;p!=a.height&&Fi(a,p)}function u2(a){a.parent=null,pN(a)}var s2={},l2={};function bN(a,u){if(!a||/^\s*$/.test(a))return null;var f=u.addModeClass?l2:s2;return f[a]||(f[a]=a.replace(/\S+/g,"cm-$&"))}function TN(a,u){var f=he("span",null,null,d?"padding-right: .1px":null),c={pre:he("pre",[f],"CodeMirror-line"),content:f,col:0,pos:0,cm:a,trailingSpace:!1,splitSpaces:a.getOption("lineWrapping")};u.measure={};for(var p=0;p<=(u.rest?u.rest.length:0);p++){var g=p?u.rest[p-1]:u.line,E=void 0;c.pos=0,c.addToken=f2,Rt(a.display.measure)&&(E=xn(g,a.doc.direction))&&(c.addToken=p2(c.addToken,E)),c.map=[];var N=u!=a.display.externalMeasured&&wt(g);h2(g,c,aN(a,g,N)),g.styleClasses&&(g.styleClasses.bgClass&&(c.bgClass=He(g.styleClasses.bgClass,c.bgClass||"")),g.styleClasses.textClass&&(c.textClass=He(g.styleClasses.textClass,c.textClass||""))),c.map.length==0&&c.map.push(0,0,c.content.appendChild(Ze(a.display.measure))),p==0?(u.measure.map=c.map,u.measure.cache={}):((u.measure.maps||(u.measure.maps=[])).push(c.map),(u.measure.caches||(u.measure.caches=[])).push({}))}if(d){var C=c.content.lastChild;(/\bcm-tab\b/.test(C.className)||C.querySelector&&C.querySelector(".cm-tab"))&&(c.content.className="cm-tab-wrap-hack")}return Ft(a,"renderLine",a,u.line,c.pre),c.pre.className&&(c.textClass=He(c.pre.className,c.textClass||"")),c}function c2(a){var u=P("span","\u2022","cm-invalidchar");return u.title="\\u"+a.charCodeAt(0).toString(16),u.setAttribute("aria-label",u.title),u}function f2(a,u,f,c,p,g,E){if(!!u){var N=a.splitSpaces?d2(u,a.trailingSpace):u,C=a.cm.state.specialChars,I=!1,U;if(!C.test(u))a.col+=u.length,U=document.createTextNode(N),a.map.push(a.pos,a.pos+u.length,U),s&&l<9&&(I=!0),a.pos+=u.length;else{U=document.createDocumentFragment();for(var K=0;;){C.lastIndex=K;var $=C.exec(u),X=$?$.index-K:u.length-K;if(X){var ae=document.createTextNode(N.slice(K,K+X));s&&l<9?U.appendChild(P("span",[ae])):U.appendChild(ae),a.map.push(a.pos,a.pos+X,ae),a.col+=X,a.pos+=X}if(!$)break;K+=X+1;var le=void 0;if($[0]==" "){var pe=a.cm.options.tabSize,be=pe-a.col%pe;le=U.appendChild(P("span",fe(be),"cm-tab")),le.setAttribute("role","presentation"),le.setAttribute("cm-text"," "),a.col+=be}else $[0]=="\r"||$[0]==`
-`?(le=U.appendChild(P("span",$[0]=="\r"?"\u240D":"\u2424","cm-invalidchar")),le.setAttribute("cm-text",$[0]),a.col+=1):(le=a.cm.options.specialCharPlaceholder($[0]),le.setAttribute("cm-text",$[0]),s&&l<9?U.appendChild(P("span",[le])):U.appendChild(le),a.col+=1);a.map.push(a.pos,a.pos+1,le),a.pos++}}if(a.trailingSpace=N.charCodeAt(u.length-1)==32,f||c||p||I||g||E){var Ne=f||"";c&&(Ne+=c),p&&(Ne+=p);var Te=P("span",[U],Ne,g);if(E)for(var Ce in E)E.hasOwnProperty(Ce)&&Ce!="style"&&Ce!="class"&&Te.setAttribute(Ce,E[Ce]);return a.content.appendChild(Te)}a.content.appendChild(U)}}function d2(a,u){if(a.length>1&&!/ /.test(a))return a;for(var f=u,c="",p=0;pI&&K.from<=I));$++);if(K.to>=U)return a(f,c,p,g,E,N,C);a(f,c.slice(0,K.to-I),p,g,null,N,C),g=null,c=c.slice(K.to-I),I=K.to}}}function _N(a,u,f,c){var p=!c&&f.widgetNode;p&&a.map.push(a.pos,a.pos+u,p),!c&&a.cm.display.input.needsContentAttribute&&(p||(p=a.content.appendChild(document.createElement("span"))),p.setAttribute("cm-marker",f.id)),p&&(a.cm.display.input.setUneditable(p),a.content.appendChild(p)),a.pos+=u,a.trailingSpace=!1}function h2(a,u,f){var c=a.markedSpans,p=a.text,g=0;if(!c){for(var E=1;EC||lt.collapsed&&qe.to==C&&qe.from==C)){if(qe.to!=null&&qe.to!=C&&X>qe.to&&(X=qe.to,le=""),lt.className&&(ae+=" "+lt.className),lt.css&&($=($?$+";":"")+lt.css),lt.startStyle&&qe.from==C&&(pe+=" "+lt.startStyle),lt.endStyle&&qe.to==X&&(Ce||(Ce=[])).push(lt.endStyle,qe.to),lt.title&&((Ne||(Ne={})).title=lt.title),lt.attributes)for(var Ht in lt.attributes)(Ne||(Ne={}))[Ht]=lt.attributes[Ht];lt.collapsed&&(!be||Xy(be.marker,lt)<0)&&(be=qe)}else qe.from>C&&X>qe.from&&(X=qe.from)}if(Ce)for(var zr=0;zr=N)break;for(var li=Math.min(N,X);;){if(U){var Jn=C+U.length;if(!be){var wr=Jn>li?U.slice(0,li-C):U;u.addToken(u,wr,K?K+ae:ae,pe,C+wr.length==X?le:"",$,Ne)}if(Jn>=li){U=U.slice(li-C),C=li;break}C=Jn,pe=""}U=p.slice(g,g=f[I++]),K=bN(f[I++],u.cm.options)}}}function EN(a,u,f){this.line=u,this.rest=a2(u),this.size=this.rest?wt(se(this.rest))-f+1:1,this.node=this.text=null,this.hidden=Fo(a,u)}function yh(a,u,f){for(var c=[],p,g=u;g2&&g.push((C.bottom+I.top)/2-f.top)}}g.push(f.bottom-f.top)}}function xN(a,u,f){if(a.line==u)return{map:a.measure.map,cache:a.measure.cache};if(a.rest){for(var c=0;cf)return{map:a.measure.maps[p],cache:a.measure.caches[p],before:!0}}}function O2(a,u){u=ca(u);var f=wt(u),c=a.display.externalMeasured=new EN(a.doc,u,f);c.lineN=f;var p=c.built=TN(a,c);return c.text=p.pre,Q(a.display.lineMeasure,p.pre),c}function CN(a,u,f,c){return xa(a,el(a,u),f,c)}function i0(a,u){if(u>=a.display.viewFrom&&u=f.lineN&&uu)&&(g=C-N,p=g-1,u>=C&&(E="right")),p!=null){if(c=a[I+2],N==C&&f==(c.insertLeft?"left":"right")&&(E=f),f=="left"&&p==0)for(;I&&a[I-2]==a[I-3]&&a[I-1].insertLeft;)c=a[(I-=3)+2],E="left";if(f=="right"&&p==C-N)for(;I=0&&(f=a[p]).left==f.right;p--);return f}function N2(a,u,f,c){var p=IN(u.map,f,c),g=p.node,E=p.start,N=p.end,C=p.collapse,I;if(g.nodeType==3){for(var U=0;U<4;U++){for(;E&&Pi(u.line.text.charAt(p.coverStart+E));)--E;for(;p.coverStart+N0&&(C=c="right");var K;a.options.lineWrapping&&(K=g.getClientRects()).length>1?I=K[c=="right"?K.length-1:0]:I=g.getBoundingClientRect()}if(s&&l<9&&!E&&(!I||!I.left&&!I.right)){var $=g.parentNode.getClientRects()[0];$?I={left:$.left,right:$.left+rl(a.display),top:$.top,bottom:$.bottom}:I=LN}for(var X=I.top-u.rect.top,ae=I.bottom-u.rect.top,le=(X+ae)/2,pe=u.view.measure.heights,be=0;be=c.text.length?(C=c.text.length,I="before"):C<=0&&(C=0,I="after"),!N)return E(I=="before"?C-1:C,I=="before");function U(ae,le,pe){var be=N[le],Ne=be.level==1;return E(pe?ae-1:ae,Ne!=pe)}var K=vr(N,C,I),$=zn,X=U(C,K,I=="before");return $!=null&&(X.other=U(C,$,I!="before")),X}function MN(a,u){var f=0;u=Ye(a.doc,u),a.options.lineWrapping||(f=rl(a.display)*u.ch);var c=Ae(a.doc,u.line),p=so(c)+bh(a.display);return{left:f,right:f,top:p,bottom:p+c.height}}function o0(a,u,f,c,p){var g=W(a,u,f);return g.xRel=p,c&&(g.outside=c),g}function u0(a,u,f){var c=a.doc;if(f+=a.display.viewOffset,f<0)return o0(c.first,0,null,-1,-1);var p=wa(c,f),g=c.first+c.size-1;if(p>g)return o0(c.first+c.size-1,Ae(c,g).text.length,null,1,1);u<0&&(u=0);for(var E=Ae(c,p);;){var N=x2(a,E,p,u,f),C=n2(E,N.ch+(N.xRel>0||N.outside>0?1:0));if(!C)return N;var I=C.find(1);if(I.line==p)return I;E=Ae(c,p=I.line)}}function qN(a,u,f,c){c-=a0(u);var p=u.text.length,g=Kt(function(E){return xa(a,f,E-1).bottom<=c},p,0);return p=Kt(function(E){return xa(a,f,E).top>c},g,p),{begin:g,end:p}}function VN(a,u,f,c){f||(f=el(a,u));var p=Th(a,u,xa(a,f,c),"line").top;return qN(a,u,f,p)}function s0(a,u,f,c){return a.bottom<=f?!1:a.top>f?!0:(c?a.left:a.right)>u}function x2(a,u,f,c,p){p-=so(u);var g=el(a,u),E=a0(u),N=0,C=u.text.length,I=!0,U=xn(u,a.doc.direction);if(U){var K=(a.options.lineWrapping?L2:C2)(a,u,f,g,U,c,p);I=K.level!=1,N=I?K.from:K.to-1,C=I?K.to:K.from-1}var $=null,X=null,ae=Kt(function(Ge){var qe=xa(a,g,Ge);return qe.top+=E,qe.bottom+=E,s0(qe,c,p,!1)?(qe.top<=p&&qe.left<=c&&($=Ge,X=qe),!0):!1},N,C),le,pe,be=!1;if(X){var Ne=c-X.left=Ce.bottom?1:0}return ae=Qr(u.text,ae,1),o0(f,ae,pe,be,c-le)}function C2(a,u,f,c,p,g,E){var N=Kt(function(K){var $=p[K],X=$.level!=1;return s0(fa(a,W(f,X?$.to:$.from,X?"before":"after"),"line",u,c),g,E,!0)},0,p.length-1),C=p[N];if(N>0){var I=C.level!=1,U=fa(a,W(f,I?C.from:C.to,I?"after":"before"),"line",u,c);s0(U,g,E,!0)&&U.top>E&&(C=p[N-1])}return C}function L2(a,u,f,c,p,g,E){var N=qN(a,u,c,E),C=N.begin,I=N.end;/\s/.test(u.text.charAt(I-1))&&I--;for(var U=null,K=null,$=0;$=I||X.to<=C)){var ae=X.level!=1,le=xa(a,c,ae?Math.min(I,X.to)-1:Math.max(C,X.from)).right,pe=lepe)&&(U=X,K=pe)}}return U||(U=p[p.length-1]),U.fromI&&(U={from:U.from,to:I,level:U.level}),U}var Bu;function tl(a){if(a.cachedTextHeight!=null)return a.cachedTextHeight;if(Bu==null){Bu=P("pre",null,"CodeMirror-line-like");for(var u=0;u<49;++u)Bu.appendChild(document.createTextNode("x")),Bu.appendChild(P("br"));Bu.appendChild(document.createTextNode("x"))}Q(a.measure,Bu);var f=Bu.offsetHeight/50;return f>3&&(a.cachedTextHeight=f),B(a.measure),f||1}function rl(a){if(a.cachedCharWidth!=null)return a.cachedCharWidth;var u=P("span","xxxxxxxxxx"),f=P("pre",[u],"CodeMirror-line-like");Q(a.measure,f);var c=u.getBoundingClientRect(),p=(c.right-c.left)/10;return p>2&&(a.cachedCharWidth=p),p||10}function l0(a){for(var u=a.display,f={},c={},p=u.gutters.clientLeft,g=u.gutters.firstChild,E=0;g;g=g.nextSibling,++E){var N=a.display.gutterSpecs[E].className;f[N]=g.offsetLeft+g.clientLeft+p,c[N]=g.clientWidth}return{fixedPos:c0(u),gutterTotalWidth:u.gutters.offsetWidth,gutterLeft:f,gutterWidth:c,wrapperWidth:u.wrapper.clientWidth}}function c0(a){return a.scroller.getBoundingClientRect().left-a.sizer.getBoundingClientRect().left}function UN(a){var u=tl(a.display),f=a.options.lineWrapping,c=f&&Math.max(5,a.display.scroller.clientWidth/rl(a.display)-3);return function(p){if(Fo(a.doc,p))return 0;var g=0;if(p.widgets)for(var E=0;E0&&(I=Ae(a.doc,C.line).text).length==C.ch){var U=te(I,I.length,a.options.tabSize)-I.length;C=W(C.line,Math.max(0,Math.round((g-DN(a.display).left)/rl(a.display))-U))}return C}function Hu(a,u){if(u>=a.display.viewTo||(u-=a.display.viewFrom,u<0))return null;for(var f=a.display.view,c=0;cu)&&(p.updateLineNumbers=u),a.curOp.viewChanged=!0,u>=p.viewTo)uo&&Zy(a.doc,u)p.viewFrom?qo(a):(p.viewFrom+=c,p.viewTo+=c);else if(u<=p.viewFrom&&f>=p.viewTo)qo(a);else if(u<=p.viewFrom){var g=Eh(a,f,f+c,1);g?(p.view=p.view.slice(g.index),p.viewFrom=g.lineN,p.viewTo+=c):qo(a)}else if(f>=p.viewTo){var E=Eh(a,u,u,-1);E?(p.view=p.view.slice(0,E.index),p.viewTo=E.lineN):qo(a)}else{var N=Eh(a,u,u,-1),C=Eh(a,f,f+c,1);N&&C?(p.view=p.view.slice(0,N.index).concat(yh(a,N.lineN,C.lineN)).concat(p.view.slice(C.index)),p.viewTo+=c):qo(a)}var I=p.externalMeasured;I&&(f=p.lineN&&u