diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index b43968731..37848a318 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -10,16 +10,25 @@ body:
installation. If you're having trouble with installation or just looking for
assistance with using NetBox, please visit our
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead.
+ - type: dropdown
+ attributes:
+ label: Deployment Type
+ description: How are you running NetBox?
+ options:
+ - Self-hosted
+ - NetBox Cloud
+ validations:
+ required: true
- type: input
attributes:
- label: NetBox version
+ label: NetBox Version
description: What version of NetBox are you currently running?
- placeholder: v3.5.8
+ placeholder: v3.6.9
validations:
required: true
- type: dropdown
attributes:
- label: Python version
+ label: Python Version
description: What version of Python are you currently running?
options:
- "3.8"
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index 5df3069ba..006fb64fc 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.5.8
+ placeholder: v3.6.9
validations:
required: true
- type: dropdown
diff --git a/.github/ISSUE_TEMPLATE/translation.yaml b/.github/ISSUE_TEMPLATE/translation.yaml
new file mode 100644
index 000000000..d07bc399d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/translation.yaml
@@ -0,0 +1,37 @@
+---
+name: 🌍 Translation
+description: Request support for a new language in the user interface
+labels: ["type: translation"]
+body:
+ - type: markdown
+ attributes:
+ value: >
+ **NOTE:** This template is used only for proposing the addition of *new* languages. Please do
+ not use it to request changes to existing translations.
+ - type: input
+ attributes:
+ label: Language
+ description: What is the name of the language in English?
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: ISO 639-1 code
+ description: >
+ What is the two-letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
+ assigned to the language?
+ validations:
+ required: true
+ - type: dropdown
+ attributes:
+ label: Volunteer
+ description: Are you a fluent speaker of this language **and** willing to contribute a translation map?
+ options:
+ - "Yes"
+ - "No"
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Comments
+ description: Any other notes you would like to share
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1d9692194..9d580baa4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,15 +31,15 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
@@ -47,7 +47,7 @@ jobs:
run: npm install -g yarn
- name: Setup Node.js with Yarn Caching
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: yarn
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
index 6019cef5d..a3e66a429 100644
--- a/.github/workflows/lock.yml
+++ b/.github/workflows/lock.yml
@@ -14,7 +14,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- - uses: dessant/lock-threads@v3
+ - uses: dessant/lock-threads@v4
with:
issue-inactive-days: 90
pr-inactive-days: 30
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 3b37aae56..22de146a2 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v6
+ - uses: actions/stale@v8
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 301fac079..471846427 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,6 +36,8 @@ NetBox users are welcome to participate in either role, on stage or in the crowd
## :bug: Reporting Bugs
+:warning: Bug reports are used to call attention to some unintended or unexpected behavior in NetBox, such as when an error occurs or when the result of taking some action is inconsistent with the documentation. **Bug reports may not be used to suggest new functionality**; please see "feature requests" below if that is your goal.
+
* 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.
diff --git a/README.md b/README.md
index 54b3e727e..6e50e5687 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
The premiere source of truth powering network automation
+
The premier source of truth powering network automation
diff --git a/base_requirements.txt b/base_requirements.txt
index f66a4c4c2..715f7ad74 100644
--- a/base_requirements.txt
+++ b/base_requirements.txt
@@ -23,8 +23,9 @@ django-filter
django-graphiql-debug-toolbar
# Modified Preorder Tree Traversal (recursive nesting of objects)
+# Pinned to 0.14.0; 0.15.0 requires Python 3.9+
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
-django-mptt
+django-mptt==0.14.0
# Context managers for PostgreSQL advisory locks
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
@@ -88,9 +89,8 @@ gunicorn
Jinja2
# Simple markup language for rendering HTML
-# https://python-markdown.github.io/change_log/
-# mkdocs currently requires Markdown v3.3
-Markdown<3.4
+# https://python-markdown.github.io/changelog/
+Markdown
# File inclusion plugin for Python-Markdown
# https://github.com/cmacmackin/markdown-include
@@ -120,9 +120,9 @@ psycopg[binary,pool]
# https://github.com/yaml/pyyaml/blob/master/CHANGES
PyYAML
-# Sentry SDK
-# https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md
-sentry-sdk
+# Requests
+# https://github.com/psf/requests/blob/main/HISTORY.md
+requests
# Social authentication framework
# https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md
diff --git a/contrib/generated_schema.json b/contrib/generated_schema.json
index 8dbcb2847..5e8507798 100644
--- a/contrib/generated_schema.json
+++ b/contrib/generated_schema.json
@@ -332,6 +332,7 @@
"100gbase-x-cfp",
"100gbase-x-cfp2",
"200gbase-x-cfp2",
+ "400gbase-x-cfp2",
"100gbase-x-cfp4",
"100gbase-x-cxp",
"100gbase-x-cpak",
@@ -341,8 +342,10 @@
"100gbase-x-qsfpdd",
"200gbase-x-qsfp56",
"200gbase-x-qsfpdd",
+ "400gbase-x-qsfp112",
"400gbase-x-qsfpdd",
"400gbase-x-osfp",
+ "400gbase-x-osfp-rhs",
"400gbase-x-cdfp",
"400gbase-x-cfp8",
"800gbase-x-qsfpdd",
diff --git a/docs/administration/error-reporting.md b/docs/administration/error-reporting.md
index 162998774..ccc0a84a5 100644
--- a/docs/administration/error-reporting.md
+++ b/docs/administration/error-reporting.md
@@ -4,27 +4,15 @@
### Enabling Error Reporting
-NetBox supports native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, simply set `SENTRY_ENABLED` to True in `configuration.py`. Errors will be sent to a Sentry ingestor maintained by the NetBox team for analysis.
-
-```python
-SENTRY_ENABLED = True
-```
-
-### Using a Custom DSN
-
-If you prefer instead to use your own Sentry ingestor, you'll need to first create a new project under your Sentry account to represent your NetBox deployment and obtain its corresponding data source name (DSN). This looks like a URL similar to the example below:
-
-```
-https://examplePublicKey@o0.ingest.sentry.io/0
-```
-
-Once you have obtained a DSN, configure Sentry in NetBox's `configuration.py` file with the following parameters:
+NetBox supports native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, set `SENTRY_ENABLED` to True and define your unique [data source name (DSN)](https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/) in `configuration.py`.
```python
SENTRY_ENABLED = True
SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
```
+Setting `SENTRY_ENABLED` to False will disable the Sentry integration.
+
### Assigning Tags
You can optionally attach one or more arbitrary tags to the outgoing error reports if desired by setting the `SENTRY_TAGS` parameter:
diff --git a/docs/administration/replicating-netbox.md b/docs/administration/replicating-netbox.md
index 531f9c027..7cc4d3832 100644
--- a/docs/administration/replicating-netbox.md
+++ b/docs/administration/replicating-netbox.md
@@ -54,7 +54,7 @@ pg_dump --username netbox --password --host localhost -s netbox > netbox_schema.
By default, NetBox stores uploaded files (such as image attachments) in its media directory. To fully replicate an instance of NetBox, you'll need to copy both the database and the media files.
!!! note
- These operations are not necessary if your installation is utilizing a [remote storage backend](../../configuration/optional-settings/#storage_backend).
+ These operations are not necessary if your installation is utilizing a [remote storage backend](../configuration/system.md#storage_backend).
### Archive the Media Directory
diff --git a/docs/configuration/data-validation.md b/docs/configuration/data-validation.md
index 9ff71758f..1b8263de3 100644
--- a/docs/configuration/data-validation.md
+++ b/docs/configuration/data-validation.md
@@ -87,3 +87,24 @@ The following colors are supported:
* `gray`
* `black`
* `white`
+
+---
+
+## PROTECTION_RULES
+
+!!! tip "Dynamic Configuration Parameter"
+
+This is a mapping of models to [custom validators](../customization/custom-validation.md) against which an object is evaluated immediately prior to its deletion. If validation fails, the object is not deleted. An example is provided below:
+
+```python
+PROTECTION_RULES = {
+ "dcim.site": [
+ {
+ "status": {
+ "eq": "decommissioning"
+ }
+ },
+ "my_plugin.validators.Validator1",
+ ]
+}
+```
diff --git a/docs/configuration/default-values.md b/docs/configuration/default-values.md
index e76930208..d90e6eafc 100644
--- a/docs/configuration/default-values.md
+++ b/docs/configuration/default-values.md
@@ -20,7 +20,7 @@ DEFAULT_DASHBOARD = [
{
'widget': 'extras.ObjectCountsWidget',
'width': 4,
- 'height': 2,
+ 'height': 3,
'title': 'Organization',
'config': {
'models': [
@@ -32,6 +32,8 @@ DEFAULT_DASHBOARD = [
},
{
'widget': 'extras.ObjectCountsWidget',
+ 'width': 4,
+ 'height': 3,
'title': 'IPAM',
'color': 'blue',
'config': {
diff --git a/docs/configuration/error-reporting.md b/docs/configuration/error-reporting.md
index d1c47e2fb..8c3526dec 100644
--- a/docs/configuration/error-reporting.md
+++ b/docs/configuration/error-reporting.md
@@ -18,6 +18,9 @@ Default: False
Set to True to enable automatic error reporting via [Sentry](https://sentry.io/).
+!!! note
+ The `sentry-sdk` Python package is required to enable Sentry integration.
+
---
## SENTRY_SAMPLE_RATE
diff --git a/docs/configuration/miscellaneous.md b/docs/configuration/miscellaneous.md
index fd410a9d4..4d4ca189e 100644
--- a/docs/configuration/miscellaneous.md
+++ b/docs/configuration/miscellaneous.md
@@ -80,19 +80,41 @@ changes in the database indefinitely.
---
+## CHANGELOG_SKIP_EMPTY_CHANGES
+
+Default: True
+
+If enabled, a change log record will not be created when an object is updated without any changes to its existing field values.
+
+!!! note
+ The object's `last_updated` field will always reflect the time of the most recent update, regardless of this parameter.
+
+---
+
+## DATA_UPLOAD_MAX_MEMORY_SIZE
+
+Default: `2621440` (2.5 MB)
+
+The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` data). Requests which exceed this size will raise a `RequestDataTooBig` exception.
+
+---
+
## ENFORCE_GLOBAL_UNIQUE
!!! tip "Dynamic Configuration Parameter"
-Default: False
+Default: True
-By default, NetBox will permit users to create duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This behavior can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to True.
+By default, NetBox will prevent the creation of duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This validation can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to False.
+
+!!! info "Changed in v3.7"
+ The default value for this parameter was changed from False to True in NetBox v3.7.
---
-## `FILE_UPLOAD_MAX_MEMORY_SIZE`
+## FILE_UPLOAD_MAX_MEMORY_SIZE
-Default: `2621440` (2.5 MB).
+Default: `2621440` (2.5 MB)
The maximum amount (in bytes) of uploaded data that will be held in memory before being written to the filesystem. Changing this setting can be useful for example to be able to upload files bigger than 2.5MB to custom scripts for processing.
diff --git a/docs/configuration/plugins.md b/docs/configuration/plugins.md
index aea60f389..a3e691f63 100644
--- a/docs/configuration/plugins.md
+++ b/docs/configuration/plugins.md
@@ -4,7 +4,7 @@
Default: Empty
-A list of installed [NetBox plugins](../../plugins/) to enable. Plugins will not take effect unless they are listed here.
+A list of installed [NetBox plugins](../plugins/index.md) to enable. Plugins will not take effect unless they are listed here.
!!! warning
Plugins extend NetBox by allowing external code to run with the same access and privileges as NetBox itself. Only install plugins from trusted sources. The NetBox maintainers make absolutely no guarantees about the integrity or security of your installation with plugins enabled.
diff --git a/docs/configuration/required-parameters.md b/docs/configuration/required-parameters.md
index 012d85762..bda365995 100644
--- a/docs/configuration/required-parameters.md
+++ b/docs/configuration/required-parameters.md
@@ -59,10 +59,7 @@ DATABASE = {
## REDIS
-[Redis](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of
-NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching
-functionality (as well as other planned features). In 2.7, the connection settings were broken down into two sections for
-task queuing and caching, allowing the user to connect to different Redis instances/databases per feature.
+[Redis](https://redis.io/) is a lightweight in-memory data store similar to memcached. NetBox employs Redis for background task queuing and other features.
Redis is configured using a configuration setting similar to `DATABASE` and these settings are the same for both of the `tasks` and `caching` subsections:
@@ -81,7 +78,7 @@ REDIS = {
'tasks': {
'HOST': 'redis.example.com',
'PORT': 1234,
- 'USERNAME': 'netbox'
+ 'USERNAME': 'netbox',
'PASSWORD': 'foobar',
'DATABASE': 0,
'SSL': False,
@@ -89,7 +86,7 @@ REDIS = {
'caching': {
'HOST': 'localhost',
'PORT': 6379,
- 'USERNAME': ''
+ 'USERNAME': '',
'PASSWORD': '',
'DATABASE': 1,
'SSL': False,
diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md
index 1e0d5c31e..e9ff7bd9f 100644
--- a/docs/customization/custom-fields.md
+++ b/docs/customization/custom-fields.md
@@ -40,14 +40,22 @@ Related custom fields can be grouped together within the UI by assigning each th
This parameter has no effect on the API representation of custom field data.
-### Visibility
+### Visibility & Editing
-When creating a custom field, there are three options for UI visibility. These control how and whether the custom field is displayed within the NetBox UI.
+!!! info "This feature was improved in NetBox v3.7."
-* **Read/write** (default): The custom field is included when viewing and editing objects.
-* **Read-only**: The custom field is displayed when viewing an object, but it cannot be edited via the UI. (It will appear in the form as a read-only field.)
+When creating a custom field, users can control the conditions under which it may be displayed and edited within the NetBox user interface. The following choices are available for controlling the display of a custom field on an object:
+
+* **Always** (default): The custom field is included when viewing an object.
+* **If Set**: The custom field is included only if a value has been defined for the object.
* **Hidden**: The custom field will never be displayed within the UI. This option is recommended for fields which are not intended for use by human users.
+Additionally, the following options are available for controlling whether custom field values can be altered within the NetBox UI:
+
+* **Yes** (default): The custom field's value may be modified when editing an object.
+* **No**: The custom field is displayed for reference when editing an object, but its value may not be modified.
+* **Hidden**: The custom field is not displayed when editing an object.
+
Note that this setting has no impact on the REST or GraphQL APIs: Custom field data will always be available via either API.
### Validation
diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md
index 3811474d2..0b1ed11df 100644
--- a/docs/customization/custom-scripts.md
+++ b/docs/customization/custom-scripts.md
@@ -288,7 +288,7 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
## Running Custom Scripts
!!! note
- To run a custom script, a user must be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below.
+ To run a custom script, a user must be assigned via permissions for `Extras > Script`, `Extras > ScriptModule`, and `Core > ManagedFile` objects. They must also be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below.

diff --git a/docs/customization/custom-validation.md b/docs/customization/custom-validation.md
index 30198117f..79aa82bc9 100644
--- a/docs/customization/custom-validation.md
+++ b/docs/customization/custom-validation.md
@@ -26,6 +26,8 @@ The `CustomValidator` class supports several validation types:
* `regex`: Application of a [regular expression](https://en.wikipedia.org/wiki/Regular_expression)
* `required`: A value must be specified
* `prohibited`: A value must _not_ be specified
+* `eq`: A value must be equal to the specified value
+* `neq`: A value must _not_ be equal to the specified value
The `min` and `max` types should be defined for numeric values, whereas `min_length`, `max_length`, and `regex` are suitable for character strings (text values). The `required` and `prohibited` validators may be used for any field, and should be passed a value of `True`.
diff --git a/docs/customization/reports.md b/docs/customization/reports.md
index b68e17bf4..a821c5da7 100644
--- a/docs/customization/reports.md
+++ b/docs/customization/reports.md
@@ -111,7 +111,7 @@ The following methods are available to log results within a report:
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status. Log messages also support using markdown syntax and will be rendered on the report result page.
-To perform additional tasks, such as sending an email or calling a webhook, before or after a report is run, extend the `pre_run()` and/or `post_run()` methods, respectively. The status of a completed report is available as `self.failed` and the results object is `self.result`.
+To perform additional tasks, such as sending an email or calling a webhook, before or after a report is run, extend the `pre_run()` and/or `post_run()` methods, respectively.
By default, reports within a module are ordered alphabetically in the reports list page. To return reports in a specific order, you can define the `report_order` variable at the end of your module. The `report_order` variable is a tuple which contains each Report class in the desired order. Any reports that are omitted from this list will be listed last.
@@ -132,7 +132,7 @@ Once you have created a report, it will appear in the reports list. Initially, r
## Running Reports
!!! note
- To run a report, a user must be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below.
+ To run a report, a user must be assigned via permissions for `Extras > Report`, `Extras > ReportModule`, and `Core > ManagedFile` objects. They must also be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below.

diff --git a/docs/development/application-registry.md b/docs/development/application-registry.md
index 41bf6cb31..570563431 100644
--- a/docs/development/application-registry.md
+++ b/docs/development/application-registry.md
@@ -31,7 +31,7 @@ A dictionary of particular features (e.g. custom fields) mapped to the NetBox mo
'dcim': ['site', 'rack', 'devicetype', ...],
...
},
- 'webhooks': {
+ 'event_rules': {
'extras': ['configcontext', 'tag', ...],
'dcim': ['site', 'rack', 'devicetype', ...],
},
@@ -41,6 +41,10 @@ A dictionary of particular features (e.g. custom fields) mapped to the NetBox mo
Supported model features are listed in the [features matrix](./models.md#features-matrix).
+### `models`
+
+This key lists all models which have been registered in NetBox which are not designated for private use. (Setting `_netbox_private` to True on a model excludes it from this list.) As with individual features under `model_features`, models are organized by app label.
+
### `plugins`
This store maintains all registered items for plugins, such as navigation menus, template extensions, etc.
@@ -49,6 +53,10 @@ This store maintains all registered items for plugins, such as navigation menus,
A dictionary mapping each model (identified by its app and label) to its search index class, if one has been registered for it.
+### `tables`
+
+A dictionary mapping table classes to lists of extra columns that have been registered by plugins using the `register_table_column()` utility function. Each column is defined as a tuple of name and column instance.
+
### `views`
A hierarchical mapping of registered views for each model. Mappings are added using the `register_model_view()` decorator, and URLs paths can be generated from these using `get_model_urls()`.
diff --git a/docs/development/extending-models.md b/docs/development/extending-models.md
index b7fd5e1e5..bf5431337 100644
--- a/docs/development/extending-models.md
+++ b/docs/development/extending-models.md
@@ -2,12 +2,25 @@
Below is a list of tasks to consider when adding a new field to a core model.
-## 1. Generate and run database migrations
+## 1. Add the field to the model class
+
+Add the field to the model, taking care to address any of the following conditions.
+
+* When adding a GenericForeignKey field, also add an index under `Meta` for its two concrete fields. For example:
+
+ ```python
+ class Meta:
+ indexes = (
+ models.Index(fields=('object_type', 'object_id')),
+ )
+ ```
+
+## 2. Generate and run database migrations
[Django migrations](https://docs.djangoproject.com/en/stable/topics/migrations/) are used to express changes to the database schema. In most cases, Django can generate these automatically, however very complex changes may require manual intervention. Always remember to specify a short but descriptive name when generating a new migration.
```
-./manage.py makemigrations -n
+./manage.py makemigrations -n --no-header
./manage.py migrate
```
@@ -16,7 +29,7 @@ Where possible, try to merge related changes into a single migration. For exampl
!!! warning "Do not alter existing migrations"
Migrations can only be merged within a release. Once a new release has been published, its migrations cannot be altered (other than for the purpose of correcting a bug).
-## 2. Add validation logic to `clean()`
+## 3. Add validation logic to `clean()`
If the new field introduces additional validation requirements (beyond what's included with the field itself), implement them in the model's `clean()` method. Remember to call the model's original method using `super()` before or after your custom validation as appropriate:
@@ -31,15 +44,15 @@ class Foo(models.Model):
raise ValidationError()
```
-## 3. Update relevant querysets
+## 4. Update relevant querysets
If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retrieving a list of objects, be sure to include the field using `prefetch_related()` as appropriate. This will optimize the view and avoid extraneous database queries.
-## 4. Update API serializer
+## 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.
-## 5. Add fields to forms
+## 6. Add fields to forms
Extend any forms to include the new field(s) as appropriate. These are found under the `forms/` directory within each app. Common forms include:
@@ -48,23 +61,23 @@ Extend any forms to include the new field(s) as appropriate. These are found und
* **CSV import** - The form used when bulk importing objects in CSV format
* **Filter** - Displays the options available for filtering a list of objects (both UI and API)
-## 6. Extend object filter set
+## 7. Extend object filter set
If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to query it in the FilterSet's `search()` method.
-## 7. Add column to object table
+## 8. Add column to object table
If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require declaring a custom column. Also add the field name to `default_columns` if the column should be present in the table by default.
-## 8. Update the SearchIndex
+## 9. Update the SearchIndex
Where applicable, add the new field to the model's SearchIndex for inclusion in global search.
-## 9. Update the UI templates
+## 10. Update the UI templates
Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated.
-## 10. Create/extend test cases
+## 11. Create/extend test cases
Create or extend the relevant test cases to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. NetBox incorporates various test suites, including:
@@ -74,8 +87,8 @@ Create or extend the relevant test cases to verify that the new field and any ac
* Model tests
* View tests
-Be diligent to ensure all of the relevant test suites are adapted or extended as necessary to test any new functionality.
+Be diligent to ensure all the relevant test suites are adapted or extended as necessary to test any new functionality.
-## 11. Update the model's documentation
+## 12. Update the model's documentation
Each model has a dedicated page in the documentation, at `models//.md`. Update this file to include any relevant information about the new field.
diff --git a/docs/development/internationalization.md b/docs/development/internationalization.md
index bdc7cbdaa..bebc97470 100644
--- a/docs/development/internationalization.md
+++ b/docs/development/internationalization.md
@@ -97,7 +97,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
1. Ensure translation support is enabled by including `{% load i18n %}` at the top of the template.
2. Use the [`{% trans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#translate-template-tag) tag (short for "translate") to wrap short strings.
-3. Longer strings may be enclosed between [`{% blocktrans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#blocktranslate-template-tag) and `{% endblocktrans %}` tags to improve readability and to enable variable replacement.
+3. Longer strings may be enclosed between [`{% blocktrans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#blocktranslate-template-tag) and `{% endblocktrans %}` tags to improve readability and to enable variable replacement. (Remember to include the `trimmed` argument to trim whitespace between the tags.)
4. Avoid passing HTML within translated strings where possible, as this can complicate the work needed of human translators to develop message maps.
```
@@ -107,7 +107,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
{# A longer string with a context variable #}
-{% blocktrans with count=object.circuits.count %}
+{% blocktrans trimmed with count=object.circuits.count %}
There are {count} circuits. Would you like to continue?
{% endblocktrans %}
```
diff --git a/docs/development/models.md b/docs/development/models.md
index d4838570a..19b7be6de 100644
--- a/docs/development/models.md
+++ b/docs/development/models.md
@@ -10,19 +10,19 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
Depending on its classification, each NetBox model may support various features which enhance its operation. Each feature is enabled by inheriting from its designated mixin class, and some features also make use of the [application registry](./application-registry.md#model_features).
-| Feature | Feature Mixin | Registry Key | Description |
-|------------------------------------------------------------|-------------------------|--------------------|--------------------------------------------------------------------------------|
-| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | - | Changes to these objects are automatically recorded in the change log |
-| Cloning | `CloningMixin` | - | Provides the `clone()` method to prepare a copy |
-| [Custom fields](../customization/custom-fields.md) | `CustomFieldsMixin` | `custom_fields` | These models support the addition of user-defined fields |
-| [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 |
-| [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 |
-| [Webhooks](../integrations/webhooks.md) | `WebhooksMixin` | `webhooks` | NetBox is capable of generating outgoing webhooks for these objects |
+| Feature | Feature Mixin | Registry Key | Description |
+|------------------------------------------------------------|-------------------------|--------------------|-----------------------------------------------------------------------------------------|
+| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | - | Changes to these objects are automatically recorded in the change log |
+| Cloning | `CloningMixin` | - | Provides the `clone()` method to prepare a copy |
+| [Custom fields](../customization/custom-fields.md) | `CustomFieldsMixin` | `custom_fields` | These models support the addition of user-defined fields |
+| [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 |
+| [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 |
+| [Event rules](../features/event-rules.md) | `EventRulesMixin` | `event_rules` | Event rules can send webhooks or run custom scripts automatically in response to events |
## Models Index
@@ -52,7 +52,6 @@ These are considered the "core" application models which are used to model netwo
* [ipam.FHRPGroup](../models/ipam/fhrpgroup.md)
* [ipam.IPAddress](../models/ipam/ipaddress.md)
* [ipam.IPRange](../models/ipam/iprange.md)
-* [ipam.L2VPN](../models/ipam/l2vpn.md)
* [ipam.Prefix](../models/ipam/prefix.md)
* [ipam.RouteTarget](../models/ipam/routetarget.md)
* [ipam.Service](../models/ipam/service.md)
@@ -63,6 +62,13 @@ These are considered the "core" application models which are used to model netwo
* [tenancy.Tenant](../models/tenancy/tenant.md)
* [virtualization.Cluster](../models/virtualization/cluster.md)
* [virtualization.VirtualMachine](../models/virtualization/virtualmachine.md)
+* [vpn.IKEPolicy](../models/vpn/ikepolicy.md)
+* [vpn.IKEProposal](../models/vpn/ikeproposal.md)
+* [vpn.IPSecPolicy](../models/vpn/ipsecpolicy.md)
+* [vpn.IPSecProfile](../models/vpn/ipsecprofile.md)
+* [vpn.IPSecProposal](../models/vpn/ipsecproposal.md)
+* [vpn.L2VPN](../models/vpn/l2vpn.md)
+* [vpn.Tunnel](../models/vpn/tunnel.md)
* [wireless.WirelessLAN](../models/wireless/wirelesslan.md)
* [wireless.WirelessLink](../models/wireless/wirelesslink.md)
@@ -75,6 +81,7 @@ Organization models are used to organize and classify primary models.
* [dcim.Manufacturer](../models/dcim/manufacturer.md)
* [dcim.Platform](../models/dcim/platform.md)
* [dcim.RackRole](../models/dcim/rackrole.md)
+* [ipam.ASNRange](../models/ipam/asnrange.md)
* [ipam.RIR](../models/ipam/rir.md)
* [ipam.Role](../models/ipam/role.md)
* [ipam.VLANGroup](../models/ipam/vlangroup.md)
@@ -107,11 +114,12 @@ Component models represent individual physical or virtual components belonging t
* [dcim.PowerOutlet](../models/dcim/poweroutlet.md)
* [dcim.PowerPort](../models/dcim/powerport.md)
* [dcim.RearPort](../models/dcim/rearport.md)
+* [virtualization.VirtualDisk](../models/virtualization/virtualdisk.md)
* [virtualization.VMInterface](../models/virtualization/vminterface.md)
### Component Template Models
-These function as templates to effect the replication of device and virtual machine components. Component template models support a limited feature set, including change logging, custom validation, and webhooks.
+These function as templates to effect the replication of device and virtual machine components. Component template models support a limited feature set, including change logging, custom validation, and event rules.
* [dcim.ConsolePortTemplate](../models/dcim/consoleporttemplate.md)
* [dcim.ConsoleServerPortTemplate](../models/dcim/consoleserverporttemplate.md)
diff --git a/docs/development/search.md b/docs/development/search.md
index 6ccffa7af..1c4eec169 100644
--- a/docs/development/search.md
+++ b/docs/development/search.md
@@ -17,6 +17,7 @@ class MyModelIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('site', 'device', 'status', 'description')
```
A SearchIndex subclass defines both its model and a list of two-tuples specifying which model fields to be indexed and the weight (precedence) associated with each. Guidance on weight assignment for fields is provided below.
diff --git a/docs/development/signals.md b/docs/development/signals.md
index 8a5d8e43f..8783b74a3 100644
--- a/docs/development/signals.md
+++ b/docs/development/signals.md
@@ -9,3 +9,27 @@ This signal is sent by models which inherit from `CustomValidationMixin` at the
### Receivers
* `extras.signals.run_custom_validators()`
+
+## core.job_start
+
+This signal is sent whenever a [background job](../features/background-jobs.md) is started.
+
+### Receivers
+
+* `extras.signals.process_job_start_event_rules()`
+
+## core.job_end
+
+This signal is sent whenever a [background job](../features/background-jobs.md) is terminated.
+
+### Receivers
+
+* `extras.signals.process_job_end_event_rules()`
+
+## core.pre_sync
+
+This signal is sent when the [DataSource](../models/core/datasource.md) model's `sync()` method is called.
+
+## core.post_sync
+
+This signal is sent when a [DataSource](../models/core/datasource.md) finishes synchronizing.
diff --git a/docs/features/api-integration.md b/docs/features/api-integration.md
index 8c0843bfe..94a39d731 100644
--- a/docs/features/api-integration.md
+++ b/docs/features/api-integration.md
@@ -26,9 +26,9 @@ To learn more about this feature, check out the [GraphQL API documentation](../i
## Webhooks
-A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are an excellent mechanism for building event-based automation processes.
+A webhook is a mechanism for conveying to some external system a change that has taken place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. To do this, first create a [webhook](../models/extras/webhook.md) identifying the remote receiver (URL), HTTP method, and any other necessary parameters. Then, define an [event rule](../models/extras/eventrule.md) which is triggered by device changes to transmit the webhook.
-To learn more about this feature, check out the [webhooks documentation](../integrations/webhooks.md).
+When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are an excellent mechanism for building event-based automation processes. To learn more about this feature, check out the [webhooks documentation](../integrations/webhooks.md).
## Prometheus Metrics
diff --git a/docs/features/configuration-rendering.md b/docs/features/configuration-rendering.md
index 9d212a34e..a87a6eae4 100644
--- a/docs/features/configuration-rendering.md
+++ b/docs/features/configuration-rendering.md
@@ -37,6 +37,14 @@ Configuration templates are written in the [Jinja2 templating language](https://
When rendered for a specific NetBox device, the template's `device` variable will be populated with the device instance, and `ntp_servers` will be pulled from the device's available context data. The resulting output will be a valid configuration segment that can be applied directly to a compatible network device.
+### Context Data
+
+The objet for which the configuration is being rendered is made available as template context as `device` or `virtualmachine` for devices and virtual machines, respectively. Additionally, NetBox model classes can be accessed by the app or plugin in which they reside. For example:
+
+```
+There are {{ dcim.Site.objects.count() }} sites.
+```
+
## Rendering Templates
### Device Configurations
diff --git a/docs/features/event-rules.md b/docs/features/event-rules.md
new file mode 100644
index 000000000..0e9535223
--- /dev/null
+++ b/docs/features/event-rules.md
@@ -0,0 +1,31 @@
+# Event Rules
+
+NetBox includes the ability to execute certain functions in response to internal object changes. These include:
+
+* [Scripts](../customization/custom-scripts.md) execution
+* [Webhooks](../integrations/webhooks.md) execution
+
+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.
+
+Each event must be associated with at least one NetBox object type and at least one event (e.g. create, update, or delete).
+
+## Conditional Event Rules
+
+An event rule may include a set of conditional logic expressed in JSON used to control whether an event triggers for a specific object. For example, you may wish to trigger an event for devices only when the `status` field of an object is "active":
+
+```json
+{
+ "and": [
+ {
+ "attr": "status.value",
+ "value": "active"
+ }
+ ]
+}
+```
+
+For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md).
+
+## Event Rule Processing
+
+When a change is detected, any resulting events are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing event(s) to be processed. The events are then extracted from the queue by the `rqworker` process. The current event queue and any failed events can be inspected in the admin UI under System > Background Tasks.
diff --git a/docs/features/search.md b/docs/features/search.md
index 07394af97..92422cad9 100644
--- a/docs/features/search.md
+++ b/docs/features/search.md
@@ -8,6 +8,9 @@ When entering a search query, the user can choose a specific lookup type: exact
Custom fields defined by NetBox administrators are also included in search results if configured with a search weight. Additionally, NetBox plugins can register their own custom models for inclusion alongside core models.
+!!! note
+ NetBox does not index any static choice field's (including custom fields of type "Selection" or "Multiple selection").
+
## Saved Filters
Each type of object in NetBox is accompanied by an extensive set of filters, each tied to a specific attribute, which enable the creation of complex queries. Often you'll find that certain queries are used routinely to apply some set of prescribed conditions to a query. Once a set of filters has been applied, NetBox offers the option to save it for future use.
diff --git a/docs/features/synchronized-data.md b/docs/features/synchronized-data.md
index f266519b6..a070d0ce1 100644
--- a/docs/features/synchronized-data.md
+++ b/docs/features/synchronized-data.md
@@ -10,7 +10,6 @@ To enable remote data synchronization, the NetBox administrator first designates
(Local disk paths are considered "remote" in this context as they exist outside NetBox's database. These paths could also be mapped to external network shares.)
-
!!! 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.
@@ -23,3 +22,6 @@ The following NetBox models can be associated with replicated data files:
* Export templates
Once a data has been designated for a local instance, its data will be replaced with the content of the replicated file. When the replicated file is updated in the future (via synchronization jobs), the local instance will be flagged as having out-of-date data. A user can then synchronize these objects individually or in bulk to effect the update. This two-stage process ensures that automated synchronization tasks do not immediately affect production data.
+
+!!! note "Permissions"
+ A user must be assigned the `core.sync_datasource` permission in order to synchronize local files from a remote data source.
diff --git a/docs/features/vpn-tunnels.md b/docs/features/vpn-tunnels.md
new file mode 100644
index 000000000..4ebb91ab7
--- /dev/null
+++ b/docs/features/vpn-tunnels.md
@@ -0,0 +1,49 @@
+# Tunnels
+
+NetBox can model private tunnels formed among virtual termination points across your network. Typical tunnel implementations include GRE, IP-in-IP, and IPSec. A tunnel may be terminated to two or more device or virtual machine interfaces. For convenient organization, tunnels may be assigned to user-defined groups.
+
+```mermaid
+flowchart TD
+ Termination1[TunnelTermination]
+ Termination2[TunnelTermination]
+ Interface1[Interface]
+ Interface2[Interface]
+ Tunnel --> Termination1 & Termination2
+ Termination1 --> Interface1
+ Termination2 --> Interface2
+ Interface1 --> Device
+ Interface2 --> VirtualMachine
+
+click Tunnel "../../models/vpn/tunnel/"
+click TunnelTermination1 "../../models/vpn/tunneltermination/"
+click TunnelTermination2 "../../models/vpn/tunneltermination/"
+```
+
+# IPSec & IKE
+
+NetBox includes robust support for modeling IPSec & IKE policies. These are used to define encryption and authentication parameters for IPSec tunnels.
+
+```mermaid
+flowchart TD
+ subgraph IKEProposals[Proposals]
+ IKEProposal1[IKEProposal]
+ IKEProposal2[IKEProposal]
+ end
+ subgraph IPSecProposals[Proposals]
+ IPSecProposal1[IPSecProposal]
+ IPSecProposal2[IPSecProposal]
+ end
+ IKEProposals --> IKEPolicy
+ IPSecProposals --> IPSecPolicy
+ IKEPolicy & IPSecPolicy--> IPSecProfile
+ IPSecProfile --> Tunnel
+
+click IKEProposal1 "../../models/vpn/ikeproposal/"
+click IKEProposal2 "../../models/vpn/ikeproposal/"
+click IKEPolicy "../../models/vpn/ikepolicy/"
+click IPSecProposal1 "../../models/vpn/ipsecproposal/"
+click IPSecProposal2 "../../models/vpn/ipsecproposal/"
+click IPSecPolicy "../../models/vpn/ipsecpolicy/"
+click IPSecProfile "../../models/vpn/ipsecprofile/"
+click Tunnel "../../models/vpn/tunnel/"
+```
diff --git a/docs/index.md b/docs/index.md
index 6a53403d6..84334337b 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,6 +1,6 @@
{style="height: 100px; margin-bottom: 3em"}
-# The Premiere Network Source of Truth
+# The Premier Network Source of Truth
NetBox is the leading solution for modeling and documenting modern networks. By combining the traditional disciplines of IP address management (IPAM) and datacenter infrastructure management (DCIM) with powerful APIs and extensions, NetBox provides the ideal "source of truth" to power network automation. Read on to discover why thousands of organizations worldwide put NetBox at the heart of their infrastructure.
@@ -32,7 +32,7 @@ In addition to its expansive and robust data model, NetBox offers myriad mechani
* Custom fields
* Custom model validation
* Export templates
-* Webhooks
+* Event rules
* Plugins
* REST & GraphQL APIs
diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md
index 0713d12e3..4043416a3 100644
--- a/docs/installation/3-netbox.md
+++ b/docs/installation/3-netbox.md
@@ -227,6 +227,17 @@ sudo sh -c "echo 'boto3' >> /opt/netbox/local_requirements.txt"
!!! info
These packages were previously required in NetBox v3.5 but now are optional.
+### Sentry Integration
+
+NetBox may be configured to send error reports to [Sentry](../administration/error-reporting.md) for analysis. This integration requires installation of the `sentry-sdk` Python library.
+
+```no-highlight
+sudo sh -c "echo 'sentry-sdk' >> /opt/netbox/local_requirements.txt"
+```
+
+!!! info
+ Sentry integration was previously included by default in NetBox v3.6 but is now optional.
+
## Run the Upgrade Script
Once NetBox has been configured, we're ready to proceed with the actual installation. We'll run the packaged upgrade script (`upgrade.sh`) to perform the following actions:
diff --git a/docs/installation/6-ldap.md b/docs/installation/6-ldap.md
index 1cd3e1f0a..6ee1c9901 100644
--- a/docs/installation/6-ldap.md
+++ b/docs/installation/6-ldap.md
@@ -148,6 +148,126 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600
!!! warning
Authentication will fail if the groups (the distinguished names) do not exist in the LDAP directory.
+## Authenticating with Active Directory
+
+Integrating Active Directory for authentication can be a bit challenging as it may require handling different login formats. This solution will allow users to log in either using their full User Principal Name (UPN) or their username alone, by filtering the DN according to either the `sAMAccountName` or the `userPrincipalName`. The following configuration options will allow your users to enter their usernames in the format `username` or `username@domain.tld`.
+
+Just as before, the configuration options are defined in the file ldap_config.py. First, modify the `AUTH_LDAP_USER_SEARCH` option to match the following:
+
+```python
+AUTH_LDAP_USER_SEARCH = LDAPSearch(
+ "ou=Users,dc=example,dc=com",
+ ldap.SCOPE_SUBTREE,
+ "(|(userPrincipalName=%(user)s)(sAMAccountName=%(user)s))"
+)
+```
+
+In addition, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to `None` as described in the previous sections. Next, modify `AUTH_LDAP_USER_ATTR_MAP` to match the following:
+
+```python
+AUTH_LDAP_USER_ATTR_MAP = {
+ "username": "sAMAccountName",
+ "email": "mail",
+ "first_name": "givenName",
+ "last_name": "sn",
+}
+```
+
+Finally, we need to add one more configuration option, `AUTH_LDAP_USER_QUERY_FIELD`. The following should be added to your LDAP configuration file:
+
+```python
+AUTH_LDAP_USER_QUERY_FIELD = "username"
+```
+
+With these configuration options, your users will be able to log in either with or without the UPN suffix.
+
+### Example Configuration
+
+!!! info
+ This configuration is intended to serve as a template, but may need to be modified in accordance with your environment.
+
+```python
+import ldap
+from django_auth_ldap.config import LDAPSearch, NestedGroupOfNamesType
+
+# Server URI
+AUTH_LDAP_SERVER_URI = "ldaps://ad.example.com:3269"
+
+# The following may be needed if you are binding to Active Directory.
+AUTH_LDAP_CONNECTION_OPTIONS = {
+ ldap.OPT_REFERRALS: 0
+}
+
+# Set the DN and password for the NetBox service account.
+AUTH_LDAP_BIND_DN = "CN=NETBOXSA,OU=Service Accounts,DC=example,DC=com"
+AUTH_LDAP_BIND_PASSWORD = "demo"
+
+# Include this setting if you want to ignore certificate errors. This might be needed to accept a self-signed cert.
+# Note that this is a NetBox-specific setting which sets:
+# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
+LDAP_IGNORE_CERT_ERRORS = False
+
+# Include this setting if you want to validate the LDAP server certificates against a CA certificate directory on your server
+# Note that this is a NetBox-specific setting which sets:
+# ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, LDAP_CA_CERT_DIR)
+LDAP_CA_CERT_DIR = '/etc/ssl/certs'
+
+# Include this setting if you want to validate the LDAP server certificates against your own CA.
+# Note that this is a NetBox-specific setting which sets:
+# ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, LDAP_CA_CERT_FILE)
+LDAP_CA_CERT_FILE = '/path/to/example-CA.crt'
+
+# This search matches users with the sAMAccountName equal to the provided username. This is required if the user's
+# username is not in their DN (Active Directory).
+AUTH_LDAP_USER_SEARCH = LDAPSearch(
+ "ou=Users,dc=example,dc=com",
+ ldap.SCOPE_SUBTREE,
+ "(|(userPrincipalName=%(user)s)(sAMAccountName=%(user)s))"
+)
+
+# If a user's DN is producible from their username, we don't need to search.
+AUTH_LDAP_USER_DN_TEMPLATE = None
+
+# You can map user attributes to Django attributes as so.
+AUTH_LDAP_USER_ATTR_MAP = {
+ "username": "sAMAccountName",
+ "email": "mail",
+ "first_name": "givenName",
+ "last_name": "sn",
+}
+
+AUTH_LDAP_USER_QUERY_FIELD = "username"
+
+# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group
+# hierarchy.
+AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
+ "dc=example,dc=com",
+ ldap.SCOPE_SUBTREE,
+ "(objectClass=group)"
+)
+AUTH_LDAP_GROUP_TYPE = NestedGroupOfNamesType()
+
+# Define a group required to login.
+AUTH_LDAP_REQUIRE_GROUP = "CN=NETBOX_USERS,DC=example,DC=com"
+
+# Mirror LDAP group assignments.
+AUTH_LDAP_MIRROR_GROUPS = True
+
+# Define special user types using groups. Exercise great caution when assigning superuser status.
+AUTH_LDAP_USER_FLAGS_BY_GROUP = {
+ "is_active": "cn=active,ou=groups,dc=example,dc=com",
+ "is_staff": "cn=staff,ou=groups,dc=example,dc=com",
+ "is_superuser": "cn=superuser,ou=groups,dc=example,dc=com"
+}
+
+# For more granular permissions, we can map LDAP groups to Django groups.
+AUTH_LDAP_FIND_GROUP_PERMS = True
+
+# Cache groups for one hour to reduce LDAP traffic
+AUTH_LDAP_CACHE_TIMEOUT = 3600
+AUTH_LDAP_ALWAYS_UPDATE_USER = True
+```
+
## Troubleshooting LDAP
`systemctl restart netbox` restarts the NetBox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
diff --git a/docs/installation/index.md b/docs/installation/index.md
index da50fa5fa..5affdf247 100644
--- a/docs/installation/index.md
+++ b/docs/installation/index.md
@@ -1,5 +1,8 @@
# Installation
+!!! info "NetBox Cloud"
+ The instructions below are for installing NetBox as a standalone, self-hosted application. For a Cloud-delivered solution, check out [NetBox Cloud](https://netboxlabs.com/netbox-cloud/) by NetBox Labs.
+
The installation instructions provided here have been tested to work on Ubuntu 22.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
VIDEO
diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md
index a65794037..0aaf94b1e 100644
--- a/docs/installation/upgrading.md
+++ b/docs/installation/upgrading.md
@@ -59,7 +59,7 @@ Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if pres
```no-highlight
# Set $OLDVER to the NetBox version currently installed
-NEWVER=3.4.9
+OLDVER=3.4.9
sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/
sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/
diff --git a/docs/integrations/synchronized-data.md b/docs/integrations/synchronized-data.md
index 805cbe15b..d72501fd5 100644
--- a/docs/integrations/synchronized-data.md
+++ b/docs/integrations/synchronized-data.md
@@ -2,6 +2,9 @@
Some NetBox models support automatic synchronization of certain attributes from remote [data sources](../models/core/datasource.md), such as a git repository hosted on GitHub or GitLab. Data from the authoritative remote source is synchronized locally in NetBox as [data files](../models/core/datafile.md).
+!!! note "Permissions"
+ A user must be assigned the `core.sync_datasource` permission in order to synchronize local files from a remote data source. This is accomplished by creating a permission for the "Core > Data Source" object type with the `sync` action, and assigning it to the desired user and/or group.
+
The following features support the use of synchronized data:
* [Configuration templates](../features/configuration-rendering.md)
diff --git a/docs/integrations/webhooks.md b/docs/integrations/webhooks.md
index 9a1094988..8913fd99c 100644
--- a/docs/integrations/webhooks.md
+++ b/docs/integrations/webhooks.md
@@ -1,11 +1,9 @@
# Webhooks
-NetBox can be configured to transmit outgoing webhooks to remote systems in response to internal object changes. The receiver can act on the data in these webhook messages to perform related tasks.
+NetBox can be configured via [Event Rules](../features/event-rules.md) to transmit outgoing webhooks to remote systems in response to internal object changes. The receiver can act on the data in these webhook messages to perform related tasks.
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. Webhooks will be sent automatically by NetBox whenever the configured constraints are met.
-Each webhook must be associated with at least one NetBox object type and at least one event (create, update, or delete). Users can specify the receiver URL, HTTP request type (`GET`, `POST`, etc.), content type, and headers. A request body can also be specified; if left blank, this will default to a serialized representation of the affected object.
-
!!! warning "Security Notice"
Webhooks support the inclusion of user-submitted code to generate the URL, custom headers, and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users.
@@ -70,26 +68,12 @@ If no body template is specified, the request body will be populated with a JSON
}
```
-## Conditional Webhooks
-
-A webhook may include a set of conditional logic expressed in JSON used to control whether a webhook triggers for a specific object. For example, you may wish to trigger a webhook for devices only when the `status` field of an object is "active":
-
-```json
-{
- "and": [
- {
- "attr": "status.value",
- "value": "active"
- }
- ]
-}
-```
-
-For more detail, see the reference documentation for NetBox's [conditional logic](../reference/conditions.md).
+!!! note
+ The setting of conditional webhooks has been moved to [Event Rules](../features/event-rules.md) since NetBox 3.7
## Webhook Processing
-When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks.
+Using [Event Rules](../features/event-rules.md), when a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under System > Background Tasks.
A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI.
diff --git a/docs/introduction.md b/docs/introduction.md
index 8f62d842a..b8442dad7 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -19,10 +19,13 @@ NetBox was built specifically to serve the needs of network engineers and operat
* Device modeling using pre-defined types
* Virtual chassis and device contexts
* Network, power, and console cabling with SVG traces
+* Breakout cables
* Power distribution modeling
* Data circuit and provider tracking
* Wireless LAN and point-to-point links
-* L2 VPN overlays
+* VPN tunnels
+* IKE & IPSec policies
+* Layer 2 VPN overlays
* FHRP groups (VRRP, HSRP, etc.)
* Application service bindings
* Virtual machines & clusters
@@ -30,13 +33,14 @@ NetBox was built specifically to serve the needs of network engineers and operat
* Tenant ownership assignment
* Device & VM configuration contexts for advanced configuration rendering
* Custom fields for data model extension
-* Custom validation rules
+* Custom validation & protection rules
* Custom reports & scripts executable directly within the UI
* Extensive plugin framework for adding custom functionality
* Single sign-on (SSO) authentication
* Robust object-based permissions
* Detailed, automatic change logging
* Global search engine
+* Event-driven scripts & webhooks
## What NetBox Is Not
diff --git a/docs/media/admin_ui_run_permission.png b/docs/media/admin_ui_run_permission.png
index a7aaa79b8..9c5b3e733 100644
Binary files a/docs/media/admin_ui_run_permission.png and b/docs/media/admin_ui_run_permission.png differ
diff --git a/docs/media/misc/netbox_logo.png b/docs/media/misc/netbox_logo.png
new file mode 100644
index 000000000..c6e0a58e6
Binary files /dev/null and b/docs/media/misc/netbox_logo.png differ
diff --git a/docs/models/dcim/interface.md b/docs/models/dcim/interface.md
index 42b570964..3667dabd5 100644
--- a/docs/models/dcim/interface.md
+++ b/docs/models/dcim/interface.md
@@ -77,6 +77,9 @@ If selected, this component will be treated as if a cable has been connected.
Virtual interfaces can be bound to a physical parent interface. This is helpful for modeling virtual interfaces which employ encapsulation on a physical interface, such as an 802.1Q VLAN-tagged subinterface.
+!!! note
+ An interface with one or more child interfaces assigned cannot be deleted until all its child interfaces have been deleted or reassigned.
+
### Bridged Interface
Interfaces can be bridged to other interfaces on a device in two manners: symmetric or grouped.
diff --git a/docs/models/dcim/inventoryitem.md b/docs/models/dcim/inventoryitem.md
index f61586eda..b9029f75c 100644
--- a/docs/models/dcim/inventoryitem.md
+++ b/docs/models/dcim/inventoryitem.md
@@ -19,7 +19,7 @@ The parent inventory item to which this item is assigned (optional).
### Name
-The inventory item's name. Must be unique to the parent device.
+The inventory item's name. If the inventory item is assigned to a parent item, its name must be unique among its siblings (all items belonging to the same parent item).
### Label
diff --git a/docs/models/dcim/platform.md b/docs/models/dcim/platform.md
index dc332da74..0914d0aa6 100644
--- a/docs/models/dcim/platform.md
+++ b/docs/models/dcim/platform.md
@@ -23,17 +23,3 @@ If designated, this platform will be available for use only to devices assigned
### Configuration Template
The default [configuration template](../extras/configtemplate.md) for devices assigned to this platform.
-
-### NAPALM Driver
-
-!!! warning "Deprecated Field"
- NAPALM integration was removed from NetBox core in v3.5 and is now available as a [plugin](https://github.com/netbox-community/netbox-napalm). This field will be removed in NetBox v3.6.
-
-The [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html) associated with this platform.
-
-### NAPALM Arguments
-
-!!! warning "Deprecated Field"
- NAPALM integration was removed from NetBox core in v3.5 and is now available as a [plugin](https://github.com/netbox-community/netbox-napalm). This field will be removed in NetBox v3.6.
-
-Any additional arguments to send when invoking the NAPALM driver assigned to this platform.
diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md
index bf0c4755a..e68ddb79d 100644
--- a/docs/models/extras/customfield.md
+++ b/docs/models/extras/customfield.md
@@ -64,16 +64,25 @@ Defines how filters are evaluated against custom field values.
| Loose | Match any occurrence of the value |
| Exact | Match only the complete field value |
-### UI Visibility
+### UI Visible
-Controls how and whether the custom field is displayed within the NetBox user interface.
+Controls whether the custom field is displayed for objects within the NetBox user interface.
-| Option | Description |
-|-------------------|--------------------------------------------------|
-| Read/write | Display and permit editing (default) |
-| Read-only | Display field but disallow editing |
-| Hidden | Do not display field in the UI |
-| Hidden (if unset) | Display in the UI only when a value has been set |
+| Option | Description |
+|--------|----------------------------------------------------------------|
+| Always | The field is always displayed when viewing an object (default) |
+| If set | The field is displayed only if a value has been defined |
+| Hidden | The field is not displayed when viewing an object |
+
+### UI Editable
+
+Controls whether the custom field is editable on objects within the NetBox user interface.
+
+| Option | Description |
+|--------|------------------------------------------------------------------------------|
+| Yes | The field's value may be changed when editing an object (default) |
+| No | The field's value is displayed when editing an object but may not be altered |
+| Hidden | The field is not displayed when editing an object |
### Default
diff --git a/docs/models/extras/eventrule.md b/docs/models/extras/eventrule.md
new file mode 100644
index 000000000..c105a2630
--- /dev/null
+++ b/docs/models/extras/eventrule.md
@@ -0,0 +1,35 @@
+# EventRule
+
+An event rule is a mechanism for automatically taking an action (such as running a script or sending a webhook) in response to an event in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating an event for device objects and designating a webhook to be transmitted. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver.
+
+See the [event rules documentation](../../features/event-rules.md) for more information.
+
+## Fields
+
+### Name
+
+A unique human-friendly name.
+
+### Content Types
+
+The type(s) of object in NetBox that will trigger the rule.
+
+### Enabled
+
+If not selected, the event rule will not be processed.
+
+### Events
+
+The events 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 |
+
+### Conditions
+
+A set of [prescribed conditions](../../reference/conditions.md) against which the triggering object will be evaluated. If the conditions are defined but not met by the object, no action will be taken. An event rule that does not define any conditions will _always_ trigger.
diff --git a/docs/models/ipam/l2vpntermination.md b/docs/models/ipam/l2vpntermination.md
deleted file mode 100644
index c3c27b8d2..000000000
--- a/docs/models/ipam/l2vpntermination.md
+++ /dev/null
@@ -1,18 +0,0 @@
-# L2VPN Termination
-
-A L2VPN termination is the attachment of an [L2VPN](./l2vpn.md) to an [interface](../dcim/interface.md) or [VLAN](./vlan.md). Note that the L2VPNs of the following types may have only two terminations assigned to them:
-
-* VPWS
-* EPL
-* EP-LAN
-* EP-TREE
-
-## Fields
-
-### L2VPN
-
-The [L2VPN](./l2vpn.md) instance.
-
-### VLAN or Interface
-
-The [VLAN](./vlan.md), [device interface](../dcim/interface.md), or [virtual machine interface](../virtualization/virtualmachine.md) attached to the L2VPN.
diff --git a/docs/models/virtualization/virtualdisk.md b/docs/models/virtualization/virtualdisk.md
new file mode 100644
index 000000000..9d256bb66
--- /dev/null
+++ b/docs/models/virtualization/virtualdisk.md
@@ -0,0 +1,13 @@
+# Virtual Disks
+
+A virtual disk is used to model discrete virtual hard disks assigned to [virtual machines](./virtualmachine.md).
+
+## Fields
+
+### Name
+
+A human-friendly name that is unique to the assigned virtual machine.
+
+### Size
+
+The allocated disk size, in gigabytes.
diff --git a/docs/models/virtualization/vminterface.md b/docs/models/virtualization/vminterface.md
index 264fb95ba..d923bdd5d 100644
--- a/docs/models/virtualization/vminterface.md
+++ b/docs/models/virtualization/vminterface.md
@@ -16,6 +16,9 @@ The interface's name. Must be unique to the assigned VM.
Identifies the parent interface of a subinterface (e.g. used to employ encapsulation).
+!!! note
+ An interface with one or more child interfaces assigned cannot be deleted until all its child interfaces have been deleted or reassigned.
+
### Bridged Interface
An interface on the same VM with which this interface is bridged.
diff --git a/docs/models/vpn/ikepolicy.md b/docs/models/vpn/ikepolicy.md
new file mode 100644
index 000000000..7b739072b
--- /dev/null
+++ b/docs/models/vpn/ikepolicy.md
@@ -0,0 +1,25 @@
+# IKE Policies
+
+An [Internet Key Exhcnage (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) policy defines an IKE version, mode, and set of [proposals](./ikeproposal.md) to be used in IKE negotiation. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
+
+## Fields
+
+### Name
+
+The unique user-assigned name for the policy.
+
+### Version
+
+The IKE version employed (v1 or v2).
+
+### Mode
+
+The IKE mode employed (main or aggressive).
+
+### Proposals
+
+One or more [IKE proposals](./ikeproposal.md) supported for use by this policy.
+
+### Pre-shared Key
+
+A pre-shared secret key associated with this policy (optional).
diff --git a/docs/models/vpn/ikeproposal.md b/docs/models/vpn/ikeproposal.md
new file mode 100644
index 000000000..312ec1f6c
--- /dev/null
+++ b/docs/models/vpn/ikeproposal.md
@@ -0,0 +1,39 @@
+# IKE Proposals
+
+An [Internet Key Exhcnage (IKE)](https://en.wikipedia.org/wiki/Internet_Key_Exchange) proposal defines a set of parameters used to establish a secure bidirectional connection across an untrusted medium, such as the Internet. IKE proposals defined in NetBox can be referenced by [IKE policies](./ikepolicy.md), which are in turn employed by [IPSec profiles](./ipsecprofile.md).
+
+!!! note
+ Some platforms refer to IKE proposals as [ISAKMP](https://en.wikipedia.org/wiki/Internet_Security_Association_and_Key_Management_Protocol), which is a framework for authentication and key exchange which employs IKE.
+
+## Fields
+
+### Name
+
+The unique user-assigned name for the proposal.
+
+### Authentication Method
+
+The strategy employed for authenticating the IKE peer. Available options are listed below.
+
+| Name |
+|----------------|
+| Pre-shared key |
+| Certificate |
+| RSA signature |
+| DSA signature |
+
+### Encryption Algorithm
+
+The protocol employed for data encryption. Options include DES, 3DES, and various flavors of AES.
+
+### Authentication Algorithm
+
+The mechanism employed to ensure data integrity. Options include MD5 and SHA HMAC implementations. Specifying an authentication algorithm is optional, as some encryption algorithms (e.g. AES-GCM) provide authentication natively.
+
+### Group
+
+The [Diffie-Hellman group](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange) supported by the proposal. Group IDs are [managed by IANA](https://www.iana.org/assignments/ikev2-parameters/ikev2-parameters.xhtml#ikev2-parameters-8).
+
+### SA Lifetime
+
+The maximum lifetime for the IKE security association (SA), in seconds.
diff --git a/docs/models/vpn/ipsecpolicy.md b/docs/models/vpn/ipsecpolicy.md
new file mode 100644
index 000000000..3283d3b23
--- /dev/null
+++ b/docs/models/vpn/ipsecpolicy.md
@@ -0,0 +1,17 @@
+# IPSec Policy
+
+An [IPSec](https://en.wikipedia.org/wiki/IPsec) policy defines a set of [proposals](./ikeproposal.md) to be used in the formation of IPSec tunnels. A perfect forward secrecy (PFS) group may optionally also be defined. These policies are referenced by [IPSec profiles](./ipsecprofile.md).
+
+## Fields
+
+### Name
+
+The unique user-assigned name for the policy.
+
+### Proposals
+
+One or more [IPSec proposals](./ipsecproposal.md) supported for use by this policy.
+
+### PFS Group
+
+The [perfect forward secrecy (PFS)](https://en.wikipedia.org/wiki/Forward_secrecy) group supported by this policy (optional).
diff --git a/docs/models/vpn/ipsecprofile.md b/docs/models/vpn/ipsecprofile.md
new file mode 100644
index 000000000..1ad1ce7d5
--- /dev/null
+++ b/docs/models/vpn/ipsecprofile.md
@@ -0,0 +1,21 @@
+# IPSec Profile
+
+An [IPSec](https://en.wikipedia.org/wiki/IPsec) profile defines an [IKE policy](./ikepolicy.md), [IPSec policy](./ipsecpolicy.md), and IPSec mode used for establishing an IPSec tunnel.
+
+## Fields
+
+### Name
+
+The unique user-assigned name for the profile.
+
+### Mode
+
+The IPSec mode employed by the profile: Encapsulating Security Payload (ESP) or Authentication Header (AH).
+
+### IKE Policy
+
+The [IKE policy](./ikepolicy.md) associated with the profile.
+
+### IPSec Policy
+
+The [IPSec policy](./ipsecpolicy.md) associated with the profile.
diff --git a/docs/models/vpn/ipsecproposal.md b/docs/models/vpn/ipsecproposal.md
new file mode 100644
index 000000000..ad3279d7a
--- /dev/null
+++ b/docs/models/vpn/ipsecproposal.md
@@ -0,0 +1,31 @@
+# IPSec Proposal
+
+An [IPSec](https://en.wikipedia.org/wiki/IPsec) proposal defines a set of parameters used in negotiating security associations for IPSec tunnels. IPSec proposals defined in NetBox can be referenced by [IPSec policies](./ipsecpolicy.md), which are in turn employed by [IPSec profiles](./ipsecprofile.md).
+
+## Fields
+
+### Name
+
+The unique user-assigned name for the proposal.
+
+### Encryption Algorithm
+
+The protocol employed for data encryption. Options include DES, 3DES, and various flavors of AES.
+
+!!! note
+ If an encryption algorithm is not specified, an authentication algorithm must be specified.
+
+### Authentication Algorithm
+
+The mechanism employed to ensure data integrity. Options include MD5 and SHA HMAC implementations.
+
+!!! note
+ If an authentication algorithm is not specified, an encryption algorithm must be specified.
+
+### SA Lifetime (Seconds)
+
+The maximum amount of time for which the security association (SA) may be active, in seconds.
+
+### SA Lifetime (Data)
+
+The maximum amount of data which can be transferred within the security association (SA) before it must be rebuilt, in kilobytes.
diff --git a/docs/models/ipam/l2vpn.md b/docs/models/vpn/l2vpn.md
similarity index 81%
rename from docs/models/ipam/l2vpn.md
rename to docs/models/vpn/l2vpn.md
index e7ee1e187..79b7435bf 100644
--- a/docs/models/ipam/l2vpn.md
+++ b/docs/models/vpn/l2vpn.md
@@ -1,6 +1,6 @@
# L2VPN
-A L2VPN object is NetBox is a representation of a layer 2 bridge technology such as VXLAN, VPLS, or EPL. Each L2VPN can be identified by name as well as by an optional unique identifier (VNI would be an example). Once created, L2VPNs can be terminated to [interfaces](../dcim/interface.md) and [VLANs](./vlan.md).
+A L2VPN object is NetBox is a representation of a layer 2 bridge technology such as VXLAN, VPLS, or EPL. Each L2VPN can be identified by name as well as by an optional unique identifier (VNI would be an example). Once created, L2VPNs can be terminated to [interfaces](../dcim/interface.md) and [VLANs](../ipam/vlan.md).
## Fields
@@ -38,4 +38,4 @@ An optional numeric identifier. This can be used to track a pseudowire ID, for e
### Import & Export Targets
-The [route targets](./routetarget.md) associated with this L2VPN to control the import and export of forwarding information.
+The [route targets](../ipam/routetarget.md) associated with this L2VPN to control the import and export of forwarding information.
diff --git a/docs/models/vpn/l2vpntermination.md b/docs/models/vpn/l2vpntermination.md
new file mode 100644
index 000000000..e20677d21
--- /dev/null
+++ b/docs/models/vpn/l2vpntermination.md
@@ -0,0 +1,18 @@
+# L2VPN Termination
+
+A L2VPN termination is the attachment of an [L2VPN](./l2vpn.md) to an [interface](../dcim/interface.md) or [VLAN](../ipam/vlan.md). Note that the L2VPNs of the following types may have only two terminations assigned to them:
+
+* VPWS
+* EPL
+* EP-LAN
+* EP-TREE
+
+## Fields
+
+### L2VPN
+
+The [L2VPN](./l2vpn.md) instance.
+
+### VLAN or Interface
+
+The [VLAN](../ipam/vlan.md), [device interface](../dcim/interface.md), or [virtual machine interface](../virtualization/virtualmachine.md) attached to the L2VPN.
diff --git a/docs/models/vpn/tunnel.md b/docs/models/vpn/tunnel.md
new file mode 100644
index 000000000..31625f7d6
--- /dev/null
+++ b/docs/models/vpn/tunnel.md
@@ -0,0 +1,38 @@
+# Tunnels
+
+A tunnel represents a private virtual connection established among two or more endpoints across a shared infrastructure by employing protocol encapsulation. Common encapsulation techniques include [Generic Routing Encapsulation (GRE)](https://en.wikipedia.org/wiki/Generic_Routing_Encapsulation), [IP-in-IP](https://en.wikipedia.org/wiki/IP_in_IP), and [IPSec](https://en.wikipedia.org/wiki/IPsec). NetBox supports modeling both peer-to-peer and hub-and-spoke tunnel topologies.
+
+Device and virtual machine interfaces are associated to tunnels by creating [tunnel terminations](./tunneltermination.md).
+
+## Fields
+
+### Name
+
+A unique name assigned to the tunnel for identification.
+
+### Status
+
+The operational status of the tunnel. By default, the following statuses are available:
+
+* Planned
+* Active
+* Disabled
+
+!!! tip "Custom tunnel statuses"
+ Additional tunnel statuses may be defined by setting `Tunnel.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
+
+### Group
+
+The [administrative group](./tunnelgroup.md) to which this tunnel is assigned (optional).
+
+### Encapsulation
+
+The encapsulation protocol or technique employed to effect the tunnel. NetBox supports GRE, IP-in-IP, and IPSec encapsulations.
+
+### Tunnel ID
+
+An optional numeric identifier for the tunnel.
+
+### IPSec Profile
+
+For IPSec tunnels, this is the [IPSec Profile](./ipsecprofile.md) employed to negotiate security associations.
diff --git a/docs/models/vpn/tunnelgroup.md b/docs/models/vpn/tunnelgroup.md
new file mode 100644
index 000000000..7e3a5c3cc
--- /dev/null
+++ b/docs/models/vpn/tunnelgroup.md
@@ -0,0 +1,13 @@
+# Tunnel Group
+
+[Tunnels](./tunnel.md) can be arranged into administrative groups for organization. For example, you might crete a group to manage all peer-to-peer tunnels inside a mesh network. The assignment of a tunnel 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/vpn/tunneltermination.md b/docs/models/vpn/tunneltermination.md
new file mode 100644
index 000000000..8400eaa86
--- /dev/null
+++ b/docs/models/vpn/tunneltermination.md
@@ -0,0 +1,30 @@
+# Tunnel Terminations
+
+A tunnel termination connects a device or virtual machine interface to a [tunnel](./tunnel.md). The tunnel must be created before any terminations may be added.
+
+## Fields
+
+### Tunnel
+
+The [tunnel](./tunnel.md) to which this termination is made.
+
+### Role
+
+The functional role of the attached interface. The following options are available:
+
+| Name | Description |
+|-------|--------------------------------------------------|
+| Peer | An endpoint in a point-to-point or mesh topology |
+| Hub | A central point in a hub-and-spoke topology |
+| Spoke | An edge point in a hub-and-spoke topology |
+
+!!! note
+ Multiple hub terminations may be attached to a tunnel.
+
+### Termination
+
+The device or virtual machine interface terminated to the tunnel.
+
+### Outside IP
+
+The public or underlay IP address with which this termination is associated. This is the IP to which peers will route tunneled traffic.
diff --git a/docs/plugins/development/data-backends.md b/docs/plugins/development/data-backends.md
new file mode 100644
index 000000000..feffa5bed
--- /dev/null
+++ b/docs/plugins/development/data-backends.md
@@ -0,0 +1,23 @@
+# Data Backends
+
+[Data sources](../../models/core/datasource.md) can be defined to reference data which exists on systems of record outside NetBox, such as a git repository or Amazon S3 bucket. Plugins can register their own backend classes to introduce support for additional resource types. This is done by subclassing NetBox's `DataBackend` class.
+
+```python title="data_backends.py"
+from netbox.data_backends import DataBackend
+
+class MyDataBackend(DataBackend):
+ name = 'mybackend'
+ label = 'My Backend'
+ ...
+```
+
+To register one or more data backends with NetBox, define a list named `backends` at the end of this file:
+
+```python title="data_backends.py"
+backends = [MyDataBackend]
+```
+
+!!! tip
+ The path to the list of search indexes can be modified by setting `data_backends` in the PluginConfig instance.
+
+::: core.data_backends.DataBackend
diff --git a/docs/plugins/development/index.md b/docs/plugins/development/index.md
index dcbad9d8d..4db1d5ef6 100644
--- a/docs/plugins/development/index.md
+++ b/docs/plugins/development/index.md
@@ -69,7 +69,7 @@ The plugin source directory contains all the actual Python code and other resour
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
```python
-from extras.plugins import PluginConfig
+from netbox.plugins import PluginConfig
class FooBarConfig(PluginConfig):
name = 'foo_bar'
@@ -109,6 +109,7 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
| `queues` | A list of custom background task queues to create |
| `search_extensions` | The dotted path to the list of search index classes (default: `search.indexes`) |
+| `data_backends` | The dotted path to the list of data source backend classes (default: `data_backends.backends`) |
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
@@ -120,7 +121,7 @@ All required settings must be configured by the user. If a configuration paramet
Plugin configuration parameters can be accessed using the `get_plugin_config()` function. For example:
```python
- from extras.plugins import get_plugin_config
+ from netbox.plugins import get_plugin_config
get_plugin_config('my_plugin', 'verbose_name')
```
diff --git a/docs/plugins/development/models.md b/docs/plugins/development/models.md
index 8394813f8..902ee9c82 100644
--- a/docs/plugins/development/models.md
+++ b/docs/plugins/development/models.md
@@ -60,6 +60,10 @@ class MyModel(NetBoxModel):
This attribute specifies the URL at which the documentation for this model can be reached. By default, it will return `/static/docs/models///`. Plugin models can override this to return a custom URL. For example, you might direct the user to your plugin's documentation hosted on [ReadTheDocs](https://readthedocs.org/).
+#### `_netbox_private`
+
+By default, any model introduced by a plugin will appear in the list of available object types e.g. when creating a custom field or certain dashboard widgets. If your model is intended only for "behind the scenes use" and should not be exposed to end users, set `_netbox_private` to True. This will omit it from the list of general-purpose object types.
+
### Enabling Features Individually
If you prefer instead to enable only a subset of these features for a plugin model, NetBox provides a discrete "mix-in" class for each feature. You can subclass each of these individually when defining your model. (Your model will also need to inherit from Django's built-in `Model` class.)
@@ -119,14 +123,17 @@ For more information about database migrations, see the [Django documentation](h
::: netbox.models.features.CustomValidationMixin
+::: netbox.models.features.EventRulesMixin
+
+!!! note
+ `EventRulesMixin` was renamed from `WebhooksMixin` in NetBox v3.7.
+
::: netbox.models.features.ExportTemplatesMixin
::: netbox.models.features.JournalingMixin
::: netbox.models.features.TagsMixin
-::: netbox.models.features.WebhooksMixin
-
## Choice Sets
For model fields which support the selection of one or more values from a predefined list of choices, NetBox provides the `ChoiceSet` utility class. This can be used in place of a regular choices tuple to provide enhanced functionality, namely dynamic configuration and colorization. (See [Django's documentation](https://docs.djangoproject.com/en/stable/ref/models/fields/#choices) on the `choices` parameter for supported model fields.)
diff --git a/docs/plugins/development/navigation.md b/docs/plugins/development/navigation.md
index 3e7762184..dc895b2ab 100644
--- a/docs/plugins/development/navigation.md
+++ b/docs/plugins/development/navigation.md
@@ -5,7 +5,7 @@
A plugin can register its own submenu as part of NetBox's navigation menu. This is done by defining a variable named `menu` in `navigation.py`, pointing to an instance of the `PluginMenu` class. Each menu must define a label and grouped menu items (discussed below), and may optionally specify an icon. An example is shown below.
```python title="navigation.py"
-from extras.plugins import PluginMenu
+from netbox.plugins import PluginMenu
menu = PluginMenu(
label='My Plugin',
@@ -49,7 +49,7 @@ menu_items = (item1, item2, item3)
Each menu item represents a link and (optionally) a set of buttons comprising one entry in NetBox's navigation menu. Menu items are defined as PluginMenuItem instances. An example is shown below.
```python title="navigation.py"
-from extras.plugins import PluginMenuButton, PluginMenuItem
+from netbox.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
item1 = PluginMenuItem(
@@ -64,12 +64,15 @@ item1 = PluginMenuItem(
A `PluginMenuItem` has the following attributes:
-| Attribute | Required | Description |
-|---------------|----------|------------------------------------------------------|
-| `link` | Yes | Name of the URL path to which this menu item links |
-| `link_text` | Yes | The text presented to the user |
-| `permissions` | - | A list of permissions required to display this link |
-| `buttons` | - | An iterable of PluginMenuButton instances to include |
+| Attribute | Required | Description |
+|---------------|----------|----------------------------------------------------------------------------------------------------------|
+| `link` | Yes | Name of the URL path to which this menu item links |
+| `link_text` | Yes | The text presented to the user |
+| `permissions` | - | A list of permissions required to display this link |
+| `staff_only` | - | Display only for users who have `is_staff` set to true (any specified permissions will also be required) |
+| `buttons` | - | An iterable of PluginMenuButton instances to include |
+
+!!! info "The `staff_only` attribute was introduced in NetBox v3.6.1."
## Menu Buttons
diff --git a/docs/plugins/development/search.md b/docs/plugins/development/search.md
index e3b861f00..e54844cf0 100644
--- a/docs/plugins/development/search.md
+++ b/docs/plugins/development/search.md
@@ -14,8 +14,11 @@ class MyModelIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('site', 'device', 'status', 'description')
```
+Fields listed in `display_attrs` will not be cached for search, but will be displayed alongside the object when it appears in global search results. This is helpful for conveying to the user additional information about an object.
+
To register one or more indexes with NetBox, define a list named `indexes` at the end of this file:
```python
diff --git a/docs/plugins/development/tables.md b/docs/plugins/development/tables.md
index f846139f0..9d57a9603 100644
--- a/docs/plugins/development/tables.md
+++ b/docs/plugins/development/tables.md
@@ -87,3 +87,28 @@ The table column classes listed below are supported for use in plugins. These cl
options:
members:
- __init__
+
+## Extending Core Tables
+
+!!! info "This feature was introduced in NetBox v3.7."
+
+Plugins can register their own custom columns on core tables using the `register_table_column()` utility function. This allows a plugin to attach additional information, such as relationships to its own models, to built-in object lists.
+
+```python
+import django_tables2
+from django.utils.translation import gettext_lazy as _
+
+from dcim.tables import SiteTable
+from utilities.tables import register_table_column
+
+mycol = django_tables2.Column(
+ verbose_name=_('My Column'),
+ accessor=django_tables2.A('description')
+)
+
+register_table_column(mycol, 'foo', SiteTable)
+```
+
+You'll typically want to define an accessor identifying the desired model field or relationship when defining a custom column. See the [django-tables2 documentation](https://django-tables2.readthedocs.io/) for more information on creating custom columns.
+
+::: utilities.tables.register_table_column
diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md
index 3d0e87a68..1730b0ebd 100644
--- a/docs/plugins/development/views.md
+++ b/docs/plugins/development/views.md
@@ -206,7 +206,7 @@ For example, accessing `{{ request.user }}` within a template will return the cu
Declared subclasses should be gathered into a list or tuple for integration with NetBox. By default, NetBox looks for an iterable named `template_extensions` within a `template_content.py` file. (This can be overridden by setting `template_extensions` to a custom value on the plugin's PluginConfig.) An example is below.
```python
-from extras.plugins import PluginTemplateExtension
+from netbox.plugins import PluginTemplateExtension
from .models import Animal
class SiteAnimalCount(PluginTemplateExtension):
diff --git a/docs/reference/conditions.md b/docs/reference/conditions.md
index 514006b01..fc571c05e 100644
--- a/docs/reference/conditions.md
+++ b/docs/reference/conditions.md
@@ -116,7 +116,7 @@ Multiple conditions can be combined into nested sets using AND or OR logic. This
]
},
{
- "attr": "tags",
+ "attr": "tags.slug",
"value": "exempt",
"op": "contains"
}
diff --git a/docs/reference/filtering.md b/docs/reference/filtering.md
index 7ddda6f3c..5a672ed11 100644
--- a/docs/reference/filtering.md
+++ b/docs/reference/filtering.md
@@ -61,13 +61,14 @@ These lookup expressions can be applied by adding a suffix to the desired field'
Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
-| Filter | Description |
-|--------|-------------|
-| `n` | Not equal to |
-| `lt` | Less than |
-| `lte` | Less than or equal to |
-| `gt` | Greater than |
-| `gte` | Greater than or equal to |
+| Filter | Description |
+|---------|--------------------------|
+| `n` | Not equal to |
+| `lt` | Less than |
+| `lte` | Less than or equal to |
+| `gt` | Greater than |
+| `gte` | Greater than or equal to |
+| `empty` | Is empty/null (boolean) |
Here is an example of a numeric field lookup expression that will return all VLANs with a VLAN ID greater than 900:
@@ -79,18 +80,18 @@ GET /api/ipam/vlans/?vid__gt=900
String based (char) fields (Name, Address, etc) support these lookup expressions:
-| Filter | Description |
-|--------|-------------|
-| `n` | Not equal to |
-| `ic` | Contains (case-insensitive) |
-| `nic` | Does not contain (case-insensitive) |
-| `isw` | Starts with (case-insensitive) |
-| `nisw` | Does not start with (case-insensitive) |
-| `iew` | Ends with (case-insensitive) |
-| `niew` | Does not end with (case-insensitive) |
-| `ie` | Exact match (case-insensitive) |
-| `nie` | Inverse exact match (case-insensitive) |
-| `empty` | Is empty (boolean) |
+| Filter | Description |
+|---------|----------------------------------------|
+| `n` | Not equal to |
+| `ic` | Contains (case-insensitive) |
+| `nic` | Does not contain (case-insensitive) |
+| `isw` | Starts with (case-insensitive) |
+| `nisw` | Does not start with (case-insensitive) |
+| `iew` | Ends with (case-insensitive) |
+| `niew` | Does not end with (case-insensitive) |
+| `ie` | Exact match (case-insensitive) |
+| `nie` | Inverse exact match (case-insensitive) |
+| `empty` | Is empty/null (boolean) |
Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
diff --git a/docs/reference/markdown.md b/docs/reference/markdown.md
index 7f280686d..0759fa2ec 100644
--- a/docs/reference/markdown.md
+++ b/docs/reference/markdown.md
@@ -171,23 +171,23 @@ Some text to show that the reference links can follow later.
Here's the NetBox logo (hover to see the title text):
Inline-style:
-
+
Reference-style:
![alt text][logo]
-[logo]: /static/netbox_logo.png "Logo Title Text 2"
+[logo]: /media/misc/netbox_logo.png "Logo Title Text 2"
```
Here's the NetBox logo (hover to see the title text):
Inline-style:
-
+
Reference-style:
![alt text][logo]
-[logo]: /static/netbox_logo.png "Logo Title Text 2"
+[logo]: ../media/misc/netbox_logo.png "Logo Title Text 2"
diff --git a/docs/release-notes/index.md b/docs/release-notes/index.md
index 4d812938f..f01d3160f 100644
--- a/docs/release-notes/index.md
+++ b/docs/release-notes/index.md
@@ -10,6 +10,26 @@ Minor releases are published in April, August, and December of each calendar yea
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
+#### [Version 3.7](./version-3.7.md) (December 2023)
+
+* VPN Tunnels ([#9816](https://github.com/netbox-community/netbox/issues/9816))
+* Event Rules ([#14132](https://github.com/netbox-community/netbox/issues/14132))
+* Virtual Machine Disks ([#8356](https://github.com/netbox-community/netbox/issues/8356))
+* Object Protection Rules ([#10244](https://github.com/netbox-community/netbox/issues/10244))
+* Improved Custom Field Visibility Controls ([#13299](https://github.com/netbox-community/netbox/issues/13299))
+* Improved Global Search Results ([#14134](https://github.com/netbox-community/netbox/issues/14134))
+* Table Column Registration for Plugins ([#14173](https://github.com/netbox-community/netbox/issues/14173))
+* Data Backend Registration for Plugins ([#13381](https://github.com/netbox-community/netbox/issues/13381))
+
+#### [Version 3.6](./version-3.6.md) (August 2023)
+
+* Relocated Admin UI Views ([#12589](https://github.com/netbox-community/netbox/issues/12589), [#12590](https://github.com/netbox-community/netbox/issues/12590), [#12591](https://github.com/netbox-community/netbox/issues/12591), [#13044](https://github.com/netbox-community/netbox/issues/13044))
+* Configurable Default Permissions ([#13038](https://github.com/netbox-community/netbox/issues/13038))
+* User Bookmarks ([#8248](https://github.com/netbox-community/netbox/issues/8248))
+* Custom Field Choice Sets ([#12988](https://github.com/netbox-community/netbox/issues/12988))
+* Pre-Defined Location Choices for Custom Fields ([#12194](https://github.com/netbox-community/netbox/issues/12194))
+* Restrict Tag Usage by Object Type ([#11541](https://github.com/netbox-community/netbox/issues/11541))
+
#### [Version 3.5](./version-3.5.md) (April 2023)
* Customizable Dashboard ([#9416](https://github.com/netbox-community/netbox/issues/9416))
diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md
index f7778275b..959247b30 100644
--- a/docs/release-notes/version-3.5.md
+++ b/docs/release-notes/version-3.5.md
@@ -1,6 +1,32 @@
# NetBox v3.5
-## v3.5.9 (FUTURE)
+## v3.5.9 (2023-08-28)
+
+### Enhancements
+
+* [#12489](https://github.com/netbox-community/netbox/issues/12489) - Dynamically render location and device lists under site and location views
+* [#12825](https://github.com/netbox-community/netbox/issues/12825) - Display assigned values count per obejct type under custom field view
+* [#13313](https://github.com/netbox-community/netbox/issues/13313) - Enable filtering IP ranges by containing prefix
+* [#13415](https://github.com/netbox-community/netbox/issues/13415) - Include request object in custom link renderer on tables
+* [#13536](https://github.com/netbox-community/netbox/issues/13536) - Move child VLANs list to a separate tab under VLAN group view
+* [#13542](https://github.com/netbox-community/netbox/issues/13542) - Pass additional HTTP headers through to custom script context
+* [#13585](https://github.com/netbox-community/netbox/issues/13585) - Introduce `empty` lookup for numeric value filters
+
+### Bug Fixes
+
+* [#11272](https://github.com/netbox-community/netbox/issues/11272) - Fix localization support for device position field
+* [#13358](https://github.com/netbox-community/netbox/issues/13358) - Git backend should send HTTP auth headers only if credentials have been defined
+* [#13477](https://github.com/netbox-community/netbox/issues/13477) - Fix filtering of modified objects after bulk import/update
+* [#13478](https://github.com/netbox-community/netbox/issues/13478) - Fix filtering of export templates by content type under web UI
+* [#13500](https://github.com/netbox-community/netbox/issues/13500) - Fix form validation for bulk update of L2VPN terminations via bulk import form
+* [#13503](https://github.com/netbox-community/netbox/issues/13503) - Fix utilization graph proportions when localization is enabled
+* [#13507](https://github.com/netbox-community/netbox/issues/13507) - Avoid raising exception for invalid content type during global search
+* [#13516](https://github.com/netbox-community/netbox/issues/13516) - Plugin utility functions should be importable from `extras.plugins`
+* [#13530](https://github.com/netbox-community/netbox/issues/13530) - Ensure script log messages can be serialized as JSON data
+* [#13543](https://github.com/netbox-community/netbox/issues/13543) - Config context tab under device/VM view should not require `extras.view_configcontext` permission
+* [#13544](https://github.com/netbox-community/netbox/issues/13544) - Ensure `reindex` command clears all cached values when not in lazy mode
+* [#13556](https://github.com/netbox-community/netbox/issues/13556) - Correct REST API representation of VDC status choice
+* [#13569](https://github.com/netbox-community/netbox/issues/13569) - Fix selection widgets for related interfaces when bulk editing interfaces under device view
---
diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md
index 062e96556..75a51c9cf 100644
--- a/docs/release-notes/version-3.6.md
+++ b/docs/release-notes/version-3.6.md
@@ -1,34 +1,253 @@
# NetBox v3.6
-## v3.6-beta2 (2023-08-16)
+## v3.6.9 (2023-12-28)
+
+### Enhancements
+
+* [#14631](https://github.com/netbox-community/netbox/issues/14631) - All models can be filtered and searched by their description field (where applicable)
### Bug Fixes
-* [#13351](https://github.com/netbox-community/netbox/issues/13351) - Fix missing text due to incorrectly applied translation tags
-* [#13361](https://github.com/netbox-community/netbox/issues/13361) - Extra choices field on custom field choice set form should not be required
-* [#13363](https://github.com/netbox-community/netbox/issues/13363) - Fix API endpoint for custom field choice selector in forms
-* [#13376](https://github.com/netbox-community/netbox/issues/13376) - Restrict add/remove tag fields by model on bulk edit forms
-* [#13410](https://github.com/netbox-community/netbox/issues/13410) - Fix rendering of custom choice fields with large number of choices
-* [#13433](https://github.com/netbox-community/netbox/issues/13433) - User field on API token form should be required
-* [#13434](https://github.com/netbox-community/netbox/issues/13434) - Randomly generate initial keys prior to the creation of new tokens
-* [#13437](https://github.com/netbox-community/netbox/issues/13437) - Display bookmark button only for relevant objects
+* [#14482](https://github.com/netbox-community/netbox/issues/14482) - Fix validation error when attempting to move a primary IP address to a new parent object
+* [#14620](https://github.com/netbox-community/netbox/issues/14620) - Permit setting device type U height to 0 during bulk edit
+* [#14621](https://github.com/netbox-community/netbox/issues/14621) - Fix error when using the device search filter
---
-## v3.6-beta1 (2023-08-02)
+## v3.6.8 (2023-12-27)
+
+### Enhancements
+
+* [#11039](https://github.com/netbox-community/netbox/issues/11039) - List parent prefixes under IP range view
+* [#14507](https://github.com/netbox-community/netbox/issues/14507) - Print new NetBox version when running upgrade script
+* [#14538](https://github.com/netbox-community/netbox/issues/14538) - Add the `available_at_site` filter for VLANs
+* [#14596](https://github.com/netbox-community/netbox/issues/14596) - Match against description field when searching for devices
+
+### Bug Fixes
+
+* [#11816](https://github.com/netbox-community/netbox/issues/11816) - Correct display of error message when attempting invalid VLAN site & group assignment
+* [#12731](https://github.com/netbox-community/netbox/issues/12731) - Fix custom validation for many-to-many fields
+* [#13606](https://github.com/netbox-community/netbox/issues/13606) - Fix filtering custom multi-choice fields by null
+* [#13649](https://github.com/netbox-community/netbox/issues/13649) - Correct calculation of absolute lengths for zero-length cables
+* [#13812](https://github.com/netbox-community/netbox/issues/13812) - Update status of remote data source when syncing fails via `syncdatasource` management command
+* [#13909](https://github.com/netbox-community/netbox/issues/13909) - Fix cloning of objects which have a multi-choice custom field
+* [#14517](https://github.com/netbox-community/netbox/issues/14517) - Ensure reservations tab is always displayed under rack view
+* [#14532](https://github.com/netbox-community/netbox/issues/14532) - Device/VM change record should accurately reflect when primary/OOB IP is deleted
+* [#14549](https://github.com/netbox-community/netbox/issues/14549) - Fix association of job results when executing scripts via `runscript` management command
+* [#14560](https://github.com/netbox-community/netbox/issues/14560) - Do not escape exclamation marks in custom link URLs
+* [#14575](https://github.com/netbox-community/netbox/issues/14575) - Fix display of the tags column under VDC table
+* [#14613](https://github.com/netbox-community/netbox/issues/14613) - Fix display of current configuration parameters in UI
+
+---
+
+## v3.6.7 (2023-12-15)
+
+### Enhancements
+
+* [#12751](https://github.com/netbox-community/netbox/issues/12751) - Designate fields to expand by default for object selector widget
+* [#14148](https://github.com/netbox-community/netbox/issues/14148) - Add tags column to L2VPN terminations column
+* [#14390](https://github.com/netbox-community/netbox/issues/14390) - Add `classes` parameter to `copy_content` template tag
+* [#14467](https://github.com/netbox-community/netbox/issues/14467) - Change custom field choice delimiter from comma to colon
+
+### Bug Fixes
+
+* [#13983](https://github.com/netbox-community/netbox/issues/13983) - Fix bulk import support for custom field choices
+* [#14081](https://github.com/netbox-community/netbox/issues/14081) - Ensure accuracy of parent object counters when deleting related objects
+* [#14249](https://github.com/netbox-community/netbox/issues/14249) - Fix server error when authenticating via IP-restricted API tokens using IPv6
+* [#14392](https://github.com/netbox-community/netbox/issues/14392) - Fix bulk operations for plugin models under admin UI
+* [#14397](https://github.com/netbox-community/netbox/issues/14397) - Fix exception on non-JSON request to `/available-ips/` API endpoints
+* [#14401](https://github.com/netbox-community/netbox/issues/14401) - Rack `starting_unit` cannot be zero
+* [#14432](https://github.com/netbox-community/netbox/issues/14432) - Populate custom field default values for components when creating a device
+* [#14448](https://github.com/netbox-community/netbox/issues/14448) - Fix exception when creating a power feed with rack and panel in different sites
+* [#14505](https://github.com/netbox-community/netbox/issues/14505) - Fix the assignment of tags to L2VPN terminations
+* [#14512](https://github.com/netbox-community/netbox/issues/14512) - Remove unneeded annotations from queries when using REST API brief mode
+* [#14515](https://github.com/netbox-community/netbox/issues/14515) - Ensure user config is created automatically for all user accounts
+* [#14522](https://github.com/netbox-community/netbox/issues/14522) - Fix filtering contact assignments by group
+* [#14533](https://github.com/netbox-community/netbox/issues/14533) - Fix quick search under VLAN group VLANs list
+
+---
+
+## v3.6.6 (2023-11-29)
+
+### Enhancements
+
+* [#13735](https://github.com/netbox-community/netbox/issues/13735) - Show complete region hierarchy in UI for all relevant objects
+
+### Bug Fixes
+
+* [#14056](https://github.com/netbox-community/netbox/issues/14056) - Record a pre-change snapshot when bulk editing objects via CSV
+* [#14187](https://github.com/netbox-community/netbox/issues/14187) - Raise a validation error when attempting to create a duplicate script or report
+* [#14199](https://github.com/netbox-community/netbox/issues/14199) - Fix jobs list for reports with a custom name
+* [#14239](https://github.com/netbox-community/netbox/issues/14239) - Fix CustomFieldChoiceSet search filter
+* [#14242](https://github.com/netbox-community/netbox/issues/14242) - Enable export templates for contact assignments
+* [#14299](https://github.com/netbox-community/netbox/issues/14299) - Webhook timestamps should be in proper ISO 8601 format
+* [#14325](https://github.com/netbox-community/netbox/issues/14325) - Fix numeric ordering of service ports
+* [#14339](https://github.com/netbox-community/netbox/issues/14339) - Correctly hash local user password when set via REST API
+* [#14343](https://github.com/netbox-community/netbox/issues/14343) - Fix ordering ASN table by ASDOT column
+* [#14346](https://github.com/netbox-community/netbox/issues/14346) - Fix running reports via REST API
+* [#14349](https://github.com/netbox-community/netbox/issues/14349) - Fix custom validation support for remote data sources
+* [#14363](https://github.com/netbox-community/netbox/issues/14363) - Fix bulk editing of interfaces assigned to VM with no cluster
+
+---
+
+## v3.6.5 (2023-11-09)
+
+### Enhancements
+
+* [#12741](https://github.com/netbox-community/netbox/issues/12741) - Add selector widget to platform field on device & virtual machine forms
+* [#13022](https://github.com/netbox-community/netbox/issues/13022) - Introduce support for assigning IP addresses when bulk importing services
+* [#13587](https://github.com/netbox-community/netbox/issues/13587) - Annotate units of measurement on power port table columns
+* [#13669](https://github.com/netbox-community/netbox/issues/13669) - Add bulk import button to contact assignments list view
+* [#13723](https://github.com/netbox-community/netbox/issues/13723) - Add inventory items column to interfaces table
+* [#13743](https://github.com/netbox-community/netbox/issues/13743) - Add site column to power feeds table
+* [#13936](https://github.com/netbox-community/netbox/issues/13936) - Add primary IPv4 and IPv6 filters for virtual machines and VDCs
+* [#13951](https://github.com/netbox-community/netbox/issues/13951) - Add device & virtual machine fields to service filter form
+* [#14085](https://github.com/netbox-community/netbox/issues/14085) - Strip trailing port number from value returned by `get_client_ip()`
+* [#14101](https://github.com/netbox-community/netbox/issues/14101) - Add greater/less than mask length filters for IP addresses
+* [#14112](https://github.com/netbox-community/netbox/issues/14112) - Add tab listing child items under inventory item view
+* [#14113](https://github.com/netbox-community/netbox/issues/14113) - Add optional parent column to inventory items table
+* [#14220](https://github.com/netbox-community/netbox/issues/14220) - Order available columns alphabetically in table configuration form
+* [#14221](https://github.com/netbox-community/netbox/issues/14221) - Add contact group column on contact assignments table
+
+### Bug Fixes
+
+* [#14033](https://github.com/netbox-community/netbox/issues/14033) - Avoid exception when attempting to connect both ends of a cable to the same object
+* [#14117](https://github.com/netbox-community/netbox/issues/14117) - Check that enough rear port positions have been selected to accommodate the number of front ports being created
+* [#14166](https://github.com/netbox-community/netbox/issues/14166) - Permit user login when maintenance mode is enabled
+* [#14182](https://github.com/netbox-community/netbox/issues/14182) - Ensure the active configuration is restored upon clearing cache
+* [#14195](https://github.com/netbox-community/netbox/issues/14195) - Correct permissions evaluation for ASN range child ASNs view
+* [#14223](https://github.com/netbox-community/netbox/issues/14223) - Disable ordering of jobs by assigned object
+
+---
+
+## v3.6.4 (2023-10-17)
+
+### Enhancements
+
+* [#12831](https://github.com/netbox-community/netbox/issues/12831) - Include circuit description in cable trace SVG image
+* [#12872](https://github.com/netbox-community/netbox/issues/12872) - Introduce the `DATA_UPLOAD_MAX_MEMORY_SIZE` configuration parameter
+* [#13950](https://github.com/netbox-community/netbox/issues/13950) - Display custom choice field labels rather than values in UI
+* [#13957](https://github.com/netbox-community/netbox/issues/13957) - Add DNS name filter on IP addresses list
+* [#13962](https://github.com/netbox-community/netbox/issues/13962) - Add a copy-to-clipboard button for API tokens
+* [#13972](https://github.com/netbox-community/netbox/issues/13972) - Introduce a filter to find unterminated cables
+
+### Bug Fixes
+
+* [#11987](https://github.com/netbox-community/netbox/issues/11987) - Fix validation of bulk cable updates via bulk import form
+* [#12328](https://github.com/netbox-community/netbox/issues/12328) - Ensure generic foreign key relationships are populated in REST API serializations of objects
+* [#12336](https://github.com/netbox-community/netbox/issues/12336) - Employ PostgreSQL advisory locks to avoid duplicate MPTT tree IDs when bulk creating objects
+* [#13064](https://github.com/netbox-community/netbox/issues/13064) - Fix resetting of checkbox fields triggered by HTMX form re-rendering
+* [#13440](https://github.com/netbox-community/netbox/issues/13440) - Fix support for assigning a tenant when creating "next available" VLANs via the REST API
+* [#13746](https://github.com/netbox-community/netbox/issues/13746) - Fix support for setting custom field values when creating "next available" IP addresses via the REST API
+* [#13872](https://github.com/netbox-community/netbox/issues/13872) - Add CSV delimiter field to file upload tab under bulk object upload views
+* [#13876](https://github.com/netbox-community/netbox/issues/13876) - Fix support for assigning an interface when creating "next available" IP addresses via the REST API
+* [#13910](https://github.com/netbox-community/netbox/issues/13910) - Correct "add device" button link under platform view
+* [#13944](https://github.com/netbox-community/netbox/issues/13944) - Correct serialization of several report attributes in the REST API
+* [#13966](https://github.com/netbox-community/netbox/issues/13966) - Restore "last login" column on users table
+* [#14013](https://github.com/netbox-community/netbox/issues/14013) - Fix device role filter choices under inventory items list filters
+* [#14023](https://github.com/netbox-community/netbox/issues/14023) - Fix exception when bulk disconnecting interfaces connected to the same cable
+* [#14025](https://github.com/netbox-community/netbox/issues/14025) - Fix exception when viewing a script that begins with the same name as another
+* [#14026](https://github.com/netbox-community/netbox/issues/14026) - Optimize the automatic creation of available IP addresses for large prefixes
+* [#14042](https://github.com/netbox-community/netbox/issues/14042) - Fix duplicated child object count decrements when removing objects in bulk
+
+---
+
+## v3.6.3 (2023-09-26)
+
+### Enhancements
+
+* [#12732](https://github.com/netbox-community/netbox/issues/12732) - Add toggle to hide disconnected interfaces under device view
+
+### Bug Fixes
+
+* [#11079](https://github.com/netbox-community/netbox/issues/11079) - Enable tracing cable paths across multiple cables in parallel
+* [#11901](https://github.com/netbox-community/netbox/issues/11901) - Fix `IndexError` exception when manipulating terminations for existing cables via REST API
+* [#13506](https://github.com/netbox-community/netbox/issues/13506) - Enable creating a config template which references a data file via the REST API
+* [#13666](https://github.com/netbox-community/netbox/issues/13666) - Cleanly handle reports without any test methods defined
+* [#13839](https://github.com/netbox-community/netbox/issues/13839) - Restore original text color for HTML code elements
+* [#13843](https://github.com/netbox-community/netbox/issues/13843) - Fix assignment of VLAN group scope during bulk edit
+* [#13845](https://github.com/netbox-community/netbox/issues/13845) - Fix `AttributeError` exception when attaching front/rear images to a device type
+* [#13849](https://github.com/netbox-community/netbox/issues/13849) - Fix `KeyError` exception when deleting an object which references a configured choice value that has been removed
+* [#13859](https://github.com/netbox-community/netbox/issues/13859) - Fix invalid response when searching for custom choice field values returns no matches
+* [#13864](https://github.com/netbox-community/netbox/issues/13864) - Correct default background color for dashboard widget headers
+* [#13871](https://github.com/netbox-community/netbox/issues/13871) - Fix rack filtering for empty location during device bulk import
+* [#13891](https://github.com/netbox-community/netbox/issues/13891) - Allow designating an IP address as primary for device/VM while assigning it to an interface
+
+---
+
+## v3.6.2 (2023-09-20)
+
+### Enhancements
+
+* [#13245](https://github.com/netbox-community/netbox/issues/13245) - Add interface types for QSFP112 and OSFP-RHS
+* [#13563](https://github.com/netbox-community/netbox/issues/13563) - Add support for other delimiting characters when using CSV import
+
+### Bug Fixes
+
+* [#11209](https://github.com/netbox-community/netbox/issues/11209) - Hide available IP/VLAN listing when sorting under a parent prefix or VLAN range
+* [#11617](https://github.com/netbox-community/netbox/issues/11617) - Raise validation error on the presence of an unknown CSV header during bulk import
+* [#12219](https://github.com/netbox-community/netbox/issues/12219) - Fix dashboard widget heading contrast under dark mode
+* [#12685](https://github.com/netbox-community/netbox/issues/12685) - Render Markdown in custom field help text on object edit forms
+* [#13653](https://github.com/netbox-community/netbox/issues/13653) - Tweak color of error text to improve legibility
+* [#13701](https://github.com/netbox-community/netbox/issues/13701) - Correct display of power feed legs under device view
+* [#13706](https://github.com/netbox-community/netbox/issues/13706) - Restore extra filters dropdown on device interfaces list
+* [#13721](https://github.com/netbox-community/netbox/issues/13721) - Filter VLAN choices by selected site (if any) when creating a prefix
+* [#13727](https://github.com/netbox-community/netbox/issues/13727) - Fix exception when viewing rendered config for VM without a role assigned
+* [#13745](https://github.com/netbox-community/netbox/issues/13745) - Optimize counter field migrations for large databases
+* [#13756](https://github.com/netbox-community/netbox/issues/13756) - Fix exception when sorting module bay list by installed module status
+* [#13757](https://github.com/netbox-community/netbox/issues/13757) - Fix RecursionError exception when assigning config context to a device type
+* [#13767](https://github.com/netbox-community/netbox/issues/13767) - Fix support for comments when creating a new service via web UI
+* [#13782](https://github.com/netbox-community/netbox/issues/13782) - Fix tag exclusion support for contact assignments
+* [#13791](https://github.com/netbox-community/netbox/issues/13791) - Preserve whitespace in values when performing bulk rename of objects via web UI
+* [#13809](https://github.com/netbox-community/netbox/issues/13809) - Avoid TypeError exception when editing active configuration with statically defined `CUSTOM_VALIDATORS`
+* [#13813](https://github.com/netbox-community/netbox/issues/13813) - Fix member count for newly created virtual chassis
+* [#13818](https://github.com/netbox-community/netbox/issues/13818) - Restore missing tags field on L2VPN termination edit form
+
+---
+
+## v3.6.1 (2023-09-06)
+
+### Enhancements
+
+* [#12870](https://github.com/netbox-community/netbox/issues/12870) - Support setting token expiration time using the provisioning API endpoint
+* [#13444](https://github.com/netbox-community/netbox/issues/13444) - Add bulk rename functionality to the global device component lists
+* [#13638](https://github.com/netbox-community/netbox/issues/13638) - Add optional `staff_only` attribute to MenuItem
+
+### Bug Fixes
+
+* [#12553](https://github.com/netbox-community/netbox/issues/12552) - Ensure `family` attribute is always returned when creating aggregates and prefixes via REST API
+* [#13619](https://github.com/netbox-community/netbox/issues/13619) - Fix exception when viewing IP address assigned to a virtual machine
+* [#13596](https://github.com/netbox-community/netbox/issues/13596) - Always display "render config" tab for devices and virtual machines
+* [#13620](https://github.com/netbox-community/netbox/issues/13620) - Show admin menu items only for staff users
+* [#13622](https://github.com/netbox-community/netbox/issues/13622) - Fix exception when viewing current config and no revisions have been created
+* [#13626](https://github.com/netbox-community/netbox/issues/13626) - Correct filtering of recent activity list under user view
+* [#13628](https://github.com/netbox-community/netbox/issues/13628) - Remove stale references to obsolete NAPALM integration
+* [#13630](https://github.com/netbox-community/netbox/issues/13630) - Fix display of active status under user view
+* [#13632](https://github.com/netbox-community/netbox/issues/13632) - Avoid raising exception when checking if FHRP group IP address is primary
+* [#13642](https://github.com/netbox-community/netbox/issues/13642) - Suppress warning about unreflected model changes when applying migrations
+* [#13657](https://github.com/netbox-community/netbox/issues/13657) - Fix decoding of data file content
+* [#13674](https://github.com/netbox-community/netbox/issues/13674) - Fix retrieving individual report via REST API
+* [#13682](https://github.com/netbox-community/netbox/issues/13682) - Fix error message returned when validation of custom field default value fails
+* [#13684](https://github.com/netbox-community/netbox/issues/13684) - Enable modifying the configuration when maintenance mode is enabled
+
+---
+
+## v3.6.0 (2023-08-30)
### Breaking Changes
* PostgreSQL 11 is no longer supported (dropped in Django 4.2). NetBox v3.6 requires PostgreSQL 12 or later.
+* The `boto3` and `dulwich` packages are no longer installed automatically. If needed for S3/git remote data backend support, add them to `local_requirements.txt` to ensure their installation.
* The `device_role` field on the Device model has been renamed to `role`. The `device_role` field has been temporarily retained on the REST API serializer for devices for backward compatibility, but is read-only.
* The `choices` array field has been removed from the CustomField model. Any defined choices are automatically migrated to CustomFieldChoiceSets, accessible via the new `choice_set` field on the CustomField model.
* The `napalm_driver` and `napalm_args` fields (which were deprecated in v3.5) have been removed from the Platform model.
+* The `device` and `device_id` filter for interfaces will no longer include interfaces from virtual chassis peers. Two new filters, `virtual_chassis_member` and `virtual_chassis_member_id`, have been introduced to match all interfaces belonging to the specified device's virtual chassis (if any).
* Reports and scripts are now returned within a `results` list when fetched via the REST API, consistent with other models.
* Superusers can no longer retrieve API token keys via the web UI if [`ALLOW_TOKEN_RETRIEVAL`](https://docs.netbox.dev/en/stable/configuration/security/#allow_token_retrieval) is disabled. (The admin view has been removed per [#13044](https://github.com/netbox-community/netbox/issues/13044).)
### New Features
-#### Relocated Admin Views ([#12589](https://github.com/netbox-community/netbox/issues/12589), [#12590](https://github.com/netbox-community/netbox/issues/12590), [#12591](https://github.com/netbox-community/netbox/issues/12591), [#13044](https://github.com/netbox-community/netbox/issues/13044))
+#### Relocated Admin UI Views ([#12589](https://github.com/netbox-community/netbox/issues/12589), [#12590](https://github.com/netbox-community/netbox/issues/12590), [#12591](https://github.com/netbox-community/netbox/issues/12591), [#13044](https://github.com/netbox-community/netbox/issues/13044))
Management views for the following object types, previously available only under the backend admin interface, have been relocated to the primary user interface:
@@ -72,6 +291,7 @@ Tags may now be restricted to use with designated object types. Tags that have n
* [#8137](https://github.com/netbox-community/netbox/issues/8137) - Add a field for designating the out-of-band (OOB) IP address for devices
* [#10197](https://github.com/netbox-community/netbox/issues/10197) - Cache the number of member devices on each virtual chassis
* [#11305](https://github.com/netbox-community/netbox/issues/11305) - Add GPS coordinate fields to the device model
+* [#11478](https://github.com/netbox-community/netbox/issues/11478) - Introduce `virtual_chassis_member` filter for interfaces & restore default behavior for `device` filter
* [#11519](https://github.com/netbox-community/netbox/issues/11519) - Add a SQL index for IP address host values to optimize queries
* [#11732](https://github.com/netbox-community/netbox/issues/11732) - Prevent inadvertent overwriting of object attributes by competing users
* [#11936](https://github.com/netbox-community/netbox/issues/11936) - Introduce support for tags and custom fields on webhooks
@@ -84,6 +304,12 @@ Tags may now be restricted to use with designated object types. Tags that have n
* [#13170](https://github.com/netbox-community/netbox/issues/13170) - Add `rf_role` to InterfaceTemplate
* [#13269](https://github.com/netbox-community/netbox/issues/13269) - Cache the number of assigned component templates for device types
+### Bug Fixes
+
+* [#13513](https://github.com/netbox-community/netbox/issues/13513) - Prevent exception when rendering bookmarks widget for anonymous user
+* [#13599](https://github.com/netbox-community/netbox/issues/13599) - Fix errant counter increments when editing device/VM components
+* [#13605](https://github.com/netbox-community/netbox/issues/13605) - Optimize cached counter migrations to avoid excessive memory consumption
+
### Other Changes
* Work has begun on introducing translation and localization support in NetBox. This work is being performed in preparation for release 4.0.
@@ -92,8 +318,9 @@ Tags may now be restricted to use with designated object types. Tags that have n
* [#11766](https://github.com/netbox-community/netbox/issues/11766) - Remove obsolete custom `ChoiceField` and `MultipleChoiceField` classes
* [#12180](https://github.com/netbox-community/netbox/issues/12180) - All API endpoints for available objects (e.g. IP addresses) now inherit from a common parent view
* [#12237](https://github.com/netbox-community/netbox/issues/12237) - Upgrade Django to v4.2
-* [#12794](https://github.com/netbox-community/netbox/issues/12794) - Avoid direct imports of Django's stock user model
* [#12320](https://github.com/netbox-community/netbox/issues/12320) - Remove obsolete fields `napalm_driver` and `napalm_args` from Platform
+* [#12794](https://github.com/netbox-community/netbox/issues/12794) - Avoid direct imports of Django's stock user model
+* [#12906](https://github.com/netbox-community/netbox/issues/12906) - The `boto3` (AWS) and `dulwich` (git) packages for remote data sources are now optional requirements
* [#12964](https://github.com/netbox-community/netbox/issues/12964) - Drop support for PostgreSQL 11
* [#13309](https://github.com/netbox-community/netbox/issues/13309) - User account-specific resources have been moved to a new `account` app for better organization
diff --git a/docs/release-notes/version-3.7.md b/docs/release-notes/version-3.7.md
new file mode 100644
index 000000000..127e241d7
--- /dev/null
+++ b/docs/release-notes/version-3.7.md
@@ -0,0 +1,138 @@
+# NetBox v3.7
+
+## v3.7.0 (2023-12-29)
+
+### Breaking Changes
+
+* The following fields have been removed from the Webhook model: `content_types`, `type_create`, `type_update`, `type_delete`, `type_job_start`, `type_job_end`, `enabled`, and `conditions`. Webhooks are now tied to events via [event rules](../features/event-rules.md). New event rules will be created for any existing webhooks automatically upon upgrade.
+* The `ui_visibility` field on the [custom field model](../models/extras/customfield.md) has been replaced with two new fields: `ui_visible` and `ui_editable`. These new fields will have their values mapped from the original field automatically upon upgrade.
+* The `FeatureQuery` class used internally for querying content types by model feature has been removed. It has been replaced by the new `with_feature()` manager method on NetBox's proxy model for ContentType (`core.models.ContentType`).
+* The internal ConfigRevision model has moved from `extras` to `core`. Configuration history will be retained throughout the upgrade process.
+* The [L2VPN](../models/vpn/l2vpn.md) and [L2VPNTermination](../models/vpn/l2vpntermination.md) models have moved from the `ipam` app to the new `vpn` app. All object data will be retained, however please note that the relevant API endpoints have likewise moved to `/api/vpn/`.
+* The `CustomFieldsMixin`, `SavedFiltersMixin`, and `TagsMixin` classes have moved from the `extras.forms.mixins` module to `netbox.forms.mixins`.
+
+### New Features
+
+#### VPN Tunnels ([#9816](https://github.com/netbox-community/netbox/issues/9816))
+
+Several new models have been introduced to enable [VPN tunnel management](../features/vpn-tunnels.md). Users can now define tunnels with two or more terminations to represent peer-to-peer or hub-and-spoke topologies. Each termination is made to a virtual interface on a device or virtual machine. Additionally, users can define IKE and IPSec proposals and policies, which can be applied to tunnels to document encryption and authentication strategies.
+
+#### Event Rules ([#14132](https://github.com/netbox-community/netbox/issues/14132))
+
+This release introduces [event rules](../features/event-rules.md), which can be used to send webhooks or execute custom scripts automatically in response to events that occur in NetBox. For example, it's now possible to run a custom script whenever a new site is created with a particular status or tag.
+
+Event rules replace and extend functionality that was previously built into the webhook model. New event rules will be created for any existing webhooks automatically upon upgrade.
+
+#### Virtual Machine Disks ([#8356](https://github.com/netbox-community/netbox/issues/8356))
+
+A new [VirtualDisk](../models/virtualization/virtualdisk.md) model has been introduced to enable tracking the assignment of discrete virtual disks to virtual machines. The `size` field has been retained on the VirtualMachine model, and will be populated automatically with the aggregate size of all assigned virtual disks. (Users who opt to eschew the new model may continue using the VirtualMachine `size` attribute independently as in previous releases.)
+
+#### Object Protection Rules ([#10244](https://github.com/netbox-community/netbox/issues/10244))
+
+A new [`PROTECTION_RULES`](../configuration/data-validation.md#protection_rules) configuration parameter has been introduced. Similar to how [custom validation rules](../customization/custom-validation.md) can be used to enforce certain values for object attributes, protection rules guard against the deletion of objects which do not meet specified criteria. This enables an administrator to prevent, for example, the deletion of a site which has a status of "active."
+
+#### Improved Custom Field Visibility Controls ([#13299](https://github.com/netbox-community/netbox/issues/13299))
+
+The `ui_visible` field on [the custom field model](../models/extras/customfield.md) has been superseded by two new fields, `ui_visible` and `ui_editable`, which control how and whether a custom field is displayed when view and editing an object, respectively. Separating these two functions into discrete fields allows more control over how each custom field is presented to users. The values of these fields will be appropriately set automatically during the upgrade process from the value of the original field.
+
+#### Improved Global Search Results ([#14134](https://github.com/netbox-community/netbox/issues/14134))
+
+Global search results now include additional context about each object, such as a description, status, and/or related objects. The set of attributes to be displayed is specific to each object type, and is defined by setting `display_attrs` under the object's [SearchIndex class](../plugins/development/search.md#netbox.search.SearchIndex).
+
+#### Table Column Registration for Plugins ([#14173](https://github.com/netbox-community/netbox/issues/14173))
+
+Plugins can now [register their own custom columns](../plugins/development/tables.md#extending-core-tables) for inclusion on core NetBox tables. For example, a plugin can register a new column on SiteTable using the new `register_table_column()` utility function, and it will become available for users to select for display.
+
+#### Data Backend Registration for Plugins ([#13381](https://github.com/netbox-community/netbox/issues/13381))
+
+Plugins can now [register their own data backends](../plugins/development/data-backends.md) for use with [synchronized data sources](../features/synchronized-data.md). This enables plugins to introduce new backends in addition to the git, S3, and local path backends provided natively.
+
+### Enhancements
+
+* [#12135](https://github.com/netbox-community/netbox/issues/12135) - Avoid orphaned interfaces by preventing the deletion of interfaces which have children assigned
+* [#12216](https://github.com/netbox-community/netbox/issues/12216) - Add a `color` field for circuit types
+* [#13230](https://github.com/netbox-community/netbox/issues/13230) - Allow device types to be excluded from consideration when calculating a rack's utilization
+* [#13334](https://github.com/netbox-community/netbox/issues/13334) - Add an `error` field to the Job model to record any errors associated with its execution
+* [#13427](https://github.com/netbox-community/netbox/issues/13427) - Introduce a mechanism for excluding models from general-purpose lists of object types
+* [#13690](https://github.com/netbox-community/netbox/issues/13690) - Display any dependent objects to be deleted prior to deleting an object via the web UI
+* [#13794](https://github.com/netbox-community/netbox/issues/13794) - Any models with a relationship to Tenant are now included automatically in the list of related objects under the tenant view
+* [#13808](https://github.com/netbox-community/netbox/issues/13808) - Add a `/render-config` REST API endpoint for virtual machines
+* [#14035](https://github.com/netbox-community/netbox/issues/14035) - Order objects of equivalent weight by value in global search results to improve readability
+* [#14147](https://github.com/netbox-community/netbox/issues/14147) - Avoid recording empty changelog entries via the new `CHANGELOG_SKIP_EMPTY_CHANGES` config parameter
+* [#14156](https://github.com/netbox-community/netbox/issues/14156) - Enable custom fields for contact assignments
+* [#14240](https://github.com/netbox-community/netbox/issues/14240) - Increase maximum values for custom field minimum & maximum numeric validators
+* [#14361](https://github.com/netbox-community/netbox/issues/14361) - Add a `description` field for webhooks
+* [#14365](https://github.com/netbox-community/netbox/issues/14365) - Introduce `job_start` and `job_end` signals to allow automated plugin actions
+* [#14434](https://github.com/netbox-community/netbox/issues/14434) - Add model-specific termination object filters for cables (e.g. `interface_id` and `consoleport_id`)
+* [#14436](https://github.com/netbox-community/netbox/issues/14436) - Add PostgreSQL indexes for all GenericForeignKey fields
+* [#14579](https://github.com/netbox-community/netbox/issues/14579) - Allow users to specify a preferred language for UI translations
+
+### Translations
+
+* [#14075](https://github.com/netbox-community/netbox/issues/14075) - Add Spanish translation
+* [#14096](https://github.com/netbox-community/netbox/issues/14096) - Add French translation
+* [#14145](https://github.com/netbox-community/netbox/issues/14145) - Add Portuguese translation
+* [#14266](https://github.com/netbox-community/netbox/issues/14266) - Add Russian translation
+
+### Bug Fixes
+
+* [#14432](https://github.com/netbox-community/netbox/issues/14432) - Fix hyperlinks for global search result attributes
+* [#14472](https://github.com/netbox-community/netbox/issues/14472) - Fix display of hidden custom fields in object edit forms
+* [#14499](https://github.com/netbox-community/netbox/issues/14499) - Relax requirements for encryption/auth algorithms on IKE & IPSec proposals
+* [#14550](https://github.com/netbox-community/netbox/issues/14550) - Fix changing action type of existing event rule
+
+### Other Changes
+
+* [#13550](https://github.com/netbox-community/netbox/issues/13550) - Optimize the format for declaring view actions under `ActionsMixin` (backward compatibility has been retained)
+* [#13645](https://github.com/netbox-community/netbox/issues/13645) - Installation of the `sentry-sdk` Python library is now required only if Sentry reporting is enabled
+* [#14036](https://github.com/netbox-community/netbox/issues/14036) - Move plugin resources from the `extras` app into `netbox` (backward compatibility has been retained)
+* [#14153](https://github.com/netbox-community/netbox/issues/14153) - Replace `FeatureQuery` with new `with_feature()` method on proxy ContentType manager
+* [#14311](https://github.com/netbox-community/netbox/issues/14311) - Move the L2VPN models from the `ipam` app to the new `vpn` app
+* [#14312](https://github.com/netbox-community/netbox/issues/14312) - Move the ConfigRevision model from the `extras` app to `core`
+* [#14326](https://github.com/netbox-community/netbox/issues/14326) - Form feature mixin classes have been moved from the `extras` app to `netbox`
+* [#14395](https://github.com/netbox-community/netbox/issues/14395) - Move `extras.webhooks_worker.process_webhook()` to `extras.webhooks.send_webhook()` (backward compatibility has been retained)
+* [#14424](https://github.com/netbox-community/netbox/issues/14424) - Remove change logging functionality from StagedChange
+* [#14458](https://github.com/netbox-community/netbox/issues/14458) - Remove the obsolete `clearcache` management command
+* [#14536](https://github.com/netbox-community/netbox/issues/14536) - Enforce uniqueness by default for non-VRF prefixes & IP addresses (`ENFORCE_GLOBAL_UNIQUE` now defaults to true)
+
+### REST API Changes
+
+* Introduced the following endpoints:
+ * `/api/extras/event-rules/`
+ * `/api/virtualization/virtual-disks/`
+ * `/api/vpn/ike-policies/`
+ * `/api/vpn/ike-proposals/`
+ * `/api/vpn/ipsec-policies/`
+ * `/api/vpn/ipsec-profiles/`
+ * `/api/vpn/ipsec-proposals/`
+ * `/api/vpn/tunnels/`
+ * `/api/vpn/tunnel-terminations/`
+* The following endpoints have been moved:
+ * `/api/ipam/l2vpns/` -> `/api/vpn/l2vpns/`
+ * `/api/ipam/l2vpn-terminations/` -> `/api/vpn/l2vpn-terminations/`
+* circuits.CircuitType
+ * Added the optional `color` choice field
+* core.Job
+ * Added the read-only `error` character field
+* extras.Webhook
+ * Removed the following fields (these have been moved to the new `EventRule` model):
+ * `content_types`
+ * `type_create`
+ * `type_update`
+ * `type_delete`
+ * `type_job_start`
+ * `type_job_end`
+ * `enabled`
+ * `conditions`
+ * Add the optional `description` field
+* dcim.DeviceType
+ * Added the `exclude_from_utilization` boolean field
+* extras.CustomField
+ * Removed the `ui_visibility` field
+ * Added the `ui_visible` and `ui_editable` choice fields
+* tenancy.ContactAssignment
+ * Added support for custom fields
+* virtualization.VirtualDisk
+ * Added the read-only `virtual_disk_count` integer field
+* virtualization.VirtualMachine
+ * Added the `/render-config` endpoint
diff --git a/mkdocs.yml b/mkdocs.yml
index cc16434de..5a7e00c2c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -53,8 +53,8 @@ markdown_extensions:
- admonition
- attr_list
- pymdownx.emoji:
- emoji_index: !!python/name:materialx.emoji.twemoji
- emoji_generator: !!python/name:materialx.emoji.to_svg
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.superfences:
custom_fences:
- name: mermaid
@@ -74,6 +74,7 @@ nav:
- Circuits: 'features/circuits.md'
- Wireless: 'features/wireless.md'
- Virtualization: 'features/virtualization.md'
+ - VPN Tunnels: 'features/vpn-tunnels.md'
- Tenancy: 'features/tenancy.md'
- Contacts: 'features/contacts.md'
- Search: 'features/search.md'
@@ -82,6 +83,7 @@ nav:
- Synchronized Data: 'features/synchronized-data.md'
- Change Logging: 'features/change-logging.md'
- Journaling: 'features/journaling.md'
+ - Event Rules: 'features/event-rules.md'
- Background Jobs: 'features/background-jobs.md'
- Auth & Permissions: 'features/authentication-permissions.md'
- API & Integration: 'features/api-integration.md'
@@ -136,6 +138,7 @@ nav:
- Forms: 'plugins/development/forms.md'
- Filters & Filter Sets: 'plugins/development/filtersets.md'
- Search: 'plugins/development/search.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'
@@ -213,6 +216,7 @@ nav:
- CustomField: 'models/extras/customfield.md'
- CustomFieldChoiceSet: 'models/extras/customfieldchoiceset.md'
- CustomLink: 'models/extras/customlink.md'
+ - EventRule: 'models/extras/eventrule.md'
- ExportTemplate: 'models/extras/exporttemplate.md'
- ImageAttachment: 'models/extras/imageattachment.md'
- JournalEntry: 'models/extras/journalentry.md'
@@ -228,8 +232,6 @@ nav:
- FHRPGroupAssignment: 'models/ipam/fhrpgroupassignment.md'
- IPAddress: 'models/ipam/ipaddress.md'
- IPRange: 'models/ipam/iprange.md'
- - L2VPN: 'models/ipam/l2vpn.md'
- - L2VPNTermination: 'models/ipam/l2vpntermination.md'
- Prefix: 'models/ipam/prefix.md'
- RIR: 'models/ipam/rir.md'
- Role: 'models/ipam/role.md'
@@ -250,7 +252,19 @@ nav:
- ClusterGroup: 'models/virtualization/clustergroup.md'
- ClusterType: 'models/virtualization/clustertype.md'
- VMInterface: 'models/virtualization/vminterface.md'
+ - VirtualDisk: 'models/virtualization/virtualdisk.md'
- VirtualMachine: 'models/virtualization/virtualmachine.md'
+ - VPN:
+ - IKEPolicy: 'models/vpn/ikepolicy.md'
+ - IKEProposal: 'models/vpn/ikeproposal.md'
+ - IPSecPolicy: 'models/vpn/ipsecpolicy.md'
+ - IPSecProfile: 'models/vpn/ipsecprofile.md'
+ - IPSecProposal: 'models/vpn/ipsecproposal.md'
+ - L2VPN: 'models/vpn/l2vpn.md'
+ - L2VPNTermination: 'models/vpn/l2vpntermination.md'
+ - Tunnel: 'models/vpn/tunnel.md'
+ - TunnelGroup: 'models/vpn/tunnelgroup.md'
+ - TunnelTermination: 'models/vpn/tunneltermination.md'
- Wireless:
- WirelessLAN: 'models/wireless/wirelesslan.md'
- WirelessLANGroup: 'models/wireless/wirelesslangroup.md'
@@ -276,6 +290,7 @@ nav:
- git Cheat Sheet: 'development/git-cheat-sheet.md'
- Release Notes:
- Summary: 'release-notes/index.md'
+ - Version 3.7: 'release-notes/version-3.7.md'
- Version 3.6: 'release-notes/version-3.6.md'
- Version 3.5: 'release-notes/version-3.5.md'
- Version 3.4: 'release-notes/version-3.4.md'
diff --git a/netbox/account/models.py b/netbox/account/models.py
index 5d6575040..bd5879a85 100644
--- a/netbox/account/models.py
+++ b/netbox/account/models.py
@@ -7,6 +7,8 @@ class UserToken(Token):
"""
Proxy model for users to manage their own API tokens.
"""
+ _netbox_private = True
+
class Meta:
proxy = True
verbose_name = 'token'
diff --git a/netbox/account/views.py b/netbox/account/views.py
index 3156b2102..3dbba9b29 100644
--- a/netbox/account/views.py
+++ b/netbox/account/views.py
@@ -13,6 +13,7 @@ from django.shortcuts import render, resolve_url
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
+from django.utils.translation import gettext_lazy as _
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import View
from social_core.backends.utils import load_backends
@@ -193,8 +194,16 @@ class UserConfigView(LoginRequiredMixin, View):
if form.is_valid():
form.save()
- messages.success(request, "Your preferences have been updated.")
- return redirect('account:preferences')
+ messages.success(request, _("Your preferences have been updated."))
+ response = redirect('account:preferences')
+
+ # Set/clear language cookie
+ if language := form.cleaned_data['locale.language']:
+ response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
+ else:
+ response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
+
+ return response
return render(request, self.template_name, {
'form': form,
diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py
index f4abda645..5223de339 100644
--- a/netbox/circuits/api/serializers.py
+++ b/netbox/circuits/api/serializers.py
@@ -85,7 +85,7 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
class Meta:
model = CircuitType
fields = [
- 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
+ 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
'circuit_count',
]
diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py
index e28238fea..97be1cf57 100644
--- a/netbox/circuits/filtersets.py
+++ b/netbox/circuits/filtersets.py
@@ -67,13 +67,14 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class Meta:
model = Provider
- fields = ['id', 'name', 'slug']
+ fields = ['id', 'name', 'slug', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
+ Q(description__icontains=value) |
Q(accounts__account__icontains=value) |
Q(accounts__name__icontains=value) |
Q(comments__icontains=value)
@@ -101,6 +102,7 @@ class ProviderAccountFilterSet(NetBoxModelFilterSet):
return queryset
return queryset.filter(
Q(name__icontains=value) |
+ Q(description__icontains=value) |
Q(account__icontains=value) |
Q(comments__icontains=value)
).distinct()
@@ -137,7 +139,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta:
model = CircuitType
- fields = ['id', 'name', 'slug', 'description']
+ fields = ['id', 'name', 'slug', 'color', 'description']
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
@@ -154,12 +156,12 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account',
queryset=ProviderAccount.objects.all(),
- label=_('ProviderAccount (ID)'),
+ label=_('Provider account (ID)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__provider_network',
queryset=ProviderNetwork.objects.all(),
- label=_('ProviderNetwork (ID)'),
+ label=_('Provider network (ID)'),
)
type_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitType.objects.all(),
diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py
index 1a9366583..5c416bff9 100644
--- a/netbox/circuits/forms/bulk_edit.py
+++ b/netbox/circuits/forms/bulk_edit.py
@@ -7,7 +7,7 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice
-from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = (
@@ -91,6 +91,10 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
+ color = ColorField(
+ label=_('Color'),
+ required=False
+ )
description = forms.CharField(
label=_('Description'),
max_length=200,
@@ -99,9 +103,9 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
model = CircuitType
fieldsets = (
- (None, ('description',)),
+ (None, ('color', 'description')),
)
- nullable_fields = ('description',)
+ nullable_fields = ('color', 'description')
class CircuitBulkEditForm(NetBoxModelBulkEditForm):
diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py
index d2217b45b..0c30e3cda 100644
--- a/netbox/circuits/forms/bulk_import.py
+++ b/netbox/circuits/forms/bulk_import.py
@@ -3,6 +3,7 @@ from django import forms
from circuits.choices import CircuitStatusChoices
from circuits.models import *
from dcim.models import Site
+from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
@@ -64,7 +65,10 @@ class CircuitTypeImportForm(NetBoxModelImportForm):
class Meta:
model = CircuitType
- fields = ('name', 'slug', 'description', 'tags')
+ fields = ('name', 'slug', 'color', 'description', 'tags')
+ help_texts = {
+ 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00
'),
+ }
class CircuitImportForm(NetBoxModelImportForm):
diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py
index 1fb239023..1e1abd068 100644
--- a/netbox/circuits/forms/filtersets.py
+++ b/netbox/circuits/forms/filtersets.py
@@ -7,7 +7,7 @@ from dcim.models import Region, Site, SiteGroup
from ipam.models import ASN
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
-from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
+from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.widgets import DatePicker, NumberWithOptions
__all__ = (
@@ -88,7 +88,7 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
label=_('Provider')
)
service_id = forms.CharField(
- label=_('Service id'),
+ label=_('Service ID'),
max_length=100,
required=False
)
@@ -97,8 +97,17 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
class CircuitTypeFilterForm(NetBoxModelFilterSetForm):
model = CircuitType
+ fieldsets = (
+ (None, ('q', 'filter_id', 'tag')),
+ (_('Attributes'), ('color',)),
+ )
tag = TagFilterField(model)
+ color = ColorField(
+ label=_('Color'),
+ required=False
+ )
+
class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Circuit
@@ -110,6 +119,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
)
+ selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(),
required=False,
diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py
index 8a540032e..0809cb2f4 100644
--- a/netbox/circuits/forms/model_forms.py
+++ b/netbox/circuits/forms/model_forms.py
@@ -76,14 +76,14 @@ class CircuitTypeForm(NetBoxModelForm):
fieldsets = (
(_('Circuit Type'), (
- 'name', 'slug', 'description', 'tags',
+ 'name', 'slug', 'color', 'description', 'tags',
)),
)
class Meta:
model = CircuitType
fields = [
- 'name', 'slug', 'description', 'tags',
+ 'name', 'slug', 'color', 'description', 'tags',
]
diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py
index 33cb5749d..fc33a81a7 100644
--- a/netbox/circuits/graphql/types.py
+++ b/netbox/circuits/graphql/types.py
@@ -64,7 +64,8 @@ class CircuitType(NetBoxObjectType, ContactsMixin):
@strawberry.django.type(
models.CircuitType,
- fields='__all__',
+ # fields='__all__',
+ exclude=['color',], # bug - remove color from exclude
filters=CircuitTypeFilter
)
class CircuitTypeType(OrganizationalObjectType):
diff --git a/netbox/circuits/migrations/0043_circuittype_color.py b/netbox/circuits/migrations/0043_circuittype_color.py
new file mode 100644
index 000000000..6c4dffeb6
--- /dev/null
+++ b/netbox/circuits/migrations/0043_circuittype_color.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.5 on 2023-10-20 21:25
+
+from django.db import migrations
+import utilities.fields
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('circuits', '0042_provideraccount'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='circuittype',
+ name='color',
+ field=utilities.fields.ColorField(blank=True, max_length=6),
+ ),
+ ]
diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py
index 0322b67c6..4dc775364 100644
--- a/netbox/circuits/models/circuits.py
+++ b/netbox/circuits/models/circuits.py
@@ -7,6 +7,7 @@ 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 utilities.fields import ColorField
__all__ = (
'Circuit',
@@ -20,6 +21,11 @@ class CircuitType(OrganizationalModel):
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band".
"""
+ color = ColorField(
+ verbose_name=_('color'),
+ blank=True
+ )
+
def get_absolute_url(self):
return reverse('circuits:circuittype', args=[self.pk])
diff --git a/netbox/circuits/search.py b/netbox/circuits/search.py
index b80f92d4d..c22b400eb 100644
--- a/netbox/circuits/search.py
+++ b/netbox/circuits/search.py
@@ -10,6 +10,7 @@ class CircuitIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('provider', 'provider_account', 'type', 'status', 'tenant', 'description')
@register_search
@@ -22,6 +23,7 @@ class CircuitTerminationIndex(SearchIndex):
('port_speed', 2000),
('upstream_speed', 2000),
)
+ display_attrs = ('circuit', 'site', 'provider_network', 'description')
@register_search
@@ -32,6 +34,7 @@ class CircuitTypeIndex(SearchIndex):
('slug', 110),
('description', 500),
)
+ display_attrs = ('description',)
@register_search
@@ -42,6 +45,7 @@ class ProviderIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('description',)
class ProviderAccountIndex(SearchIndex):
@@ -51,6 +55,7 @@ class ProviderAccountIndex(SearchIndex):
('account', 200),
('comments', 5000),
)
+ display_attrs = ('provider', 'account', 'description')
@register_search
@@ -62,3 +67,4 @@ class ProviderNetworkIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('provider', 'service_id', 'description')
diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py
index 6a05983e6..6ae727eca 100644
--- a/netbox/circuits/tables/circuits.py
+++ b/netbox/circuits/tables/circuits.py
@@ -28,6 +28,7 @@ class CircuitTypeTable(NetBoxTable):
linkify=True,
verbose_name=_('Name'),
)
+ color = columns.ColorColumn()
tags = columns.TagColumn(
url_name='circuits:circuittype_list'
)
@@ -40,7 +41,7 @@ class CircuitTypeTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = CircuitType
fields = (
- 'pk', 'id', 'name', 'circuit_count', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions',
+ 'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py
index e3380a1e5..6553179ec 100644
--- a/netbox/circuits/tests/test_filtersets.py
+++ b/netbox/circuits/tests/test_filtersets.py
@@ -25,8 +25,8 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
ASN.objects.bulk_create(asns)
providers = (
- Provider(name='Provider 1', slug='provider-1'),
- Provider(name='Provider 2', slug='provider-2'),
+ Provider(name='Provider 1', slug='provider-1', description='foobar1'),
+ Provider(name='Provider 2', slug='provider-2', description='foobar2'),
Provider(name='Provider 3', slug='provider-3'),
Provider(name='Provider 4', slug='provider-4'),
Provider(name='Provider 5', slug='provider-5'),
@@ -74,6 +74,10 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
))
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Provider 1', 'Provider 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -82,6 +86,10 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'slug': ['provider-1', 'provider-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_asn_id(self): # ASN object assignment
asns = ASN.objects.all()[:2]
params = {'asn_id': [asns[0].pk, asns[1].pk]}
@@ -122,6 +130,10 @@ class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
))
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Circuit Type 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@@ -227,6 +239,10 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
))
CircuitTermination.objects.bulk_create(circuit_terminations)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_cid(self):
params = {'cid': ['Test Circuit 1', 'Test Circuit 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -369,6 +385,10 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
Cable(a_terminations=[circuit_terminations[0]], b_terminations=[circuit_terminations[1]]).save()
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_term_side(self):
params = {'term_side': 'A'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
@@ -440,6 +460,10 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
)
ProviderNetwork.objects.bulk_create(provider_networks)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Provider Network 1', 'Provider Network 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -477,6 +501,10 @@ class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
)
ProviderAccount.objects.bulk_create(provider_accounts)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Provider Account 1', 'Provider Account 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
diff --git a/netbox/core/api/serializers.py b/netbox/core/api/serializers.py
index 4117a609c..4ae426df5 100644
--- a/netbox/core/api/serializers.py
+++ b/netbox/core/api/serializers.py
@@ -4,6 +4,7 @@ from core.choices import *
from core.models import *
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
+from netbox.utils import get_data_backend_choices
from users.api.nested_serializers import NestedUserSerializer
from .nested_serializers import *
@@ -19,7 +20,7 @@ class DataSourceSerializer(NetBoxModelSerializer):
view_name='core-api:datasource-detail'
)
type = ChoiceField(
- choices=DataSourceTypeChoices
+ choices=get_data_backend_choices()
)
status = ChoiceField(
choices=DataSourceStatusChoices,
@@ -68,5 +69,5 @@ class JobSerializer(BaseModelSerializer):
model = Job
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
- 'started', 'completed', 'user', 'data', 'job_id',
+ 'started', 'completed', 'user', 'data', 'error', 'job_id',
]
diff --git a/netbox/core/apps.py b/netbox/core/apps.py
index ffcf0b4ea..2d999c57e 100644
--- a/netbox/core/apps.py
+++ b/netbox/core/apps.py
@@ -1,4 +1,15 @@
from django.apps import AppConfig
+from django.db import models
+from django.db.migrations.operations import AlterModelOptions
+
+from utilities.migration import custom_deconstruct
+
+# Ignore verbose_name & verbose_name_plural Meta options when calculating model migrations
+AlterModelOptions.ALTER_OPTION_KEYS.remove('verbose_name')
+AlterModelOptions.ALTER_OPTION_KEYS.remove('verbose_name_plural')
+
+# Use our custom destructor to ignore certain attributes when calculating field migrations
+models.Field.deconstruct = custom_deconstruct
class CoreConfig(AppConfig):
diff --git a/netbox/core/choices.py b/netbox/core/choices.py
index 0067dfed8..8d7050414 100644
--- a/netbox/core/choices.py
+++ b/netbox/core/choices.py
@@ -7,18 +7,6 @@ from utilities.choices import ChoiceSet
# Data sources
#
-class DataSourceTypeChoices(ChoiceSet):
- LOCAL = 'local'
- GIT = 'git'
- AMAZON_S3 = 'amazon-s3'
-
- CHOICES = (
- (LOCAL, _('Local'), 'gray'),
- (GIT, _('Git'), 'blue'),
- (AMAZON_S3, _('Amazon S3'), 'blue'),
- )
-
-
class DataSourceStatusChoices(ChoiceSet):
NEW = 'new'
QUEUED = 'queued'
diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py
index d947602a6..9ff0b4d63 100644
--- a/netbox/core/data_backends.py
+++ b/netbox/core/data_backends.py
@@ -10,61 +10,24 @@ from django import forms
from django.conf import settings
from django.utils.translation import gettext as _
-from netbox.registry import registry
-from .choices import DataSourceTypeChoices
+from netbox.data_backends import DataBackend
+from netbox.utils import register_data_backend
from .exceptions import SyncError
__all__ = (
- 'LocalBackend',
'GitBackend',
+ 'LocalBackend',
'S3Backend',
)
logger = logging.getLogger('netbox.data_backends')
-def register_backend(name):
- """
- Decorator for registering a DataBackend class.
- """
-
- def _wrapper(cls):
- registry['data_backends'][name] = cls
- return cls
-
- return _wrapper
-
-
-class DataBackend:
- parameters = {}
- sensitive_parameters = []
-
- # Prevent Django's template engine from calling the backend
- # class when referenced via DataSource.backend_class
- do_not_call_in_templates = True
-
- def __init__(self, url, **kwargs):
- self.url = url
- self.params = kwargs
- self.config = self.init_config()
-
- def init_config(self):
- """
- Hook to initialize the instance's configuration.
- """
- return
-
- @property
- def url_scheme(self):
- return urlparse(self.url).scheme.lower()
-
- @contextmanager
- def fetch(self):
- raise NotImplemented()
-
-
-@register_backend(DataSourceTypeChoices.LOCAL)
+@register_data_backend()
class LocalBackend(DataBackend):
+ name = 'local'
+ label = _('Local')
+ is_local = True
@contextmanager
def fetch(self):
@@ -74,20 +37,22 @@ class LocalBackend(DataBackend):
yield local_path
-@register_backend(DataSourceTypeChoices.GIT)
+@register_data_backend()
class GitBackend(DataBackend):
+ name = 'git'
+ label = 'Git'
parameters = {
'username': forms.CharField(
required=False,
label=_('Username'),
widget=forms.TextInput(attrs={'class': 'form-control'}),
- help_text=_("Only used for cloning with HTTP / HTTPS"),
+ help_text=_("Only used for cloning with HTTP(S)"),
),
'password': forms.CharField(
required=False,
label=_('Password'),
widget=forms.TextInput(attrs={'class': 'form-control'}),
- help_text=_("Only used for cloning with HTTP / HTTPS"),
+ help_text=_("Only used for cloning with HTTP(S)"),
),
'branch': forms.CharField(
required=False,
@@ -125,12 +90,13 @@ class GitBackend(DataBackend):
}
if self.url_scheme in ('http', 'https'):
- clone_args.update(
- {
- "username": self.params.get('username'),
- "password": self.params.get('password'),
- }
- )
+ if self.params.get('username'):
+ clone_args.update(
+ {
+ "username": self.params.get('username'),
+ "password": self.params.get('password'),
+ }
+ )
logger.debug(f"Cloning git repo: {self.url}")
try:
@@ -143,8 +109,10 @@ class GitBackend(DataBackend):
local_path.cleanup()
-@register_backend(DataSourceTypeChoices.AMAZON_S3)
+@register_data_backend()
class S3Backend(DataBackend):
+ name = 'amazon-s3'
+ label = 'Amazon S3'
parameters = {
'aws_access_key_id': forms.CharField(
label=_('AWS access key ID'),
diff --git a/netbox/core/filtersets.py b/netbox/core/filtersets.py
index 62a58086a..902e240ee 100644
--- a/netbox/core/filtersets.py
+++ b/netbox/core/filtersets.py
@@ -4,10 +4,12 @@ from django.utils.translation import gettext as _
import django_filters
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
+from netbox.utils import get_data_backend_choices
from .choices import *
from .models import *
__all__ = (
+ 'ConfigRevisionFilterSet',
'DataFileFilterSet',
'DataSourceFilterSet',
'JobFilterSet',
@@ -16,7 +18,7 @@ __all__ = (
class DataSourceFilterSet(NetBoxModelFilterSet):
type = django_filters.MultipleChoiceFilter(
- choices=DataSourceTypeChoices,
+ choices=get_data_backend_choices,
null_value=None
)
status = django_filters.MultipleChoiceFilter(
@@ -26,7 +28,7 @@ class DataSourceFilterSet(NetBoxModelFilterSet):
class Meta:
model = DataSource
- fields = ('id', 'name', 'enabled')
+ fields = ('id', 'name', 'enabled', 'description')
def search(self, queryset, name, value):
if not value.strip():
@@ -122,3 +124,23 @@ class JobFilterSet(BaseFilterSet):
Q(user__username__icontains=value) |
Q(name__icontains=value)
)
+
+
+class ConfigRevisionFilterSet(BaseFilterSet):
+ q = django_filters.CharFilter(
+ method='search',
+ label=_('Search'),
+ )
+
+ class Meta:
+ model = ConfigRevision
+ fields = [
+ 'id',
+ ]
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(comment__icontains=value)
+ )
diff --git a/netbox/core/forms/bulk_edit.py b/netbox/core/forms/bulk_edit.py
index a4ecd646f..dcc92c6f0 100644
--- a/netbox/core/forms/bulk_edit.py
+++ b/netbox/core/forms/bulk_edit.py
@@ -1,10 +1,9 @@
from django import forms
from django.utils.translation import gettext_lazy as _
-from core.choices import DataSourceTypeChoices
from core.models import *
from netbox.forms import NetBoxModelBulkEditForm
-from utilities.forms import add_blank_choice
+from netbox.utils import get_data_backend_choices
from utilities.forms.fields import CommentField
from utilities.forms.widgets import BulkEditNullBooleanSelect
@@ -16,9 +15,8 @@ __all__ = (
class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
type = forms.ChoiceField(
label=_('Type'),
- choices=add_blank_choice(DataSourceTypeChoices),
- required=False,
- initial=''
+ choices=get_data_backend_choices,
+ required=False
)
enabled = forms.NullBooleanField(
required=False,
diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py
index f7a6f3595..f21bd3f87 100644
--- a/netbox/core/forms/filtersets.py
+++ b/netbox/core/forms/filtersets.py
@@ -1,18 +1,18 @@
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 core.choices import *
from core.models import *
-from extras.forms.mixins import SavedFiltersMixin
-from extras.utils import FeatureQuery
from netbox.forms import NetBoxModelFilterSetForm
+from netbox.forms.mixins import SavedFiltersMixin
+from netbox.utils import get_data_backend_choices
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
__all__ = (
+ 'ConfigRevisionFilterForm',
'DataFileFilterForm',
'DataSourceFilterForm',
'JobFilterForm',
@@ -27,7 +27,7 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
)
type = forms.MultipleChoiceField(
label=_('Type'),
- choices=DataSourceTypeChoices,
+ choices=get_data_backend_choices,
required=False
)
status = forms.MultipleChoiceField(
@@ -68,7 +68,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
)
object_type = ContentTypeChoiceField(
label=_('Object Type'),
- queryset=ContentType.objects.filter(FeatureQuery('jobs').get_query()),
+ queryset=ContentType.objects.with_feature('jobs'),
required=False,
)
status = forms.MultipleChoiceField(
@@ -124,3 +124,9 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
api_url='/api/users/users/',
)
)
+
+
+class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
+ fieldsets = (
+ (None, ('q', 'filter_id')),
+ )
diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py
index 01d5474c6..652728734 100644
--- a/netbox/core/forms/model_forms.py
+++ b/netbox/core/forms/model_forms.py
@@ -1,23 +1,34 @@
import copy
+import json
from django import forms
+from django.conf import settings
from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin
from core.models import *
+from netbox.config import get_config, PARAMS
from netbox.forms import NetBoxModelForm
from netbox.registry import registry
-from utilities.forms import get_field_value
+from netbox.utils import get_data_backend_choices
+from utilities.forms import BootstrapMixin, get_field_value
from utilities.forms.fields import CommentField
from utilities.forms.widgets import HTMXSelect
__all__ = (
+ 'ConfigRevisionForm',
'DataSourceForm',
'ManagedFileForm',
)
+EMPTY_VALUES = ('', None, [], ())
+
class DataSourceForm(NetBoxModelForm):
+ type = forms.ChoiceField(
+ choices=get_data_backend_choices,
+ widget=HTMXSelect()
+ )
comments = CommentField()
class Meta:
@@ -26,7 +37,6 @@ class DataSourceForm(NetBoxModelForm):
'name', 'type', 'source_url', 'enabled', 'description', 'comments', 'ignore_rules', 'tags',
]
widgets = {
- 'type': HTMXSelect(),
'ignore_rules': forms.Textarea(
attrs={
'rows': 5,
@@ -56,12 +66,13 @@ class DataSourceForm(NetBoxModelForm):
# Add backend-specific form fields
self.backend_fields = []
- for name, form_field in backend.parameters.items():
- field_name = f'backend_{name}'
- self.backend_fields.append(field_name)
- self.fields[field_name] = copy.copy(form_field)
- if self.instance and self.instance.parameters:
- self.fields[field_name].initial = self.instance.parameters.get(name)
+ if backend:
+ for name, form_field in backend.parameters.items():
+ field_name = f'backend_{name}'
+ self.backend_fields.append(field_name)
+ self.fields[field_name] = copy.copy(form_field)
+ if self.instance and self.instance.parameters:
+ self.fields[field_name].initial = self.instance.parameters.get(name)
def save(self, *args, **kwargs):
@@ -106,3 +117,113 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
new_file.write(self.cleaned_data['upload_file'].read())
return super().save(*args, **kwargs)
+
+
+class ConfigFormMetaclass(forms.models.ModelFormMetaclass):
+
+ def __new__(mcs, name, bases, attrs):
+
+ # Emulate a declared field for each supported configuration parameter
+ param_fields = {}
+ for param in PARAMS:
+ field_kwargs = {
+ 'required': False,
+ 'label': param.label,
+ 'help_text': param.description,
+ }
+ field_kwargs.update(**param.field_kwargs)
+ param_fields[param.name] = param.field(**field_kwargs)
+ attrs.update(param_fields)
+
+ return super().__new__(mcs, name, bases, attrs)
+
+
+class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMetaclass):
+ """
+ Form for creating a new ConfigRevision.
+ """
+
+ fieldsets = (
+ (_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
+ (_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
+ (_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
+ (_('Security'), ('ALLOWED_URL_SCHEMES',)),
+ (_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
+ (_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
+ (_('Validation'), ('CUSTOM_VALIDATORS', 'PROTECTION_RULES')),
+ (_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
+ (_('Miscellaneous'), (
+ 'MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL',
+ )),
+ (_('Config Revision'), ('comment',))
+ )
+
+ class Meta:
+ model = ConfigRevision
+ fields = '__all__'
+ widgets = {
+ 'BANNER_LOGIN': forms.Textarea(attrs={'class': 'font-monospace'}),
+ 'BANNER_MAINTENANCE': forms.Textarea(attrs={'class': 'font-monospace'}),
+ 'BANNER_TOP': forms.Textarea(attrs={'class': 'font-monospace'}),
+ 'BANNER_BOTTOM': forms.Textarea(attrs={'class': 'font-monospace'}),
+ 'CUSTOM_VALIDATORS': forms.Textarea(attrs={'class': 'font-monospace'}),
+ 'PROTECTION_RULES': forms.Textarea(attrs={'class': 'font-monospace'}),
+ 'comment': forms.Textarea(),
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Append current parameter values to form field help texts and check for static configurations
+ config = get_config()
+ for param in PARAMS:
+ value = getattr(config, param.name)
+
+ # Set the field's initial value, if it can be serialized. (This may not be the case e.g. for
+ # CUSTOM_VALIDATORS, which may reference Python objects.)
+ try:
+ json.dumps(value)
+ if type(value) in (tuple, list):
+ self.fields[param.name].initial = ', '.join(value)
+ else:
+ self.fields[param.name].initial = value
+ except TypeError:
+ pass
+
+ # Check whether this parameter is statically configured (e.g. in configuration.py)
+ if hasattr(settings, param.name):
+ self.fields[param.name].disabled = True
+ self.fields[param.name].help_text = _(
+ 'This parameter has been defined statically and cannot be modified.'
+ )
+ continue
+
+ # Set the field's help text
+ help_text = self.fields[param.name].help_text
+ if help_text:
+ help_text += ' ' # Line break
+ help_text += _('Current value: {value} ').format(value=value or '—')
+ if value == param.default:
+ help_text += _(' (default)')
+ self.fields[param.name].help_text = help_text
+
+ def save(self, commit=True):
+ instance = super().save(commit=False)
+
+ # Populate JSON data on the instance
+ instance.data = self.render_json()
+
+ if commit:
+ instance.save()
+
+ return instance
+
+ def render_json(self):
+ json = {}
+
+ # Iterate through each field and populate non-empty values
+ for field_name in self.declared_fields:
+ if field_name in self.cleaned_data and self.cleaned_data[field_name] not in EMPTY_VALUES:
+ json[field_name] = self.cleaned_data[field_name]
+
+ return json
diff --git a/netbox/core/jobs.py b/netbox/core/jobs.py
index d25981920..264313e62 100644
--- a/netbox/core/jobs.py
+++ b/netbox/core/jobs.py
@@ -25,7 +25,7 @@ def sync_datasource(job, *args, **kwargs):
job.terminate()
except Exception as e:
- job.terminate(status=JobStatusChoices.STATUS_ERRORED)
+ 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)
diff --git a/netbox/core/management/commands/clearcache.py b/netbox/core/management/commands/clearcache.py
deleted file mode 100644
index 22843c490..000000000
--- a/netbox/core/management/commands/clearcache.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from django.core.cache import cache
-from django.core.management.base import BaseCommand
-
-
-class Command(BaseCommand):
- """Command to clear the entire cache."""
- help = 'Clears the cache.'
-
- def handle(self, *args, **kwargs):
- cache.clear()
- self.stdout.write('Cache has been cleared.', ending="\n")
diff --git a/netbox/core/management/commands/makemigrations.py b/netbox/core/management/commands/makemigrations.py
index 10874418a..ce40bd3cc 100644
--- a/netbox/core/management/commands/makemigrations.py
+++ b/netbox/core/management/commands/makemigrations.py
@@ -1,18 +1,6 @@
-# noinspection PyUnresolvedReferences
from django.conf import settings
from django.core.management.base import CommandError
from django.core.management.commands.makemigrations import Command as _Command
-from django.db import models
-from django.db.migrations.operations import AlterModelOptions
-
-from utilities.migration import custom_deconstruct
-
-# Monkey patch AlterModelOptions to ignore verbose name attributes
-AlterModelOptions.ALTER_OPTION_KEYS.remove('verbose_name')
-AlterModelOptions.ALTER_OPTION_KEYS.remove('verbose_name_plural')
-
-# Set our custom deconstructor for fields
-models.Field.deconstruct = custom_deconstruct
class Command(_Command):
diff --git a/netbox/core/management/commands/migrate.py b/netbox/core/management/commands/migrate.py
deleted file mode 100644
index 8d5e45a40..000000000
--- a/netbox/core/management/commands/migrate.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# noinspection PyUnresolvedReferences
-from django.core.management.commands.migrate import Command
-from django.db import models
-
-from utilities.migration import custom_deconstruct
-
-models.Field.deconstruct = custom_deconstruct
diff --git a/netbox/core/management/commands/nbshell.py b/netbox/core/management/commands/nbshell.py
index 674a878c7..eeefe502b 100644
--- a/netbox/core/management/commands/nbshell.py
+++ b/netbox/core/management/commands/nbshell.py
@@ -6,10 +6,11 @@ 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.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand
-APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'wireless')
+from core.models import ContentType
+
+APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
BANNER_TEXT = """### NetBox interactive shell ({node})
### Python {python} | Django {django} | NetBox {netbox}
diff --git a/netbox/core/management/commands/syncdatasource.py b/netbox/core/management/commands/syncdatasource.py
index 3d73f70ab..aa8137952 100644
--- a/netbox/core/management/commands/syncdatasource.py
+++ b/netbox/core/management/commands/syncdatasource.py
@@ -1,5 +1,6 @@
from django.core.management.base import BaseCommand, CommandError
+from core.choices import DataSourceStatusChoices
from core.models import DataSource
@@ -33,9 +34,13 @@ class Command(BaseCommand):
for i, datasource in enumerate(datasources, start=1):
self.stdout.write(f"[{i}] Syncing {datasource}... ", ending='')
self.stdout.flush()
- datasource.sync()
- self.stdout.write(datasource.get_status_display())
- self.stdout.flush()
+ try:
+ datasource.sync()
+ self.stdout.write(datasource.get_status_display())
+ self.stdout.flush()
+ except Exception as e:
+ DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
+ raise e
if len(options['name']) > 1:
self.stdout.write(f"Finished.")
diff --git a/netbox/core/migrations/0003_job.py b/netbox/core/migrations/0003_job.py
index ab6f058ff..f2fe41afb 100644
--- a/netbox/core/migrations/0003_job.py
+++ b/netbox/core/migrations/0003_job.py
@@ -4,7 +4,6 @@ from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
-import extras.utils
class Migration(migrations.Migration):
@@ -30,7 +29,7 @@ class Migration(migrations.Migration):
('status', models.CharField(default='pending', max_length=30)),
('data', models.JSONField(blank=True, null=True)),
('job_id', models.UUIDField(unique=True)),
- ('object_type', models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('jobs'), on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype')),
+ ('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
diff --git a/netbox/core/migrations/0006_datasource_type_remove_choices.py b/netbox/core/migrations/0006_datasource_type_remove_choices.py
new file mode 100644
index 000000000..0ad8d8854
--- /dev/null
+++ b/netbox/core/migrations/0006_datasource_type_remove_choices.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.6 on 2023-10-20 17:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0005_job_created_auto_now'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='datasource',
+ name='type',
+ field=models.CharField(max_length=50),
+ ),
+ ]
diff --git a/netbox/core/migrations/0007_job_add_error_field.py b/netbox/core/migrations/0007_job_add_error_field.py
new file mode 100644
index 000000000..e2e173bfd
--- /dev/null
+++ b/netbox/core/migrations/0007_job_add_error_field.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.6 on 2023-10-23 20:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0006_datasource_type_remove_choices'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='job',
+ name='error',
+ field=models.TextField(blank=True, editable=False),
+ ),
+ ]
diff --git a/netbox/core/migrations/0008_contenttype_proxy.py b/netbox/core/migrations/0008_contenttype_proxy.py
new file mode 100644
index 000000000..ac11d906a
--- /dev/null
+++ b/netbox/core/migrations/0008_contenttype_proxy.py
@@ -0,0 +1,29 @@
+# Generated by Django 4.2.6 on 2023-10-31 19:38
+
+import core.models.contenttypes
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('core', '0007_job_add_error_field'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ContentType',
+ fields=[
+ ],
+ options={
+ 'proxy': True,
+ 'indexes': [],
+ 'constraints': [],
+ },
+ bases=('contenttypes.contenttype',),
+ managers=[
+ ('objects', core.models.contenttypes.ContentTypeManager()),
+ ],
+ ),
+ ]
diff --git a/netbox/core/migrations/0009_configrevision.py b/netbox/core/migrations/0009_configrevision.py
new file mode 100644
index 000000000..e7f817a16
--- /dev/null
+++ b/netbox/core/migrations/0009_configrevision.py
@@ -0,0 +1,31 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0008_contenttype_proxy'),
+ ]
+
+ operations = [
+ migrations.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.CreateModel(
+ name='ConfigRevision',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('comment', models.CharField(blank=True, max_length=200)),
+ ('data', models.JSONField(blank=True, null=True)),
+ ],
+ options={
+ 'verbose_name': 'config revision',
+ 'verbose_name_plural': 'config revisions',
+ 'ordering': ['-created'],
+ },
+ ),
+ ],
+ # Table will be renamed from extras_configrevision in extras/0101_move_configrevision
+ database_operations=[],
+ ),
+ ]
diff --git a/netbox/core/migrations/0010_gfk_indexes.py b/netbox/core/migrations/0010_gfk_indexes.py
new file mode 100644
index 000000000..d51bc67ad
--- /dev/null
+++ b/netbox/core/migrations/0010_gfk_indexes.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.7 on 2023-12-07 16:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0009_configrevision'),
+ ]
+
+ operations = [
+ migrations.AddIndex(
+ model_name='job',
+ index=models.Index(fields=['object_type', 'object_id'], name='core_job_object__c664ac_idx'),
+ ),
+ ]
diff --git a/netbox/core/models/__init__.py b/netbox/core/models/__init__.py
index 185622f5f..2c30ce02b 100644
--- a/netbox/core/models/__init__.py
+++ b/netbox/core/models/__init__.py
@@ -1,3 +1,5 @@
+from .config import *
+from .contenttypes import *
from .data import *
from .files import *
from .jobs import *
diff --git a/netbox/core/models/config.py b/netbox/core/models/config.py
new file mode 100644
index 000000000..6c8e41477
--- /dev/null
+++ b/netbox/core/models/config.py
@@ -0,0 +1,66 @@
+from django.core.cache import cache
+from django.db import models
+from django.urls import reverse
+from django.utils.translation import gettext, gettext_lazy as _
+
+from utilities.querysets import RestrictedQuerySet
+
+__all__ = (
+ 'ConfigRevision',
+)
+
+
+class ConfigRevision(models.Model):
+ """
+ An atomic revision of NetBox's configuration.
+ """
+ created = models.DateTimeField(
+ verbose_name=_('created'),
+ auto_now_add=True
+ )
+ comment = models.CharField(
+ verbose_name=_('comment'),
+ max_length=200,
+ blank=True
+ )
+ data = models.JSONField(
+ blank=True,
+ null=True,
+ verbose_name=_('configuration data')
+ )
+
+ objects = RestrictedQuerySet.as_manager()
+
+ class Meta:
+ ordering = ['-created']
+ verbose_name = _('config revision')
+ verbose_name_plural = _('config revisions')
+
+ def __str__(self):
+ if not self.pk:
+ return gettext('Default configuration')
+ if self.is_active:
+ return gettext('Current configuration')
+ return gettext('Config revision #{id}').format(id=self.pk)
+
+ def __getattr__(self, item):
+ if item in self.data:
+ return self.data[item]
+ return super().__getattribute__(item)
+
+ def get_absolute_url(self):
+ if not self.pk:
+ return reverse('core:config') # Default config view
+ return reverse('core:configrevision', args=[self.pk])
+
+ def activate(self):
+ """
+ Cache the configuration data.
+ """
+ cache.set('config', self.data, None)
+ cache.set('config_version', self.pk, None)
+ activate.alters_data = True
+
+ @property
+ def is_active(self):
+ return cache.get('config_version') == self.pk
diff --git a/netbox/core/models/contenttypes.py b/netbox/core/models/contenttypes.py
new file mode 100644
index 000000000..c98184c3d
--- /dev/null
+++ b/netbox/core/models/contenttypes.py
@@ -0,0 +1,50 @@
+from django.contrib.contenttypes.models import ContentType as ContentType_, ContentTypeManager as ContentTypeManager_
+from django.db.models import Q
+
+from netbox.registry import registry
+
+__all__ = (
+ 'ContentType',
+ 'ContentTypeManager',
+)
+
+
+class ContentTypeManager(ContentTypeManager_):
+
+ def public(self):
+ """
+ Filter the base queryset to return only ContentTypes corresponding to "public" models; those which are listed
+ in registry['models'] and intended for reference by other objects.
+ """
+ q = Q()
+ for app_label, models in registry['models'].items():
+ q |= Q(app_label=app_label, model__in=models)
+ return self.get_queryset().filter(q)
+
+ def with_feature(self, feature):
+ """
+ Return the ContentTypes only for models which are registered as supporting the specified feature. For example,
+ we can find all ContentTypes for models which support webhooks with
+
+ ContentType.objects.with_feature('event_rules')
+ """
+ if feature not in registry['model_features']:
+ raise KeyError(
+ f"{feature} is not a registered model feature! Valid features are: {registry['model_features'].keys()}"
+ )
+
+ q = Q()
+ for app_label, models in registry['model_features'][feature].items():
+ q |= Q(app_label=app_label, model__in=models)
+
+ return self.get_queryset().filter(q)
+
+
+class ContentType(ContentType_):
+ """
+ Wrap Django's native ContentType model to use our custom manager.
+ """
+ objects = ContentTypeManager()
+
+ class Meta:
+ proxy = True
diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py
index 8e372c2eb..efda879af 100644
--- a/netbox/core/models/data.py
+++ b/netbox/core/models/data.py
@@ -6,7 +6,6 @@ from urllib.parse import urlparse
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models
@@ -45,9 +44,7 @@ class DataSource(JobsMixin, PrimaryModel):
)
type = models.CharField(
verbose_name=_('type'),
- max_length=50,
- choices=DataSourceTypeChoices,
- default=DataSourceTypeChoices.LOCAL
+ max_length=50
)
source_url = models.CharField(
max_length=200,
@@ -96,8 +93,9 @@ class DataSource(JobsMixin, PrimaryModel):
def docs_url(self):
return f'{settings.STATIC_URL}docs/models/{self._meta.app_label}/{self._meta.model_name}/'
- def get_type_color(self):
- return DataSourceTypeChoices.colors.get(self.type)
+ def get_type_display(self):
+ if backend := registry['data_backends'].get(self.type):
+ return backend.label
def get_status_color(self):
return DataSourceStatusChoices.colors.get(self.status)
@@ -110,10 +108,6 @@ class DataSource(JobsMixin, PrimaryModel):
def backend_class(self):
return registry['data_backends'].get(self.type)
- @property
- def is_local(self):
- return self.type == DataSourceTypeChoices.LOCAL
-
@property
def ready_for_sync(self):
return self.enabled and self.status not in (
@@ -122,9 +116,16 @@ class DataSource(JobsMixin, PrimaryModel):
)
def clean(self):
+ super().clean()
+
+ # Validate data backend type
+ if self.type and self.type not in registry['data_backends']:
+ raise ValidationError({
+ 'type': _("Unknown backend type: {type}".format(type=self.type))
+ })
# Ensure URL scheme matches selected type
- if self.type == DataSourceTypeChoices.LOCAL and self.url_scheme not in ('file', ''):
+ 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)"
})
@@ -316,7 +317,7 @@ class DataFile(models.Model):
if not self.data:
return None
try:
- return bytes(self.data, 'utf-8')
+ return self.data.decode('utf-8')
except UnicodeDecodeError:
return None
@@ -367,7 +368,7 @@ class AutoSyncRecord(models.Model):
related_name='+'
)
object_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
on_delete=models.CASCADE,
related_name='+'
)
@@ -377,6 +378,8 @@ class AutoSyncRecord(models.Model):
fk_field='object_id'
)
+ _netbox_private = True
+
class Meta:
constraints = (
models.UniqueConstraint(
diff --git a/netbox/core/models/files.py b/netbox/core/models/files.py
index 38d82463e..5a321bdc3 100644
--- a/netbox/core/models/files.py
+++ b/netbox/core/models/files.py
@@ -2,6 +2,7 @@ import logging
import os
from django.conf import settings
+from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext as _
@@ -44,6 +45,7 @@ class ManagedFile(SyncedDataMixin, models.Model):
)
objects = RestrictedQuerySet.as_manager()
+ _netbox_private = True
class Meta:
ordering = ('file_root', 'file_path')
@@ -84,6 +86,14 @@ class ManagedFile(SyncedDataMixin, models.Model):
self.file_path = os.path.basename(self.data_path)
self.data_file.write_to_disk(self.full_path, overwrite=True)
+ def clean(self):
+ super().clean()
+
+ # Ensure that the file root and path make a unique pair
+ if self._meta.model.objects.filter(file_root=self.file_root, file_path=self.file_path).exclude(pk=self.pk).exists():
+ raise ValidationError(
+ f"A {self._meta.verbose_name.lower()} with this file path already exists ({self.file_root}/{self.file_path}).")
+
def delete(self, *args, **kwargs):
# Delete file from disk
try:
diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py
index 61b0e64fa..7cc62a15a 100644
--- a/netbox/core/models/jobs.py
+++ b/netbox/core/models/jobs.py
@@ -3,7 +3,7 @@ import uuid
import django_rq
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.urls import reverse
@@ -11,12 +11,13 @@ from django.utils import timezone
from django.utils.translation import gettext as _
from core.choices import JobStatusChoices
+from core.models import ContentType
+from core.signals import job_end, job_start
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
-from extras.utils import FeatureQuery
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, get_rq_retry
+from utilities.rqworker import get_queue_for_model
__all__ = (
'Job',
@@ -28,9 +29,8 @@ class Job(models.Model):
Tracks the lifecycle of a job which represents a background task (e.g. the execution of a custom script).
"""
object_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
related_name='jobs',
- limit_choices_to=FeatureQuery('jobs'),
on_delete=models.CASCADE,
)
object_id = models.PositiveBigIntegerField(
@@ -92,6 +92,11 @@ class Job(models.Model):
null=True,
blank=True
)
+ error = models.TextField(
+ verbose_name=_('error'),
+ editable=False,
+ blank=True
+ )
job_id = models.UUIDField(
verbose_name=_('job ID'),
unique=True
@@ -101,6 +106,9 @@ class Job(models.Model):
class Meta:
ordering = ['-created']
+ indexes = (
+ models.Index(fields=('object_type', 'object_id')),
+ )
verbose_name = _('job')
verbose_name_plural = _('jobs')
@@ -118,6 +126,15 @@ class Job(models.Model):
def get_status_color(self):
return JobStatusChoices.colors.get(self.status)
+ def clean(self):
+ super().clean()
+
+ # Validate the assigned object type
+ if self.object_type not in ContentType.objects.with_feature('jobs'):
+ raise ValidationError(
+ _("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type)
+ )
+
@property
def duration(self):
if not self.completed:
@@ -155,10 +172,10 @@ class Job(models.Model):
self.status = JobStatusChoices.STATUS_RUNNING
self.save()
- # Handle webhooks
- self.trigger_webhooks(event=EVENT_JOB_START)
+ # Send signal
+ job_start.send(self)
- def terminate(self, status=JobStatusChoices.STATUS_COMPLETED):
+ def terminate(self, status=JobStatusChoices.STATUS_COMPLETED, error=None):
"""
Mark the job as completed, optionally specifying a particular termination status.
"""
@@ -168,11 +185,13 @@ class Job(models.Model):
# Mark the job as completed
self.status = status
+ if error:
+ self.error = error
self.completed = timezone.now()
self.save()
- # Handle webhooks
- self.trigger_webhooks(event=EVENT_JOB_END)
+ # Send signal
+ job_end.send(self)
@classmethod
def enqueue(cls, func, instance, name='', user=None, schedule_at=None, interval=None, **kwargs):
@@ -208,28 +227,3 @@ class Job(models.Model):
queue.enqueue(func, job_id=str(job.job_id), job=job, **kwargs)
return job
-
- def trigger_webhooks(self, event):
- from extras.models import Webhook
-
- rq_queue_name = get_config().QUEUE_MAPPINGS.get('webhook', RQ_QUEUE_DEFAULT)
- rq_queue = django_rq.get_queue(rq_queue_name, is_async=False)
-
- # Fetch any webhooks matching this object type and action
- webhooks = Webhook.objects.filter(
- **{f'type_{event}': True},
- content_types=self.object_type,
- enabled=True
- )
-
- for webhook in webhooks:
- rq_queue.enqueue(
- "extras.webhooks_worker.process_webhook",
- webhook=webhook,
- model_name=self.object_type.model,
- event=event,
- data=self.data,
- timestamp=str(timezone.now()),
- username=self.user.username,
- retry=get_rq_retry()
- )
diff --git a/netbox/core/search.py b/netbox/core/search.py
index e6d3005e6..158911e6a 100644
--- a/netbox/core/search.py
+++ b/netbox/core/search.py
@@ -11,6 +11,7 @@ class DataSourceIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('type', 'status', 'description')
@register_search
@@ -19,3 +20,4 @@ class DataFileIndex(SearchIndex):
fields = (
('path', 200),
)
+ display_attrs = ('source',)
diff --git a/netbox/core/signals.py b/netbox/core/signals.py
index a39a87c6a..f884a27b4 100644
--- a/netbox/core/signals.py
+++ b/netbox/core/signals.py
@@ -1,10 +1,19 @@
+from django.db.models.signals import post_save
from django.dispatch import Signal, receiver
+from .models import ConfigRevision
+
__all__ = (
+ 'job_end',
+ 'job_start',
'post_sync',
'pre_sync',
)
+# Job signals
+job_start = Signal()
+job_end = Signal()
+
# DataSource signals
pre_sync = Signal()
post_sync = Signal()
@@ -19,3 +28,11 @@ def auto_sync(instance, **kwargs):
for autosync in AutoSyncRecord.objects.filter(datafile__source=instance).prefetch_related('object'):
autosync.object.sync(save=True)
+
+
+@receiver(post_save, sender=ConfigRevision)
+def update_config(sender, instance, **kwargs):
+ """
+ Update the cached NetBox configuration when a new ConfigRevision is created.
+ """
+ instance.activate()
diff --git a/netbox/core/tables/__init__.py b/netbox/core/tables/__init__.py
index 052f68b68..69f9d8a48 100644
--- a/netbox/core/tables/__init__.py
+++ b/netbox/core/tables/__init__.py
@@ -1,2 +1,3 @@
+from .config import *
from .data import *
from .jobs import *
diff --git a/netbox/core/tables/columns.py b/netbox/core/tables/columns.py
new file mode 100644
index 000000000..93f1e3901
--- /dev/null
+++ b/netbox/core/tables/columns.py
@@ -0,0 +1,20 @@
+import django_tables2 as tables
+
+from netbox.registry import registry
+
+__all__ = (
+ 'BackendTypeColumn',
+)
+
+
+class BackendTypeColumn(tables.Column):
+ """
+ Display a data backend type.
+ """
+ def render(self, value):
+ if backend := registry['data_backends'].get(value):
+ return backend.label
+ return value
+
+ def value(self, value):
+ return value
diff --git a/netbox/core/tables/config.py b/netbox/core/tables/config.py
new file mode 100644
index 000000000..9d4cb6393
--- /dev/null
+++ b/netbox/core/tables/config.py
@@ -0,0 +1,33 @@
+from django.utils.translation import gettext_lazy as _
+
+from core.models import ConfigRevision
+from netbox.tables import NetBoxTable, columns
+
+__all__ = (
+ 'ConfigRevisionTable',
+)
+
+REVISION_BUTTONS = """
+{% if not record.is_active %}
+
+
+
+{% endif %}
+"""
+
+
+class ConfigRevisionTable(NetBoxTable):
+ is_active = columns.BooleanColumn(
+ verbose_name=_('Is Active'),
+ )
+ actions = columns.ActionsColumn(
+ actions=('delete',),
+ extra_buttons=REVISION_BUTTONS
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = ConfigRevision
+ fields = (
+ 'pk', 'id', 'is_active', 'created', 'comment',
+ )
+ default_columns = ('pk', 'id', 'is_active', 'created', 'comment')
diff --git a/netbox/core/tables/data.py b/netbox/core/tables/data.py
index 1ecc42369..4059ea9bc 100644
--- a/netbox/core/tables/data.py
+++ b/netbox/core/tables/data.py
@@ -3,6 +3,7 @@ import django_tables2 as tables
from core.models import *
from netbox.tables import NetBoxTable, columns
+from .columns import BackendTypeColumn
__all__ = (
'DataFileTable',
@@ -15,8 +16,8 @@ class DataSourceTable(NetBoxTable):
verbose_name=_('Name'),
linkify=True
)
- type = columns.ChoiceFieldColumn(
- verbose_name=_('Type'),
+ type = BackendTypeColumn(
+ verbose_name=_('Type')
)
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
@@ -34,8 +35,8 @@ class DataSourceTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = DataSource
fields = (
- 'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'comments', 'parameters', 'created',
- 'last_updated', 'file_count',
+ 'pk', 'id', 'name', 'type', 'status', 'enabled', 'source_url', 'description', 'comments', 'parameters',
+ 'created', 'last_updated', 'file_count',
)
default_columns = ('pk', 'name', 'type', 'status', 'enabled', 'description', 'file_count')
diff --git a/netbox/core/tables/jobs.py b/netbox/core/tables/jobs.py
index 32ca67f7f..ac27224b3 100644
--- a/netbox/core/tables/jobs.py
+++ b/netbox/core/tables/jobs.py
@@ -19,7 +19,8 @@ class JobTable(NetBoxTable):
)
object = tables.Column(
verbose_name=_('Object'),
- linkify=True
+ linkify=True,
+ orderable=False
)
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
@@ -47,7 +48,7 @@ class JobTable(NetBoxTable):
model = Job
fields = (
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'scheduled', 'interval', 'started',
- 'completed', 'user', 'job_id',
+ 'completed', 'user', 'error', 'job_id',
)
default_columns = (
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
diff --git a/netbox/core/tests/test_api.py b/netbox/core/tests/test_api.py
index dc6d6a5ce..cd25761f0 100644
--- a/netbox/core/tests/test_api.py
+++ b/netbox/core/tests/test_api.py
@@ -2,7 +2,6 @@ from django.urls import reverse
from django.utils import timezone
from utilities.testing import APITestCase, APIViewTestCases
-from ..choices import *
from ..models import *
@@ -26,26 +25,26 @@ class DataSourceTest(APIViewTestCases.APIViewTestCase):
@classmethod
def setUpTestData(cls):
data_sources = (
- DataSource(name='Data Source 1', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source1/'),
- DataSource(name='Data Source 2', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source2/'),
- DataSource(name='Data Source 3', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source3/'),
+ DataSource(name='Data Source 1', type='local', source_url='file:///var/tmp/source1/'),
+ DataSource(name='Data Source 2', type='local', source_url='file:///var/tmp/source2/'),
+ DataSource(name='Data Source 3', type='local', source_url='file:///var/tmp/source3/'),
)
DataSource.objects.bulk_create(data_sources)
cls.create_data = [
{
'name': 'Data Source 4',
- 'type': DataSourceTypeChoices.GIT,
+ 'type': 'git',
'source_url': 'https://example.com/git/source4'
},
{
'name': 'Data Source 5',
- 'type': DataSourceTypeChoices.GIT,
+ 'type': 'git',
'source_url': 'https://example.com/git/source5'
},
{
'name': 'Data Source 6',
- 'type': DataSourceTypeChoices.GIT,
+ 'type': 'git',
'source_url': 'https://example.com/git/source6'
},
]
@@ -63,7 +62,7 @@ class DataFileTest(
def setUpTestData(cls):
datasource = DataSource.objects.create(
name='Data Source 1',
- type=DataSourceTypeChoices.LOCAL,
+ type='local',
source_url='file:///var/tmp/source1/'
)
diff --git a/netbox/core/tests/test_filtersets.py b/netbox/core/tests/test_filtersets.py
index e1e916f70..e6e52a8b3 100644
--- a/netbox/core/tests/test_filtersets.py
+++ b/netbox/core/tests/test_filtersets.py
@@ -18,21 +18,23 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
data_sources = (
DataSource(
name='Data Source 1',
- type=DataSourceTypeChoices.LOCAL,
+ type='local',
source_url='file:///var/tmp/source1/',
status=DataSourceStatusChoices.NEW,
- enabled=True
+ enabled=True,
+ description='foobar1'
),
DataSource(
name='Data Source 2',
- type=DataSourceTypeChoices.LOCAL,
+ type='local',
source_url='file:///var/tmp/source2/',
status=DataSourceStatusChoices.SYNCING,
- enabled=True
+ enabled=True,
+ description='foobar2'
),
DataSource(
name='Data Source 3',
- type=DataSourceTypeChoices.GIT,
+ type='git',
source_url='https://example.com/git/source3',
status=DataSourceStatusChoices.COMPLETED,
enabled=False
@@ -40,12 +42,20 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
)
DataSource.objects.bulk_create(data_sources)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Data Source 1', 'Data Source 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_type(self):
- params = {'type': [DataSourceTypeChoices.LOCAL]}
+ params = {'type': ['local']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_enabled(self):
@@ -66,9 +76,9 @@ class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod
def setUpTestData(cls):
data_sources = (
- DataSource(name='Data Source 1', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source1/'),
- DataSource(name='Data Source 2', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source2/'),
- DataSource(name='Data Source 3', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source3/'),
+ DataSource(name='Data Source 1', type='local', source_url='file:///var/tmp/source1/'),
+ DataSource(name='Data Source 2', type='local', source_url='file:///var/tmp/source2/'),
+ DataSource(name='Data Source 3', type='local', source_url='file:///var/tmp/source3/'),
)
DataSource.objects.bulk_create(data_sources)
@@ -97,6 +107,10 @@ class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
)
DataFile.objects.bulk_create(data_files)
+ def test_q(self):
+ params = {'q': 'file1.txt'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_source(self):
sources = DataSource.objects.all()
params = {'source_id': [sources[0].pk, sources[1].pk]}
diff --git a/netbox/core/tests/test_views.py b/netbox/core/tests/test_views.py
index 4a50a8d05..16d07f376 100644
--- a/netbox/core/tests/test_views.py
+++ b/netbox/core/tests/test_views.py
@@ -1,7 +1,6 @@
from django.utils import timezone
from utilities.testing import ViewTestCases, create_tags
-from ..choices import *
from ..models import *
@@ -11,9 +10,9 @@ class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@classmethod
def setUpTestData(cls):
data_sources = (
- DataSource(name='Data Source 1', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source1/'),
- DataSource(name='Data Source 2', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source2/'),
- DataSource(name='Data Source 3', type=DataSourceTypeChoices.LOCAL, source_url='file:///var/tmp/source3/'),
+ DataSource(name='Data Source 1', type='local', source_url='file:///var/tmp/source1/'),
+ DataSource(name='Data Source 2', type='local', source_url='file:///var/tmp/source2/'),
+ DataSource(name='Data Source 3', type='local', source_url='file:///var/tmp/source3/'),
)
DataSource.objects.bulk_create(data_sources)
@@ -21,7 +20,7 @@ class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.form_data = {
'name': 'Data Source X',
- 'type': DataSourceTypeChoices.GIT,
+ 'type': 'git',
'source_url': 'http:///exmaple/com/foo/bar/',
'description': 'Something',
'comments': 'Foo bar baz',
@@ -29,10 +28,10 @@ class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
- f"name,type,source_url,enabled",
- f"Data Source 4,{DataSourceTypeChoices.LOCAL},file:///var/tmp/source4/,true",
- f"Data Source 5,{DataSourceTypeChoices.LOCAL},file:///var/tmp/source4/,true",
- f"Data Source 6,{DataSourceTypeChoices.GIT},http:///exmaple/com/foo/bar/,false",
+ "name,type,source_url,enabled",
+ "Data Source 4,local,file:///var/tmp/source4/,true",
+ "Data Source 5,local,file:///var/tmp/source4/,true",
+ "Data Source 6,git,http:///exmaple/com/foo/bar/,false",
)
cls.csv_update_data = (
@@ -60,7 +59,7 @@ class DataFileTestCase(
def setUpTestData(cls):
datasource = DataSource.objects.create(
name='Data Source 1',
- type=DataSourceTypeChoices.LOCAL,
+ type='local',
source_url='file:///var/tmp/source1/'
)
diff --git a/netbox/core/urls.py b/netbox/core/urls.py
index 1bd56c92b..77c0d3194 100644
--- a/netbox/core/urls.py
+++ b/netbox/core/urls.py
@@ -25,4 +25,14 @@ urlpatterns = (
path('jobs//', views.JobView.as_view(), name='job'),
path('jobs//delete/', views.JobDeleteView.as_view(), name='job_delete'),
+ # Config revisions
+ path('config-revisions/', views.ConfigRevisionListView.as_view(), name='configrevision_list'),
+ path('config-revisions/add/', views.ConfigRevisionEditView.as_view(), name='configrevision_add'),
+ path('config-revisions/delete/', views.ConfigRevisionBulkDeleteView.as_view(), name='configrevision_bulk_delete'),
+ path('config-revisions//restore/', views.ConfigRevisionRestoreView.as_view(), name='configrevision_restore'),
+ path('config-revisions//', include(get_model_urls('core', 'configrevision'))),
+
+ # Configuration
+ path('config/', views.ConfigView.as_view(), name='config'),
+
)
diff --git a/netbox/core/views.py b/netbox/core/views.py
index d3dc2b1c2..537c33d9d 100644
--- a/netbox/core/views.py
+++ b/netbox/core/views.py
@@ -1,10 +1,14 @@
from django.contrib import messages
-from django.shortcuts import get_object_or_404, redirect
+from django.core.cache import cache
+from django.http import HttpResponseForbidden
+from django.shortcuts import get_object_or_404, redirect, render
+from django.views.generic import View
+from netbox.config import get_config, PARAMS
from netbox.views import generic
from netbox.views.generic.base import BaseObjectView
from utilities.utils import count_related
-from utilities.views import register_model_view
+from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
from . import filtersets, forms, tables
from .models import *
@@ -98,7 +102,9 @@ class DataFileListView(generic.ObjectListView):
filterset = filtersets.DataFileFilterSet
filterset_form = forms.DataFileFilterForm
table = tables.DataFileTable
- actions = ('bulk_delete',)
+ actions = {
+ 'bulk_delete': {'delete'},
+ }
@register_model_view(DataFile)
@@ -126,7 +132,10 @@ class JobListView(generic.ObjectListView):
filterset = filtersets.JobFilterSet
filterset_form = forms.JobFilterForm
table = tables.JobTable
- actions = ('export', 'delete', 'bulk_delete')
+ actions = {
+ 'export': {'view'},
+ 'bulk_delete': {'delete'},
+ }
class JobView(generic.ObjectView):
@@ -141,3 +150,85 @@ class JobBulkDeleteView(generic.BulkDeleteView):
queryset = Job.objects.all()
filterset = filtersets.JobFilterSet
table = tables.JobTable
+
+
+#
+# Config Revisions
+#
+
+class ConfigView(generic.ObjectView):
+ queryset = ConfigRevision.objects.all()
+
+ def get_object(self, **kwargs):
+ revision_id = cache.get('config_version')
+ try:
+ return ConfigRevision.objects.get(pk=revision_id)
+ except ConfigRevision.DoesNotExist:
+ # Fall back to using the active config data if no record is found
+ return ConfigRevision(
+ data=get_config()
+ )
+
+
+class ConfigRevisionListView(generic.ObjectListView):
+ queryset = ConfigRevision.objects.all()
+ filterset = filtersets.ConfigRevisionFilterSet
+ filterset_form = forms.ConfigRevisionFilterForm
+ table = tables.ConfigRevisionTable
+
+
+@register_model_view(ConfigRevision)
+class ConfigRevisionView(generic.ObjectView):
+ queryset = ConfigRevision.objects.all()
+
+
+class ConfigRevisionEditView(generic.ObjectEditView):
+ queryset = ConfigRevision.objects.all()
+ form = forms.ConfigRevisionForm
+
+
+@register_model_view(ConfigRevision, 'delete')
+class ConfigRevisionDeleteView(generic.ObjectDeleteView):
+ queryset = ConfigRevision.objects.all()
+
+
+class ConfigRevisionBulkDeleteView(generic.BulkDeleteView):
+ queryset = ConfigRevision.objects.all()
+ filterset = filtersets.ConfigRevisionFilterSet
+ table = tables.ConfigRevisionTable
+
+
+class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
+
+ def get_required_permission(self):
+ return 'core.configrevision_edit'
+
+ def get(self, request, pk):
+ candidate_config = get_object_or_404(ConfigRevision, pk=pk)
+
+ # Get the current ConfigRevision
+ config_version = get_config().version
+ current_config = ConfigRevision.objects.filter(pk=config_version).first()
+
+ params = []
+ for param in PARAMS:
+ params.append((
+ param.name,
+ current_config.data.get(param.name, None),
+ candidate_config.data.get(param.name, None)
+ ))
+
+ return render(request, 'core/configrevision_restore.html', {
+ 'object': candidate_config,
+ 'params': params,
+ })
+
+ def post(self, request, pk):
+ if not request.user.has_perm('core.configrevision_edit'):
+ return HttpResponseForbidden()
+
+ candidate_config = get_object_or_404(ConfigRevision, pk=pk)
+ candidate_config.activate()
+ messages.success(request, f"Restored configuration revision #{pk}")
+
+ return redirect(candidate_config.get_absolute_url())
diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index 341a1064a..09933f2de 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -2,8 +2,8 @@ import decimal
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
-from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from timezone_field.rest_framework import TimeZoneSerializerField
@@ -12,8 +12,7 @@ from dcim.constants import *
from dcim.models import *
from extras.api.nested_serializers import NestedConfigTemplateSerializer
from ipam.api.nested_serializers import (
- NestedASNSerializer, NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer,
- NestedVRFSerializer,
+ NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
)
from ipam.models import ASN, VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
@@ -27,6 +26,7 @@ from tenancy.api.nested_serializers import NestedTenantSerializer
from users.api.nested_serializers import NestedUserSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import NestedClusterSerializer
+from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer
from wireless.choices import *
from wireless.models import WirelessLAN
@@ -343,9 +343,9 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
model = DeviceType
fields = [
'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height',
- 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image',
- 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
- 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count',
+ 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
+ 'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+ 'device_count', 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count',
'power_outlet_template_count', 'interface_template_count', 'front_port_template_count',
'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count',
'inventory_item_template_count',
@@ -738,12 +738,12 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
class Meta(DeviceSerializer.Meta):
fields = [
'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial',
- 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow',
- 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
- 'vc_priority', 'description', 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context',
- 'config_template', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
- 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
- 'device_bay_count', 'module_bay_count', 'inventory_item_count',
+ 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
+ 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
+ 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context',
+ 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
+ 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
+ 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
]
@extend_schema_field(serializers.JSONField(allow_null=True))
@@ -758,6 +758,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True)
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
+ status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
# Related object counts
interface_count = serializers.IntegerField(read_only=True)
@@ -786,10 +787,6 @@ class ModuleSerializer(NetBoxModelSerializer):
]
-class DeviceNAPALMSerializer(serializers.Serializer):
- method = serializers.JSONField()
-
-
#
# Device components
#
diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py
index f045f1bb4..cd5a297c9 100644
--- a/netbox/dcim/api/views.py
+++ b/netbox/dcim/api/views.py
@@ -3,10 +3,8 @@ from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, OpenApiParameter
from rest_framework.decorators import action
-from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.routers import APIRootView
-from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.viewsets import ViewSet
from circuits.models import Circuit
@@ -14,16 +12,16 @@ from dcim import filtersets
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
from dcim.models import *
from dcim.svg import CableTraceSVG
-from extras.api.mixins import ConfigContextQuerySetMixin, ConfigTemplateRenderMixin
+from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
from ipam.models import Prefix, VLAN
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import StripCountAnnotationsPaginator
-from netbox.api.renderers import TextRenderer
-from netbox.api.viewsets import NetBoxModelViewSet
+from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
from netbox.constants import NESTED_SERIALIZER_PREFIX
from utilities.api import get_serializer_for_model
+from utilities.query_functions import CollateAsChar
from utilities.utils import count_related
from virtualization.models import VirtualMachine
from . import serializers
@@ -98,7 +96,7 @@ class PassThroughPortMixin(object):
# Regions
#
-class RegionViewSet(NetBoxModelViewSet):
+class RegionViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = Region.objects.add_related_count(
Region.objects.all(),
Site,
@@ -114,7 +112,7 @@ class RegionViewSet(NetBoxModelViewSet):
# Site groups
#
-class SiteGroupViewSet(NetBoxModelViewSet):
+class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = SiteGroup.objects.add_related_count(
SiteGroup.objects.all(),
Site,
@@ -149,7 +147,7 @@ class SiteViewSet(NetBoxModelViewSet):
# Locations
#
-class LocationViewSet(NetBoxModelViewSet):
+class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = Location.objects.add_related_count(
Location.objects.add_related_count(
Location.objects.all(),
@@ -350,7 +348,7 @@ class DeviceBayTemplateViewSet(NetBoxModelViewSet):
filterset_class = filtersets.DeviceBayTemplateFilterSet
-class InventoryItemTemplateViewSet(NetBoxModelViewSet):
+class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
serializer_class = serializers.InventoryItemTemplateSerializer
filterset_class = filtersets.InventoryItemTemplateFilterSet
@@ -389,7 +387,7 @@ class PlatformViewSet(NetBoxModelViewSet):
class DeviceViewSet(
SequentialBulkCreatesMixin,
ConfigContextQuerySetMixin,
- ConfigTemplateRenderMixin,
+ RenderConfigMixin,
NetBoxModelViewSet
):
queryset = Device.objects.prefetch_related(
@@ -419,23 +417,6 @@ class DeviceViewSet(
return serializers.DeviceWithConfigContextSerializer
- @action(detail=True, methods=['post'], url_path='render-config', renderer_classes=[JSONRenderer, TextRenderer])
- def render_config(self, request, pk):
- """
- Resolve and render the preferred ConfigTemplate for this Device.
- """
- device = self.get_object()
- configtemplate = device.get_config_template()
- if not configtemplate:
- return Response({'error': 'No config template found for this device.'}, status=HTTP_400_BAD_REQUEST)
-
- # Compile context data
- context_data = device.get_config_context()
- context_data.update(request.data)
- context_data.update({'device': device})
-
- return self.render_configtemplate(request, configtemplate, context_data)
-
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
queryset = VirtualDeviceContext.objects.prefetch_related(
@@ -505,6 +486,10 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
filterset_class = filtersets.InterfaceFilterSet
brief_prefetch_fields = ['device']
+ def get_bulk_destroy_queryset(self):
+ # Ensure child interfaces are deleted prior to their parents
+ return self.get_queryset().order_by('device', 'parent', CollateAsChar('_name'))
+
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = FrontPort.objects.prefetch_related(
@@ -538,7 +523,7 @@ class DeviceBayViewSet(NetBoxModelViewSet):
brief_prefetch_fields = ['device']
-class InventoryItemViewSet(NetBoxModelViewSet):
+class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
serializer_class = serializers.InventoryItemSerializer
filterset_class = filtersets.InventoryItemFilterSet
diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py
index 1bcf61b20..2ba24e0aa 100644
--- a/netbox/dcim/choices.py
+++ b/netbox/dcim/choices.py
@@ -80,10 +80,10 @@ class RackWidthChoices(ChoiceSet):
WIDTH_23IN = 23
CHOICES = (
- (WIDTH_10IN, _('10 inches')),
- (WIDTH_19IN, _('19 inches')),
- (WIDTH_21IN, _('21 inches')),
- (WIDTH_23IN, _('23 inches')),
+ (WIDTH_10IN, _('{n} inches').format(n=10)),
+ (WIDTH_19IN, _('{n} inches').format(n=19)),
+ (WIDTH_21IN, _('{n} inches').format(n=21)),
+ (WIDTH_23IN, _('{n} inches').format(n=23)),
)
@@ -837,8 +837,10 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
TYPE_200GE_QSFP_DD = '200gbase-x-qsfpdd'
TYPE_400GE_CFP2 = '400gbase-x-cfp2'
+ TYPE_400GE_QSFP112 = '400gbase-x-qsfp112'
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
TYPE_400GE_OSFP = '400gbase-x-osfp'
+ TYPE_400GE_OSFP_RHS = '400gbase-x-osfp-rhs'
TYPE_400GE_CDFP = '400gbase-x-cdfp'
TYPE_400GE_CFP8 = '400gbase-x-cfp8'
TYPE_800GE_QSFP_DD = '800gbase-x-qsfpdd'
@@ -989,8 +991,10 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_100GE_QSFP_DD, 'QSFP-DD (100GE)'),
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
(TYPE_200GE_QSFP_DD, 'QSFP-DD (200GE)'),
+ (TYPE_400GE_QSFP112, 'QSFP112 (400GE)'),
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
+ (TYPE_400GE_OSFP_RHS, 'OSFP-RHS (400GE)'),
(TYPE_400GE_CDFP, 'CDFP (400GE)'),
(TYPE_400GE_CFP8, 'CPF8 (400GE)'),
(TYPE_800GE_QSFP_DD, 'QSFP-DD (800GE)'),
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index f7ae8591f..68edc93f6 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -1,10 +1,13 @@
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 circuits.models import CircuitTermination
from extras.filtersets import LocalConfigContextFilterSet
from extras.models import ConfigTemplate
-from ipam.models import ASN, L2VPN, IPAddress, VRF
+from ipam.filtersets import PrimaryIPFilterSet
+from ipam.models import ASN, IPAddress, VRF
from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
)
@@ -16,6 +19,7 @@ from utilities.filters import (
TreeNodeMultipleChoiceFilter,
)
from virtualization.models import Cluster
+from vpn.models import L2VPN
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
from .choices import *
from .constants import *
@@ -324,7 +328,7 @@ 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'
+ 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
]
def search(self, queryset, name, value):
@@ -335,6 +339,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
Q(facility_id__icontains=value) |
Q(serial__icontains=value.strip()) |
Q(asset_tag__icontains=value.strip()) |
+ Q(description__icontains=value) |
Q(comments__icontains=value)
)
@@ -496,7 +501,8 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
class Meta:
model = DeviceType
fields = [
- 'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
+ 'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
+ 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'description',
]
def search(self, queryset, name, value):
@@ -506,6 +512,7 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
Q(manufacturer__name__icontains=value) |
Q(model__icontains=value) |
Q(part_number__icontains=value) |
+ Q(description__icontains=value) |
Q(comments__icontains=value)
)
@@ -590,7 +597,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
class Meta:
model = ModuleType
- fields = ['id', 'model', 'part_number', 'weight', 'weight_unit']
+ fields = ['id', 'model', 'part_number', 'weight', 'weight_unit', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -599,6 +606,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
Q(manufacturer__name__icontains=value) |
Q(model__icontains=value) |
Q(part_number__icontains=value) |
+ Q(description__icontains=value) |
Q(comments__icontains=value)
)
@@ -638,7 +646,10 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
def search(self, queryset, name, value):
if not value.strip():
return queryset
- return queryset.filter(name__icontains=value)
+ return queryset.filter(
+ Q(name__icontains=value) |
+ Q(description__icontains=value)
+ )
class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
@@ -653,21 +664,21 @@ class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
class Meta:
model = ConsolePortTemplate
- fields = ['id', 'name', 'type']
+ fields = ['id', 'name', 'type', 'description']
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta:
model = ConsoleServerPortTemplate
- fields = ['id', 'name', 'type']
+ fields = ['id', 'name', 'type', 'description']
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta:
model = PowerPortTemplate
- fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
+ fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@@ -678,7 +689,7 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
class Meta:
model = PowerOutletTemplate
- fields = ['id', 'name', 'type', 'feed_leg']
+ fields = ['id', 'name', 'type', 'feed_leg', 'description']
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@@ -702,7 +713,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
class Meta:
model = InterfaceTemplate
- fields = ['id', 'name', 'type', 'enabled', 'mgmt_only']
+ fields = ['id', 'name', 'type', 'enabled', 'mgmt_only', 'description']
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@@ -713,7 +724,7 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
class Meta:
model = FrontPortTemplate
- fields = ['id', 'name', 'type', 'color']
+ fields = ['id', 'name', 'type', 'color', 'description']
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@@ -724,21 +735,21 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
class Meta:
model = RearPortTemplate
- fields = ['id', 'name', 'type', 'color', 'positions']
+ fields = ['id', 'name', 'type', 'color', 'positions', 'description']
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta:
model = ModuleBayTemplate
- fields = ['id', 'name']
+ fields = ['id', 'name', 'description']
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta:
model = DeviceBayTemplate
- fields = ['id', 'name']
+ fields = ['id', 'name', 'description']
class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
@@ -771,7 +782,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
class Meta:
model = InventoryItemTemplate
- fields = ['id', 'name', 'label', 'part_id']
+ fields = ['id', 'name', 'label', 'part_id', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -817,7 +828,13 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
fields = ['id', 'name', 'slug', 'description']
-class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
+class DeviceFilterSet(
+ NetBoxModelFilterSet,
+ TenancyFilterSet,
+ ContactModelFilterSet,
+ LocalConfigContextFilterSet,
+ PrimaryIPFilterSet,
+):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='device_type__manufacturer',
queryset=Manufacturer.objects.all(),
@@ -993,16 +1010,6 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
method='_device_bays',
label=_('Has device bays'),
)
- primary_ip4_id = django_filters.ModelMultipleChoiceFilter(
- field_name='primary_ip4',
- queryset=IPAddress.objects.all(),
- label=_('Primary IPv4 (ID)'),
- )
- primary_ip6_id = django_filters.ModelMultipleChoiceFilter(
- field_name='primary_ip6',
- queryset=IPAddress.objects.all(),
- label=_('Primary IPv6 (ID)'),
- )
oob_ip_id = django_filters.ModelMultipleChoiceFilter(
field_name='oob_ip',
queryset=IPAddress.objects.all(),
@@ -1011,7 +1018,10 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
class Meta:
model = Device
- fields = ['id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority']
+ fields = [
+ 'id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority',
+ 'description',
+ ]
def search(self, queryset, name, value):
if not value.strip():
@@ -1021,6 +1031,7 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
Q(serial__icontains=value.strip()) |
Q(inventoryitems__serial__icontains=value.strip()) |
Q(asset_tag__icontains=value.strip()) |
+ Q(description__icontains=value.strip()) |
Q(comments__icontains=value) |
Q(primary_ip4__address__startswith=value) |
Q(primary_ip6__address__startswith=value)
@@ -1069,7 +1080,7 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
return queryset.exclude(devicebays__isnull=value)
-class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
+class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, PrimaryIPFilterSet):
device_id = django_filters.ModelMultipleChoiceFilter(
field_name='device',
queryset=Device.objects.all(),
@@ -1090,13 +1101,16 @@ class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class Meta:
model = VirtualDeviceContext
- fields = ['id', 'device', 'name']
+ fields = ['id', 'device', 'name', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
- qs_filter = Q(name__icontains=value)
+ qs_filter = (
+ Q(name__icontains=value) |
+ Q(description__icontains=value)
+ )
try:
qs_filter |= Q(identifier=int(value))
except ValueError:
@@ -1153,7 +1167,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
class Meta:
model = Module
- fields = ['id', 'status', 'asset_tag']
+ fields = ['id', 'status', 'asset_tag', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -1162,6 +1176,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
Q(device__name__icontains=value.strip()) |
Q(serial__icontains=value.strip()) |
Q(asset_tag__icontains=value.strip()) |
+ Q(description__icontains=value) |
Q(comments__icontains=value)
).distinct()
@@ -1462,17 +1477,15 @@ class InterfaceFilterSet(
PathEndpointFilterSet,
CommonInterfaceFilterSet
):
- # Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
- # members
- device = MultiValueCharFilter(
- method='filter_device',
+ virtual_chassis_member = MultiValueCharFilter(
+ method='filter_virtual_chassis_member',
field_name='name',
- label=_('Device'),
+ label=_('Virtual Chassis Interfaces for Device')
)
- device_id = MultiValueNumberFilter(
- method='filter_device_id',
+ virtual_chassis_member_id = MultiValueNumberFilter(
+ method='filter_virtual_chassis_member',
field_name='pk',
- label=_('Device (ID)'),
+ label=_('Virtual Chassis Interfaces for Device (ID)')
)
kind = django_filters.CharFilter(
method='filter_kind',
@@ -1540,23 +1553,11 @@ class InterfaceFilterSet(
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end',
]
- def filter_device(self, queryset, name, value):
+ def filter_virtual_chassis_member(self, queryset, name, value):
try:
- devices = Device.objects.filter(**{'{}__in'.format(name): value})
vc_interface_ids = []
- for device in devices:
- vc_interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
- return queryset.filter(pk__in=vc_interface_ids)
- except Device.DoesNotExist:
- return queryset.none()
-
- def filter_device_id(self, queryset, name, id_list):
- # Include interfaces belonging to peer virtual chassis members
- vc_interface_ids = []
- try:
- devices = Device.objects.filter(pk__in=id_list)
- for device in devices:
- vc_interface_ids += device.vc_interfaces(if_master=False).values_list('id', flat=True)
+ for device in Device.objects.filter(**{f'{name}__in': value}):
+ vc_interface_ids.extend(device.vc_interfaces(if_master=False).values_list('id', flat=True))
return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist:
return queryset.none()
@@ -1666,7 +1667,7 @@ class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
class Meta:
model = InventoryItemRole
- fields = ['id', 'name', 'slug', 'color']
+ fields = ['id', 'name', 'slug', 'color', 'description']
class VirtualChassisFilterSet(NetBoxModelFilterSet):
@@ -1731,13 +1732,14 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet):
class Meta:
model = VirtualChassis
- fields = ['id', 'domain', 'name']
+ fields = ['id', 'domain', 'name', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
+ Q(description__icontains=value) |
Q(members__name__icontains=value) |
Q(domain__icontains=value)
)
@@ -1759,6 +1761,10 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
method='filter_by_cable_end_b',
field_name='terminations__termination_id'
)
+ unterminated = django_filters.BooleanFilter(
+ method='_unterminated',
+ label=_('Unterminated'),
+ )
type = django_filters.MultipleChoiceFilter(
choices=CableTypeChoices
)
@@ -1800,14 +1806,47 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
field_name='site__slug'
)
+ # Termination object filters
+ consoleport_id = MultiValueNumberFilter(
+ method='filter_by_consoleport'
+ )
+ consoleserverport_id = MultiValueNumberFilter(
+ method='filter_by_consoleserverport'
+ )
+ powerport_id = MultiValueNumberFilter(
+ method='filter_by_powerport'
+ )
+ poweroutlet_id = MultiValueNumberFilter(
+ method='filter_by_poweroutlet'
+ )
+ interface_id = MultiValueNumberFilter(
+ method='filter_by_interface'
+ )
+ frontport_id = MultiValueNumberFilter(
+ method='filter_by_frontport'
+ )
+ rearport_id = MultiValueNumberFilter(
+ method='filter_by_rearport'
+ )
+ powerfeed_id = MultiValueNumberFilter(
+ method='filter_by_powerfeed'
+ )
+ circuittermination_id = MultiValueNumberFilter(
+ method='filter_by_circuittermination'
+ )
+
class Meta:
model = Cable
- fields = ['id', 'label', 'length', 'length_unit']
+ fields = ['id', 'label', 'length', 'length_unit', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
- return queryset.filter(label__icontains=value)
+ qs_filter = (
+ Q(label__icontains=value) |
+ Q(description__icontains=value)
+ )
+ return queryset.filter(qs_filter)
def filter_by_termination(self, queryset, name, value):
# Filter by a related object cached on CableTermination. Note the underscore preceding the field name.
@@ -1826,6 +1865,55 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
# Filter by termination id and cable_end type
return self.filter_by_cable_end(queryset, name, value, CableEndChoices.SIDE_B)
+ def _unterminated(self, queryset, name, value):
+ if value:
+ terminated_ids = (
+ queryset.filter(terminations__cable_end=CableEndChoices.SIDE_A)
+ .filter(terminations__cable_end=CableEndChoices.SIDE_B)
+ .values("id")
+ )
+ return queryset.exclude(id__in=terminated_ids)
+ else:
+ return queryset.filter(terminations__cable_end=CableEndChoices.SIDE_A).filter(
+ terminations__cable_end=CableEndChoices.SIDE_B
+ )
+
+ def filter_by_termination_object(self, queryset, model, value):
+ # Filter by specific termination object(s)
+ content_type = ContentType.objects.get_for_model(model)
+ cable_ids = CableTermination.objects.filter(
+ termination_type=content_type,
+ termination_id__in=value
+ ).values_list('cable', flat=True)
+ return queryset.filter(pk__in=cable_ids)
+
+ def filter_by_consoleport(self, queryset, name, value):
+ return self.filter_by_termination_object(queryset, ConsolePort, value)
+
+ def filter_by_consoleserverport(self, queryset, name, value):
+ return self.filter_by_termination_object(queryset, ConsoleServerPort, value)
+
+ def filter_by_powerport(self, queryset, name, value):
+ return self.filter_by_termination_object(queryset, PowerPort, value)
+
+ def filter_by_poweroutlet(self, queryset, name, value):
+ return self.filter_by_termination_object(queryset, PowerOutlet, value)
+
+ def filter_by_interface(self, queryset, name, value):
+ return self.filter_by_termination_object(queryset, Interface, value)
+
+ def filter_by_frontport(self, queryset, name, value):
+ return self.filter_by_termination_object(queryset, FrontPort, value)
+
+ def filter_by_rearport(self, queryset, name, value):
+ return self.filter_by_termination_object(queryset, RearPort, value)
+
+ def filter_by_powerfeed(self, queryset, name, value):
+ return self.filter_by_termination_object(queryset, PowerFeed, value)
+
+ def filter_by_circuittermination(self, queryset, name, value):
+ return self.filter_by_termination_object(queryset, CircuitTermination, value)
+
class CableTerminationFilterSet(BaseFilterSet):
termination_type = ContentTypeFilter()
@@ -1881,13 +1969,14 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class Meta:
model = PowerPanel
- fields = ['id', 'name']
+ fields = ['id', 'name', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
- Q(name__icontains=value)
+ Q(name__icontains=value) |
+ Q(description__icontains=value)
)
return queryset.filter(qs_filter)
@@ -1948,6 +2037,7 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
model = PowerFeed
fields = [
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'cable_end',
+ 'description',
]
def search(self, queryset, name, value):
@@ -1955,6 +2045,7 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
return queryset
qs_filter = (
Q(name__icontains=value) |
+ Q(description__icontains=value) |
Q(power_panel__name__icontains=value) |
Q(comments__icontains=value)
)
diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py
index 02aa5a3e4..2a84a9a51 100644
--- a/netbox/dcim/forms/bulk_create.py
+++ b/netbox/dcim/forms/bulk_create.py
@@ -1,9 +1,9 @@
from django import forms
+from django.utils.translation import gettext_lazy as _
from dcim.models import *
-from django.utils.translation import gettext_lazy as _
-from extras.forms import CustomFieldsMixin
from extras.models import Tag
+from netbox.forms.mixins import CustomFieldsMixin
from utilities.forms import BootstrapMixin, form_from_model
from utilities.forms.fields import DynamicModelMultipleChoiceField, ExpandableNameField
from .object_create import ComponentCreateForm
diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py
index cacf1f72b..68d8d4f89 100644
--- a/netbox/dcim/forms/bulk_edit.py
+++ b/netbox/dcim/forms/bulk_edit.py
@@ -412,7 +412,7 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
)
u_height = forms.IntegerField(
label=_('U height'),
- min_value=1,
+ min_value=0,
required=False
)
is_full_depth = forms.NullBooleanField(
@@ -420,6 +420,11 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
widget=BulkEditNullBooleanSelect(),
label=_('Is full depth')
)
+ exclude_from_utilization = forms.NullBooleanField(
+ required=False,
+ widget=BulkEditNullBooleanSelect(),
+ label=_('Exclude from utilization')
+ )
airflow = forms.ChoiceField(
label=_('Airflow'),
choices=add_blank_choice(DeviceAirflowChoices),
@@ -445,7 +450,10 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
model = DeviceType
fieldsets = (
- (_('Device Type'), ('manufacturer', 'default_platform', 'part_number', 'u_height', 'is_full_depth', 'airflow', 'description')),
+ (_('Device Type'), (
+ 'manufacturer', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
+ 'airflow', 'description',
+ )),
(_('Weight'), ('weight', 'weight_unit')),
)
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'description', 'comments')
diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
index a8e75e3c2..d63873b59 100644
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -118,7 +118,9 @@ class SiteImportForm(NetBoxModelImportForm):
)
help_texts = {
'time_zone': mark_safe(
- _('Time zone (available options )')
+ '{} ({} )'.format(
+ _('Time zone'), _('available options')
+ )
)
}
@@ -165,7 +167,7 @@ class RackRoleImportForm(NetBoxModelImportForm):
model = RackRole
fields = ('name', 'slug', 'color', 'description', 'tags')
help_texts = {
- 'color': mark_safe(_('RGB color in hexadecimal (e.g. 00ff00
)')),
+ 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00
'),
}
@@ -333,8 +335,8 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
class Meta:
model = DeviceType
fields = [
- 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
- 'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments', 'tags',
+ 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization',
+ 'is_full_depth', 'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'comments', 'tags',
]
@@ -375,7 +377,7 @@ class DeviceRoleImportForm(NetBoxModelImportForm):
model = DeviceRole
fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags')
help_texts = {
- 'color': mark_safe(_('RGB color in hexadecimal (e.g. 00ff00
)')),
+ 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00
'),
}
@@ -547,9 +549,9 @@ class DeviceImportForm(BaseDeviceImportForm):
params = {
f"site__{self.fields['site'].to_field_name}": data.get('site'),
}
- if 'location' in data:
+ if location := data.get('location'):
params.update({
- f"location__{self.fields['location'].to_field_name}": data.get('location'),
+ f"location__{self.fields['location'].to_field_name}": location,
})
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
@@ -790,7 +792,9 @@ class InterfaceImportForm(NetBoxModelImportForm):
queryset=VirtualDeviceContext.objects.all(),
required=False,
to_field_name='name',
- help_text=_('VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")')
+ help_text=mark_safe(
+ _('VDC names separated by commas, encased with double quotes. Example:') + ' vdc1,vdc2,vdc3
'
+ )
)
type = CSVChoiceField(
label=_('Type'),
@@ -1085,7 +1089,7 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm):
model = InventoryItemRole
fields = ('name', 'slug', 'color', 'description')
help_texts = {
- 'color': mark_safe(_('RGB color in hexadecimal (e.g. 00ff00
)')),
+ 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00
'),
}
@@ -1096,38 +1100,38 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm):
class CableImportForm(NetBoxModelImportForm):
# Termination A
side_a_device = CSVModelChoiceField(
- label=_('Side a device'),
+ label=_('Side A device'),
queryset=Device.objects.all(),
to_field_name='name',
- help_text=_('Side A device')
+ help_text=_('Device name')
)
side_a_type = CSVContentTypeField(
- label=_('Side a type'),
+ label=_('Side A type'),
queryset=ContentType.objects.all(),
limit_choices_to=CABLE_TERMINATION_MODELS,
- help_text=_('Side A type')
+ help_text=_('Termination type')
)
side_a_name = forms.CharField(
- label=_('Side a name'),
- help_text=_('Side A component name')
+ label=_('Side A name'),
+ help_text=_('Termination name')
)
# Termination B
side_b_device = CSVModelChoiceField(
- label=_('Side b device'),
+ label=_('Side B device'),
queryset=Device.objects.all(),
to_field_name='name',
- help_text=_('Side B device')
+ help_text=_('Device name')
)
side_b_type = CSVContentTypeField(
- label=_('Side b type'),
+ label=_('Side B type'),
queryset=ContentType.objects.all(),
limit_choices_to=CABLE_TERMINATION_MODELS,
- help_text=_('Side B type')
+ help_text=_('Termination type')
)
side_b_name = forms.CharField(
- label=_('Side b name'),
- help_text=_('Side B component name')
+ label=_('Side B name'),
+ help_text=_('Termination name')
)
# Cable attributes
@@ -1164,7 +1168,7 @@ class CableImportForm(NetBoxModelImportForm):
'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
]
help_texts = {
- 'color': mark_safe(_('RGB color in hexadecimal (e.g. 00ff00
)')),
+ 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00
'),
}
def _clean_side(self, side):
@@ -1188,7 +1192,7 @@ class CableImportForm(NetBoxModelImportForm):
termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name)
else:
termination_object = model.objects.get(device=device, name=name)
- if termination_object.cable is not None:
+ if termination_object.cable is not None and termination_object.cable != self.instance:
raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
except ObjectDoesNotExist:
raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
diff --git a/netbox/dcim/forms/common.py b/netbox/dcim/forms/common.py
index 77543af12..3be4d08e8 100644
--- a/netbox/dcim/forms/common.py
+++ b/netbox/dcim/forms/common.py
@@ -116,17 +116,17 @@ class ModuleCommonForm(forms.Form):
# It is not possible to adopt components already belonging to a module
if adopt_components and existing_item and existing_item.module:
raise forms.ValidationError(
- _("Cannot adopt {name} '{resolved_name}' as it already belongs to a module").format(
- name=template.component_model.__name__,
- resolved_name=resolved_name
+ _("Cannot adopt {model} {name} as it already belongs to a module").format(
+ model=template.component_model.__name__,
+ name=resolved_name
)
)
# If we are not adopting components we error if the component exists
if not adopt_components and resolved_name in installed_components:
raise forms.ValidationError(
- _("{name} - {resolved_name} already exists").format(
- name=template.component_model.__name__,
- resolved_name=resolved_name
+ _("A {model} named {name} already exists").format(
+ model=template.component_model.__name__,
+ name=resolved_name
)
)
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index 43e5f4481..95c441381 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -7,12 +7,13 @@ from dcim.constants import *
from dcim.models import *
from extras.forms import LocalConfigContextFilterForm
from extras.models import ConfigTemplate
-from ipam.models import ASN, L2VPN, VRF
+from ipam.models import ASN, VRF
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.widgets import APISelectMultiple, NumberWithOptions
+from vpn.models import L2VPN
from wireless.choices import *
__all__ = (
@@ -109,7 +110,7 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
required=False,
label=_('Device type')
)
- role_id = DynamicModelMultipleChoiceField(
+ device_role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Device role')
@@ -164,6 +165,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
)
+ selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
status = forms.MultipleChoiceField(
label=_('Status'),
choices=SiteStatusChoices,
@@ -247,6 +249,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
(_('Weight'), ('weight', 'max_weight', 'weight_unit')),
)
+ selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
@@ -419,6 +422,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
)),
(_('Weight'), ('weight', 'weight_unit')),
)
+ selector_fields = ('filter_id', 'q', 'manufacturer_id')
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
@@ -543,6 +547,7 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
)),
(_('Weight'), ('weight', 'weight_unit')),
)
+ selector_fields = ('filter_id', 'q', 'manufacturer_id')
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
@@ -619,6 +624,7 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
class PlatformFilterForm(NetBoxModelFilterSetForm):
model = Platform
+ selector_fields = ('filter_id', 'q', 'manufacturer_id')
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
@@ -653,6 +659,7 @@ class DeviceFilterForm(
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
))
)
+ selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
@@ -910,7 +917,7 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Location'), ('site_id', 'location_id', 'rack_id', 'device_id')),
- (_('Attributes'), ('type', 'status', 'color', 'length', 'length_unit')),
+ (_('Attributes'), ('type', 'status', 'color', 'length', 'length_unit', 'unterminated')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
)
region_id = DynamicModelMultipleChoiceField(
@@ -979,6 +986,13 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
choices=add_blank_choice(CableLengthUnitChoices),
required=False
)
+ unterminated = forms.NullBooleanField(
+ label=_('Unterminated'),
+ required=False,
+ widget=forms.Select(
+ choices=BOOLEAN_WITH_BLANK_CHOICES
+ )
+ )
tag = TagFilterField(model)
@@ -989,6 +1003,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
)
+ selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
@@ -1136,7 +1151,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'speed')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
- (_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
+ (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
@@ -1158,7 +1173,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'speed')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
- (_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
+ (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
@@ -1180,7 +1195,7 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
- (_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
+ (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
@@ -1197,7 +1212,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
- (_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
+ (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
@@ -1217,9 +1232,10 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
(_('PoE'), ('poe_mode', 'poe_type')),
(_('Wireless'), ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
- (_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
+ (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
+ selector_fields = ('filter_id', 'q', 'device_id')
vdc_id = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(),
required=False,
@@ -1324,7 +1340,7 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'color')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
- (_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
+ (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Cable'), ('cabled', 'occupied')),
)
model = FrontPort
@@ -1346,7 +1362,7 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'type', 'color')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
- (_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
+ (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Cable'), ('cabled', 'occupied')),
)
type = forms.MultipleChoiceField(
@@ -1367,7 +1383,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'position')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
- (_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
+ (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
)
tag = TagFilterField(model)
position = forms.CharField(
@@ -1382,7 +1398,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
- (_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
+ (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
)
tag = TagFilterField(model)
@@ -1393,7 +1409,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
(None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
- (_('Device'), ('device_type_id', 'role_id', 'device_id', 'virtual_chassis_id')),
+ (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
)
role_id = DynamicModelMultipleChoiceField(
queryset=InventoryItemRole.objects.all(),
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
index 879b15da2..da3a2bea4 100644
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -302,7 +302,8 @@ class DeviceTypeForm(NetBoxModelForm):
fieldsets = (
(_('Device Type'), ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')),
(_('Chassis'), (
- 'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
+ 'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
+ 'weight', 'weight_unit',
)),
(_('Images'), ('front_image', 'rear_image')),
)
@@ -310,9 +311,9 @@ class DeviceTypeForm(NetBoxModelForm):
class Meta:
model = DeviceType
fields = [
- 'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'is_full_depth',
- 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'description',
- 'comments', 'tags',
+ 'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization',
+ 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image',
+ 'description', 'comments', 'tags',
]
widgets = {
'front_image': ClearableFileInput(attrs={
@@ -421,12 +422,13 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
label=_('Position'),
required=False,
help_text=_("The lowest-numbered unit occupied by the device"),
+ localize=True,
widget=APISelect(
api_url='/api/dcim/racks/{{rack}}/elevation/',
attrs={
'disabled-indicator': 'device',
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
- }
+ },
)
)
device_type = DynamicModelChoiceField(
@@ -441,7 +443,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
platform = DynamicModelChoiceField(
label=_('Platform'),
queryset=Platform.objects.all(),
- required=False
+ required=False,
+ selector=True
)
cluster = DynamicModelChoiceField(
label=_('Cluster'),
@@ -1110,7 +1113,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
required=False,
label=_('Parent interface'),
query_params={
- 'device_id': '$device',
+ 'virtual_chassis_member_id': '$device',
}
)
bridge = DynamicModelChoiceField(
@@ -1118,7 +1121,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
required=False,
label=_('Bridged interface'),
query_params={
- 'device_id': '$device',
+ 'virtual_chassis_member_id': '$device',
}
)
lag = DynamicModelChoiceField(
@@ -1126,7 +1129,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
required=False,
label=_('LAG interface'),
query_params={
- 'device_id': '$device',
+ 'virtual_chassis_member_id': '$device',
'type': 'lag',
}
)
diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py
index abd7bd6f6..ea842508f 100644
--- a/netbox/dcim/forms/object_create.py
+++ b/netbox/dcim/forms/object_create.py
@@ -151,6 +151,23 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
)
self.fields['rear_port'].choices = choices
+ def clean(self):
+
+ # Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
+ # positions
+ frontport_count = len(self.cleaned_data['name'])
+ rearport_count = len(self.cleaned_data['rear_port'])
+ if frontport_count != rearport_count:
+ raise forms.ValidationError({
+ 'rear_port': _(
+ "The number of front port templates to be created ({frontport_count}) must match the selected "
+ "number of rear port positions ({rearport_count})."
+ ).format(
+ frontport_count=frontport_count,
+ rearport_count=rearport_count
+ )
+ })
+
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
@@ -291,6 +308,22 @@ class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
)
self.fields['rear_port'].choices = choices
+ def clean(self):
+
+ # Check that the number of FrontPorts to be created matches the selected number of RearPort positions
+ frontport_count = len(self.cleaned_data['name'])
+ rearport_count = len(self.cleaned_data['rear_port'])
+ if frontport_count != rearport_count:
+ raise forms.ValidationError({
+ 'rear_port': _(
+ "The number of front ports to be created ({frontport_count}) must match the selected number of "
+ "rear port positions ({rearport_count})."
+ ).format(
+ frontport_count=frontport_count,
+ rearport_count=rearport_count
+ )
+ })
+
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
diff --git a/netbox/dcim/migrations/0174_rack_starting_unit.py b/netbox/dcim/migrations/0174_rack_starting_unit.py
index e32738660..2d2b5f826 100644
--- a/netbox/dcim/migrations/0174_rack_starting_unit.py
+++ b/netbox/dcim/migrations/0174_rack_starting_unit.py
@@ -1,5 +1,6 @@
# Generated by Django 4.1.9 on 2023-05-31 15:47
+import django.core.validators
from django.db import migrations, models
@@ -12,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='rack',
name='starting_unit',
- field=models.PositiveSmallIntegerField(default=1),
+ field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1)]),
),
]
diff --git a/netbox/dcim/migrations/0176_device_component_counters.py b/netbox/dcim/migrations/0176_device_component_counters.py
index b570ddbd5..60857ecb9 100644
--- a/netbox/dcim/migrations/0176_device_component_counters.py
+++ b/netbox/dcim/migrations/0176_device_component_counters.py
@@ -2,47 +2,22 @@ from django.db import migrations
from django.db.models import Count
import utilities.fields
+from utilities.counters import update_counts
def recalculate_device_counts(apps, schema_editor):
Device = apps.get_model("dcim", "Device")
- devices = list(Device.objects.all().annotate(
- _console_port_count=Count('consoleports', distinct=True),
- _console_server_port_count=Count('consoleserverports', distinct=True),
- _power_port_count=Count('powerports', distinct=True),
- _power_outlet_count=Count('poweroutlets', distinct=True),
- _interface_count=Count('interfaces', distinct=True),
- _front_port_count=Count('frontports', distinct=True),
- _rear_port_count=Count('rearports', distinct=True),
- _device_bay_count=Count('devicebays', distinct=True),
- _module_bay_count=Count('modulebays', distinct=True),
- _inventory_item_count=Count('inventoryitems', distinct=True),
- ))
- for device in devices:
- device.console_port_count = device._console_port_count
- device.console_server_port_count = device._console_server_port_count
- device.power_port_count = device._power_port_count
- device.power_outlet_count = device._power_outlet_count
- device.interface_count = device._interface_count
- device.front_port_count = device._front_port_count
- device.rear_port_count = device._rear_port_count
- device.device_bay_count = device._device_bay_count
- device.module_bay_count = device._module_bay_count
- device.inventory_item_count = device._inventory_item_count
-
- Device.objects.bulk_update(devices, [
- 'console_port_count',
- 'console_server_port_count',
- 'power_port_count',
- 'power_outlet_count',
- 'interface_count',
- 'front_port_count',
- 'rear_port_count',
- 'device_bay_count',
- 'module_bay_count',
- 'inventory_item_count',
- ])
+ update_counts(Device, 'console_port_count', 'consoleports')
+ update_counts(Device, 'console_server_port_count', 'consoleserverports')
+ update_counts(Device, 'power_port_count', 'powerports')
+ update_counts(Device, 'power_outlet_count', 'poweroutlets')
+ update_counts(Device, 'interface_count', 'interfaces')
+ update_counts(Device, 'front_port_count', 'frontports')
+ update_counts(Device, 'rear_port_count', 'rearports')
+ update_counts(Device, 'device_bay_count', 'devicebays')
+ update_counts(Device, 'module_bay_count', 'modulebays')
+ update_counts(Device, 'inventory_item_count', 'inventoryitems')
class Migration(migrations.Migration):
diff --git a/netbox/dcim/migrations/0177_devicetype_component_counters.py b/netbox/dcim/migrations/0177_devicetype_component_counters.py
index 66d1460d9..b452ce2d9 100644
--- a/netbox/dcim/migrations/0177_devicetype_component_counters.py
+++ b/netbox/dcim/migrations/0177_devicetype_component_counters.py
@@ -2,47 +2,22 @@ from django.db import migrations
from django.db.models import Count
import utilities.fields
+from utilities.counters import update_counts
def recalculate_devicetype_template_counts(apps, schema_editor):
DeviceType = apps.get_model("dcim", "DeviceType")
- device_types = list(DeviceType.objects.all().annotate(
- _console_port_template_count=Count('consoleporttemplates', distinct=True),
- _console_server_port_template_count=Count('consoleserverporttemplates', distinct=True),
- _power_port_template_count=Count('powerporttemplates', distinct=True),
- _power_outlet_template_count=Count('poweroutlettemplates', distinct=True),
- _interface_template_count=Count('interfacetemplates', distinct=True),
- _front_port_template_count=Count('frontporttemplates', distinct=True),
- _rear_port_template_count=Count('rearporttemplates', distinct=True),
- _device_bay_template_count=Count('devicebaytemplates', distinct=True),
- _module_bay_template_count=Count('modulebaytemplates', distinct=True),
- _inventory_item_template_count=Count('inventoryitemtemplates', distinct=True),
- ))
- for devicetype in device_types:
- devicetype.console_port_template_count = devicetype._console_port_template_count
- devicetype.console_server_port_template_count = devicetype._console_server_port_template_count
- devicetype.power_port_template_count = devicetype._power_port_template_count
- devicetype.power_outlet_template_count = devicetype._power_outlet_template_count
- devicetype.interface_template_count = devicetype._interface_template_count
- devicetype.front_port_template_count = devicetype._front_port_template_count
- devicetype.rear_port_template_count = devicetype._rear_port_template_count
- devicetype.device_bay_template_count = devicetype._device_bay_template_count
- devicetype.module_bay_template_count = devicetype._module_bay_template_count
- devicetype.inventory_item_template_count = devicetype._inventory_item_template_count
-
- DeviceType.objects.bulk_update(device_types, [
- 'console_port_template_count',
- 'console_server_port_template_count',
- 'power_port_template_count',
- 'power_outlet_template_count',
- 'interface_template_count',
- 'front_port_template_count',
- 'rear_port_template_count',
- 'device_bay_template_count',
- 'module_bay_template_count',
- 'inventory_item_template_count',
- ])
+ update_counts(DeviceType, 'console_port_template_count', 'consoleporttemplates')
+ update_counts(DeviceType, 'console_server_port_template_count', 'consoleserverporttemplates')
+ update_counts(DeviceType, 'power_port_template_count', 'powerporttemplates')
+ update_counts(DeviceType, 'power_outlet_template_count', 'poweroutlettemplates')
+ update_counts(DeviceType, 'interface_template_count', 'interfacetemplates')
+ update_counts(DeviceType, 'front_port_template_count', 'frontporttemplates')
+ update_counts(DeviceType, 'rear_port_template_count', 'rearporttemplates')
+ update_counts(DeviceType, 'device_bay_template_count', 'devicebaytemplates')
+ update_counts(DeviceType, 'module_bay_template_count', 'modulebaytemplates')
+ update_counts(DeviceType, 'inventory_item_template_count', 'inventoryitemtemplates')
class Migration(migrations.Migration):
diff --git a/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py b/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py
index e3ade1344..99b304b66 100644
--- a/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py
+++ b/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py
@@ -2,17 +2,13 @@ from django.db import migrations
from django.db.models import Count
import utilities.fields
+from utilities.counters import update_counts
def populate_virtualchassis_members(apps, schema_editor):
VirtualChassis = apps.get_model('dcim', 'VirtualChassis')
- vcs = list(VirtualChassis.objects.annotate(_member_count=Count('members', distinct=True)))
-
- for vc in vcs:
- vc.member_count = vc._member_count
-
- VirtualChassis.objects.bulk_update(vcs, ['member_count'])
+ update_counts(VirtualChassis, 'member_count', 'members')
class Migration(migrations.Migration):
diff --git a/netbox/dcim/migrations/0182_zero_length_cable_fix.py b/netbox/dcim/migrations/0182_zero_length_cable_fix.py
new file mode 100644
index 000000000..080e00717
--- /dev/null
+++ b/netbox/dcim/migrations/0182_zero_length_cable_fix.py
@@ -0,0 +1,22 @@
+from django.db import migrations
+
+
+def update_cable_lengths(apps, schema_editor):
+ Cable = apps.get_model('dcim', 'Cable')
+
+ # Set the absolute length for any zero-length Cables
+ Cable.objects.filter(length=0).update(_abs_length=0)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0181_rename_device_role_device_role'),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ code=update_cable_lengths,
+ reverse_code=migrations.RunPython.noop
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0183_devicetype_exclude_from_utilization.py b/netbox/dcim/migrations/0183_devicetype_exclude_from_utilization.py
new file mode 100644
index 000000000..f9f2c20b4
--- /dev/null
+++ b/netbox/dcim/migrations/0183_devicetype_exclude_from_utilization.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.5 on 2023-10-20 22:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('dcim', '0182_zero_length_cable_fix'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='devicetype',
+ name='exclude_from_utilization',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0184_protect_child_interfaces.py b/netbox/dcim/migrations/0184_protect_child_interfaces.py
new file mode 100644
index 000000000..3459e23fc
--- /dev/null
+++ b/netbox/dcim/migrations/0184_protect_child_interfaces.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.6 on 2023-10-20 11:48
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0183_devicetype_exclude_from_utilization'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='interface',
+ name='parent',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='child_interfaces', to='dcim.interface'),
+ ),
+ ]
diff --git a/netbox/dcim/migrations/0185_gfk_indexes.py b/netbox/dcim/migrations/0185_gfk_indexes.py
new file mode 100644
index 000000000..84cdc53ff
--- /dev/null
+++ b/netbox/dcim/migrations/0185_gfk_indexes.py
@@ -0,0 +1,25 @@
+# Generated by Django 4.2.7 on 2023-12-07 16:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0184_protect_child_interfaces'),
+ ]
+
+ operations = [
+ migrations.AddIndex(
+ model_name='cabletermination',
+ index=models.Index(fields=['termination_type', 'termination_id'], name='dcim_cablet_termina_884752_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='inventoryitem',
+ index=models.Index(fields=['component_type', 'component_id'], name='dcim_invent_compone_0560bb_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='inventoryitemtemplate',
+ index=models.Index(fields=['component_type', 'component_id'], name='dcim_invent_compone_77b5f8_idx'),
+ ),
+ ]
diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py
index de7ba0eb6..d1c80d0be 100644
--- a/netbox/dcim/models/cables.py
+++ b/netbox/dcim/models/cables.py
@@ -2,7 +2,6 @@ import itertools
from collections import defaultdict
from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Sum
@@ -10,17 +9,17 @@ from django.dispatch import Signal
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
+from core.models import ContentType
from dcim.choices import *
from dcim.constants import *
from dcim.fields import PathField
from dcim.utils import decompile_path_node, object_to_path_node
from netbox.models import ChangeLoggedModel, PrimaryModel
-
from utilities.fields import ColorField
from utilities.querysets import RestrictedQuerySet
from utilities.utils import to_meters
from wireless.models import WirelessLink
-from .device_components import FrontPort, RearPort
+from .device_components import FrontPort, RearPort, PathEndpoint
__all__ = (
'Cable',
@@ -98,10 +97,10 @@ class Cable(PrimaryModel):
super().__init__(*args, **kwargs)
# A copy of the PK to be used by __str__ in case the object is deleted
- self._pk = self.pk
+ self._pk = self.__dict__.get('id')
# Cache the original status so we can check later if it's been changed
- self._orig_status = self.status
+ self._orig_status = self.__dict__.get('status')
self._terminations_modified = False
@@ -180,6 +179,17 @@ class Cable(PrimaryModel):
if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}")
+ if a_type == b_type:
+ # can't directly use self.a_terminations here as possible they
+ # don't have pk yet
+ a_pks = set(obj.pk for obj in self.a_terminations if obj.pk)
+ b_pks = set(obj.pk for obj in self.b_terminations if obj.pk)
+
+ if (a_pks & b_pks):
+ raise ValidationError(
+ _("A and B terminations cannot connect to the same object.")
+ )
+
# Run clean() on any new CableTerminations
for termination in self.a_terminations:
CableTermination(cable=self, cable_end='A', termination=termination).clean()
@@ -190,7 +200,7 @@ class Cable(PrimaryModel):
_created = self.pk is None
# Store the given length (if any) in meters for use in database ordering
- if self.length and self.length_unit:
+ if self.length is not None and self.length_unit:
self._abs_length = to_meters(self.length, self.length_unit)
else:
self._abs_length = None
@@ -247,7 +257,7 @@ class CableTermination(ChangeLoggedModel):
verbose_name=_('end')
)
termination_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
limit_choices_to=CABLE_TERMINATION_MODELS,
on_delete=models.PROTECT,
related_name='+'
@@ -288,6 +298,9 @@ class CableTermination(ChangeLoggedModel):
class Meta:
ordering = ('cable', 'cable_end', 'pk')
+ indexes = (
+ models.Index(fields=('termination_type', 'termination_id')),
+ )
constraints = (
models.UniqueConstraint(
fields=('termination_type', 'termination_id'),
@@ -431,6 +444,8 @@ class CablePath(models.Model):
)
_nodes = PathField()
+ _netbox_private = True
+
class Meta:
verbose_name = _('cable path')
verbose_name_plural = _('cable paths')
@@ -518,9 +533,16 @@ class CablePath(models.Model):
# Terminations must all be of the same type
assert all(isinstance(t, type(terminations[0])) for t in terminations[1:])
+ # All mid-span terminations must all be attached to the same device
+ if not isinstance(terminations[0], PathEndpoint):
+ assert all(isinstance(t, type(terminations[0])) for t in terminations[1:])
+ assert all(t.parent_object == terminations[0].parent_object for t in terminations[1:])
+
# Check for a split path (e.g. rear port fanning out to multiple front ports with
# different cables attached)
- if len(set(t.link for t in terminations)) > 1:
+ if len(set(t.link for t in terminations)) > 1 and (
+ position_stack and len(terminations) != len(position_stack[-1])
+ ):
is_split = True
break
@@ -529,46 +551,68 @@ class CablePath(models.Model):
object_to_path_node(t) for t in terminations
])
- # Step 2: Determine the attached link (Cable or WirelessLink), if any
- link = terminations[0].link
- if link is None and len(path) == 1:
- # If this is the start of the path and no link exists, return None
- return None
- elif link is None:
+ # Step 2: Determine the attached links (Cable or WirelessLink), if any
+ links = [termination.link for termination in terminations if termination.link is not None]
+ if len(links) == 0:
+ if len(path) == 1:
+ # If this is the start of the path and no link exists, return None
+ return None
# Otherwise, halt the trace if no link exists
break
- assert type(link) in (Cable, WirelessLink)
+ assert all(type(link) in (Cable, WirelessLink) for link in links)
+ assert all(isinstance(link, type(links[0])) for link in links)
- # Step 3: Record the link and update path status if not "connected"
- path.append([object_to_path_node(link)])
- if hasattr(link, 'status') and link.status != LinkStatusChoices.STATUS_CONNECTED:
+ # Step 3: Record asymmetric paths as split
+ not_connected_terminations = [termination.link for termination in terminations if termination.link is None]
+ if len(not_connected_terminations) > 0:
+ is_complete = False
+ is_split = True
+
+ # Step 4: Record the links, keeping cables in order to allow for SVG rendering
+ cables = []
+ for link in links:
+ if object_to_path_node(link) not in cables:
+ cables.append(object_to_path_node(link))
+ path.append(cables)
+
+ # Step 5: Update the path status if a link is not connected
+ links_status = [link.status for link in links if link.status != LinkStatusChoices.STATUS_CONNECTED]
+ if any([status != LinkStatusChoices.STATUS_CONNECTED for status in links_status]):
is_active = False
- # Step 4: Determine the far-end terminations
- if isinstance(link, Cable):
+ # Step 6: Determine the far-end terminations
+ if isinstance(links[0], Cable):
termination_type = ContentType.objects.get_for_model(terminations[0])
local_cable_terminations = CableTermination.objects.filter(
termination_type=termination_type,
termination_id__in=[t.pk for t in terminations]
)
- # Terminations must all belong to same end of Cable
- local_cable_end = local_cable_terminations[0].cable_end
- assert all(ct.cable_end == local_cable_end for ct in local_cable_terminations[1:])
- remote_cable_terminations = CableTermination.objects.filter(
- cable=link,
- cable_end='A' if local_cable_end == 'B' else 'B'
- )
+
+ q_filter = Q()
+ for lct in local_cable_terminations:
+ cable_end = 'A' if lct.cable_end == 'B' else 'B'
+ q_filter |= Q(cable=lct.cable, cable_end=cable_end)
+
+ remote_cable_terminations = CableTermination.objects.filter(q_filter)
remote_terminations = [ct.termination for ct in remote_cable_terminations]
else:
# WirelessLink
- remote_terminations = [link.interface_b] if link.interface_a is terminations[0] else [link.interface_a]
+ remote_terminations = [
+ link.interface_b if link.interface_a is terminations[0] else link.interface_a for link in links
+ ]
- # Step 5: Record the far-end termination object(s)
+ # Remote Terminations must all be of the same type, otherwise return a split path
+ if not all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
+ is_complete = False
+ is_split = True
+ break
+
+ # Step 7: Record the far-end termination object(s)
path.append([
object_to_path_node(t) for t in remote_terminations if t is not None
])
- # Step 6: Determine the "next hop" terminations, if applicable
+ # Step 8: Determine the "next hop" terminations, if applicable
if not remote_terminations:
break
@@ -577,20 +621,32 @@ class CablePath(models.Model):
rear_ports = RearPort.objects.filter(
pk__in=[t.rear_port_id for t in remote_terminations]
)
- if len(rear_ports) > 1:
- assert all(rp.positions == 1 for rp in rear_ports)
- elif rear_ports[0].positions > 1:
+ if len(rear_ports) > 1 or rear_ports[0].positions > 1:
position_stack.append([fp.rear_port_position for fp in remote_terminations])
terminations = rear_ports
elif isinstance(remote_terminations[0], RearPort):
-
- if len(remote_terminations) > 1 or remote_terminations[0].positions == 1:
+ if len(remote_terminations) == 1 and remote_terminations[0].positions == 1:
front_ports = FrontPort.objects.filter(
rear_port_id__in=[rp.pk for rp in remote_terminations],
rear_port_position=1
)
+ # Obtain the individual front ports based on the termination and all positions
+ elif len(remote_terminations) > 1 and position_stack:
+ positions = position_stack.pop()
+
+ # Ensure we have a number of positions equal to the amount of remote terminations
+ assert len(remote_terminations) == len(positions)
+
+ # Get our front ports
+ q_filter = Q()
+ for rt in remote_terminations:
+ position = positions.pop()
+ q_filter |= Q(rear_port_id=rt.pk, rear_port_position=position)
+ assert q_filter is not Q()
+ front_ports = FrontPort.objects.filter(q_filter)
+ # Obtain the individual front ports based on the termination and position
elif position_stack:
front_ports = FrontPort.objects.filter(
rear_port_id=remote_terminations[0].pk,
@@ -632,9 +688,16 @@ class CablePath(models.Model):
terminations = [circuit_termination]
- # Anything else marks the end of the path
else:
- is_complete = True
+ # Check for non-symmetric path
+ if all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
+ is_complete = True
+ elif len(remote_terminations) == 0:
+ is_complete = False
+ else:
+ # Unsupported topology, mark as split and exit
+ is_complete = False
+ is_split = True
break
return cls(
@@ -740,3 +803,15 @@ class CablePath(models.Model):
return [
ct.get_peer_termination() for ct in nodes
]
+
+ def get_asymmetric_nodes(self):
+ """
+ Return all available next segments in a split cable path.
+ """
+ from circuits.models import CircuitTermination
+ asymmetric_nodes = []
+ for nodes in self.path_objects:
+ if type(nodes[0]) in [RearPort, FrontPort, CircuitTermination]:
+ asymmetric_nodes.extend([node for node in nodes if node.link is None])
+
+ return asymmetric_nodes
diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py
index f58d2bbca..dacd7ec3e 100644
--- a/netbox/dcim/models/device_component_templates.py
+++ b/netbox/dcim/models/device_component_templates.py
@@ -1,5 +1,4 @@
from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@@ -89,7 +88,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
super().__init__(*args, **kwargs)
# Cache the original DeviceType ID for reference under clean()
- self._original_device_type = self.device_type_id
+ self._original_device_type = self.__dict__.get('device_type_id')
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
@@ -534,14 +533,16 @@ class FrontPortTemplate(ModularComponentTemplateModel):
# Validate rear port assignment
if self.rear_port.device_type != self.device_type:
raise ValidationError(
- _("Rear port ({}) must belong to the same device type").format(self.rear_port)
+ _("Rear port ({name}) must belong to the same device type").format(name=self.rear_port)
)
# Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions:
raise ValidationError(
- _("Invalid rear port position ({}); rear port {} has only {} positions").format(
- self.rear_port_position, self.rear_port.name, self.rear_port.positions
+ _("Invalid rear port position ({position}); rear port {name} has only {count} positions").format(
+ position=self.rear_port_position,
+ name=self.rear_port.name,
+ count=self.rear_port.positions
)
)
@@ -707,7 +708,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
db_index=True
)
component_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
limit_choices_to=MODULAR_COMPONENT_TEMPLATE_MODELS,
on_delete=models.PROTECT,
related_name='+',
@@ -748,6 +749,9 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
class Meta:
ordering = ('device_type__id', 'parent__id', '_name')
+ indexes = (
+ models.Index(fields=('component_type', 'component_id')),
+ )
constraints = (
models.UniqueConstraint(
fields=('device_type', 'parent', 'name'),
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index e18f25e4f..ef235078f 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -1,7 +1,6 @@
from functools import cached_property
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
-from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@@ -86,7 +85,7 @@ class ComponentModel(NetBoxModel):
super().__init__(*args, **kwargs)
# Cache the original Device ID for reference under clean()
- self._original_device = self.device_id
+ self._original_device = self.__dict__.get('device_id')
def __str__(self):
if self.label:
@@ -537,7 +536,7 @@ class BaseInterface(models.Model):
)
parent = models.ForeignKey(
to='self',
- on_delete=models.SET_NULL,
+ on_delete=models.RESTRICT,
related_name='child_interfaces',
null=True,
blank=True,
@@ -567,6 +566,10 @@ class BaseInterface(models.Model):
return super().save(*args, **kwargs)
+ @property
+ def tunnel_termination(self):
+ return self.tunnel_terminations.first()
+
@property
def count_ipaddresses(self):
return self.ip_addresses.count()
@@ -720,8 +723,14 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
object_id_field='interface_id',
related_query_name='+'
)
+ tunnel_terminations = GenericRelation(
+ to='vpn.TunnelTermination',
+ content_type_field='termination_type',
+ object_id_field='termination_id',
+ related_query_name='interface'
+ )
l2vpn_terminations = GenericRelation(
- to='ipam.L2VPNTermination',
+ to='vpn.L2VPNTermination',
content_type_field='assigned_object_type',
object_id_field='assigned_object_id',
related_query_name='interface',
@@ -799,9 +808,9 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
if self.bridge and self.bridge.device != self.device:
if self.device.virtual_chassis is None:
raise ValidationError({
- 'bridge': _("""
- The selected bridge interface ({bridge}) belongs to a different device
- ({device}).""").format(bridge=self.bridge, device=self.bridge.device)
+ 'bridge': _(
+ "The selected bridge interface ({bridge}) belongs to a different device ({device})."
+ ).format(bridge=self.bridge, device=self.bridge.device)
})
elif self.bridge.device.virtual_chassis != self.device.virtual_chassis:
raise ValidationError({
@@ -889,10 +898,10 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
# Validate untagged VLAN
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
raise ValidationError({
- 'untagged_vlan': _("""
- The untagged VLAN ({untagged_vlan}) must belong to the same site as the
- interface's parent device, or it must be global.
- """).format(untagged_vlan=self.untagged_vlan)
+ 'untagged_vlan': _(
+ "The untagged VLAN ({untagged_vlan}) must belong to the same site as the interface's parent "
+ "device, or it must be global."
+ ).format(untagged_vlan=self.untagged_vlan)
})
def save(self, *args, **kwargs):
@@ -1067,9 +1076,10 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
frontport_count = self.frontports.count()
if self.positions < frontport_count:
raise ValidationError({
- "positions": _("""
- The number of positions cannot be less than the number of mapped front ports
- ({frontport_count})""").format(frontport_count=frontport_count)
+ "positions": _(
+ "The number of positions cannot be less than the number of mapped front ports "
+ "({frontport_count})"
+ ).format(frontport_count=frontport_count)
})
@@ -1180,7 +1190,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
db_index=True
)
component_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
limit_choices_to=MODULAR_COMPONENT_MODELS,
on_delete=models.PROTECT,
related_name='+',
@@ -1240,6 +1250,9 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
class Meta:
ordering = ('device__id', 'parent__id', '_name')
+ indexes = (
+ models.Index(fields=('component_type', 'component_id')),
+ )
constraints = (
models.UniqueConstraint(
fields=('device', 'parent', 'name'),
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index 857251caf..4b9689a22 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -4,6 +4,7 @@ import yaml
from functools import cached_property
from django.core.exceptions import ValidationError
+from django.core.files.storage import default_storage
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import F, ProtectedError
@@ -15,7 +16,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
-from extras.models import ConfigContextModel
+from extras.models import ConfigContextModel, CustomField
from extras.querysets import ConfigContextModelQuerySet
from netbox.config import ConfigItem
from netbox.models import OrganizationalModel, PrimaryModel
@@ -105,10 +106,15 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
default=1.0,
verbose_name=_('height (U)')
)
+ exclude_from_utilization = models.BooleanField(
+ default=False,
+ verbose_name=_('exclude from utilization'),
+ help_text=_('Devices of this type are excluded when calculating rack utilization.')
+ )
is_full_depth = models.BooleanField(
default=True,
verbose_name=_('is full depth'),
- help_text=_('Device consumes both front and rear rack faces')
+ help_text=_('Device consumes both front and rear rack faces.')
)
subdevice_role = models.CharField(
max_length=50,
@@ -205,11 +211,11 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
super().__init__(*args, **kwargs)
# Save a copy of u_height for validation in clean()
- self._original_u_height = self.u_height
+ self._original_u_height = self.__dict__.get('u_height')
# Save references to the original front/rear images
- self._original_front_image = self.front_image
- self._original_rear_image = self.rear_image
+ 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])
@@ -296,8 +302,10 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
)
if d.position not in u_available:
raise ValidationError({
- 'u_height': _("Device {} in rack {} does not have sufficient space to accommodate a height of "
- "{}U").format(d, d.rack, self.u_height)
+ 'u_height': _(
+ "Device {device} in rack {rack} does not have sufficient space to accommodate a "
+ "height of {height}U"
+ ).format(device=d, rack=d.rack, height=self.u_height)
})
# If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position.
@@ -332,10 +340,10 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
ret = super().save(*args, **kwargs)
# Delete any previously uploaded image files that are no longer in use
- if self.front_image != self._original_front_image:
- self._original_front_image.delete(save=False)
- if self.rear_image != self._original_rear_image:
- self._original_rear_image.delete(save=False)
+ if self._original_front_image and self.front_image != self._original_front_image:
+ default_storage.delete(self._original_front_image)
+ if self._original_rear_image and self.rear_image != self._original_rear_image:
+ default_storage.delete(self._original_rear_image)
return ret
@@ -914,7 +922,7 @@ class Device(
if self.primary_ip4:
if self.primary_ip4.family != 4:
raise ValidationError({
- 'primary_ip4': _("{primary_ip4} is not an IPv4 address.").format(primary_ip4=self.primary_ip4)
+ 'primary_ip4': _("{ip} is not an IPv4 address.").format(ip=self.primary_ip4)
})
if self.primary_ip4.assigned_object in vc_interfaces:
pass
@@ -923,13 +931,13 @@ class Device(
else:
raise ValidationError({
'primary_ip4': _(
- "The specified IP address ({primary_ip4}) is not assigned to this device."
- ).format(primary_ip4=self.primary_ip4)
+ "The specified IP address ({ip}) is not assigned to this device."
+ ).format(ip=self.primary_ip4)
})
if self.primary_ip6:
if self.primary_ip6.family != 6:
raise ValidationError({
- 'primary_ip6': _("{primary_ip6} is not an IPv6 address.").format(primary_ip6=self.primary_ip6m)
+ 'primary_ip6': _("{ip} is not an IPv6 address.").format(ip=self.primary_ip6)
})
if self.primary_ip6.assigned_object in vc_interfaces:
pass
@@ -938,8 +946,8 @@ class Device(
else:
raise ValidationError({
'primary_ip6': _(
- "The specified IP address ({primary_ip6}) is not assigned to this device."
- ).format(primary_ip6=self.primary_ip6)
+ "The specified IP address ({ip}) is not assigned to this device."
+ ).format(ip=self.primary_ip6)
})
if self.oob_ip:
if self.oob_ip.assigned_object in vc_interfaces:
@@ -957,17 +965,19 @@ class Device(
raise ValidationError({
'platform': _(
"The assigned platform is limited to {platform_manufacturer} device types, but this device's "
- "type belongs to {device_type_manufacturer}."
+ "type belongs to {devicetype_manufacturer}."
).format(
platform_manufacturer=self.platform.manufacturer,
- device_type_manufacturer=self.device_type.manufacturer
+ devicetype_manufacturer=self.device_type.manufacturer
)
})
# A Device can only be assigned to a Cluster in the same Site (or no Site)
if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
raise ValidationError({
- 'cluster': _("The assigned cluster belongs to a different site ({})").format(self.cluster.site)
+ 'cluster': _("The assigned cluster belongs to a different site ({site})").format(
+ site=self.cluster.site
+ )
})
# Validate virtual chassis assignment
@@ -984,11 +994,17 @@ class Device(
bulk_create: If True, bulk_create() will be called to create all components in a single query
(default). Otherwise, save() will be called on each instance individually.
"""
+ components = [obj.instantiate(device=self) for obj in queryset]
+ if not components:
+ return
+
+ # Set default values for any applicable custom fields
+ model = queryset.model.component_model
+ if cf_defaults := CustomField.objects.get_defaults_for_model(model):
+ for component in components:
+ component.custom_field_data = cf_defaults
+
if bulk_create:
- components = [obj.instantiate(device=self) for obj in queryset]
- if not components:
- return
- model = components[0]._meta.model
model.objects.bulk_create(components)
# Manually send the post_save signal for each of the newly created components
for component in components:
@@ -1001,8 +1017,7 @@ class Device(
update_fields=None
)
else:
- for obj in queryset:
- component = obj.instantiate(device=self)
+ for component in components:
component.save()
def save(self, *args, **kwargs):
@@ -1439,8 +1454,8 @@ class VirtualDeviceContext(PrimaryModel):
if primary_ip.family != family:
raise ValidationError({
f'primary_ip{family}': _(
- "{primary_ip} is not an IPv{family} address."
- ).format(family=family, primary_ip=primary_ip)
+ "{ip} is not an IPv{family} address."
+ ).format(family=family, ip=primary_ip)
})
device_interfaces = self.device.vc_interfaces(if_master=False)
if primary_ip.assigned_object not in device_interfaces:
diff --git a/netbox/dcim/models/mixins.py b/netbox/dcim/models/mixins.py
index 95f6d41fe..9be8dc0a3 100644
--- a/netbox/dcim/models/mixins.py
+++ b/netbox/dcim/models/mixins.py
@@ -69,7 +69,7 @@ class RenderConfigMixin(models.Model):
"""
if self.config_template:
return self.config_template
- if self.role.config_template:
+ if self.role and self.role.config_template:
return self.role.config_template
if self.platform and self.platform.config_template:
return self.platform.config_template
diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py
index 83e5eb23a..62578d6c4 100644
--- a/netbox/dcim/models/power.py
+++ b/netbox/dcim/models/power.py
@@ -174,8 +174,13 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
# Rack must belong to same Site as PowerPanel
if self.rack and self.rack.site != self.power_panel.site:
- raise ValidationError(_("Rack {} ({}) and power panel {} ({}) are in different sites").format(
- self.rack, self.rack.site, self.power_panel, self.power_panel.site
+ raise ValidationError(_(
+ "Rack {rack} ({rack_site}) and power panel {powerpanel} ({powerpanel_site}) are in different sites."
+ ).format(
+ rack=self.rack,
+ rack_site=self.rack.site,
+ powerpanel=self.power_panel,
+ powerpanel_site=self.power_panel.site
))
# AC voltage cannot be negative
diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py
index ef0dde4da..3cb4e0225 100644
--- a/netbox/dcim/models/racks.py
+++ b/netbox/dcim/models/racks.py
@@ -141,6 +141,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
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(
@@ -357,7 +358,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
return [u for u in elevation.values()]
- def get_available_units(self, u_height=1, rack_face=None, exclude=None):
+ def get_available_units(self, u_height=1, rack_face=None, exclude=None, ignore_excluded_devices=False):
"""
Return a list of units within the rack available to accommodate a device of a given U height (default 1).
Optionally exclude one or more devices when calculating empty units (needed when moving a device from one
@@ -366,9 +367,13 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
:param u_height: Minimum number of contiguous free units required
:param rack_face: The face of the rack (front or rear) required; 'None' if device is full depth
:param exclude: List of devices IDs to exclude (useful when moving a device within a rack)
+ :param ignore_excluded_devices: Ignore devices that are marked to exclude from utilization calculations
"""
# Gather all devices which consume U space within the rack
devices = self.devices.prefetch_related('device_type').filter(position__gte=1)
+ if ignore_excluded_devices:
+ devices = devices.exclude(device_type__exclude_from_utilization=True)
+
if exclude is not None:
devices = devices.exclude(pk__in=exclude)
@@ -453,7 +458,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
"""
# Determine unoccupied units
total_units = len(list(self.units))
- available_units = self.get_available_units(u_height=0.5)
+ available_units = self.get_available_units(u_height=0.5, ignore_excluded_devices=True)
# Remove reserved units
for ru in self.get_reserved_units():
@@ -558,9 +563,9 @@ class RackReservation(PrimaryModel):
invalid_units = [u for u in self.units if u not in self.rack.units]
if invalid_units:
raise ValidationError({
- 'units': _("Invalid unit(s) for {}U rack: {}").format(
- self.rack.u_height,
- ', '.join([str(u) for u in invalid_units]),
+ 'units': _("Invalid unit(s) for {height}U rack: {unit_list}").format(
+ height=self.rack.u_height,
+ unit_list=', '.join([str(u) for u in invalid_units])
),
})
@@ -571,8 +576,8 @@ class RackReservation(PrimaryModel):
conflicting_units = [u for u in self.units if u in reserved_units]
if conflicting_units:
raise ValidationError({
- 'units': _('The following units have already been reserved: {}').format(
- ', '.join([str(u) for u in conflicting_units]),
+ 'units': _('The following units have already been reserved: {unit_list}').format(
+ unit_list=', '.join([str(u) for u in conflicting_units])
)
})
diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py
index f70c729f4..18cf75a9a 100644
--- a/netbox/dcim/search.py
+++ b/netbox/dcim/search.py
@@ -10,6 +10,7 @@ class CableIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('type', 'status', 'tenant', 'label', 'description')
@register_search
@@ -21,6 +22,7 @@ class ConsolePortIndex(SearchIndex):
('description', 500),
('speed', 2000),
)
+ display_attrs = ('device', 'label', 'type', 'description')
@register_search
@@ -32,6 +34,7 @@ class ConsoleServerPortIndex(SearchIndex):
('description', 500),
('speed', 2000),
)
+ display_attrs = ('device', 'label', 'type', 'description')
@register_search
@@ -44,6 +47,10 @@ class DeviceIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = (
+ 'site', 'location', 'rack', 'status', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag',
+ 'description',
+ )
@register_search
@@ -54,6 +61,7 @@ class DeviceBayIndex(SearchIndex):
('label', 200),
('description', 500),
)
+ display_attrs = ('device', 'label', 'description')
@register_search
@@ -64,6 +72,7 @@ class DeviceRoleIndex(SearchIndex):
('slug', 110),
('description', 500),
)
+ display_attrs = ('description',)
@register_search
@@ -75,6 +84,7 @@ class DeviceTypeIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('manufacturer', 'part_number', 'description')
@register_search
@@ -85,6 +95,7 @@ class FrontPortIndex(SearchIndex):
('label', 200),
('description', 500),
)
+ display_attrs = ('device', 'label', 'type', 'description')
@register_search
@@ -99,6 +110,7 @@ class InterfaceIndex(SearchIndex):
('mtu', 2000),
('speed', 2000),
)
+ display_attrs = ('device', 'label', 'type', 'mac_address', 'wwn', 'description')
@register_search
@@ -112,6 +124,7 @@ class InventoryItemIndex(SearchIndex):
('description', 500),
('part_id', 2000),
)
+ display_attrs = ('device', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', 'description')
@register_search
@@ -122,6 +135,7 @@ class LocationIndex(SearchIndex):
('slug', 110),
('description', 500),
)
+ display_attrs = ('site', 'status', 'tenant', 'description')
@register_search
@@ -132,6 +146,7 @@ class ManufacturerIndex(SearchIndex):
('slug', 110),
('description', 500),
)
+ display_attrs = ('description',)
@register_search
@@ -143,6 +158,7 @@ class ModuleIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'description')
@register_search
@@ -153,6 +169,7 @@ class ModuleBayIndex(SearchIndex):
('label', 200),
('description', 500),
)
+ display_attrs = ('device', 'label', 'position', 'description')
@register_search
@@ -164,6 +181,7 @@ class ModuleTypeIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('manufacturer', 'model', 'part_number', 'description')
@register_search
@@ -174,6 +192,7 @@ class PlatformIndex(SearchIndex):
('slug', 110),
('description', 500),
)
+ display_attrs = ('manufacturer', 'description')
@register_search
@@ -184,6 +203,7 @@ class PowerFeedIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('power_panel', 'rack', 'status', 'description')
@register_search
@@ -194,6 +214,7 @@ class PowerOutletIndex(SearchIndex):
('label', 200),
('description', 500),
)
+ display_attrs = ('device', 'label', 'type', 'description')
@register_search
@@ -204,6 +225,7 @@ class PowerPanelIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('site', 'location', 'description')
@register_search
@@ -216,6 +238,7 @@ class PowerPortIndex(SearchIndex):
('maximum_draw', 2000),
('allocated_draw', 2000),
)
+ display_attrs = ('device', 'label', 'type', 'description')
@register_search
@@ -229,6 +252,9 @@ class RackIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = (
+ 'site', 'location', 'facility_id', 'tenant', 'status', 'role', 'serial', 'asset_tag', 'description',
+ )
@register_search
@@ -238,6 +264,7 @@ class RackReservationIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('rack', 'tenant', 'user', 'description')
@register_search
@@ -248,6 +275,7 @@ class RackRoleIndex(SearchIndex):
('slug', 110),
('description', 500),
)
+ display_attrs = ('description',)
@register_search
@@ -258,6 +286,7 @@ class RearPortIndex(SearchIndex):
('label', 200),
('description', 500),
)
+ display_attrs = ('device', 'label', 'type', 'description')
@register_search
@@ -268,6 +297,7 @@ class RegionIndex(SearchIndex):
('slug', 110),
('description', 500),
)
+ display_attrs = ('parent', 'description')
@register_search
@@ -282,6 +312,7 @@ class SiteIndex(SearchIndex):
('shipping_address', 2000),
('comments', 5000),
)
+ display_attrs = ('region', 'group', 'status', 'tenant', 'facility', 'description')
@register_search
@@ -292,6 +323,7 @@ class SiteGroupIndex(SearchIndex):
('slug', 110),
('description', 500),
)
+ display_attrs = ('parent', 'description')
@register_search
@@ -303,6 +335,7 @@ class VirtualChassisIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('master', 'domain', 'description')
@register_search
@@ -314,3 +347,4 @@ class VirtualDeviceContextIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('device', 'status', 'identifier', 'tenant', 'description')
diff --git a/netbox/dcim/svg/cables.py b/netbox/dcim/svg/cables.py
index 9413726fa..d7365161e 100644
--- a/netbox/dcim/svg/cables.py
+++ b/netbox/dcim/svg/cables.py
@@ -32,11 +32,18 @@ class Node(Hyperlink):
color: Box fill color (RRGGBB format)
labels: An iterable of text strings. Each label will render on a new line within the box.
radius: Box corner radius, for rounded corners (default: 10)
+ object: A copy of the object to allow reference when drawing cables to determine which cables are connected to
+ which terminations.
"""
- def __init__(self, position, width, url, color, labels, radius=10, **extra):
+ object = None
+
+ def __init__(self, position, width, url, color, labels, radius=10, object=object, **extra):
super(Node, self).__init__(href=url, target='_parent', **extra)
+ # Save object for reference by cable systems
+ self.object = object
+
x, y = position
# Add the box
@@ -77,7 +84,7 @@ class Connector(Group):
labels: Iterable of text labels
"""
- def __init__(self, start, url, color, labels=[], **extra):
+ def __init__(self, start, url, color, labels=[], description=[], **extra):
super().__init__(class_='connector', **extra)
self.start = start
@@ -104,6 +111,8 @@ class Connector(Group):
text_coords = (start[0] + PADDING * 2, cursor - LINE_HEIGHT / 2)
text = Text(label, insert=text_coords, class_='bold' if not i else [])
link.add(text)
+ if len(description) > 0:
+ link.set_desc("\n".join(description))
self.add(link)
@@ -150,7 +159,10 @@ class CableTraceSVG:
labels.append(location_label)
elif instance._meta.model_name == 'circuit':
labels[0] = f'Circuit {instance}'
+ labels.append(instance.type)
labels.append(instance.provider)
+ if instance.description:
+ labels.append(instance.description)
elif instance._meta.model_name == 'circuittermination':
if instance.xconnect_id:
labels.append(f'{instance.xconnect_id}')
@@ -170,6 +182,8 @@ class CableTraceSVG:
if hasattr(instance, 'role'):
# Device
return instance.role.color
+ elif instance._meta.model_name == 'circuit' and instance.type.color:
+ return instance.type.color
else:
# Other parent object
return 'e0e0e0'
@@ -206,7 +220,8 @@ class CableTraceSVG:
url=f'{self.base_url}{term.get_absolute_url()}',
color=self._get_color(term),
labels=self._get_labels(term),
- radius=5
+ radius=5,
+ object=term
)
nodes_height = max(nodes_height, node.box['height'])
nodes.append(node)
@@ -238,22 +253,65 @@ class CableTraceSVG:
Polyline(points=points, style=f'stroke: #{connector.color}'),
))
- def draw_cable(self, cable):
- labels = [
- f'Cable {cable}',
- cable.get_status_display()
- ]
- if cable.type:
- labels.append(cable.get_type_display())
- if cable.length and cable.length_unit:
- labels.append(f'{cable.length} {cable.get_length_unit_display()}')
+ def draw_cable(self, cable, terminations, cable_count=0):
+ """
+ Draw a single cable. Terminations and cable count are passed for determining position and padding
+
+ :param cable: The cable to draw
+ :param terminations: List of terminations to build positioning data off of
+ :param cable_count: Count of all cables on this layer for determining whether to collapse description into a
+ tooltip.
+ """
+
+ # If the cable count is higher than 2, collapse the description into a tooltip
+ if cable_count > 2:
+ # Use the cable __str__ function to denote the cable
+ labels = [f'{cable}']
+
+ # Include the label and the status description in the tooltip
+ description = [
+ f'Cable {cable}',
+ cable.get_status_display()
+ ]
+
+ if cable.type:
+ # Include the cable type in the tooltip
+ description.append(cable.get_type_display())
+ if cable.length is not None and cable.length_unit:
+ # Include the cable length in the tooltip
+ description.append(f'{cable.length} {cable.get_length_unit_display()}')
+ else:
+ labels = [
+ f'Cable {cable}',
+ cable.get_status_display()
+ ]
+ description = []
+ if cable.type:
+ labels.append(cable.get_type_display())
+ if cable.length is not None and cable.length_unit:
+ # Include the cable length in the tooltip
+ labels.append(f'{cable.length} {cable.get_length_unit_display()}')
+
+ # If there is only one termination, center on that termination
+ # Otherwise average the center across the terminations
+ if len(terminations) == 1:
+ center = terminations[0].bottom_center[0]
+ else:
+ # Get a list of termination centers
+ termination_centers = [term.bottom_center[0] for term in terminations]
+ # Average the centers
+ center = sum(termination_centers) / len(termination_centers)
+
+ # Create the connector
connector = Connector(
- start=(self.center + OFFSET, self.cursor),
+ start=(center, self.cursor),
color=cable.color or '000000',
url=f'{self.base_url}{cable.get_absolute_url()}',
- labels=labels
+ labels=labels,
+ description=description
)
+ # Set the cursor position
self.cursor += connector.height
return connector
@@ -334,34 +392,52 @@ class CableTraceSVG:
# Connector (a Cable or WirelessLink)
if links:
- link = links[0] # Remove Cable from list
+ link_cables = {}
+ fanin = False
+ fanout = False
- # Cable
- if type(link) is Cable:
+ # Determine if we have fanins or fanouts
+ if len(near_ends) > len(set(links)):
+ self.cursor += FANOUT_HEIGHT
+ fanin = True
+ if len(far_ends) > len(set(links)):
+ fanout = True
+ cursor = self.cursor
+ for link in links:
+ # Cable
+ if type(link) is Cable and not link_cables.get(link.pk):
+ # Reset cursor
+ self.cursor = cursor
+ # Generate a list of terminations connected to this cable
+ near_end_link_terminations = [term for term in terminations if term.object.cable == link]
+ # Draw the cable
+ cable = self.draw_cable(link, near_end_link_terminations, cable_count=len(links))
+ # Add cable to the list of cables
+ link_cables.update({link.pk: cable})
+ # Add cable to drawing
+ self.connectors.append(cable)
- # Account for fan-ins height
- if len(near_ends) > 1:
- self.cursor += FANOUT_HEIGHT
+ # Draw fan-ins
+ if len(near_ends) > 1 and fanin:
+ for term in terminations:
+ if term.object.cable == link:
+ self.draw_fanin(term, cable)
- cable = self.draw_cable(link)
- self.connectors.append(cable)
-
- # Draw fan-ins
- if len(near_ends) > 1:
- for term in terminations:
- self.draw_fanin(term, cable)
-
- # WirelessLink
- elif type(link) is WirelessLink:
- wirelesslink = self.draw_wirelesslink(link)
- self.connectors.append(wirelesslink)
+ # WirelessLink
+ elif type(link) is WirelessLink:
+ wirelesslink = self.draw_wirelesslink(link)
+ self.connectors.append(wirelesslink)
# Far end termination(s)
if len(far_ends) > 1:
- self.cursor += FANOUT_HEIGHT
- terminations = self.draw_terminations(far_ends)
- for term in terminations:
- self.draw_fanout(term, cable)
+ if fanout:
+ self.cursor += FANOUT_HEIGHT
+ terminations = self.draw_terminations(far_ends)
+ for term in terminations:
+ if hasattr(term.object, 'cable') and link_cables.get(term.object.cable.pk):
+ self.draw_fanout(term, link_cables.get(term.object.cable.pk))
+ else:
+ self.draw_terminations(far_ends)
elif far_ends:
self.draw_terminations(far_ends)
else:
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 68c24ca14..4c863e12a 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -64,9 +64,19 @@ def get_interface_state_attribute(record):
Get interface enabled state as string to attach to DOM element.
"""
if record.enabled:
- return "enabled"
+ return 'enabled'
else:
- return "disabled"
+ return 'disabled'
+
+
+def get_interface_connected_attribute(record):
+ """
+ Get interface disconnected state as string to attach to DOM element.
+ """
+ if record.mark_connected or record.cable:
+ return 'connected'
+ else:
+ return 'disconnected'
#
@@ -456,6 +466,12 @@ class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
'args': [Accessor('device_id')],
}
)
+ maximum_draw = tables.Column(
+ verbose_name=_('Maximum draw (W)')
+ )
+ allocated_draw = tables.Column(
+ verbose_name=_('Allocated draw (W)')
+ )
tags = columns.TagColumn(
url_name='dcim:powerport_list'
)
@@ -568,6 +584,12 @@ class BaseInterfaceTable(NetBoxTable):
orderable=False,
verbose_name=_('L2VPN')
)
+ tunnel = tables.Column(
+ accessor=tables.A('tunnel_termination__tunnel'),
+ linkify=True,
+ orderable=False,
+ verbose_name=_('Tunnel')
+ )
untagged_vlan = tables.Column(
verbose_name=_('Untagged VLAN'),
linkify=True
@@ -615,6 +637,10 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
verbose_name=_('VRF'),
linkify=True
)
+ inventory_items = tables.ManyToManyColumn(
+ linkify_item=True,
+ verbose_name=_('Inventory Items'),
+ )
tags = columns.TagColumn(
url_name='dcim:interface_list'
)
@@ -626,7 +652,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn',
- 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
+ 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'inventory_items', 'created',
+ 'last_updated',
)
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
@@ -662,8 +689,8 @@ class DeviceInterfaceTable(InterfaceTable):
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
- 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups',
- 'untagged_vlan', 'tagged_vlans', 'actions',
+ 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses',
+ 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
@@ -674,6 +701,7 @@ class DeviceInterfaceTable(InterfaceTable):
'data-name': lambda record: record.name,
'data-enabled': get_interface_state_attribute,
'data-type': lambda record: record.type,
+ 'data-connected': get_interface_connected_attribute
}
@@ -871,8 +899,9 @@ class ModuleBayTable(DeviceComponentTable):
url_name='dcim:modulebay_list'
)
module_status = columns.TemplateColumn(
- verbose_name=_('Module Status'),
- template_code=MODULEBAY_STATUS
+ accessor=tables.A('installed_module__status'),
+ template_code=MODULEBAY_STATUS,
+ verbose_name=_('Module Status')
)
class Meta(DeviceComponentTable.Meta):
@@ -921,6 +950,10 @@ class InventoryItemTable(DeviceComponentTable):
discovered = columns.BooleanColumn(
verbose_name=_('Discovered'),
)
+ parent = tables.Column(
+ linkify=True,
+ verbose_name=_('Parent'),
+ )
tags = columns.TagColumn(
url_name='dcim:inventoryitem_list'
)
@@ -929,7 +962,7 @@ class InventoryItemTable(DeviceComponentTable):
class Meta(NetBoxTable.Meta):
model = models.InventoryItem
fields = (
- 'pk', 'id', 'name', 'device', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
+ 'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial',
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
)
default_columns = (
@@ -1052,7 +1085,7 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
- url_name='dcim:vdc_list'
+ url_name='dcim:virtualdevicecontext_list'
)
class Meta(NetBoxTable.Meta):
diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py
index 7d8884fc1..fad238c6e 100644
--- a/netbox/dcim/tables/devicetypes.py
+++ b/netbox/dcim/tables/devicetypes.py
@@ -98,6 +98,7 @@ class DeviceTypeTable(NetBoxTable):
verbose_name=_('U Height'),
template_code='{{ value|floatformat }}'
)
+ exclude_from_utilization = columns.BooleanColumn()
weight = columns.TemplateColumn(
verbose_name=_('Weight'),
template_code=WEIGHT,
@@ -142,9 +143,9 @@ class DeviceTypeTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = models.DeviceType
fields = (
- 'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height', 'is_full_depth',
- 'subdevice_role', 'airflow', 'weight', 'description', 'comments', 'instance_count', 'tags', 'created',
- 'last_updated',
+ 'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height',
+ 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
+ 'description', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
diff --git a/netbox/dcim/tables/power.py b/netbox/dcim/tables/power.py
index e4735bd57..40a58ad81 100644
--- a/netbox/dcim/tables/power.py
+++ b/netbox/dcim/tables/power.py
@@ -87,6 +87,11 @@ class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
linkify=True,
verbose_name=_('Tenant')
)
+ site = tables.Column(
+ accessor='rack__site',
+ linkify=True,
+ verbose_name=_('Site'),
+ )
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
@@ -97,9 +102,9 @@ class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
class Meta(NetBoxTable.Meta):
model = PowerFeed
fields = (
- 'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
- 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power', 'tenant',
- 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
+ 'pk', 'id', 'name', 'power_panel', 'site', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage',
+ 'phase', 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power',
+ 'tenant', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index e0f38afef..1862893ff 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -316,8 +316,8 @@ INTERFACE_BUTTONS = """
{% if perms.dcim.add_interface %}
Child Interface
{% endif %}
- {% if perms.ipam.add_l2vpntermination %}
- L2VPN Termination
+ {% if perms.vpn.add_l2vpntermination %}
+ L2VPN Termination
{% endif %}
{% if perms.ipam.add_fhrpgroupassignment %}
Assign FHRP Group
@@ -359,6 +359,16 @@ INTERFACE_BUTTONS = """
{% endif %}
+{% elif record.type == 'virtual' %}
+ {% if perms.vpn.add_tunnel and not record.tunnel_termination %}
+
+
+
+ {% elif perms.vpn.delete_tunneltermination and record.tunnel_termination %}
+
+
+
+ {% endif %}
{% elif record.is_wired and perms.dcim.add_cable %}
diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py
index 1ce362963..f36b11033 100644
--- a/netbox/dcim/tests/test_api.py
+++ b/netbox/dcim/tests/test_api.py
@@ -6,6 +6,7 @@ from rest_framework import status
from dcim.choices import *
from dcim.constants import *
from dcim.models import *
+from extras.models import ConfigTemplate
from ipam.models import ASN, RIR, VLAN, VRF
from netbox.api.serializers import GenericObjectSerializer
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
@@ -1265,6 +1266,22 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
+ def test_render_config(self):
+ configtemplate = ConfigTemplate.objects.create(
+ name='Config Template 1',
+ template_code='Config for device {{ device.name }}'
+ )
+
+ device = Device.objects.first()
+ device.config_template = configtemplate
+ device.save()
+
+ self.add_permissions('dcim.add_device')
+ url = reverse('dcim-api:device-detail', kwargs={'pk': device.pk}) + 'render-config/'
+ response = self.client.post(url, {}, format='json', **self.header)
+ self.assertHttpStatus(response, status.HTTP_200_OK)
+ self.assertEqual(response.data['content'], f'Config for device {device.name}')
+
class ModuleTest(APIViewTestCases.APIViewTestCase):
model = Module
@@ -1607,6 +1624,33 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
},
]
+ def test_bulk_delete_child_interfaces(self):
+ interface1 = Interface.objects.get(name='Interface 1')
+ device = interface1.device
+ self.add_permissions('dcim.delete_interface')
+
+ # Create a child interface
+ child = Interface.objects.create(
+ device=device,
+ name='Interface 1A',
+ type=InterfaceTypeChoices.TYPE_VIRTUAL,
+ parent=interface1
+ )
+ self.assertEqual(device.interfaces.count(), 4)
+
+ # Attempt to delete only the parent interface
+ url = self._get_detail_url(interface1)
+ self.client.delete(url, **self.header)
+ self.assertEqual(device.interfaces.count(), 4) # Parent was not deleted
+
+ # Attempt to bulk delete parent & child together
+ data = [
+ {"id": interface1.pk},
+ {"id": child.pk},
+ ]
+ self.client.delete(self._get_list_url(), data, format='json', **self.header)
+ self.assertEqual(device.interfaces.count(), 2) # Child & parent were both deleted
+
class FrontPortTest(APIViewTestCases.APIViewTestCase):
model = FrontPort
diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py
index d25333aed..a827939f7 100644
--- a/netbox/dcim/tests/test_cablepaths.py
+++ b/netbox/dcim/tests/test_cablepaths.py
@@ -15,6 +15,7 @@ class CablePathTestCase(TestCase):
1XX: Test direct connections between different endpoint types
2XX: Test different cable topologies
3XX: Test responses to changes in existing objects
+ 4XX: Test to exclude specific cable topologies
"""
@classmethod
def setUpTestData(cls):
@@ -33,12 +34,11 @@ class CablePathTestCase(TestCase):
circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type')
cls.circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 1')
- def assertPathExists(self, nodes, **kwargs):
+ def _get_cablepath(self, nodes, **kwargs):
"""
- Assert that a CablePath from origin to destination with a specific intermediate path exists.
+ Return a given cable path
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
- :param is_active: Boolean indicating whether the end-to-end path is complete and active (optional)
:return: The matching CablePath (if any)
"""
@@ -48,12 +48,29 @@ class CablePathTestCase(TestCase):
path.append([object_to_path_node(node) for node in step])
else:
path.append([object_to_path_node(step)])
+ return CablePath.objects.filter(path=path, **kwargs).first()
- cablepath = CablePath.objects.filter(path=path, **kwargs).first()
+ def assertPathExists(self, nodes, **kwargs):
+ """
+ Assert that a CablePath from origin to destination with a specific intermediate path exists. Returns the
+ first matching CablePath, if found.
+
+ :param nodes: Iterable of steps, with each step being either a single node or a list of nodes
+ """
+ cablepath = self._get_cablepath(nodes, **kwargs)
self.assertIsNotNone(cablepath, msg='CablePath not found')
return cablepath
+ def assertPathDoesNotExist(self, nodes, **kwargs):
+ """
+ Assert that a specific CablePath does *not* exist.
+
+ :param nodes: Iterable of steps, with each step being either a single node or a list of nodes
+ """
+ cablepath = self._get_cablepath(nodes, **kwargs)
+ self.assertIsNone(cablepath, msg='Unexpected CablePath found')
+
def assertPathIsSet(self, origin, cablepath, msg=None):
"""
Assert that a specific CablePath instance is set as the path on the origin.
@@ -1695,6 +1712,291 @@ class CablePathTestCase(TestCase):
self.assertPathIsSet(interface3, path3)
self.assertPathIsSet(interface4, path4)
+ def test_219_interface_to_interface_duplex_via_multiple_rearports(self):
+ """
+ [IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
+ [FP3] [RP3] --C4-- [RP4] [FP4]
+ """
+ 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)
+ rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1)
+ rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', 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
+ )
+ frontport3 = FrontPort.objects.create(
+ device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1
+ )
+ frontport4 = FrontPort.objects.create(
+ device=self.device, name='Front Port 4', rear_port=rearport4, rear_port_position=1
+ )
+
+ cable2 = Cable(
+ a_terminations=[rearport1],
+ b_terminations=[rearport2]
+ )
+ cable2.save()
+ cable4 = Cable(
+ a_terminations=[rearport3],
+ b_terminations=[rearport4]
+ )
+ cable4.save()
+ self.assertEqual(CablePath.objects.count(), 0)
+
+ # Create cable1
+ cable1 = Cable(
+ a_terminations=[interface1],
+ b_terminations=[frontport1, frontport3]
+ )
+ cable1.save()
+ self.assertPathExists(
+ (interface1, cable1, (frontport1, frontport3), (rearport1, rearport3), (cable2, cable4), (rearport2, rearport4), (frontport2, frontport4)),
+ is_complete=False
+ )
+ self.assertEqual(CablePath.objects.count(), 1)
+
+ # Create cable 3
+ cable3 = Cable(
+ a_terminations=[frontport2, frontport4],
+ b_terminations=[interface2]
+ )
+ cable3.save()
+ self.assertPathExists(
+ (
+ interface1, cable1, (frontport1, frontport3), (rearport1, rearport3), (cable2, cable4),
+ (rearport2, rearport4), (frontport2, frontport4), cable3, interface2
+ ),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertPathExists(
+ (
+ interface2, cable3, (frontport2, frontport4), (rearport2, rearport4), (cable2, cable4),
+ (rearport1, rearport3), (frontport1, frontport3), cable1, interface1
+ ),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertEqual(CablePath.objects.count(), 2)
+
+ def test_220_interface_to_interface_duplex_via_multiple_front_and_rear_ports(self):
+ """
+ [IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
+ [IF2] --C5-- [FP3] [RP3] --C4-- [RP4] [FP4]
+ """
+ interface1 = Interface.objects.create(device=self.device, name='Interface 1')
+ interface2 = Interface.objects.create(device=self.device, name='Interface 2')
+ interface3 = Interface.objects.create(device=self.device, name='Interface 3')
+ 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)
+ rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1)
+ rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', 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
+ )
+ frontport3 = FrontPort.objects.create(
+ device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1
+ )
+ frontport4 = FrontPort.objects.create(
+ device=self.device, name='Front Port 4', rear_port=rearport4, rear_port_position=1
+ )
+
+ cable2 = Cable(
+ a_terminations=[rearport1],
+ b_terminations=[rearport2]
+ )
+ cable2.save()
+ cable4 = Cable(
+ a_terminations=[rearport3],
+ b_terminations=[rearport4]
+ )
+ cable4.save()
+ self.assertEqual(CablePath.objects.count(), 0)
+
+ # Create cable1
+ cable1 = Cable(
+ a_terminations=[interface1],
+ b_terminations=[frontport1]
+ )
+ cable1.save()
+ self.assertPathExists(
+ (
+ interface1, cable1, frontport1, rearport1, cable2, rearport2, frontport2
+ ),
+ is_complete=False
+ )
+ # Create cable1
+ cable5 = Cable(
+ a_terminations=[interface3],
+ b_terminations=[frontport3]
+ )
+ cable5.save()
+ self.assertPathExists(
+ (
+ interface3, cable5, frontport3, rearport3, cable4, rearport4, frontport4
+ ),
+ is_complete=False
+ )
+ self.assertEqual(CablePath.objects.count(), 2)
+
+ # Create cable 3
+ cable3 = Cable(
+ a_terminations=[frontport2, frontport4],
+ b_terminations=[interface2]
+ )
+ cable3.save()
+ self.assertPathExists(
+ (
+ interface2, cable3, (frontport2, frontport4), (rearport2, rearport4), (cable2, cable4),
+ (rearport1, rearport3), (frontport1, frontport3), (cable1, cable5), (interface1, interface3)
+ ),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertPathExists(
+ (
+ interface1, cable1, frontport1, rearport1, cable2, rearport2, frontport2, cable3, interface2
+ ),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertPathExists(
+ (
+ interface3, cable5, frontport3, rearport3, cable4, rearport4, frontport4, cable3, interface2
+ ),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertEqual(CablePath.objects.count(), 3)
+
+ def test_221_non_symmetric_paths(self):
+ """
+ [IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- -------------------------------------- [IF2]
+ [IF2] --C5-- [FP3] [RP3] --C4-- [RP4] [FP4] --C6-- [FP5] [RP5] --C7-- [RP6] [FP6] --C3---/
+ """
+ interface1 = Interface.objects.create(device=self.device, name='Interface 1')
+ interface2 = Interface.objects.create(device=self.device, name='Interface 2')
+ interface3 = Interface.objects.create(device=self.device, name='Interface 3')
+ 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)
+ rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1)
+ rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1)
+ rearport5 = RearPort.objects.create(device=self.device, name='Rear Port 5', positions=1)
+ rearport6 = RearPort.objects.create(device=self.device, name='Rear Port 6', 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
+ )
+ frontport3 = FrontPort.objects.create(
+ device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1
+ )
+ frontport4 = FrontPort.objects.create(
+ device=self.device, name='Front Port 4', rear_port=rearport4, rear_port_position=1
+ )
+ frontport5 = FrontPort.objects.create(
+ device=self.device, name='Front Port 5', rear_port=rearport5, rear_port_position=1
+ )
+ frontport6 = FrontPort.objects.create(
+ device=self.device, name='Front Port 6', rear_port=rearport6, rear_port_position=1
+ )
+
+ cable2 = Cable(
+ a_terminations=[rearport1],
+ b_terminations=[rearport2],
+ label='C2'
+ )
+ cable2.save()
+ cable4 = Cable(
+ a_terminations=[rearport3],
+ b_terminations=[rearport4],
+ label='C4'
+ )
+ cable4.save()
+ cable6 = Cable(
+ a_terminations=[frontport4],
+ b_terminations=[frontport5],
+ label='C6'
+ )
+ cable6.save()
+ cable7 = Cable(
+ a_terminations=[rearport5],
+ b_terminations=[rearport6],
+ label='C7'
+ )
+ cable7.save()
+ self.assertEqual(CablePath.objects.count(), 0)
+
+ # Create cable1
+ cable1 = Cable(
+ a_terminations=[interface1],
+ b_terminations=[frontport1],
+ label='C1'
+ )
+ cable1.save()
+ self.assertPathExists(
+ (
+ interface1, cable1, frontport1, rearport1, cable2, rearport2, frontport2
+ ),
+ is_complete=False
+ )
+ # Create cable1
+ cable5 = Cable(
+ a_terminations=[interface3],
+ b_terminations=[frontport3],
+ label='C5'
+ )
+ cable5.save()
+ self.assertPathExists(
+ (
+ interface3, cable5, frontport3, rearport3, cable4, rearport4, frontport4, cable6, frontport5, rearport5,
+ cable7, rearport6, frontport6
+ ),
+ is_complete=False
+ )
+ self.assertEqual(CablePath.objects.count(), 2)
+
+ # Create cable 3
+ cable3 = Cable(
+ a_terminations=[frontport2, frontport6],
+ b_terminations=[interface2],
+ label='C3'
+ )
+ cable3.save()
+ self.assertPathExists(
+ (
+ interface2, cable3, (frontport2, frontport6), (rearport2, rearport6), (cable2, cable7),
+ (rearport1, rearport5), (frontport1, frontport5), (cable1, cable6)
+ ),
+ is_complete=False,
+ is_split=True
+ )
+ self.assertPathExists(
+ (
+ interface1, cable1, frontport1, rearport1, cable2, rearport2, frontport2, cable3, interface2
+ ),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertPathExists(
+ (
+ interface3, cable5, frontport3, rearport3, cable4, rearport4, frontport4, cable6, frontport5, rearport5,
+ cable7, rearport6, frontport6, cable3, interface2
+ ),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertEqual(CablePath.objects.count(), 3)
+
def test_301_create_path_via_existing_cable(self):
"""
[IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
@@ -1845,3 +2147,93 @@ class CablePathTestCase(TestCase):
is_complete=True,
is_active=True
)
+
+ def test_401_exclude_midspan_devices(self):
+ """
+ [IF1] --C1-- [FP1][Test Device][RP1] --C2-- [RP2][Test Device][FP2] --C3-- [IF2]
+ [FP3][Test mid-span Device][RP3] --C4-- [RP4][Test mid-span Device][FP4] /
+ """
+ device = Device.objects.create(
+ site=self.site,
+ device_type=self.device.device_type,
+ device_role=self.device.device_role,
+ name='Test mid-span Device'
+ )
+ 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)
+ rearport3 = RearPort.objects.create(device=device, name='Rear Port 3', positions=1)
+ rearport4 = RearPort.objects.create(device=device, name='Rear Port 4', 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
+ )
+ frontport3 = FrontPort.objects.create(
+ device=device, name='Front Port 3', rear_port=rearport3, rear_port_position=1
+ )
+ frontport4 = FrontPort.objects.create(
+ device=device, name='Front Port 4', rear_port=rearport4, rear_port_position=1
+ )
+
+ cable2 = Cable(
+ a_terminations=[rearport1],
+ b_terminations=[rearport2],
+ label='C2'
+ )
+ cable2.save()
+ cable4 = Cable(
+ a_terminations=[rearport3],
+ b_terminations=[rearport4],
+ label='C4'
+ )
+ cable4.save()
+ self.assertEqual(CablePath.objects.count(), 0)
+
+ # Create cable1
+ cable1 = Cable(
+ a_terminations=[interface1],
+ b_terminations=[frontport1, frontport3],
+ label='C1'
+ )
+ with self.assertRaises(AssertionError):
+ cable1.save()
+
+ self.assertPathDoesNotExist(
+ (
+ interface1, cable1, (frontport1, frontport3), (rearport1, rearport3), (cable2, cable4),
+ (rearport2, rearport4), (frontport2, frontport4)
+ ),
+ is_complete=False
+ )
+ self.assertEqual(CablePath.objects.count(), 0)
+
+ # Create cable 3
+ cable3 = Cable(
+ a_terminations=[frontport2, frontport4],
+ b_terminations=[interface2],
+ label='C3'
+ )
+
+ with self.assertRaises(AssertionError):
+ cable3.save()
+
+ self.assertPathDoesNotExist(
+ (
+ interface2, cable3, (frontport2, frontport4), (rearport2, rearport4), (cable2, cable4),
+ (rearport1, rearport3), (frontport1, frontport2), cable1, interface1
+ ),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertPathDoesNotExist(
+ (
+ interface1, cable1, (frontport1, frontport3), (rearport1, rearport3), (cable2, cable4),
+ (rearport2, rearport4), (frontport2, frontport4), cable3, interface2
+ ),
+ is_complete=True,
+ is_active=True
+ )
+ self.assertEqual(CablePath.objects.count(), 0)
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index 48600bf41..89d15a0ef 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -1,6 +1,7 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
+from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.choices import *
from dcim.filtersets import *
from dcim.models import *
@@ -17,6 +18,14 @@ User = get_user_model()
class DeviceComponentFilterSetTests:
+ def test_q(self):
+ params = {'q': 'First'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_description(self):
+ params = {'description': ['First', 'Second']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_device_type(self):
device_types = DeviceType.objects.all()[:2]
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
@@ -32,6 +41,22 @@ class DeviceComponentFilterSetTests:
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+class DeviceComponentTemplateFilterSetTests:
+
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ 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_devicetype_id(self):
+ device_types = DeviceType.objects.all()[:2]
+ params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Region.objects.all()
filterset = RegionFilterSet
@@ -40,9 +65,9 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
regions = (
- Region(name='Region 1', slug='region-1', description='A'),
- Region(name='Region 2', slug='region-2', description='B'),
- Region(name='Region 3', slug='region-3', description='C'),
+ Region(name='Region 1', slug='region-1', description='foobar1'),
+ Region(name='Region 2', slug='region-2', description='foobar2'),
+ Region(name='Region 3', slug='region-3', description='foobar3'),
)
for region in regions:
region.save()
@@ -58,6 +83,10 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
for region in child_regions:
region.save()
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Region 1', 'Region 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -67,7 +96,7 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
- params = {'description': ['A', 'B']}
+ params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self):
@@ -86,9 +115,9 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
sitegroups = (
- SiteGroup(name='Site Group 1', slug='site-group-1', description='A'),
- SiteGroup(name='Site Group 2', slug='site-group-2', description='B'),
- SiteGroup(name='Site Group 3', slug='site-group-3', description='C'),
+ SiteGroup(name='Site Group 1', slug='site-group-1', description='foobar1'),
+ SiteGroup(name='Site Group 2', slug='site-group-2', description='foobar2'),
+ SiteGroup(name='Site Group 3', slug='site-group-3', description='foobar3'),
)
for sitegroup in sitegroups:
sitegroup.save()
@@ -104,6 +133,10 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
for sitegroup in child_sitegroups:
sitegroup.save()
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Site Group 1', 'Site Group 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -113,7 +146,7 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
- params = {'description': ['A', 'B']}
+ params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self):
@@ -172,7 +205,7 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
sites = (
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', latitude=10, longitude=10, description='foobar1'),
- Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', latitude=20, longitude=20, description='foobar1'),
+ Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', latitude=20, longitude=20, description='foobar2'),
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', latitude=30, longitude=30),
)
Site.objects.bulk_create(sites)
@@ -180,6 +213,10 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
sites[1].asns.set([asns[1]])
sites[2].asns.set([asns[2]])
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Site 1', 'Site 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -285,13 +322,17 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
location.save()
locations = (
- Location(name='Location 1', slug='location-1', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, description='A'),
- Location(name='Location 2', slug='location-2', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, description='B'),
- Location(name='Location 3', slug='location-3', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='C'),
+ Location(name='Location 1', slug='location-1', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, description='foobar1'),
+ Location(name='Location 2', slug='location-2', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, description='foobar2'),
+ Location(name='Location 3', slug='location-3', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='foobar3'),
)
for location in locations:
location.save()
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Location 1', 'Location 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -305,7 +346,7 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
- params = {'description': ['A', 'B']}
+ params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self):
@@ -351,6 +392,10 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
)
RackRole.objects.bulk_create(rack_roles)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Rack Role 1', 'Rack Role 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -429,12 +474,79 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
racks = (
- Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=10, max_weight=1000, weight_unit=WeightUnitChoices.UNIT_POUND),
- Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=20, max_weight=2000, weight_unit=WeightUnitChoices.UNIT_POUND),
- Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH, weight=30, max_weight=3000, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
+ Rack(
+ name='Rack 1',
+ facility_id='rack-1',
+ site=sites[0],
+ location=locations[0],
+ tenant=tenants[0],
+ status=RackStatusChoices.STATUS_ACTIVE,
+ role=rack_roles[0],
+ serial='ABC',
+ asset_tag='1001',
+ type=RackTypeChoices.TYPE_2POST,
+ width=RackWidthChoices.WIDTH_19IN,
+ u_height=42,
+ desc_units=False,
+ outer_width=100,
+ outer_depth=100,
+ outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
+ weight=10,
+ max_weight=1000,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar1'
+ ),
+ Rack(
+ name='Rack 2',
+ facility_id='rack-2',
+ site=sites[1],
+ location=locations[1],
+ tenant=tenants[1],
+ status=RackStatusChoices.STATUS_PLANNED,
+ role=rack_roles[1],
+ serial='DEF',
+ asset_tag='1002',
+ type=RackTypeChoices.TYPE_4POST,
+ width=RackWidthChoices.WIDTH_21IN,
+ u_height=43,
+ desc_units=False,
+ outer_width=200,
+ outer_depth=200,
+ outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
+ weight=20,
+ max_weight=2000,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar2'
+ ),
+ Rack(
+ name='Rack 3',
+ facility_id='rack-3',
+ site=sites[2],
+ location=locations[2],
+ tenant=tenants[2],
+ status=RackStatusChoices.STATUS_RESERVED,
+ role=rack_roles[2],
+ serial='GHI',
+ asset_tag='1003',
+ type=RackTypeChoices.TYPE_CABINET,
+ width=RackWidthChoices.WIDTH_23IN,
+ u_height=44,
+ desc_units=True,
+ outer_width=300,
+ outer_depth=300,
+ outer_unit=RackDimensionUnitChoices.UNIT_INCH,
+ weight=30,
+ max_weight=3000,
+ weight_unit=WeightUnitChoices.UNIT_KILOGRAM,
+ description='foobar3'
+ ),
)
Rack.objects.bulk_create(racks)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Rack 1', 'Rack 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -447,6 +559,10 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'asset_tag': ['1001', '1002']}
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_type(self):
params = {'type': [RackTypeChoices.TYPE_2POST, RackTypeChoices.TYPE_4POST]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -626,10 +742,14 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
reservations = (
RackReservation(rack=racks[0], units=[1, 2, 3], user=users[0], tenant=tenants[0], description='foobar1'),
RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1], tenant=tenants[1], description='foobar2'),
- RackReservation(rack=racks[2], units=[7, 8, 9], user=users[2], tenant=tenants[2]),
+ RackReservation(rack=racks[2], units=[7, 8, 9], user=users[2], tenant=tenants[2], description='foobar3'),
)
RackReservation.objects.bulk_create(reservations)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -692,12 +812,16 @@ class ManufacturerTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
manufacturers = (
- Manufacturer(name='Manufacturer 1', slug='manufacturer-1', description='A'),
- Manufacturer(name='Manufacturer 2', slug='manufacturer-2', description='B'),
- Manufacturer(name='Manufacturer 3', slug='manufacturer-3', description='C'),
+ Manufacturer(name='Manufacturer 1', slug='manufacturer-1', description='foobar1'),
+ Manufacturer(name='Manufacturer 2', slug='manufacturer-2', description='foobar2'),
+ Manufacturer(name='Manufacturer 3', slug='manufacturer-3', description='foobar3'),
)
Manufacturer.objects.bulk_create(manufacturers)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Manufacturer 1', 'Manufacturer 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -707,7 +831,7 @@ class ManufacturerTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
- params = {'description': ['A', 'B']}
+ params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -733,9 +857,47 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
Platform.objects.bulk_create(platforms)
device_types = (
- DeviceType(manufacturer=manufacturers[0], default_platform=platforms[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, front_image='front.png', rear_image='rear.png', weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
- DeviceType(manufacturer=manufacturers[1], default_platform=platforms[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
- DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT, weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
+ DeviceType(
+ manufacturer=manufacturers[0],
+ default_platform=platforms[0],
+ model='Model 1',
+ slug='model-1',
+ part_number='Part Number 1',
+ u_height=1,
+ is_full_depth=True,
+ front_image='front.png',
+ rear_image='rear.png',
+ weight=10,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar1'
+ ),
+ DeviceType(
+ manufacturer=manufacturers[1],
+ default_platform=platforms[1],
+ model='Model 2',
+ slug='model-2',
+ part_number='Part Number 2',
+ u_height=2,
+ is_full_depth=True,
+ subdevice_role=SubdeviceRoleChoices.ROLE_PARENT,
+ airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR,
+ weight=20,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar2'
+ ),
+ DeviceType(
+ manufacturer=manufacturers[2],
+ model='Model 3',
+ slug='model-3',
+ part_number='Part Number 3',
+ u_height=3,
+ is_full_depth=False,
+ subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
+ airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT,
+ weight=30,
+ weight_unit=WeightUnitChoices.UNIT_KILOGRAM,
+ description='foobar3'
+ ),
)
DeviceType.objects.bulk_create(device_types)
@@ -781,6 +943,10 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
inventory_item = InventoryItemTemplate(device_type=device_types[1], name='Inventory Item 1')
inventory_item.save()
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_model(self):
params = {'model': ['Model 1', 'Model 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -793,6 +959,10 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'part_number': ['Part Number 1', 'Part Number 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_u_height(self):
params = {'u_height': [1, 2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -915,9 +1085,30 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
Manufacturer.objects.bulk_create(manufacturers)
module_types = (
- ModuleType(manufacturer=manufacturers[0], model='Model 1', part_number='Part Number 1', weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
- ModuleType(manufacturer=manufacturers[1], model='Model 2', part_number='Part Number 2', weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
- ModuleType(manufacturer=manufacturers[2], model='Model 3', part_number='Part Number 3', weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
+ ModuleType(
+ manufacturer=manufacturers[0],
+ model='Model 1',
+ part_number='Part Number 1',
+ weight=10,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar1'
+ ),
+ ModuleType(
+ manufacturer=manufacturers[1],
+ model='Model 2',
+ part_number='Part Number 2',
+ weight=20,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar2'
+ ),
+ ModuleType(
+ manufacturer=manufacturers[2],
+ model='Model 3',
+ part_number='Part Number 3',
+ weight=30,
+ weight_unit=WeightUnitChoices.UNIT_KILOGRAM,
+ description='foobar3'
+ ),
)
ModuleType.objects.bulk_create(module_types)
@@ -952,6 +1143,10 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
FrontPortTemplate(module_type=module_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]),
))
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_model(self):
params = {'model': ['Model 1', 'Model 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -960,6 +1155,10 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'part_number': ['Part Number 1', 'Part Number 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_manufacturer(self):
manufacturers = Manufacturer.objects.all()[:2]
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
@@ -1012,7 +1211,7 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
+class ConsolePortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = ConsolePortTemplate.objects.all()
filterset = ConsolePortTemplateFilterSet
@@ -1029,22 +1228,17 @@ class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
DeviceType.objects.bulk_create(device_types)
ConsolePortTemplate.objects.bulk_create((
- ConsolePortTemplate(device_type=device_types[0], name='Console Port 1'),
- ConsolePortTemplate(device_type=device_types[1], name='Console Port 2'),
- ConsolePortTemplate(device_type=device_types[2], name='Console Port 3'),
+ ConsolePortTemplate(device_type=device_types[0], name='Console Port 1', description='foobar1'),
+ ConsolePortTemplate(device_type=device_types[1], name='Console Port 2', description='foobar2'),
+ ConsolePortTemplate(device_type=device_types[2], name='Console Port 3', description='foobar3'),
))
def test_name(self):
params = {'name': ['Console Port 1', 'Console Port 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- def test_devicetype_id(self):
- device_types = DeviceType.objects.all()[:2]
- params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-class ConsoleServerPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
+class ConsoleServerPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = ConsoleServerPortTemplate.objects.all()
filterset = ConsoleServerPortTemplateFilterSet
@@ -1061,22 +1255,17 @@ class ConsoleServerPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
DeviceType.objects.bulk_create(device_types)
ConsoleServerPortTemplate.objects.bulk_create((
- ConsoleServerPortTemplate(device_type=device_types[0], name='Console Server Port 1'),
- ConsoleServerPortTemplate(device_type=device_types[1], name='Console Server Port 2'),
- ConsoleServerPortTemplate(device_type=device_types[2], name='Console Server Port 3'),
+ ConsoleServerPortTemplate(device_type=device_types[0], name='Console Server Port 1', description='foobar1'),
+ ConsoleServerPortTemplate(device_type=device_types[1], name='Console Server Port 2', description='foobar2'),
+ ConsoleServerPortTemplate(device_type=device_types[2], name='Console Server Port 3', description='foobar3'),
))
def test_name(self):
params = {'name': ['Console Server Port 1', 'Console Server Port 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- def test_devicetype_id(self):
- device_types = DeviceType.objects.all()[:2]
- params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-class PowerPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
+class PowerPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = PowerPortTemplate.objects.all()
filterset = PowerPortTemplateFilterSet
@@ -1093,20 +1282,33 @@ class PowerPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
DeviceType.objects.bulk_create(device_types)
PowerPortTemplate.objects.bulk_create((
- PowerPortTemplate(device_type=device_types[0], name='Power Port 1', maximum_draw=100, allocated_draw=50),
- PowerPortTemplate(device_type=device_types[1], name='Power Port 2', maximum_draw=200, allocated_draw=100),
- PowerPortTemplate(device_type=device_types[2], name='Power Port 3', maximum_draw=300, allocated_draw=150),
+ PowerPortTemplate(
+ device_type=device_types[0],
+ name='Power Port 1',
+ maximum_draw=100,
+ allocated_draw=50,
+ description='foobar1'
+ ),
+ PowerPortTemplate(
+ device_type=device_types[1],
+ name='Power Port 2',
+ maximum_draw=200,
+ allocated_draw=100,
+ description='foobar2'
+ ),
+ PowerPortTemplate(
+ device_type=device_types[2],
+ name='Power Port 3',
+ maximum_draw=300,
+ allocated_draw=150,
+ description='foobar3'
+ ),
))
def test_name(self):
params = {'name': ['Power Port 1', 'Power Port 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- def test_devicetype_id(self):
- device_types = DeviceType.objects.all()[:2]
- params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
def test_maximum_draw(self):
params = {'maximum_draw': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1116,7 +1318,7 @@ class PowerPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-class PowerOutletTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
+class PowerOutletTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = PowerOutletTemplate.objects.all()
filterset = PowerOutletTemplateFilterSet
@@ -1133,26 +1335,36 @@ class PowerOutletTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
DeviceType.objects.bulk_create(device_types)
PowerOutletTemplate.objects.bulk_create((
- PowerOutletTemplate(device_type=device_types[0], name='Power Outlet 1', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A),
- PowerOutletTemplate(device_type=device_types[1], name='Power Outlet 2', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B),
- PowerOutletTemplate(device_type=device_types[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C),
+ PowerOutletTemplate(
+ device_type=device_types[0],
+ name='Power Outlet 1',
+ feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A,
+ description='foobar1'
+ ),
+ PowerOutletTemplate(
+ device_type=device_types[1],
+ name='Power Outlet 2',
+ feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B,
+ description='foobar2'
+ ),
+ PowerOutletTemplate(
+ device_type=device_types[2],
+ name='Power Outlet 3',
+ feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C,
+ description='foobar3'
+ ),
))
def test_name(self):
params = {'name': ['Power Outlet 1', 'Power Outlet 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- def test_devicetype_id(self):
- device_types = DeviceType.objects.all()[:2]
- params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
- 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)
-class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
+class InterfaceTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = InterfaceTemplate.objects.all()
filterset = InterfaceTemplateFilterSet
@@ -1176,7 +1388,8 @@ class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
enabled=True,
mgmt_only=True,
poe_mode=InterfacePoEModeChoices.MODE_PD,
- poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
+ poe_type=InterfacePoETypeChoices.TYPE_1_8023AF,
+ description='foobar1'
),
InterfaceTemplate(
device_type=device_types[1],
@@ -1185,13 +1398,15 @@ class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
enabled=False,
mgmt_only=False,
poe_mode=InterfacePoEModeChoices.MODE_PSE,
- poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
+ poe_type=InterfacePoETypeChoices.TYPE_2_8023AT,
+ description='foobar2'
),
InterfaceTemplate(
device_type=device_types[2],
name='Interface 3',
type=InterfaceTypeChoices.TYPE_1GE_SFP,
- mgmt_only=False
+ mgmt_only=False,
+ description='foobar3'
),
)
InterfaceTemplate.objects.bulk_create(interface_templates)
@@ -1203,11 +1418,6 @@ class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'name': ['Interface 1', 'Interface 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- def test_devicetype_id(self):
- device_types = DeviceType.objects.all()[:2]
- params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
def test_type(self):
params = {'type': [InterfaceTypeChoices.TYPE_1GE_FIXED, InterfaceTypeChoices.TYPE_1GE_GBIC]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1237,7 +1447,7 @@ class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
+class FrontPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = FrontPortTemplate.objects.all()
filterset = FrontPortTemplateFilterSet
@@ -1261,20 +1471,36 @@ class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
RearPortTemplate.objects.bulk_create(rear_ports)
FrontPortTemplate.objects.bulk_create((
- FrontPortTemplate(device_type=device_types[0], name='Front Port 1', rear_port=rear_ports[0], type=PortTypeChoices.TYPE_8P8C, color=ColorChoices.COLOR_RED),
- FrontPortTemplate(device_type=device_types[1], name='Front Port 2', rear_port=rear_ports[1], type=PortTypeChoices.TYPE_110_PUNCH, color=ColorChoices.COLOR_GREEN),
- FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], type=PortTypeChoices.TYPE_BNC, color=ColorChoices.COLOR_BLUE),
+ FrontPortTemplate(
+ device_type=device_types[0],
+ name='Front Port 1',
+ rear_port=rear_ports[0],
+ type=PortTypeChoices.TYPE_8P8C,
+ color=ColorChoices.COLOR_RED,
+ description='foobar1'
+ ),
+ FrontPortTemplate(
+ device_type=device_types[1],
+ name='Front Port 2',
+ rear_port=rear_ports[1],
+ type=PortTypeChoices.TYPE_110_PUNCH,
+ color=ColorChoices.COLOR_GREEN,
+ description='foobar2'
+ ),
+ FrontPortTemplate(
+ device_type=device_types[2],
+ name='Front Port 3',
+ rear_port=rear_ports[2],
+ type=PortTypeChoices.TYPE_BNC,
+ color=ColorChoices.COLOR_BLUE,
+ description='foobar3'
+ ),
))
def test_name(self):
params = {'name': ['Front Port 1', 'Front Port 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- def test_devicetype_id(self):
- device_types = DeviceType.objects.all()[:2]
- params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
def test_type(self):
params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1284,7 +1510,7 @@ class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
+class RearPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = RearPortTemplate.objects.all()
filterset = RearPortTemplateFilterSet
@@ -1301,20 +1527,36 @@ class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
DeviceType.objects.bulk_create(device_types)
RearPortTemplate.objects.bulk_create((
- RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, color=ColorChoices.COLOR_RED, positions=1),
- RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, color=ColorChoices.COLOR_GREEN, positions=2),
- RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, color=ColorChoices.COLOR_BLUE, positions=3),
+ RearPortTemplate(
+ device_type=device_types[0],
+ name='Rear Port 1',
+ type=PortTypeChoices.TYPE_8P8C,
+ color=ColorChoices.COLOR_RED,
+ positions=1,
+ description='foobar1'
+ ),
+ RearPortTemplate(
+ device_type=device_types[1],
+ name='Rear Port 2',
+ type=PortTypeChoices.TYPE_110_PUNCH,
+ color=ColorChoices.COLOR_GREEN,
+ positions=2,
+ description='foobar2'
+ ),
+ RearPortTemplate(
+ device_type=device_types[2],
+ name='Rear Port 3',
+ type=PortTypeChoices.TYPE_BNC,
+ color=ColorChoices.COLOR_BLUE,
+ positions=3,
+ description='foobar3'
+ ),
))
def test_name(self):
params = {'name': ['Rear Port 1', 'Rear Port 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- def test_devicetype_id(self):
- device_types = DeviceType.objects.all()[:2]
- params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
def test_type(self):
params = {'type': [PortTypeChoices.TYPE_8P8C, PortTypeChoices.TYPE_110_PUNCH]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1328,7 +1570,7 @@ class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-class ModuleBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
+class ModuleBayTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = ModuleBayTemplate.objects.all()
filterset = ModuleBayTemplateFilterSet
@@ -1345,22 +1587,17 @@ class ModuleBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
DeviceType.objects.bulk_create(device_types)
ModuleBayTemplate.objects.bulk_create((
- ModuleBayTemplate(device_type=device_types[0], name='Module Bay 1'),
- ModuleBayTemplate(device_type=device_types[1], name='Module Bay 2'),
- ModuleBayTemplate(device_type=device_types[2], name='Module Bay 3'),
+ 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'),
))
def test_name(self):
params = {'name': ['Module Bay 1', 'Module Bay 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- def test_devicetype_id(self):
- device_types = DeviceType.objects.all()[:2]
- params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-class DeviceBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
+class DeviceBayTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = DeviceBayTemplate.objects.all()
filterset = DeviceBayTemplateFilterSet
@@ -1377,22 +1614,17 @@ class DeviceBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
DeviceType.objects.bulk_create(device_types)
DeviceBayTemplate.objects.bulk_create((
- DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'),
- DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'),
- DeviceBayTemplate(device_type=device_types[2], name='Device Bay 3'),
+ DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1', description='foobar1'),
+ DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2', description='foobar2'),
+ DeviceBayTemplate(device_type=device_types[2], name='Device Bay 3', description='foobar3'),
))
def test_name(self):
params = {'name': ['Device Bay 1', 'Device Bay 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- def test_devicetype_id(self):
- device_types = DeviceType.objects.all()[:2]
- params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-class InventoryItemTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
+class InventoryItemTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTests, ChangeLoggedFilterSetTests):
queryset = InventoryItemTemplate.objects.all()
filterset = InventoryItemTemplateFilterSet
@@ -1420,9 +1652,33 @@ class InventoryItemTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
InventoryItemRole.objects.bulk_create(inventory_item_roles)
inventory_item_templates = (
- InventoryItemTemplate(device_type=device_types[0], name='Inventory Item 1', label='A', role=inventory_item_roles[0], manufacturer=manufacturers[0], part_id='1001'),
- InventoryItemTemplate(device_type=device_types[1], name='Inventory Item 2', label='B', role=inventory_item_roles[1], manufacturer=manufacturers[1], part_id='1002'),
- InventoryItemTemplate(device_type=device_types[2], name='Inventory Item 3', label='C', role=inventory_item_roles[2], manufacturer=manufacturers[2], part_id='1003'),
+ InventoryItemTemplate(
+ device_type=device_types[0],
+ name='Inventory Item 1',
+ label='A',
+ role=inventory_item_roles[0],
+ manufacturer=manufacturers[0],
+ part_id='1001',
+ description='foobar1'
+ ),
+ InventoryItemTemplate(
+ device_type=device_types[1],
+ name='Inventory Item 2',
+ label='B',
+ role=inventory_item_roles[1],
+ manufacturer=manufacturers[1],
+ part_id='1002',
+ description='foobar2'
+ ),
+ InventoryItemTemplate(
+ device_type=device_types[2],
+ name='Inventory Item 3',
+ label='C',
+ role=inventory_item_roles[2],
+ manufacturer=manufacturers[2],
+ part_id='1003',
+ description='foobar3'
+ ),
)
for item in inventory_item_templates:
item.save()
@@ -1486,6 +1742,10 @@ class DeviceRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
)
DeviceRole.objects.bulk_create(roles)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Device Role 1', 'Device Role 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1524,12 +1784,16 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
Manufacturer.objects.bulk_create(manufacturers)
platforms = (
- Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], description='A'),
- Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], description='B'),
- Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='C'),
+ Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], description='foobar1'),
+ Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], description='foobar2'),
+ Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='foobar3'),
)
Platform.objects.bulk_create(platforms)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Platform 1', 'Platform 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1539,7 +1803,7 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
- params = {'description': ['A', 'B']}
+ params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_manufacturer(self):
@@ -1647,9 +1911,66 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
devices = (
- Device(name='Device 1', device_type=device_types[0], role=roles[0], platform=platforms[0], tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], location=locations[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, latitude=10, longitude=10, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}),
- Device(name='Device 2', device_type=device_types[1], role=roles[1], platform=platforms[1], tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], location=locations[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, latitude=20, longitude=20, status=DeviceStatusChoices.STATUS_STAGED, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, cluster=clusters[1]),
- Device(name='Device 3', device_type=device_types[2], role=roles[2], platform=platforms[2], tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], location=locations[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, latitude=30, longitude=30, status=DeviceStatusChoices.STATUS_FAILED, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT, cluster=clusters[2]),
+ Device(
+ name='Device 1',
+ device_type=device_types[0],
+ role=roles[0],
+ platform=platforms[0],
+ tenant=tenants[0],
+ serial='ABC',
+ asset_tag='1001',
+ site=sites[0],
+ location=locations[0],
+ rack=racks[0],
+ position=1,
+ face=DeviceFaceChoices.FACE_FRONT,
+ latitude=10,
+ longitude=10,
+ status=DeviceStatusChoices.STATUS_ACTIVE,
+ cluster=clusters[0],
+ local_context_data={"foo": 123},
+ description='foobar1'
+ ),
+ Device(
+ name='Device 2',
+ device_type=device_types[1],
+ role=roles[1],
+ platform=platforms[1],
+ tenant=tenants[1],
+ serial='DEF',
+ asset_tag='1002',
+ site=sites[1],
+ location=locations[1],
+ rack=racks[1],
+ position=2,
+ face=DeviceFaceChoices.FACE_FRONT,
+ latitude=20,
+ longitude=20,
+ status=DeviceStatusChoices.STATUS_STAGED,
+ airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR,
+ cluster=clusters[1],
+ description='foobar2'
+ ),
+ Device(
+ name='Device 3',
+ device_type=device_types[2],
+ role=roles[2],
+ platform=platforms[2],
+ tenant=tenants[2],
+ serial='GHI',
+ asset_tag='1003',
+ site=sites[2],
+ location=locations[2],
+ rack=racks[2],
+ position=3,
+ face=DeviceFaceChoices.FACE_REAR,
+ latitude=30,
+ longitude=30,
+ status=DeviceStatusChoices.STATUS_FAILED,
+ airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT,
+ cluster=clusters[2],
+ description='foobar3'
+ ),
)
Device.objects.bulk_create(devices)
@@ -1711,6 +2032,10 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1)
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Device 1', 'Device 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1718,6 +2043,10 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'name': ['DEVICE 1', 'DEVICE 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_asset_tag(self):
params = {'asset_tag': ['1001', '1002']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1977,18 +2306,88 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
ModuleBay.objects.bulk_create(module_bays)
modules = (
- Module(device=devices[0], module_bay=module_bays[0], module_type=module_types[0], status=ModuleStatusChoices.STATUS_ACTIVE, serial='A', asset_tag='A'),
- Module(device=devices[0], module_bay=module_bays[1], module_type=module_types[1], status=ModuleStatusChoices.STATUS_ACTIVE, serial='B', asset_tag='B'),
- Module(device=devices[0], module_bay=module_bays[2], module_type=module_types[2], status=ModuleStatusChoices.STATUS_ACTIVE, serial='C', asset_tag='C'),
- Module(device=devices[1], module_bay=module_bays[3], module_type=module_types[0], status=ModuleStatusChoices.STATUS_ACTIVE, serial='D', asset_tag='D'),
- Module(device=devices[1], module_bay=module_bays[4], module_type=module_types[1], status=ModuleStatusChoices.STATUS_ACTIVE, serial='E', asset_tag='E'),
- Module(device=devices[1], module_bay=module_bays[5], module_type=module_types[2], status=ModuleStatusChoices.STATUS_ACTIVE, serial='F', asset_tag='F'),
- Module(device=devices[2], module_bay=module_bays[6], module_type=module_types[0], status=ModuleStatusChoices.STATUS_ACTIVE, serial='G', asset_tag='G'),
- Module(device=devices[2], module_bay=module_bays[7], module_type=module_types[1], status=ModuleStatusChoices.STATUS_PLANNED, serial='H', asset_tag='H'),
- Module(device=devices[2], module_bay=module_bays[8], module_type=module_types[2], status=ModuleStatusChoices.STATUS_FAILED, serial='I', asset_tag='I'),
+ Module(
+ device=devices[0],
+ module_bay=module_bays[0],
+ module_type=module_types[0],
+ status=ModuleStatusChoices.STATUS_ACTIVE,
+ serial='A',
+ asset_tag='A',
+ description='foobar1'
+ ),
+ Module(
+ device=devices[0],
+ module_bay=module_bays[1],
+ module_type=module_types[1],
+ status=ModuleStatusChoices.STATUS_ACTIVE,
+ serial='B',
+ asset_tag='B',
+ description='foobar2'
+ ),
+ Module(
+ device=devices[0],
+ module_bay=module_bays[2],
+ module_type=module_types[2],
+ status=ModuleStatusChoices.STATUS_ACTIVE,
+ serial='C',
+ asset_tag='C',
+ description='foobar3'
+ ),
+ Module(
+ device=devices[1],
+ module_bay=module_bays[3],
+ module_type=module_types[0],
+ status=ModuleStatusChoices.STATUS_ACTIVE,
+ serial='D',
+ asset_tag='D'
+ ),
+ Module(
+ device=devices[1],
+ module_bay=module_bays[4],
+ module_type=module_types[1],
+ status=ModuleStatusChoices.STATUS_ACTIVE,
+ serial='E',
+ asset_tag='E'
+ ),
+ Module(
+ device=devices[1],
+ module_bay=module_bays[5],
+ module_type=module_types[2],
+ status=ModuleStatusChoices.STATUS_ACTIVE,
+ serial='F',
+ asset_tag='F'
+ ),
+ Module(
+ device=devices[2],
+ module_bay=module_bays[6],
+ module_type=module_types[0],
+ status=ModuleStatusChoices.STATUS_ACTIVE,
+ serial='G',
+ asset_tag='G'
+ ),
+ Module(
+ device=devices[2],
+ module_bay=module_bays[7],
+ module_type=module_types[1],
+ status=ModuleStatusChoices.STATUS_PLANNED,
+ serial='H',
+ asset_tag='H'
+ ),
+ Module(
+ device=devices[2],
+ module_bay=module_bays[8],
+ module_type=module_types[2],
+ status=ModuleStatusChoices.STATUS_FAILED,
+ serial='I',
+ asset_tag='I'
+ ),
)
Module.objects.bulk_create(modules)
+ 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]}
@@ -2003,6 +2402,10 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'module_type': [module_types[0].model, module_types[1].model]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_module_bay(self):
module_bays = ModuleBay.objects.all()[:2]
params = {'module_bay_id': [module_bays[0].pk, module_bays[1].pk]}
@@ -2822,11 +3225,56 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
)
Rack.objects.bulk_create(racks)
+ # VirtualChassis assignment for filtering
+ virtual_chassis = VirtualChassis(name='Virtual Chassis')
+ virtual_chassis.save()
+
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 1A',
+ device_type=device_types[0],
+ role=roles[0],
+ site=sites[0],
+ location=locations[0],
+ rack=racks[0],
+ virtual_chassis=virtual_chassis,
+ vc_position=1,
+ vc_priority=1
+ ),
+ Device(
+ name='Device 1B',
+ device_type=device_types[2],
+ role=roles[2],
+ site=sites[2],
+ location=locations[2],
+ rack=racks[2],
+ virtual_chassis=virtual_chassis,
+ vc_position=2,
+ vc_priority=1
+ ),
+ 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]
+ ),
+ # For cable connections
+ Device(
+ name=None,
+ device_type=device_types[2],
+ role=roles[2],
+ site=sites[3]
+ ),
)
Device.objects.bulk_create(devices)
@@ -2834,6 +3282,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
ModuleBay(device=devices[0], name='Module Bay 1'),
ModuleBay(device=devices[1], name='Module Bay 2'),
ModuleBay(device=devices[2], name='Module Bay 3'),
+ ModuleBay(device=devices[3], name='Module Bay 4'),
)
ModuleBay.objects.bulk_create(module_bays)
@@ -2841,6 +3290,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
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(device=devices[2], module_bay=module_bays[2], module_type=module_type),
+ Module(device=devices[3], module_bay=module_bays[3], module_type=module_type),
)
Module.objects.bulk_create(modules)
@@ -2853,16 +3303,11 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
# Virtual Device Context Creation
vdcs = (
- VirtualDeviceContext(device=devices[3], name='VDC 1', identifier=1, status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
- VirtualDeviceContext(device=devices[3], name='VDC 2', identifier=2, status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
+ VirtualDeviceContext(device=devices[4], name='VDC 1', identifier=1, status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
+ VirtualDeviceContext(device=devices[4], name='VDC 2', identifier=2, status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
)
VirtualDeviceContext.objects.bulk_create(vdcs)
- # VirtualChassis assignment for filtering
- virtual_chassis = VirtualChassis.objects.create(master=devices[0])
- Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1)
- Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
-
interfaces = (
Interface(
device=devices[0],
@@ -2885,6 +3330,13 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
Interface(
device=devices[1],
module=modules[1],
+ name='VC Chassis Interface',
+ type=InterfaceTypeChoices.TYPE_1GE_SFP,
+ enabled=True
+ ),
+ Interface(
+ device=devices[2],
+ module=modules[2],
name='Interface 2',
label='B',
type=InterfaceTypeChoices.TYPE_1GE_GBIC,
@@ -2901,8 +3353,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
),
Interface(
- device=devices[2],
- module=modules[2],
+ device=devices[3],
+ module=modules[3],
name='Interface 3',
label='C',
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
@@ -2919,7 +3371,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
),
Interface(
- device=devices[3],
+ device=devices[4],
name='Interface 4',
label='D',
type=InterfaceTypeChoices.TYPE_OTHER,
@@ -2932,7 +3384,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
),
Interface(
- device=devices[3],
+ device=devices[4],
name='Interface 5',
label='E',
type=InterfaceTypeChoices.TYPE_OTHER,
@@ -2941,7 +3393,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
tx_power=40
),
Interface(
- device=devices[3],
+ device=devices[4],
name='Interface 6',
label='F',
type=InterfaceTypeChoices.TYPE_OTHER,
@@ -2950,7 +3402,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
tx_power=40
),
Interface(
- device=devices[3],
+ device=devices[4],
name='Interface 7',
type=InterfaceTypeChoices.TYPE_80211AC,
rf_role=WirelessRoleChoices.ROLE_AP,
@@ -2959,7 +3411,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
rf_channel_width=22
),
Interface(
- device=devices[3],
+ device=devices[4],
name='Interface 8',
type=InterfaceTypeChoices.TYPE_80211AC,
rf_role=WirelessRoleChoices.ROLE_STATION,
@@ -2977,8 +3429,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
interfaces[7].vdcs.set([vdcs[1]])
# Cables
- Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]]).save()
- Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]]).save()
+ Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save()
+ Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save()
# Third pair is not connected
def test_name(self):
@@ -2991,7 +3443,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
def test_enabled(self):
params = {'enabled': 'true'}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
params = {'enabled': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -3011,7 +3463,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
params = {'mgmt_only': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'mgmt_only': 'false'}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
def test_poe_mode(self):
params = {'poe_mode': [InterfacePoEModeChoices.MODE_PD, InterfacePoEModeChoices.MODE_PSE]}
@@ -3116,6 +3568,14 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
params = {'device': [devices[0].name, devices[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_virtual_chassis_member(self):
+ # Device 1A & 3 have 1 management interface, Device 1B has 1 interfaces
+ devices = Device.objects.filter(name__in=['Device 1A', 'Device 3'])
+ params = {'virtual_chassis_member_id': [devices[0].pk, devices[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ params = {'virtual_chassis_member': [devices[0].name, devices[1].name]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
def test_module(self):
modules = Module.objects.all()[:2]
params = {'module_id': [modules[0].pk, modules[1].pk]}
@@ -3125,23 +3585,23 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'cabled': False}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
def test_occupied(self):
params = {'occupied': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'occupied': False}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'connected': False}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
def test_kind(self):
params = {'kind': 'physical'}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
params = {'kind': 'virtual'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
@@ -4044,12 +4504,31 @@ class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
roles = (
- InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1', color='ff0000'),
- InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2', color='00ff00'),
- InventoryItemRole(name='Inventory Item Role 3', slug='inventory-item-role-3', color='0000ff'),
+ InventoryItemRole(
+ name='Inventory Item Role 1',
+ slug='inventory-item-role-1',
+ color='ff0000',
+ description='foobar1'
+ ),
+ InventoryItemRole(
+ name='Inventory Item Role 2',
+ slug='inventory-item-role-2',
+ color='00ff00',
+ description='foobar2'
+ ),
+ InventoryItemRole(
+ name='Inventory Item Role 3',
+ slug='inventory-item-role-3',
+ color='0000ff',
+ description='foobar3'
+ ),
)
InventoryItemRole.objects.bulk_create(roles)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Inventory Item Role 1', 'Inventory Item Role 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -4058,6 +4537,10 @@ class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'slug': ['inventory-item-role-1', 'inventory-item-role-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_color(self):
params = {'color': ['ff0000', '00ff00']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -4108,9 +4591,9 @@ class VirtualChassisTestCase(TestCase, ChangeLoggedFilterSetTests):
Device.objects.bulk_create(devices)
virtual_chassis = (
- VirtualChassis(name='VC 1', master=devices[0], domain='Domain 1'),
- VirtualChassis(name='VC 2', master=devices[2], domain='Domain 2'),
- VirtualChassis(name='VC 3', master=devices[4], domain='Domain 3'),
+ VirtualChassis(name='VC 1', master=devices[0], domain='Domain 1', description='foobar1'),
+ VirtualChassis(name='VC 2', master=devices[2], domain='Domain 2', description='foobar2'),
+ VirtualChassis(name='VC 3', master=devices[4], domain='Domain 3', description='foobar3'),
)
VirtualChassis.objects.bulk_create(virtual_chassis)
@@ -4118,6 +4601,10 @@ class VirtualChassisTestCase(TestCase, ChangeLoggedFilterSetTests):
Device.objects.filter(pk=devices[3].pk).update(virtual_chassis=virtual_chassis[1])
Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[2])
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_domain(self):
params = {'domain': ['Domain 1', 'Domain 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -4133,6 +4620,10 @@ class VirtualChassisTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'name': ['VC 1', 'VC 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_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -4218,20 +4709,142 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
Interface(device=devices[4], name='Interface 10', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[5], name='Interface 11', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[5], name='Interface 12', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+ Interface(device=devices[5], name='Interface 13', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
)
Interface.objects.bulk_create(interfaces)
console_port = ConsolePort.objects.create(device=devices[0], name='Console Port 1')
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
+ power_port = PowerPort.objects.create(device=devices[0], name='Power Port 1')
+ power_outlet = PowerOutlet.objects.create(device=devices[0], name='Power Outlet 1')
+ rear_port = RearPort.objects.create(device=devices[0], name='Rear Port 1', positions=1)
+ front_port = FrontPort.objects.create(
+ device=devices[0],
+ name='Front Port 1',
+ rear_port=rear_port,
+ rear_port_position=1
+ )
+
+ power_panel = PowerPanel.objects.create(name='Power Panel 1', site=sites[0])
+ power_feed = PowerFeed.objects.create(name='Power Feed 1', power_panel=power_panel)
+
+ provider = Provider.objects.create(name='Provider 1', slug='provider-1')
+ circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
+ circuit = Circuit.objects.create(cid='Circuit 1', provider=provider, type=circuit_type)
+ circuit_termination = CircuitTermination.objects.create(circuit=circuit, term_side='A', site=sites[0])
# Cables
- Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[2]], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
- Cable(a_terminations=[interfaces[3]], b_terminations=[interfaces[4]], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
- Cable(a_terminations=[interfaces[5]], b_terminations=[interfaces[6]], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
- Cable(a_terminations=[interfaces[7]], b_terminations=[interfaces[8]], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
- Cable(a_terminations=[interfaces[9]], b_terminations=[interfaces[10]], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
- Cable(a_terminations=[interfaces[11]], b_terminations=[interfaces[0]], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
- Cable(a_terminations=[console_port], b_terminations=[console_server_port], label='Cable 7').save()
+ cables = (
+ Cable(
+ a_terminations=[interfaces[1]],
+ b_terminations=[interfaces[2]],
+ label='Cable 1',
+ type=CableTypeChoices.TYPE_CAT3,
+ tenant=tenants[0],
+ status=LinkStatusChoices.STATUS_CONNECTED,
+ color='aa1409',
+ length=10,
+ length_unit=CableLengthUnitChoices.UNIT_FOOT,
+ description='foobar1'
+ ),
+ Cable(
+ a_terminations=[interfaces[3]],
+ b_terminations=[interfaces[4]],
+ label='Cable 2',
+ type=CableTypeChoices.TYPE_CAT3,
+ tenant=tenants[0],
+ status=LinkStatusChoices.STATUS_CONNECTED,
+ color='aa1409',
+ length=20,
+ length_unit=CableLengthUnitChoices.UNIT_FOOT,
+ description='foobar2'
+ ),
+ Cable(
+ a_terminations=[interfaces[5]],
+ b_terminations=[interfaces[6]],
+ label='Cable 3',
+ type=CableTypeChoices.TYPE_CAT5E,
+ tenant=tenants[1],
+ status=LinkStatusChoices.STATUS_CONNECTED,
+ color='f44336',
+ length=30,
+ length_unit=CableLengthUnitChoices.UNIT_FOOT,
+ description='foobar3'
+ ),
+ Cable(
+ a_terminations=[interfaces[7]],
+ b_terminations=[interfaces[8]],
+ label='Cable 4',
+ type=CableTypeChoices.TYPE_CAT5E,
+ tenant=tenants[1],
+ status=LinkStatusChoices.STATUS_PLANNED,
+ color='f44336',
+ length=40,
+ length_unit=CableLengthUnitChoices.UNIT_FOOT
+ ),
+ Cable(
+ a_terminations=[interfaces[9]],
+ b_terminations=[interfaces[10]],
+ label='Cable 5',
+ type=CableTypeChoices.TYPE_CAT6,
+ tenant=tenants[2],
+ status=LinkStatusChoices.STATUS_PLANNED,
+ color='e91e63',
+ length=10,
+ length_unit=CableLengthUnitChoices.UNIT_METER
+ ),
+ Cable(
+ a_terminations=[interfaces[11]],
+ b_terminations=[interfaces[0]],
+ label='Cable 6',
+ type=CableTypeChoices.TYPE_CAT6,
+ tenant=tenants[2],
+ status=LinkStatusChoices.STATUS_PLANNED,
+ color='e91e63',
+ length=20,
+ length_unit=CableLengthUnitChoices.UNIT_METER
+ ),
+
+ # Cables for filtering by termination object
+ Cable(
+ a_terminations=[console_port],
+ label='Cable 7'
+ ),
+ Cable(
+ a_terminations=[console_server_port],
+ label='Cable 8'
+ ),
+ Cable(
+ a_terminations=[power_port],
+ label='Cable 9'
+ ),
+ Cable(
+ a_terminations=[power_outlet],
+ label='Cable 10'
+ ),
+ Cable(
+ a_terminations=[front_port],
+ label='Cable 11'
+ ),
+ Cable(
+ a_terminations=[rear_port],
+ label='Cable 12'
+ ),
+ Cable(
+ a_terminations=[power_feed],
+ label='Cable 13'
+ ),
+ Cable(
+ a_terminations=[circuit_termination],
+ label='Cable 14'
+ ),
+ )
+ for cable in cables:
+ cable.save()
+
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_label(self):
params = {'label': ['Cable 1', 'Cable 2']}
@@ -4251,7 +4864,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_status(self):
params = {'status': [LinkStatusChoices.STATUS_CONNECTED]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11)
params = {'status': [LinkStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
@@ -4259,33 +4872,37 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'color': ['aa1409', 'f44336']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_device(self):
devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 9)
params = {'device': [devices[0].name, devices[1].name]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 9)
def test_rack(self):
racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11)
params = {'rack': [racks[0].name, racks[1].name]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11)
def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11)
params = {'location': [locations[0].name, locations[1].name]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11)
def test_site(self):
site = Site.objects.all()[:2]
params = {'site_id': [site[0].pk, site[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12)
params = {'site': [site[0].slug, site[1].slug]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12)
def test_tenant(self):
tenant = Tenant.objects.all()[:2]
@@ -4297,8 +4914,8 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_termination_types(self):
params = {'termination_a_type': 'dcim.consoleport'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
- params = {'termination_b_type': 'dcim.consoleserverport'}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ # params = {'termination_b_type': 'dcim.consoleserverport'}
+ # self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_termination_ids(self):
interface_ids = CableTermination.objects.filter(
@@ -4311,6 +4928,44 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ def test_unterminated(self):
+ params = {'unterminated': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
+ params = {'unterminated': False}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+
+ def test_consoleport(self):
+ params = {'consoleport_id': [ConsolePort.objects.first().pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_consoleserverport(self):
+ params = {'consoleserverport_id': [ConsoleServerPort.objects.first().pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_powerport(self):
+ params = {'powerport_id': [PowerPort.objects.first().pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_poweroutlet(self):
+ params = {'poweroutlet_id': [PowerOutlet.objects.first().pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_frontport(self):
+ params = {'frontport_id': [FrontPort.objects.first().pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_rearport(self):
+ params = {'rearport_id': [RearPort.objects.first().pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_powerfeed(self):
+ params = {'powerfeed_id': [PowerFeed.objects.first().pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_circuittermination(self):
+ params = {'circuittermination_id': [CircuitTermination.objects.first().pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = PowerPanel.objects.all()
@@ -4351,16 +5006,24 @@ class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests):
location.save()
power_panels = (
- PowerPanel(name='Power Panel 1', site=sites[0], location=locations[0]),
- PowerPanel(name='Power Panel 2', site=sites[1], location=locations[1]),
- PowerPanel(name='Power Panel 3', site=sites[2], location=locations[2]),
+ PowerPanel(name='Power Panel 1', site=sites[0], location=locations[0], description='foobar1'),
+ PowerPanel(name='Power Panel 2', site=sites[1], location=locations[1], description='foobar2'),
+ PowerPanel(name='Power Panel 3', site=sites[2], location=locations[2], description='foobar3'),
)
PowerPanel.objects.bulk_create(power_panels)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Power Panel 1', 'Power Panel 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_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -4459,7 +5122,8 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
phase=PowerFeedPhaseChoices.PHASE_3PHASE,
voltage=100,
amperage=100,
- max_utilization=10
+ max_utilization=10,
+ description='foobar1'
),
PowerFeed(
power_panel=power_panels[1],
@@ -4472,7 +5136,9 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
phase=PowerFeedPhaseChoices.PHASE_3PHASE,
voltage=200,
amperage=200,
- max_utilization=20),
+ max_utilization=20,
+ description='foobar2'
+ ),
PowerFeed(
power_panel=power_panels[2],
rack=racks[2],
@@ -4484,7 +5150,8 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
phase=PowerFeedPhaseChoices.PHASE_SINGLE,
voltage=300,
amperage=300,
- max_utilization=30
+ max_utilization=30,
+ description='foobar3'
),
)
PowerFeed.objects.bulk_create(power_feeds)
@@ -4501,6 +5168,10 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
Cable(a_terminations=[power_feeds[0]], b_terminations=[power_ports[0]]).save()
Cable(a_terminations=[power_feeds[1]], b_terminations=[power_ports[1]]).save()
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Power Feed 1', 'Power Feed 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -4533,6 +5204,10 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'max_utilization': [10, 20]}
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_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -4624,12 +5299,41 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
Device.objects.bulk_create(devices)
vdcs = (
- VirtualDeviceContext(device=devices[0], name='VDC 1', identifier=1, status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
- VirtualDeviceContext(device=devices[0], name='VDC 2', identifier=2, status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
- VirtualDeviceContext(device=devices[1], name='VDC 1', status=VirtualDeviceContextStatusChoices.STATUS_OFFLINE),
- VirtualDeviceContext(device=devices[1], name='VDC 2', status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
- VirtualDeviceContext(device=devices[2], name='VDC 1', status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
- VirtualDeviceContext(device=devices[2], name='VDC 2', status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
+ VirtualDeviceContext(
+ device=devices[0],
+ name='VDC 1',
+ identifier=1,
+ status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE,
+ description='foobar1'
+ ),
+ VirtualDeviceContext(
+ device=devices[0],
+ name='VDC 2',
+ identifier=2,
+ status=VirtualDeviceContextStatusChoices.STATUS_PLANNED,
+ description='foobar2'
+ ),
+ VirtualDeviceContext(
+ device=devices[1],
+ name='VDC 1',
+ status=VirtualDeviceContextStatusChoices.STATUS_OFFLINE,
+ description='foobar3'
+ ),
+ VirtualDeviceContext(
+ device=devices[1],
+ name='VDC 2',
+ status=VirtualDeviceContextStatusChoices.STATUS_PLANNED
+ ),
+ VirtualDeviceContext(
+ device=devices[2],
+ name='VDC 1',
+ status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE
+ ),
+ VirtualDeviceContext(
+ device=devices[2],
+ name='VDC 2',
+ status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE
+ ),
)
VirtualDeviceContext.objects.bulk_create(vdcs)
@@ -4645,14 +5349,24 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
addresses = (
IPAddress(assigned_object=interfaces[0], address='10.1.1.1/24'),
IPAddress(assigned_object=interfaces[1], address='10.1.1.2/24'),
+ IPAddress(assigned_object=None, address='10.1.1.3/24'),
+ IPAddress(assigned_object=interfaces[0], address='2001:db8::1/64'),
+ IPAddress(assigned_object=interfaces[1], address='2001:db8::2/64'),
+ IPAddress(assigned_object=None, address='2001:db8::3/64'),
)
IPAddress.objects.bulk_create(addresses)
vdcs[0].primary_ip4 = addresses[0]
+ vdcs[0].primary_ip6 = addresses[3]
vdcs[0].save()
vdcs[1].primary_ip4 = addresses[1]
+ vdcs[1].primary_ip6 = addresses[4]
vdcs[1].save()
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_device(self):
params = {'device': ['Device 1', 'Device 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
@@ -4661,6 +5375,10 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': ['active']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_device_id(self):
devices = Device.objects.filter(name__in=['Device 1', 'Device 2'])
params = {'device_id': [devices[0].pk, devices[1].pk]}
@@ -4671,3 +5389,17 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'has_primary_ip': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+
+ def test_primary_ip4(self):
+ addresses = IPAddress.objects.filter(address__family=4)
+ params = {'primary_ip4_id': [addresses[0].pk, addresses[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'primary_ip4_id': [addresses[2].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
+
+ def test_primary_ip6(self):
+ addresses = IPAddress.objects.filter(address__family=6)
+ params = {'primary_ip6_id': [addresses[0].pk, addresses[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'primary_ip6_id': [addresses[2].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py
index 2e5ae0d5c..d56bf0741 100644
--- a/netbox/dcim/tests/test_models.py
+++ b/netbox/dcim/tests/test_models.py
@@ -1,9 +1,11 @@
+from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.test import TestCase
from circuits.models import *
from dcim.choices import *
from dcim.models import *
+from extras.models import CustomField
from tenancy.models import Tenant
from utilities.utils import drange
@@ -238,6 +240,40 @@ class RackTestCase(TestCase):
# Check that Device1 is now assigned to Site B
self.assertEqual(Device.objects.get(pk=device1.pk).site, site_b)
+ def test_utilization(self):
+ site = Site.objects.first()
+ rack = Rack.objects.first()
+
+ Device(
+ name='Device 1',
+ role=DeviceRole.objects.first(),
+ device_type=DeviceType.objects.first(),
+ site=site,
+ rack=rack,
+ position=1
+ ).save()
+ rack.refresh_from_db()
+ self.assertEqual(rack.get_utilization(), 1 / 42 * 100)
+
+ # create device excluded from utilization calculations
+ dt = DeviceType.objects.create(
+ manufacturer=Manufacturer.objects.first(),
+ model='Device Type 4',
+ slug='device-type-4',
+ u_height=1,
+ exclude_from_utilization=True
+ )
+ Device(
+ name='Device 2',
+ role=DeviceRole.objects.first(),
+ device_type=dt,
+ site=site,
+ rack=rack,
+ position=5
+ ).save()
+ rack.refresh_from_db()
+ self.assertEqual(rack.get_utilization(), 1 / 42 * 100)
+
class DeviceTestCase(TestCase):
@@ -255,6 +291,23 @@ class DeviceTestCase(TestCase):
)
DeviceRole.objects.bulk_create(roles)
+ # Create a CustomField with a default value & assign it to all component models
+ cf1 = CustomField.objects.create(name='cf1', default='foo')
+ cf1.content_types.set(
+ ContentType.objects.filter(app_label='dcim', model__in=[
+ 'consoleport',
+ 'consoleserverport',
+ 'powerport',
+ 'poweroutlet',
+ 'interface',
+ 'rearport',
+ 'frontport',
+ 'modulebay',
+ 'devicebay',
+ 'inventoryitem',
+ ])
+ )
+
# Create DeviceType components
ConsolePortTemplate(
device_type=device_type,
@@ -266,18 +319,18 @@ class DeviceTestCase(TestCase):
name='Console Server Port 1'
).save()
- ppt = PowerPortTemplate(
+ powerport = PowerPortTemplate(
device_type=device_type,
name='Power Port 1',
maximum_draw=1000,
allocated_draw=500
)
- ppt.save()
+ powerport.save()
PowerOutletTemplate(
device_type=device_type,
name='Power Outlet 1',
- power_port=ppt,
+ power_port=powerport,
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
).save()
@@ -288,19 +341,19 @@ class DeviceTestCase(TestCase):
mgmt_only=True
).save()
- rpt = RearPortTemplate(
+ rearport = RearPortTemplate(
device_type=device_type,
name='Rear Port 1',
type=PortTypeChoices.TYPE_8P8C,
positions=8
)
- rpt.save()
+ rearport.save()
FrontPortTemplate(
device_type=device_type,
name='Front Port 1',
type=PortTypeChoices.TYPE_8P8C,
- rear_port=rpt,
+ rear_port=rearport,
rear_port_position=2
).save()
@@ -314,73 +367,93 @@ class DeviceTestCase(TestCase):
name='Device Bay 1'
).save()
+ InventoryItemTemplate(
+ device_type=device_type,
+ name='Inventory Item 1'
+ ).save()
+
def test_device_creation(self):
"""
Ensure that all Device components are copied automatically from the DeviceType.
"""
- d = Device(
+ device = Device(
site=Site.objects.first(),
device_type=DeviceType.objects.first(),
role=DeviceRole.objects.first(),
name='Test Device 1'
)
- d.save()
+ device.save()
- ConsolePort.objects.get(
- device=d,
+ consoleport = ConsolePort.objects.get(
+ device=device,
name='Console Port 1'
)
+ self.assertEqual(consoleport.cf['cf1'], 'foo')
- ConsoleServerPort.objects.get(
- device=d,
+ consoleserverport = ConsoleServerPort.objects.get(
+ device=device,
name='Console Server Port 1'
)
+ self.assertEqual(consoleserverport.cf['cf1'], 'foo')
- pp = PowerPort.objects.get(
- device=d,
+ powerport = PowerPort.objects.get(
+ device=device,
name='Power Port 1',
maximum_draw=1000,
allocated_draw=500
)
+ self.assertEqual(powerport.cf['cf1'], 'foo')
- PowerOutlet.objects.get(
- device=d,
+ poweroutlet = PowerOutlet.objects.get(
+ device=device,
name='Power Outlet 1',
- power_port=pp,
+ power_port=powerport,
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
)
+ self.assertEqual(poweroutlet.cf['cf1'], 'foo')
- Interface.objects.get(
- device=d,
+ interface = Interface.objects.get(
+ device=device,
name='Interface 1',
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
mgmt_only=True
)
+ self.assertEqual(interface.cf['cf1'], 'foo')
- rp = RearPort.objects.get(
- device=d,
+ rearport = RearPort.objects.get(
+ device=device,
name='Rear Port 1',
type=PortTypeChoices.TYPE_8P8C,
positions=8
)
+ self.assertEqual(rearport.cf['cf1'], 'foo')
- FrontPort.objects.get(
- device=d,
+ frontport = FrontPort.objects.get(
+ device=device,
name='Front Port 1',
type=PortTypeChoices.TYPE_8P8C,
- rear_port=rp,
+ rear_port=rearport,
rear_port_position=2
)
+ self.assertEqual(frontport.cf['cf1'], 'foo')
- ModuleBay.objects.get(
- device=d,
+ modulebay = ModuleBay.objects.get(
+ device=device,
name='Module Bay 1'
)
+ self.assertEqual(modulebay.cf['cf1'], 'foo')
- DeviceBay.objects.get(
- device=d,
+ devicebay = DeviceBay.objects.get(
+ device=device,
name='Device Bay 1'
)
+ self.assertEqual(devicebay.cf['cf1'], 'foo')
+
+ inventoryitem = InventoryItem.objects.get(
+ device=device,
+ name='Inventory Item 1'
+ )
+ self.assertEqual(inventoryitem.cf['cf1'], 'foo')
def test_multiple_unnamed_devices(self):
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index aff4a65b5..88e0d44f2 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -17,7 +17,7 @@ from dcim.constants import *
from dcim.models import *
from ipam.models import ASN, RIR, VLAN, VRF
from tenancy.models import Tenant
-from utilities.choices import ImportFormatChoices
+from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
from wireless.models import WirelessLAN
@@ -2014,6 +2014,7 @@ class ModuleTestCase(
'data': {
'data': '\n'.join(csv_data),
'format': ImportFormatChoices.CSV,
+ 'csv_delimiter': CSVDelimiterChoices.AUTO,
}
}
@@ -2030,6 +2031,7 @@ class ModuleTestCase(
'data': {
'data': '\n'.join(csv_data),
'format': ImportFormatChoices.CSV,
+ 'csv_delimiter': CSVDelimiterChoices.AUTO,
}
}
@@ -2106,6 +2108,7 @@ class ModuleTestCase(
'data': {
'data': '\n'.join(csv_data),
'format': ImportFormatChoices.CSV,
+ 'csv_delimiter': CSVDelimiterChoices.AUTO,
}
}
@@ -2528,6 +2531,36 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
response = self.client.get(reverse('dcim:interface_trace', kwargs={'pk': interface1.pk}))
self.assertHttpStatus(response, 200)
+ def test_bulk_delete_child_interfaces(self):
+ interface1 = Interface.objects.get(name='Interface 1')
+ device = interface1.device
+ self.add_permissions('dcim.delete_interface')
+
+ # Create a child interface
+ child = Interface.objects.create(
+ device=device,
+ name='Interface 1A',
+ type=InterfaceTypeChoices.TYPE_VIRTUAL,
+ parent=interface1
+ )
+ self.assertEqual(device.interfaces.count(), 6)
+
+ # Attempt to delete only the parent interface
+ data = {
+ 'confirm': True,
+ }
+ self.client.post(self._get_url('delete', interface1), data)
+ self.assertEqual(device.interfaces.count(), 6) # Parent was not deleted
+
+ # Attempt to bulk delete parent & child together
+ data = {
+ 'pk': [interface1.pk, child.pk],
+ 'confirm': True,
+ '_confirm': True, # Form button
+ }
+ self.client.post(self._get_url('bulk_delete'), data)
+ self.assertEqual(device.interfaces.count(), 4) # Child & parent were both deleted
+
class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = FrontPort
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index e57a5834c..497935b15 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -1,5 +1,4 @@
import traceback
-from collections import defaultdict
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
@@ -20,11 +19,13 @@ from circuits.models import Circuit, CircuitTermination
from extras.views import ObjectConfigContextView
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
from ipam.tables import InterfaceVLANTable
+from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic
from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model
+from utilities.query_functions import CollateAsChar
from utilities.utils import count_related
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
from virtualization.models import VirtualMachine
@@ -46,15 +47,11 @@ CABLE_TERMINATION_TYPES = {
class DeviceComponentsView(generic.ObjectChildrenView):
- actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename', 'bulk_disconnect')
- action_perms = defaultdict(set, **{
- 'add': {'add'},
- 'import': {'add'},
- 'bulk_edit': {'change'},
- 'bulk_delete': {'delete'},
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
'bulk_rename': {'change'},
'bulk_disconnect': {'change'},
- })
+ }
queryset = Device.objects.all()
def get_children(self, request, parent):
@@ -122,16 +119,18 @@ class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View)
if form.is_valid():
with transaction.atomic():
-
count = 0
+ cable_ids = set()
for obj in self.queryset.filter(pk__in=form.cleaned_data['pk']):
- if obj.cable is None:
- continue
- obj.cable.delete()
- count += 1
+ if obj.cable:
+ cable_ids.add(obj.cable.pk)
+ count += 1
+ for cable in Cable.objects.filter(pk__in=cable_ids):
+ cable.delete()
- messages.success(request, "Disconnected {} {}".format(
- count, self.queryset.model._meta.verbose_name_plural
+ messages.success(request, _("Disconnected {count} {type}").format(
+ count=count,
+ type=self.queryset.model._meta.verbose_name_plural
))
return redirect(return_url)
@@ -398,32 +397,8 @@ class SiteView(generic.ObjectView):
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'),
)
- locations = Location.objects.add_related_count(
- Location.objects.all(),
- Rack,
- 'location',
- 'rack_count',
- cumulative=True
- )
- locations = Location.objects.add_related_count(
- locations,
- Device,
- 'location',
- 'device_count',
- cumulative=True
- ).restrict(request.user, 'view').filter(site=instance)
-
- nonracked_devices = Device.objects.filter(
- site=instance,
- rack__isnull=True,
- parent_bay__isnull=True
- ).prefetch_related('device_type__manufacturer', 'parent_bay', 'role')
-
return {
'related_models': related_models,
- 'locations': locations,
- 'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
- 'total_nonracked_devices_count': nonracked_devices.count(),
}
@@ -495,16 +470,8 @@ class LocationView(generic.ObjectView):
(Device.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
)
- nonracked_devices = Device.objects.filter(
- location=instance,
- rack__isnull=True,
- parent_bay__isnull=True
- ).prefetch_related('device_type__manufacturer', 'parent_bay', 'role')
-
return {
'related_models': related_models,
- 'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
- 'total_nonracked_devices_count': nonracked_devices.count(),
}
@@ -725,8 +692,7 @@ class RackRackReservationsView(generic.ObjectChildrenView):
label=_('Reservations'),
badge=lambda obj: obj.reservations.count(),
permission='dcim.view_rackreservation',
- weight=510,
- hide_if_empty=True
+ weight=510
)
def get_children(self, request, parent):
@@ -2007,7 +1973,10 @@ class DeviceModuleBaysView(DeviceComponentsView):
table = tables.DeviceModuleBayTable
filterset = filtersets.ModuleBayFilterSet
template_name = 'dcim/device/modulebays.html'
- actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
tab = ViewTab(
label=_('Module Bays'),
badge=lambda obj: obj.module_bay_count,
@@ -2023,7 +1992,10 @@ class DeviceDeviceBaysView(DeviceComponentsView):
table = tables.DeviceDeviceBayTable
filterset = filtersets.DeviceBayFilterSet
template_name = 'dcim/device/devicebays.html'
- actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
tab = ViewTab(
label=_('Device Bays'),
badge=lambda obj: obj.device_bay_count,
@@ -2035,11 +2007,14 @@ class DeviceDeviceBaysView(DeviceComponentsView):
@register_model_view(Device, 'inventory')
class DeviceInventoryView(DeviceComponentsView):
- actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
child_model = InventoryItem
table = tables.DeviceInventoryItemTable
filterset = filtersets.InventoryItemFilterSet
template_name = 'dcim/device/inventory.html'
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
tab = ViewTab(
label=_('Inventory Items'),
badge=lambda obj: obj.inventory_item_count,
@@ -2055,7 +2030,6 @@ class DeviceConfigContextView(ObjectConfigContextView):
base_template = 'dcim/device/base.html'
tab = ViewTab(
label=_('Config Context'),
- permission='extras.view_configcontext',
weight=2000
)
@@ -2066,7 +2040,6 @@ class DeviceRenderConfigView(generic.ObjectView):
template_name = 'dcim/device/render_config.html'
tab = ViewTab(
label=_('Render Config'),
- permission='extras.view_configtemplate',
weight=2100
)
@@ -2218,6 +2191,11 @@ class ConsolePortListView(generic.ObjectListView):
filterset = filtersets.ConsolePortFilterSet
filterset_form = forms.ConsolePortFilterForm
table = tables.ConsolePortTable
+ template_name = 'dcim/component_list.html'
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
@register_model_view(ConsolePort)
@@ -2281,6 +2259,11 @@ class ConsoleServerPortListView(generic.ObjectListView):
filterset = filtersets.ConsoleServerPortFilterSet
filterset_form = forms.ConsoleServerPortFilterForm
table = tables.ConsoleServerPortTable
+ template_name = 'dcim/component_list.html'
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
@register_model_view(ConsoleServerPort)
@@ -2344,6 +2327,11 @@ class PowerPortListView(generic.ObjectListView):
filterset = filtersets.PowerPortFilterSet
filterset_form = forms.PowerPortFilterForm
table = tables.PowerPortTable
+ template_name = 'dcim/component_list.html'
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
@register_model_view(PowerPort)
@@ -2407,6 +2395,11 @@ class PowerOutletListView(generic.ObjectListView):
filterset = filtersets.PowerOutletFilterSet
filterset_form = forms.PowerOutletFilterForm
table = tables.PowerOutletTable
+ template_name = 'dcim/component_list.html'
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
@register_model_view(PowerOutlet)
@@ -2470,6 +2463,11 @@ class InterfaceListView(generic.ObjectListView):
filterset = filtersets.InterfaceFilterSet
filterset_form = forms.InterfaceFilterForm
table = tables.InterfaceTable
+ template_name = 'dcim/component_list.html'
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
@register_model_view(Interface)
@@ -2563,7 +2561,8 @@ class InterfaceBulkDisconnectView(BulkDisconnectView):
class InterfaceBulkDeleteView(generic.BulkDeleteView):
- queryset = Interface.objects.all()
+ # Ensure child interfaces are deleted prior to their parents
+ queryset = Interface.objects.order_by('device', 'parent', CollateAsChar('_name'))
filterset = filtersets.InterfaceFilterSet
table = tables.InterfaceTable
@@ -2581,6 +2580,11 @@ class FrontPortListView(generic.ObjectListView):
filterset = filtersets.FrontPortFilterSet
filterset_form = forms.FrontPortFilterForm
table = tables.FrontPortTable
+ template_name = 'dcim/component_list.html'
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
@register_model_view(FrontPort)
@@ -2644,6 +2648,11 @@ class RearPortListView(generic.ObjectListView):
filterset = filtersets.RearPortFilterSet
filterset_form = forms.RearPortFilterForm
table = tables.RearPortTable
+ template_name = 'dcim/component_list.html'
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
@register_model_view(RearPort)
@@ -2707,6 +2716,11 @@ class ModuleBayListView(generic.ObjectListView):
filterset = filtersets.ModuleBayFilterSet
filterset_form = forms.ModuleBayFilterForm
table = tables.ModuleBayTable
+ template_name = 'dcim/component_list.html'
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
@register_model_view(ModuleBay)
@@ -2762,6 +2776,11 @@ class DeviceBayListView(generic.ObjectListView):
filterset = filtersets.DeviceBayFilterSet
filterset_form = forms.DeviceBayFilterForm
table = tables.DeviceBayTable
+ template_name = 'dcim/component_list.html'
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
@register_model_view(DeviceBay)
@@ -2886,6 +2905,11 @@ class InventoryItemListView(generic.ObjectListView):
filterset = filtersets.InventoryItemFilterSet
filterset_form = forms.InventoryItemFilterForm
table = tables.InventoryItemTable
+ template_name = 'dcim/component_list.html'
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_rename': {'change'},
+ }
@register_model_view(InventoryItem)
@@ -2935,6 +2959,25 @@ class InventoryItemBulkDeleteView(generic.BulkDeleteView):
template_name = 'dcim/inventoryitem_bulk_delete.html'
+@register_model_view(InventoryItem, 'children')
+class InventoryItemChildrenView(generic.ObjectChildrenView):
+ queryset = InventoryItem.objects.all()
+ child_model = InventoryItem
+ table = tables.InventoryItemTable
+ filterset = filtersets.InventoryItemFilterSet
+ template_name = 'generic/object_children.html'
+ tab = ViewTab(
+ label=_('Children'),
+ badge=lambda obj: obj.child_items.count(),
+ permission='dcim.view_inventoryitem',
+ hide_if_empty=True,
+ weight=5000
+ )
+
+ def get_children(self, request, parent):
+ return parent.child_items.restrict(request.user, 'view')
+
+
#
# Inventory item roles
#
@@ -3117,7 +3160,12 @@ class CableListView(generic.ObjectListView):
filterset = filtersets.CableFilterSet
filterset_form = forms.CableFilterForm
table = tables.CableTable
- actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
+ actions = {
+ 'import': {'add'},
+ 'export': {'view'},
+ 'bulk_edit': {'change'},
+ 'bulk_delete': {'delete'},
+ }
@register_model_view(Cable)
@@ -3211,7 +3259,9 @@ class ConsoleConnectionsListView(generic.ObjectListView):
filterset_form = forms.ConsoleConnectionFilterForm
table = tables.ConsoleConnectionTable
template_name = 'dcim/connections_list.html'
- actions = ('export',)
+ actions = {
+ 'export': {'view'},
+ }
def get_extra_context(self, request):
return {
@@ -3225,7 +3275,9 @@ class PowerConnectionsListView(generic.ObjectListView):
filterset_form = forms.PowerConnectionFilterForm
table = tables.PowerConnectionTable
template_name = 'dcim/connections_list.html'
- actions = ('export',)
+ actions = {
+ 'export': {'view'},
+ }
def get_extra_context(self, request):
return {
@@ -3239,7 +3291,9 @@ class InterfaceConnectionsListView(generic.ObjectListView):
filterset_form = forms.InterfaceConnectionFilterForm
table = tables.InterfaceConnectionTable
template_name = 'dcim/connections_list.html'
- actions = ('export',)
+ actions = {
+ 'export': {'view'},
+ }
def get_extra_context(self, request):
return {
diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py
deleted file mode 100644
index 6e82ffc75..000000000
--- a/netbox/extras/admin.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# TODO: Removing this import triggers an import loop due to how form mixins are currently organized
-from .forms import ConfigRevisionForm
diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py
index b6be47bbb..1737ff9f8 100644
--- a/netbox/extras/api/mixins.py
+++ b/netbox/extras/api/mixins.py
@@ -1,10 +1,16 @@
from jinja2.exceptions import TemplateError
+from rest_framework.decorators import action
+from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
+from rest_framework.status import HTTP_400_BAD_REQUEST
+from netbox.api.renderers import TextRenderer
from .nested_serializers import NestedConfigTemplateSerializer
__all__ = (
'ConfigContextQuerySetMixin',
+ 'ConfigTemplateRenderMixin',
+ 'RenderConfigMixin',
)
@@ -31,7 +37,9 @@ class ConfigContextQuerySetMixin:
class ConfigTemplateRenderMixin:
-
+ """
+ Provides a method to return a rendered ConfigTemplate as REST API data.
+ """
def render_configtemplate(self, request, configtemplate, context):
try:
output = configtemplate.render(context=context)
@@ -50,3 +58,28 @@ class ConfigTemplateRenderMixin:
'configtemplate': template_serializer.data,
'content': output
})
+
+
+class RenderConfigMixin(ConfigTemplateRenderMixin):
+ """
+ Provides a /render-config/ endpoint for REST API views whose model may have a ConfigTemplate assigned.
+ """
+ @action(detail=True, methods=['post'], url_path='render-config', renderer_classes=[JSONRenderer, TextRenderer])
+ def render_config(self, request, pk):
+ """
+ Resolve and render the preferred ConfigTemplate for this Device.
+ """
+ instance = self.get_object()
+ object_type = instance._meta.model_name
+ configtemplate = instance.get_config_template()
+ if not configtemplate:
+ return Response({
+ 'error': f'No config template found for this {object_type}.'
+ }, status=HTTP_400_BAD_REQUEST)
+
+ # Compile context data
+ context_data = instance.get_config_context()
+ context_data.update(request.data)
+ context_data.update({object_type: instance})
+
+ return self.render_configtemplate(request, configtemplate, context_data)
diff --git a/netbox/extras/api/nested_serializers.py b/netbox/extras/api/nested_serializers.py
index a97c630d2..4bada494f 100644
--- a/netbox/extras/api/nested_serializers.py
+++ b/netbox/extras/api/nested_serializers.py
@@ -10,15 +10,25 @@ __all__ = [
'NestedCustomFieldChoiceSetSerializer',
'NestedCustomFieldSerializer',
'NestedCustomLinkSerializer',
+ 'NestedEventRuleSerializer',
'NestedExportTemplateSerializer',
'NestedImageAttachmentSerializer',
'NestedJournalEntrySerializer',
'NestedSavedFilterSerializer',
+ 'NestedScriptSerializer',
'NestedTagSerializer', # Defined in netbox.api.serializers
'NestedWebhookSerializer',
]
+class NestedEventRuleSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
+
+ class Meta:
+ model = models.EventRule
+ fields = ['id', 'url', 'display', 'name']
+
+
class NestedWebhookSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
@@ -105,3 +115,20 @@ class NestedJournalEntrySerializer(WritableNestedSerializer):
class Meta:
model = models.JournalEntry
fields = ['id', 'url', 'display', 'created']
+
+
+class NestedScriptSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='extras-api:script-detail',
+ lookup_field='full_name',
+ lookup_url_kwarg='pk'
+ )
+ name = serializers.CharField(read_only=True)
+ display = serializers.SerializerMethodField(read_only=True)
+
+ class Meta:
+ model = models.Script
+ fields = ['id', 'url', 'display', 'name']
+
+ def get_display(self, obj):
+ return f'{obj.name} ({obj.module})'
diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py
index 4da5fa629..60a30aed2 100644
--- a/netbox/extras/api/serializers.py
+++ b/netbox/extras/api/serializers.py
@@ -1,20 +1,19 @@
from django.contrib.auth import get_user_model
-from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
-from core.api.serializers import JobSerializer
from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
+from core.api.serializers import JobSerializer
+from core.models import ContentType
from dcim.api.nested_serializers import (
NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
)
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
-from drf_spectacular.utils import extend_schema_field
-from drf_spectacular.types import OpenApiTypes
from extras.choices import *
from extras.models import *
-from extras.utils import FeatureQuery
from netbox.api.exceptions import SerializerNotFound
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
@@ -39,6 +38,7 @@ __all__ = (
'CustomFieldSerializer',
'CustomLinkSerializer',
'DashboardSerializer',
+ 'EventRuleSerializer',
'ExportTemplateSerializer',
'ImageAttachmentSerializer',
'JournalEntrySerializer',
@@ -57,24 +57,59 @@ __all__ = (
)
+#
+# Event Rules
+#
+
+class EventRuleSerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
+ content_types = ContentTypeField(
+ queryset=ContentType.objects.with_feature('event_rules'),
+ many=True
+ )
+ action_type = ChoiceField(choices=EventRuleActionChoices)
+ action_object_type = ContentTypeField(
+ queryset=ContentType.objects.with_feature('event_rules'),
+ )
+ action_object = serializers.SerializerMethodField(read_only=True)
+
+ class Meta:
+ model = EventRule
+ fields = [
+ 'id', 'url', 'display', 'content_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',
+ ]
+
+ @extend_schema_field(OpenApiTypes.OBJECT)
+ def get_action_object(self, instance):
+ context = {'request': self.context['request']}
+ # We need to manually instantiate the serializer for scripts
+ if instance.action_type == EventRuleActionChoices.SCRIPT:
+ script_name = instance.action_parameters['script_name']
+ script = instance.action_object.scripts[script_name]()
+ return NestedScriptSerializer(script, context=context).data
+ else:
+ serializer = get_serializer_for_model(
+ model=instance.action_object_type.model_class(),
+ prefix=NESTED_SERIALIZER_PREFIX
+ )
+ return serializer(instance.action_object, context=context).data
+
+
#
# Webhooks
#
class WebhookSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
- content_types = ContentTypeField(
- queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
- many=True
- )
class Meta:
model = Webhook
fields = [
- 'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete',
- 'type_job_start', 'type_job_end', 'payload_url', 'enabled', 'http_method', 'http_content_type',
- 'additional_headers', 'body_template', 'secret', 'conditions', 'ssl_verification', 'ca_file_path',
- 'custom_fields', 'tags', 'created', 'last_updated',
+ 'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type',
+ 'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields',
+ 'tags', 'created', 'last_updated',
]
@@ -85,7 +120,7 @@ class WebhookSerializer(NetBoxModelSerializer):
class CustomFieldSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
content_types = ContentTypeField(
- queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
+ queryset=ContentType.objects.with_feature('custom_fields'),
many=True
)
type = ChoiceField(choices=CustomFieldTypeChoices)
@@ -96,15 +131,16 @@ class CustomFieldSerializer(ValidatedModelSerializer):
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
data_type = serializers.SerializerMethodField()
choice_set = NestedCustomFieldChoiceSetSerializer(required=False)
- ui_visibility = ChoiceField(choices=CustomFieldVisibilityChoices, required=False)
+ ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False)
+ ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False)
class Meta:
model = CustomField
fields = [
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
- 'description', 'required', 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'default',
- 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'created',
- 'last_updated',
+ 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
+ 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
+ 'created', 'last_updated',
]
def validate_type(self, value):
@@ -151,7 +187,7 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
class CustomLinkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
content_types = ContentTypeField(
- queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()),
+ queryset=ContentType.objects.with_feature('custom_links'),
many=True
)
@@ -170,7 +206,7 @@ class CustomLinkSerializer(ValidatedModelSerializer):
class ExportTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
content_types = ContentTypeField(
- queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
+ queryset=ContentType.objects.with_feature('export_templates'),
many=True
)
data_source = NestedDataSourceSerializer(
@@ -215,7 +251,7 @@ class SavedFilterSerializer(ValidatedModelSerializer):
class BookmarkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
object_type = ContentTypeField(
- queryset=ContentType.objects.filter(FeatureQuery('bookmarks').get_query()),
+ queryset=ContentType.objects.with_feature('bookmarks'),
)
object = serializers.SerializerMethodField(read_only=True)
user = NestedUserSerializer()
@@ -239,7 +275,7 @@ class BookmarkSerializer(ValidatedModelSerializer):
class TagSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
object_types = ContentTypeField(
- queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
+ queryset=ContentType.objects.with_feature('tags'),
many=True,
required=False
)
@@ -454,7 +490,7 @@ class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer
required=False
)
data_file = NestedDataFileSerializer(
- read_only=True
+ required=False
)
class Meta:
@@ -479,7 +515,7 @@ class ReportSerializer(serializers.Serializer):
module = serializers.CharField(max_length=255)
name = serializers.CharField(max_length=255)
description = serializers.CharField(max_length=255, required=False)
- test_methods = serializers.ListField(child=serializers.CharField(max_length=255))
+ test_methods = serializers.ListField(child=serializers.CharField(max_length=255), read_only=True)
result = NestedJobSerializer()
display = serializers.SerializerMethodField(read_only=True)
diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py
index 5f2b324e6..1616b8554 100644
--- a/netbox/extras/api/urls.py
+++ b/netbox/extras/api/urls.py
@@ -7,6 +7,7 @@ from . import views
router = NetBoxRouter()
router.APIRootView = views.ExtrasRootView
+router.register('event-rules', views.EventRuleViewSet)
router.register('webhooks', views.WebhookViewSet)
router.register('custom-fields', views.CustomFieldViewSet)
router.register('custom-field-choice-sets', views.CustomFieldChoiceSetViewSet)
diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py
index 06797891e..e0fca8617 100644
--- a/netbox/extras/api/views.py
+++ b/netbox/extras/api/views.py
@@ -37,6 +37,17 @@ class ExtrasRootView(APIRootView):
return 'Extras'
+#
+# EventRules
+#
+
+class EventRuleViewSet(NetBoxModelViewSet):
+ metadata_class = ContentTypeMetadata
+ queryset = EventRule.objects.all()
+ serializer_class = serializers.EventRuleSerializer
+ filterset_class = filtersets.EventRuleFilterSet
+
+
#
# Webhooks
#
@@ -82,7 +93,10 @@ class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
data = [
{'id': c[0], 'display': c[1]} for c in page
]
- return self.get_paginated_response(data)
+ else:
+ data = []
+
+ return self.get_paginated_response(data)
#
@@ -280,7 +294,7 @@ class ReportViewSet(ViewSet):
# Retrieve and run the Report. This will create a new Job.
module, report_cls = self._get_report(pk)
- report = report_cls()
+ report = report_cls
input_serializer = serializers.ReportInputSerializer(
data=request.data,
context={'report': report}
diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py
index 1061bf871..14179fb39 100644
--- a/netbox/extras/choices.py
+++ b/netbox/extras/choices.py
@@ -53,18 +53,29 @@ class CustomFieldFilterLogicChoices(ChoiceSet):
)
-class CustomFieldVisibilityChoices(ChoiceSet):
+class CustomFieldUIVisibleChoices(ChoiceSet):
- VISIBILITY_READ_WRITE = 'read-write'
- VISIBILITY_READ_ONLY = 'read-only'
- VISIBILITY_HIDDEN = 'hidden'
- VISIBILITY_HIDDEN_IFUNSET = 'hidden-ifunset'
+ ALWAYS = 'always'
+ IF_SET = 'if-set'
+ HIDDEN = 'hidden'
CHOICES = (
- (VISIBILITY_READ_WRITE, _('Read/write')),
- (VISIBILITY_READ_ONLY, _('Read-only')),
- (VISIBILITY_HIDDEN, _('Hidden')),
- (VISIBILITY_HIDDEN_IFUNSET, _('Hidden (if unset)')),
+ (ALWAYS, _('Always'), 'green'),
+ (IF_SET, _('If set'), 'yellow'),
+ (HIDDEN, _('Hidden'), 'gray'),
+ )
+
+
+class CustomFieldUIEditableChoices(ChoiceSet):
+
+ YES = 'yes'
+ NO = 'no'
+ HIDDEN = 'hidden'
+
+ CHOICES = (
+ (YES, _('Yes'), 'green'),
+ (NO, _('No'), 'red'),
+ (HIDDEN, _('Hidden'), 'gray'),
)
@@ -244,3 +255,54 @@ class ChangeActionChoices(ChoiceSet):
(ACTION_UPDATE, _('Update'), 'blue'),
(ACTION_DELETE, _('Delete'), 'red'),
)
+
+
+#
+# Dashboard widgets
+#
+
+class DashboardWidgetColorChoices(ChoiceSet):
+ BLUE = 'blue'
+ INDIGO = 'indigo'
+ PURPLE = 'purple'
+ PINK = 'pink'
+ RED = 'red'
+ ORANGE = 'orange'
+ YELLOW = 'yellow'
+ GREEN = 'green'
+ TEAL = 'teal'
+ CYAN = 'cyan'
+ GRAY = 'gray'
+ BLACK = 'black'
+ WHITE = 'white'
+
+ CHOICES = (
+ (BLUE, _('Blue')),
+ (INDIGO, _('Indigo')),
+ (PURPLE, _('Purple')),
+ (PINK, _('Pink')),
+ (RED, _('Red')),
+ (ORANGE, _('Orange')),
+ (YELLOW, _('Yellow')),
+ (GREEN, _('Green')),
+ (TEAL, _('Teal')),
+ (CYAN, _('Cyan')),
+ (GRAY, _('Gray')),
+ (BLACK, _('Black')),
+ (WHITE, _('White')),
+ )
+
+
+#
+# Event Rules
+#
+
+class EventRuleActionChoices(ChoiceSet):
+
+ WEBHOOK = 'webhook'
+ SCRIPT = 'script'
+
+ CHOICES = (
+ (WEBHOOK, _('Webhook')),
+ (SCRIPT, _('Script')),
+ )
diff --git a/netbox/extras/context_managers.py b/netbox/extras/context_managers.py
index 32323999e..8de47465e 100644
--- a/netbox/extras/context_managers.py
+++ b/netbox/extras/context_managers.py
@@ -1,25 +1,25 @@
from contextlib import contextmanager
-from netbox.context import current_request, webhooks_queue
-from .webhooks import flush_webhooks
+from netbox.context import current_request, events_queue
+from .events import flush_events
@contextmanager
-def change_logging(request):
+def event_tracking(request):
"""
- Enable change logging by connecting the appropriate signals to their receivers before code is run, and
- disconnecting them afterward.
+ Queue interesting events in memory while processing a request, then flush that queue for processing by the
+ events pipline before returning the response.
:param request: WSGIRequest object with a unique `id` set
"""
current_request.set(request)
- webhooks_queue.set([])
+ events_queue.set([])
yield
# Flush queued webhooks to RQ
- flush_webhooks(webhooks_queue.get())
+ flush_events(events_queue.get())
# Clear context vars
current_request.set(None)
- webhooks_queue.set([])
+ events_queue.set([])
diff --git a/netbox/extras/dashboard/forms.py b/netbox/extras/dashboard/forms.py
index 1e9f15408..ab708228c 100644
--- a/netbox/extras/dashboard/forms.py
+++ b/netbox/extras/dashboard/forms.py
@@ -2,9 +2,9 @@ from django import forms
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
+from extras.choices import DashboardWidgetColorChoices
from netbox.registry import registry
from utilities.forms import BootstrapMixin, add_blank_choice
-from utilities.choices import ButtonColorChoices
__all__ = (
'DashboardWidgetAddForm',
@@ -21,7 +21,7 @@ class DashboardWidgetForm(BootstrapMixin, forms.Form):
required=False
)
color = forms.ChoiceField(
- choices=add_blank_choice(ButtonColorChoices),
+ choices=add_blank_choice(DashboardWidgetColorChoices),
required=False,
)
diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py
index 3d6275f45..8cfbb4c61 100644
--- a/netbox/extras/dashboard/widgets.py
+++ b/netbox/extras/dashboard/widgets.py
@@ -7,15 +7,14 @@ import feedparser
import requests
from django import forms
from django.conf import settings
-from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
-from django.db.models import Q
from django.template.loader import render_to_string
from django.urls import NoReverseMatch, resolve, reverse
from django.utils.translation import gettext as _
+from core.models import ContentType
from extras.choices import BookmarkOrderingChoices
-from extras.utils import FeatureQuery
+from utilities.choices import ButtonColorChoices
from utilities.forms import BootstrapMixin
from utilities.permissions import get_permission_for_model
from utilities.templatetags.builtins.filters import render_markdown
@@ -33,13 +32,17 @@ __all__ = (
)
-def get_content_type_labels():
+def get_object_type_choices():
return [
(content_type_identifier(ct), content_type_name(ct))
- for ct in ContentType.objects.filter(
- FeatureQuery('export_templates').get_query() | Q(app_label='extras', model='objectchange') |
- Q(app_label='extras', model='configcontext')
- ).order_by('app_label', 'model')
+ for ct in ContentType.objects.public().order_by('app_label', 'model')
+ ]
+
+
+def get_bookmarks_object_type_choices():
+ return [
+ (content_type_identifier(ct), content_type_name(ct))
+ for ct in ContentType.objects.with_feature('bookmarks').order_by('app_label', 'model')
]
@@ -115,6 +118,22 @@ 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 {
@@ -146,7 +165,7 @@ class ObjectCountsWidget(DashboardWidget):
class ConfigForm(WidgetConfigForm):
models = forms.MultipleChoiceField(
- choices=get_content_type_labels
+ choices=get_object_type_choices
)
filters = forms.JSONField(
required=False,
@@ -195,7 +214,7 @@ class ObjectListWidget(DashboardWidget):
class ConfigForm(WidgetConfigForm):
model = forms.ChoiceField(
- choices=get_content_type_labels
+ choices=get_object_type_choices
)
page_size = forms.IntegerField(
required=False,
@@ -331,8 +350,7 @@ class BookmarksWidget(DashboardWidget):
class ConfigForm(WidgetConfigForm):
object_types = forms.MultipleChoiceField(
- # TODO: Restrict the choices by FeatureQuery('bookmarks')
- choices=get_content_type_labels,
+ choices=get_bookmarks_object_type_choices,
required=False
)
order_by = forms.ChoiceField(
@@ -346,13 +364,16 @@ class BookmarksWidget(DashboardWidget):
def render(self, request):
from extras.models import Bookmark
- bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
- if object_types := self.config.get('object_types'):
- models = get_models_from_content_types(object_types)
- conent_types = ContentType.objects.get_for_models(*models).values()
- bookmarks = bookmarks.filter(object_type__in=conent_types)
- if max_items := self.config.get('max_items'):
- bookmarks = bookmarks[:max_items]
+ if request.user.is_anonymous:
+ bookmarks = list()
+ else:
+ bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
+ if object_types := self.config.get('object_types'):
+ models = get_models_from_content_types(object_types)
+ conent_types = ContentType.objects.get_for_models(*models).values()
+ bookmarks = bookmarks.filter(object_type__in=conent_types)
+ if max_items := self.config.get('max_items'):
+ bookmarks = bookmarks[:max_items]
return render_to_string(self.template_name, {
'bookmarks': bookmarks,
diff --git a/netbox/extras/events.py b/netbox/extras/events.py
new file mode 100644
index 000000000..6d0654929
--- /dev/null
+++ b/netbox/extras/events.py
@@ -0,0 +1,178 @@
+import logging
+
+from django.conf import settings
+from django.contrib.auth import get_user_model
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils import timezone
+from django.utils.module_loading import import_string
+from django_rq import get_queue
+
+from core.models import Job
+from netbox.config import get_config
+from netbox.constants import RQ_QUEUE_DEFAULT
+from netbox.registry import registry
+from utilities.api import get_serializer_for_model
+from utilities.rqworker import get_rq_retry
+from utilities.utils import serialize_object
+from .choices import *
+from .models import EventRule, ScriptModule
+
+logger = logging.getLogger('netbox.events_processor')
+
+
+def serialize_for_event(instance):
+ """
+ Return a serialized representation of the given instance suitable for use in a queued event.
+ """
+ serializer_class = get_serializer_for_model(instance.__class__)
+ serializer_context = {
+ 'request': None,
+ }
+ serializer = serializer_class(instance, context=serializer_context)
+
+ return serializer.data
+
+
+def get_snapshots(instance, action):
+ snapshots = {
+ 'prechange': getattr(instance, '_prechange_snapshot', None),
+ 'postchange': None,
+ }
+ if action != ObjectChangeActionChoices.ACTION_DELETE:
+ # Use model's serialize_object() method if defined; fall back to serialize_object() utility function
+ if hasattr(instance, 'serialize_object'):
+ snapshots['postchange'] = instance.serialize_object()
+ else:
+ snapshots['postchange'] = serialize_object(instance)
+
+ return snapshots
+
+
+def enqueue_object(queue, instance, user, request_id, action):
+ """
+ Enqueue a serialized representation of a created/updated/deleted object for the processing of
+ events once the request has completed.
+ """
+ # Determine whether this type of object supports event rules
+ app_label = instance._meta.app_label
+ model_name = instance._meta.model_name
+ if model_name not in registry['model_features']['event_rules'].get(app_label, []):
+ return
+
+ queue.append({
+ 'content_type': ContentType.objects.get_for_model(instance),
+ 'object_id': instance.pk,
+ 'event': action,
+ 'data': serialize_for_event(instance),
+ 'snapshots': get_snapshots(instance, action),
+ 'username': user.username,
+ 'request_id': request_id
+ })
+
+
+def process_event_rules(event_rules, model_name, event, data, username, snapshots=None, request_id=None):
+ try:
+ user = get_user_model().objects.get(username=username)
+ except ObjectDoesNotExist:
+ user = None
+
+ for event_rule in event_rules:
+
+ # Evaluate event rule conditions (if any)
+ if not event_rule.eval_conditions(data):
+ return
+
+ # Webhooks
+ if event_rule.action_type == EventRuleActionChoices.WEBHOOK:
+
+ # Select the appropriate RQ queue
+ queue_name = get_config().QUEUE_MAPPINGS.get('webhook', RQ_QUEUE_DEFAULT)
+ rq_queue = get_queue(queue_name)
+
+ # Compile the task parameters
+ params = {
+ "event_rule": event_rule,
+ "model_name": model_name,
+ "event": event,
+ "data": data,
+ "snapshots": snapshots,
+ "timestamp": timezone.now().isoformat(),
+ "username": username,
+ "retry": get_rq_retry()
+ }
+ if snapshots:
+ params["snapshots"] = snapshots
+ if request_id:
+ params["request_id"] = request_id
+
+ # Enqueue the task
+ rq_queue.enqueue(
+ "extras.webhooks.send_webhook",
+ **params
+ )
+
+ # Scripts
+ elif event_rule.action_type == EventRuleActionChoices.SCRIPT:
+ # Resolve the script from action parameters
+ script_module = event_rule.action_object
+ script_name = event_rule.action_parameters['script_name']
+ script = script_module.scripts[script_name]()
+
+ # Enqueue a Job to record the script's execution
+ Job.enqueue(
+ "extras.scripts.run_script",
+ instance=script_module,
+ name=script.class_name,
+ user=user,
+ data=data
+ )
+
+ else:
+ raise ValueError(f"Unknown action type for an event rule: {event_rule.action_type}")
+
+
+def process_event_queue(events):
+ """
+ Flush a list of object representation to RQ for EventRule processing.
+ """
+ events_cache = {
+ 'type_create': {},
+ 'type_update': {},
+ 'type_delete': {},
+ }
+
+ 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']
+
+ # 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},
+ content_types=content_type,
+ enabled=True
+ )
+ event_rules = events_cache[action_flag][content_type]
+
+ process_event_rules(
+ event_rules, content_type.model, data['event'], data['data'], data['username'],
+ snapshots=data['snapshots'], request_id=data['request_id']
+ )
+
+
+def flush_events(queue):
+ """
+ Flush a list of object representation to RQ for webhook processing.
+ """
+ if queue:
+ for name in settings.EVENTS_PIPELINE:
+ try:
+ func = import_string(name)
+ func(queue)
+ except Exception as e:
+ logger.error(f"Cannot import events pipeline {name} error: {e}")
diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py
index fec067263..730499956 100644
--- a/netbox/extras/filtersets.py
+++ b/netbox/extras/filtersets.py
@@ -17,12 +17,12 @@ from .models import *
__all__ = (
'BookmarkFilterSet',
'ConfigContextFilterSet',
- 'ConfigRevisionFilterSet',
'ConfigTemplateFilterSet',
'ContentTypeFilterSet',
'CustomFieldChoiceSetFilterSet',
'CustomFieldFilterSet',
'CustomLinkFilterSet',
+ 'EventRuleFilterSet',
'ExportTemplateFilterSet',
'ImageAttachmentFilterSet',
'JournalEntryFilterSet',
@@ -39,19 +39,18 @@ class WebhookFilterSet(NetBoxModelFilterSet):
method='search',
label=_('Search'),
)
- content_type_id = MultiValueNumberFilter(
- field_name='content_types__id'
- )
- content_types = ContentTypeFilter()
http_method = django_filters.MultipleChoiceFilter(
choices=WebhookHttpMethodChoices
)
+ payload_url = MultiValueCharFilter(
+ lookup_expr='icontains'
+ )
class Meta:
model = Webhook
fields = [
- 'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'payload_url',
- 'enabled', 'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
+ 'id', 'name', 'payload_url', 'http_method', 'http_content_type', 'secret', 'ssl_verification',
+ 'ca_file_path', 'description',
]
def search(self, queryset, name, value):
@@ -59,10 +58,43 @@ class WebhookFilterSet(NetBoxModelFilterSet):
return queryset
return queryset.filter(
Q(name__icontains=value) |
+ Q(description__icontains=value) |
Q(payload_url__icontains=value)
)
+class EventRuleFilterSet(NetBoxModelFilterSet):
+ q = django_filters.CharFilter(
+ method='search',
+ label=_('Search'),
+ )
+ content_type_id = MultiValueNumberFilter(
+ field_name='content_types__id'
+ )
+ content_types = ContentTypeFilter()
+ action_type = django_filters.MultipleChoiceFilter(
+ choices=EventRuleActionChoices
+ )
+ action_object_type = ContentTypeFilter()
+ action_object_id = MultiValueNumberFilter()
+
+ class Meta:
+ model = EventRule
+ fields = [
+ 'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled',
+ 'action_type', 'description',
+ ]
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ Q(name__icontains=value) |
+ Q(description__icontains=value) |
+ Q(comments__icontains=value)
+ )
+
+
class CustomFieldFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
@@ -87,8 +119,8 @@ class CustomFieldFilterSet(BaseFilterSet):
class Meta:
model = CustomField
fields = [
- 'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visibility',
- 'weight', 'is_cloneable', 'description',
+ 'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
+ 'ui_editable', 'weight', 'is_cloneable', 'description',
]
def search(self, queryset, name, value):
@@ -122,8 +154,7 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
return queryset
return queryset.filter(
Q(name__icontains=value) |
- Q(description__icontains=value) |
- Q(extra_choices__contains=value)
+ Q(description__icontains=value)
)
def filter_by_choice(self, queryset, name, value):
@@ -513,7 +544,7 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
class Meta:
model = ConfigContext
- fields = ['id', 'name', 'is_active', 'data_synced']
+ fields = ['id', 'name', 'is_active', 'data_synced', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -625,27 +656,3 @@ class ContentTypeFilterSet(django_filters.FilterSet):
Q(app_label__icontains=value) |
Q(model__icontains=value)
)
-
-
-#
-# ConfigRevisions
-#
-
-class ConfigRevisionFilterSet(BaseFilterSet):
- q = django_filters.CharFilter(
- method='search',
- label=_('Search'),
- )
-
- class Meta:
- model = ConfigRevision
- fields = [
- 'id',
- ]
-
- def search(self, queryset, name, value):
- if not value.strip():
- return queryset
- return queryset.filter(
- Q(comment__icontains=value)
- )
diff --git a/netbox/extras/forms/__init__.py b/netbox/extras/forms/__init__.py
index e203bee46..8bebaeec2 100644
--- a/netbox/extras/forms/__init__.py
+++ b/netbox/extras/forms/__init__.py
@@ -3,5 +3,4 @@ from .filtersets import *
from .bulk_edit import *
from .bulk_import import *
from .misc import *
-from .mixins import *
from .scripts import *
diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py
index 821ce7eb2..9479fef99 100644
--- a/netbox/extras/forms/bulk_edit.py
+++ b/netbox/extras/forms/bulk_edit.py
@@ -14,6 +14,7 @@ __all__ = (
'CustomFieldBulkEditForm',
'CustomFieldChoiceSetBulkEditForm',
'CustomLinkBulkEditForm',
+ 'EventRuleBulkEditForm',
'ExportTemplateBulkEditForm',
'JournalEntryBulkEditForm',
'SavedFilterBulkEditForm',
@@ -48,11 +49,15 @@ class CustomFieldBulkEditForm(BulkEditForm):
queryset=CustomFieldChoiceSet.objects.all(),
required=False
)
- ui_visibility = forms.ChoiceField(
- label=_("UI visibility"),
- choices=add_blank_choice(CustomFieldVisibilityChoices),
- required=False,
- initial=''
+ ui_visible = forms.ChoiceField(
+ label=_("UI visible"),
+ choices=add_blank_choice(CustomFieldUIVisibleChoices),
+ required=False
+ )
+ ui_editable = forms.ChoiceField(
+ label=_("UI editable"),
+ choices=add_blank_choice(CustomFieldUIEditableChoices),
+ required=False
)
is_cloneable = forms.NullBooleanField(
label=_('Is cloneable'),
@@ -173,6 +178,44 @@ class WebhookBulkEditForm(NetBoxModelBulkEditForm):
queryset=Webhook.objects.all(),
widget=forms.MultipleHiddenInput
)
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
+ )
+ http_method = forms.ChoiceField(
+ choices=add_blank_choice(WebhookHttpMethodChoices),
+ required=False,
+ label=_('HTTP method')
+ )
+ payload_url = forms.CharField(
+ required=False,
+ label=_('Payload URL')
+ )
+ ssl_verification = forms.NullBooleanField(
+ required=False,
+ widget=BulkEditNullBooleanSelect(),
+ label=_('SSL verification')
+ )
+ secret = forms.CharField(
+ label=_('Secret'),
+ required=False
+ )
+ ca_file_path = forms.CharField(
+ required=False,
+ label=_('CA file path')
+ )
+
+ nullable_fields = ('secret', 'ca_file_path')
+
+
+class EventRuleBulkEditForm(NetBoxModelBulkEditForm):
+ model = EventRule
+
+ pk = forms.ModelMultipleChoiceField(
+ queryset=EventRule.objects.all(),
+ widget=forms.MultipleHiddenInput
+ )
enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False,
@@ -203,30 +246,8 @@ class WebhookBulkEditForm(NetBoxModelBulkEditForm):
required=False,
widget=BulkEditNullBooleanSelect()
)
- http_method = forms.ChoiceField(
- choices=add_blank_choice(WebhookHttpMethodChoices),
- required=False,
- label=_('HTTP method')
- )
- payload_url = forms.CharField(
- required=False,
- label=_('Payload URL')
- )
- ssl_verification = forms.NullBooleanField(
- required=False,
- widget=BulkEditNullBooleanSelect(),
- label=_('SSL verification')
- )
- secret = forms.CharField(
- label=_('Secret'),
- required=False
- )
- ca_file_path = forms.CharField(
- required=False,
- label=_('CA file path')
- )
- nullable_fields = ('secret', 'conditions', 'ca_file_path')
+ nullable_fields = ('description', 'conditions',)
class TagBulkEditForm(BulkEditForm):
diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py
index 466baa241..f8d3ffb7f 100644
--- a/netbox/extras/forms/bulk_import.py
+++ b/netbox/extras/forms/bulk_import.py
@@ -1,12 +1,14 @@
+import re
+
from django import forms
-from django.contrib.contenttypes.models import ContentType
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 ContentType
from extras.choices import *
from extras.models import *
-from extras.utils import FeatureQuery
from netbox.forms import NetBoxModelImportForm
from utilities.forms import CSVModelForm
from utilities.forms.fields import (
@@ -18,6 +20,7 @@ __all__ = (
'CustomFieldChoiceSetImportForm',
'CustomFieldImportForm',
'CustomLinkImportForm',
+ 'EventRuleImportForm',
'ExportTemplateImportForm',
'JournalEntryImportForm',
'SavedFilterImportForm',
@@ -29,8 +32,7 @@ __all__ = (
class CustomFieldImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField(
label=_('Content types'),
- queryset=ContentType.objects.all(),
- limit_choices_to=FeatureQuery('custom_fields'),
+ queryset=ContentType.objects.with_feature('custom_fields'),
help_text=_("One or more assigned object types")
)
type = CSVChoiceField(
@@ -40,8 +42,7 @@ class CustomFieldImportForm(CSVModelForm):
)
object_type = CSVContentTypeField(
label=_('Object type'),
- queryset=ContentType.objects.all(),
- limit_choices_to=FeatureQuery('custom_fields'),
+ queryset=ContentType.objects.public(),
required=False,
help_text=_("Object type (for object or multi-object fields)")
)
@@ -52,10 +53,17 @@ class CustomFieldImportForm(CSVModelForm):
required=False,
help_text=_('Choice set (for selection fields)')
)
- ui_visibility = CSVChoiceField(
- label=_('UI visibility'),
- choices=CustomFieldVisibilityChoices,
- help_text=_('How the custom field is displayed in the user interface')
+ ui_visible = CSVChoiceField(
+ label=_('UI visible'),
+ choices=CustomFieldUIVisibleChoices,
+ required=False,
+ help_text=_('Whether the custom field is displayed in the UI')
+ )
+ ui_editable = CSVChoiceField(
+ label=_('UI editable'),
+ choices=CustomFieldUIEditableChoices,
+ required=False,
+ help_text=_('Whether the custom field is editable in the UI')
)
class Meta:
@@ -63,7 +71,7 @@ class CustomFieldImportForm(CSVModelForm):
fields = (
'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description',
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
- 'validation_maximum', 'validation_regex', 'ui_visibility', 'is_cloneable',
+ 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable',
)
@@ -76,7 +84,10 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
extra_choices = SimpleArrayField(
base_field=forms.CharField(),
required=False,
- help_text=_('Comma-separated list of field choices')
+ help_text=_(
+ 'Quoted string of comma-separated field choices with optional labels separated by colon: '
+ '"choice1:First Choice,choice2:Second Choice"'
+ )
)
class Meta:
@@ -85,12 +96,24 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
'name', 'description', 'extra_choices', 'order_alphabetically',
)
+ def clean_extra_choices(self):
+ if isinstance(self.cleaned_data['extra_choices'], list):
+ data = []
+ for line in self.cleaned_data['extra_choices']:
+ try:
+ value, label = re.split(r'(?00ff00)')),
+ 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00
'),
}
diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py
index 1ea361a7c..c91e3b8c6 100644
--- a/netbox/extras/forms/filtersets.py
+++ b/netbox/extras/forms/filtersets.py
@@ -1,14 +1,13 @@
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 core.models import DataFile, DataSource
+from core.models import ContentType, DataFile, DataSource
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import *
from extras.models import *
-from extras.utils import FeatureQuery
from netbox.forms.base import NetBoxModelFilterSetForm
+from netbox.forms.mixins import SavedFiltersMixin
from tenancy.models import Tenant, TenantGroup
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import (
@@ -16,15 +15,14 @@ from utilities.forms.fields import (
)
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
from virtualization.models import Cluster, ClusterGroup, ClusterType
-from .mixins import *
__all__ = (
'ConfigContextFilterForm',
- 'ConfigRevisionFilterForm',
'ConfigTemplateFilterForm',
'CustomFieldChoiceSetFilterForm',
'CustomFieldFilterForm',
'CustomLinkFilterForm',
+ 'EventRuleFilterForm',
'ExportTemplateFilterForm',
'ImageAttachmentFilterForm',
'JournalEntryFilterForm',
@@ -40,12 +38,12 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
(_('Attributes'), (
- 'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visibility',
+ 'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable',
'is_cloneable',
)),
)
content_type_id = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
+ queryset=ContentType.objects.with_feature('custom_fields'),
required=False,
label=_('Object type')
)
@@ -74,10 +72,15 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
required=False,
label=_('Choice set')
)
- ui_visibility = forms.ChoiceField(
- choices=add_blank_choice(CustomFieldVisibilityChoices),
+ ui_visible = forms.ChoiceField(
+ choices=add_blank_choice(CustomFieldUIVisibleChoices),
required=False,
- label=_('UI visibility')
+ label=_('UI visible')
+ )
+ ui_editable = forms.ChoiceField(
+ choices=add_blank_choice(CustomFieldUIEditableChoices),
+ required=False,
+ label=_('UI editable')
)
is_cloneable = forms.NullBooleanField(
label=_('Is cloneable'),
@@ -109,7 +112,7 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
)
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
- queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()),
+ queryset=ContentType.objects.with_feature('custom_links'),
required=False
)
enabled = forms.NullBooleanField(
@@ -136,7 +139,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
(_('Data'), ('data_source_id', 'data_file_id')),
- (_('Attributes'), ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
+ (_('Attributes'), ('content_type_id', 'mime_type', 'file_extension', 'as_attachment')),
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@@ -151,10 +154,10 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
'source_id': '$data_source_id'
}
)
- content_types = ContentTypeMultipleChoiceField(
- label=_('Content types'),
- queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
- required=False
+ content_type_id = ContentTypeMultipleChoiceField(
+ queryset=ContentType.objects.with_feature('export_templates'),
+ required=False,
+ label=_('Content types')
)
mime_type = forms.CharField(
required=False,
@@ -180,7 +183,7 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
)
content_type_id = ContentTypeChoiceField(
label=_('Content type'),
- queryset=ContentType.objects.filter(FeatureQuery('image_attachments').get_query()),
+ queryset=ContentType.objects.with_feature('image_attachments'),
required=False
)
name = forms.CharField(
@@ -196,7 +199,7 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
)
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
- queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
+ queryset=ContentType.objects.public(),
required=False
)
enabled = forms.NullBooleanField(
@@ -221,23 +224,45 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
class WebhookFilterForm(NetBoxModelFilterSetForm):
model = Webhook
- tag = TagFilterField(model)
-
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
- (_('Attributes'), ('content_type_id', 'http_method', 'enabled')),
- (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
+ (_('Attributes'), ('payload_url', 'http_method', 'http_content_type')),
)
- content_type_id = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
- required=False,
- label=_('Object type')
+ http_content_type = forms.CharField(
+ label=_('HTTP content type'),
+ required=False
+ )
+ payload_url = forms.CharField(
+ label=_('Payload URL'),
+ required=False
)
http_method = forms.MultipleChoiceField(
choices=WebhookHttpMethodChoices,
required=False,
label=_('HTTP method')
)
+ tag = TagFilterField(model)
+
+
+class EventRuleFilterForm(NetBoxModelFilterSetForm):
+ model = EventRule
+ tag = TagFilterField(model)
+
+ fieldsets = (
+ (None, ('q', 'filter_id', 'tag')),
+ (_('Attributes'), ('content_type_id', 'action_type', 'enabled')),
+ (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
+ )
+ content_type_id = ContentTypeMultipleChoiceField(
+ queryset=ContentType.objects.with_feature('event_rules'),
+ required=False,
+ label=_('Object type')
+ )
+ action_type = forms.ChoiceField(
+ choices=add_blank_choice(EventRuleActionChoices),
+ required=False,
+ label=_('Action type')
+ )
enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False,
@@ -285,12 +310,12 @@ class WebhookFilterForm(NetBoxModelFilterSetForm):
class TagFilterForm(SavedFiltersMixin, FilterForm):
model = Tag
content_type_id = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
+ queryset=ContentType.objects.with_feature('tags'),
required=False,
label=_('Tagged object type')
)
for_object_type_id = ContentTypeChoiceField(
- queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
+ queryset=ContentType.objects.with_feature('tags'),
required=False,
label=_('Allowed object type')
)
@@ -496,9 +521,3 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
api_url='/api/extras/content-types/',
)
)
-
-
-class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
- fieldsets = (
- (None, ('q', 'filter_id')),
- )
diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py
index 47bde65f9..346225c8a 100644
--- a/netbox/extras/forms/model_forms.py
+++ b/netbox/extras/forms/model_forms.py
@@ -1,36 +1,34 @@
import json
+import re
from django import forms
-from django.conf import settings
-from django.db.models import Q
from django.contrib.contenttypes.models import ContentType
+from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin
+from core.models import ContentType
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import *
from extras.models import *
-from extras.utils import FeatureQuery
-from netbox.config import get_config, PARAMS
from netbox.forms import NetBoxModelForm
from tenancy.models import Tenant, TenantGroup
-from utilities.forms import BootstrapMixin, add_blank_choice
+from utilities.forms import BootstrapMixin, add_blank_choice, get_field_value
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, JSONField, SlugField,
)
-from utilities.forms.widgets import ChoicesWidget
+from utilities.forms.widgets import ChoicesWidget, HTMXSelect
from virtualization.models import Cluster, ClusterGroup, ClusterType
-
__all__ = (
'BookmarkForm',
'ConfigContextForm',
- 'ConfigRevisionForm',
'ConfigTemplateForm',
'CustomFieldChoiceSetForm',
'CustomFieldForm',
'CustomLinkForm',
+ 'EventRuleForm',
'ExportTemplateForm',
'ImageAttachmentForm',
'JournalEntryForm',
@@ -43,14 +41,11 @@ __all__ = (
class CustomFieldForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
- queryset=ContentType.objects.all(),
- limit_choices_to=FeatureQuery('custom_fields'),
+ queryset=ContentType.objects.with_feature('custom_fields')
)
object_type = ContentTypeChoiceField(
label=_('Object type'),
- queryset=ContentType.objects.all(),
- # TODO: Come up with a canonical way to register suitable models
- limit_choices_to=FeatureQuery('webhooks').get_query() | Q(app_label='auth', model__in=['user', 'group']),
+ queryset=ContentType.objects.public(),
required=False,
help_text=_("Type of the related object (for object/multi-object fields only)")
)
@@ -63,7 +58,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
(_('Custom Field'), (
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description',
)),
- (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visibility', 'weight', 'is_cloneable')),
+ (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')),
(_('Values'), ('default', 'choice_set')),
(_('Validation'), ('validation_minimum', 'validation_maximum', 'validation_regex')),
)
@@ -75,13 +70,15 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
'type': _(
"The type of data stored in this field. For object/multi-object fields, select the related object "
"type below."
- )
+ ),
+ 'description': _("This will be displayed as help text for the form field. Markdown is supported.")
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- # Disable changing the type of a CustomField as it almost universally causes errors if custom field data is already present.
+ # 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
@@ -90,21 +87,35 @@ class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
extra_choices = forms.CharField(
widget=ChoicesWidget(),
required=False,
- help_text=_(
+ help_text=mark_safe(_(
'Enter one choice per line. An optional label may be specified for each choice by appending it with a '
- 'comma (for example, "choice1,First Choice").'
- )
+ 'colon. Example:'
+ ) + ' choice1:First Choice
')
)
class Meta:
model = CustomFieldChoiceSet
fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically')
+ def __init__(self, *args, initial=None, **kwargs):
+ super().__init__(*args, initial=initial, **kwargs)
+
+ # Escape colons in extra_choices
+ if 'extra_choices' in self.initial and self.initial['extra_choices']:
+ choices = []
+ for choice in self.initial['extra_choices']:
+ choice = (choice[0].replace(':', '\\:'), choice[1].replace(':', '\\:'))
+ choices.append(choice)
+
+ self.initial['extra_choices'] = choices
+
def clean_extra_choices(self):
data = []
for line in self.cleaned_data['extra_choices'].splitlines():
try:
- value, label = line.split(',', maxsplit=1)
+ value, label = re.split(r'(?JSON format.')
+ )
+ action_data = JSONField(
+ required=False,
+ help_text=_('Enter parameters to pass to the action in JSON format.')
+ )
+
+ fieldsets = (
+ (_('Event Rule'), ('name', 'description', 'content_types', 'enabled', 'tags')),
+ (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
+ (_('Conditions'), ('conditions',)),
+ (_('Action'), (
+ 'action_type', 'action_choice', 'action_object_type', 'action_object_id', 'action_data',
+ )),
+ )
+
+ class Meta:
+ model = EventRule
+ fields = (
+ 'content_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'
+ )
labels = {
'type_create': _('Creations'),
'type_update': _('Updates'),
@@ -247,18 +288,90 @@ class WebhookForm(NetBoxModelForm):
'type_job_end': _('Job terminations'),
}
widgets = {
- 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
+ 'action_type': HTMXSelect(),
+ 'action_object_type': forms.HiddenInput,
+ 'action_object_id': forms.HiddenInput,
}
+ def init_script_choice(self):
+ choices = []
+ for module in ScriptModule.objects.all():
+ scripts = []
+ for script_name in module.scripts.keys():
+ name = f"{str(module.pk)}:{script_name}"
+ scripts.append((name, script_name))
+ if scripts:
+ choices.append((str(module), scripts))
+ self.fields['action_choice'].choices = choices
+
+ if self.instance.action_type == EventRuleActionChoices.SCRIPT and self.instance.action_parameters:
+ scriptmodule_id = self.instance.action_object_id
+ script_name = self.instance.action_parameters.get('script_name')
+ self.fields['action_choice'].initial = f'{scriptmodule_id}:{script_name}'
+
+ def init_webhook_choice(self):
+ initial = None
+ if self.instance.action_type == EventRuleActionChoices.WEBHOOK:
+ webhook_id = get_field_value(self, 'action_object_id')
+ initial = Webhook.objects.get(pk=webhook_id) if webhook_id else None
+ self.fields['action_choice'] = DynamicModelChoiceField(
+ label=_('Webhook'),
+ queryset=Webhook.objects.all(),
+ required=True,
+ initial=initial
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['action_object_type'].required = False
+ self.fields['action_object_id'].required = False
+
+ # Determine the action type
+ action_type = get_field_value(self, 'action_type')
+
+ if action_type == EventRuleActionChoices.WEBHOOK:
+ self.init_webhook_choice()
+ elif action_type == EventRuleActionChoices.SCRIPT:
+ self.init_script_choice()
+
+ def clean(self):
+ super().clean()
+
+ action_choice = self.cleaned_data.get('action_choice')
+ # Webhook
+ if self.cleaned_data.get('action_type') == EventRuleActionChoices.WEBHOOK:
+ self.cleaned_data['action_object_type'] = ContentType.objects.get_for_model(action_choice)
+ self.cleaned_data['action_object_id'] = action_choice.id
+ # Script
+ elif self.cleaned_data.get('action_type') == EventRuleActionChoices.SCRIPT:
+ self.cleaned_data['action_object_type'] = ContentType.objects.get_for_model(
+ ScriptModule,
+ for_concrete_model=False
+ )
+ module_id, script_name = action_choice.split(":", maxsplit=1)
+ self.cleaned_data['action_object_id'] = module_id
+
+ return self.cleaned_data
+
+ def save(self, *args, **kwargs):
+ # Set action_parameters on the instance
+ if self.cleaned_data['action_type'] == EventRuleActionChoices.SCRIPT:
+ module_id, script_name = self.cleaned_data.get('action_choice').split(":", maxsplit=1)
+ self.instance.action_parameters = {
+ 'script_name': script_name,
+ }
+ else:
+ self.instance.action_parameters = None
+
+ return super().save(*args, **kwargs)
+
class TagForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
object_types = ContentTypeMultipleChoiceField(
label=_('Object types'),
- queryset=ContentType.objects.all(),
- limit_choices_to=FeatureQuery('tags'),
+ queryset=ContentType.objects.with_feature('tags'),
required=False
)
@@ -325,7 +438,7 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
required=False
)
tenant_groups = DynamicModelMultipleChoiceField(
- label=_('Tenat groups'),
+ label=_('Tenant groups'),
queryset=TenantGroup.objects.all(),
required=False
)
@@ -452,99 +565,3 @@ class JournalEntryForm(NetBoxModelForm):
'assigned_object_type': forms.HiddenInput,
'assigned_object_id': forms.HiddenInput,
}
-
-
-EMPTY_VALUES = ('', None, [], ())
-
-
-class ConfigFormMetaclass(forms.models.ModelFormMetaclass):
-
- def __new__(mcs, name, bases, attrs):
-
- # Emulate a declared field for each supported configuration parameter
- param_fields = {}
- for param in PARAMS:
- field_kwargs = {
- 'required': False,
- 'label': param.label,
- 'help_text': param.description,
- }
- field_kwargs.update(**param.field_kwargs)
- param_fields[param.name] = param.field(**field_kwargs)
- attrs.update(param_fields)
-
- return super().__new__(mcs, name, bases, attrs)
-
-
-class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMetaclass):
- """
- Form for creating a new ConfigRevision.
- """
-
- fieldsets = (
- (_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
- (_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
- (_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
- (_('Security'), ('ALLOWED_URL_SCHEMES',)),
- (_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
- (_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
- (_('Validation'), ('CUSTOM_VALIDATORS',)),
- (_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
- (_('Miscellaneous'), ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL')),
- (_('Config Revision'), ('comment',))
- )
-
- class Meta:
- model = ConfigRevision
- fields = '__all__'
- widgets = {
- 'BANNER_LOGIN': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'BANNER_MAINTENANCE': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'BANNER_TOP': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'BANNER_BOTTOM': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'CUSTOM_VALIDATORS': forms.Textarea(attrs={'class': 'font-monospace'}),
- 'comment': forms.Textarea(),
- }
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # Append current parameter values to form field help texts and check for static configurations
- config = get_config()
- for param in PARAMS:
- value = getattr(config, param.name)
- is_static = hasattr(settings, param.name)
- if value:
- help_text = self.fields[param.name].help_text
- if help_text:
- help_text += ' ' # Line break
- help_text += _('Current value: {value} ').format(value=value)
- if is_static:
- help_text += _(' (defined statically)')
- elif value == param.default:
- help_text += _(' (default)')
- self.fields[param.name].help_text = help_text
- self.fields[param.name].initial = value
- if is_static:
- self.fields[param.name].disabled = True
-
- def save(self, commit=True):
- instance = super().save(commit=False)
-
- # Populate JSON data on the instance
- instance.data = self.render_json()
-
- if commit:
- instance.save()
-
- return instance
-
- def render_json(self):
- json = {}
-
- # Iterate through each field and populate non-empty values
- for field_name in self.declared_fields:
- if field_name in self.cleaned_data and self.cleaned_data[field_name] not in EMPTY_VALUES:
- json[field_name] = self.cleaned_data[field_name]
-
- return json
diff --git a/netbox/extras/graphql/filters.py b/netbox/extras/graphql/filters.py
index f5ce2b23a..16ee63cd9 100644
--- a/netbox/extras/graphql/filters.py
+++ b/netbox/extras/graphql/filters.py
@@ -8,6 +8,7 @@ __all__ = (
'CustomFieldFilter',
'CustomFieldChoiceSetFilter',
'CustomLinkFilter',
+ 'EventRuleFilter',
'ExportTemplateFilter',
'ImageAttachmentFilter',
'JournalEntryFilter',
@@ -43,7 +44,6 @@ class CustomFieldFilter(filtersets.CustomFieldFilterSet):
required: auto
search_weight: auto
filter_logic: auto
- ui_visibility: auto
weight: auto
is_cloneable: auto
description: auto
@@ -135,15 +135,21 @@ class TagFilter(filtersets.TagFilterSet):
class WebhookFilter(filtersets.WebhookFilterSet):
id: auto
name: auto
- type_create: auto
- type_update: auto
- type_delete: auto
- type_job_start: auto
- type_job_end: auto
payload_url: auto
- enabled: auto
http_method: auto
http_content_type: auto
secret: auto
ssl_verification: auto
ca_file_path: auto
+
+
+@strawberry.django.filter(models.EventRule, lookups=True)
+class EventRuleFilter(filtersets.EventRuleFilterSet):
+ id: auto
+ name: auto
+ enabled: auto
+ type_create: auto
+ type_update: auto
+ type_delete: auto
+ type_job_start: auto
+ type_job_end: auto
diff --git a/netbox/extras/graphql/schema.py b/netbox/extras/graphql/schema.py
index e13cc0e9f..09e399e37 100644
--- a/netbox/extras/graphql/schema.py
+++ b/netbox/extras/graphql/schema.py
@@ -72,3 +72,9 @@ class ExtrasQuery(graphene.ObjectType):
def resolve_webhook_list(root, info, **kwargs):
return gql_query_optimizer(models.Webhook.objects.all(), info)
+
+ event_rule = ObjectField(EventRuleType)
+ event_rule_list = ObjectListField(EventRuleType)
+
+ def resolve_eventrule_list(root, info, **kwargs):
+ return gql_query_optimizer(models.EventRule.objects.all(), info)
diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py
index e962859db..e11f495a1 100644
--- a/netbox/extras/graphql/types.py
+++ b/netbox/extras/graphql/types.py
@@ -12,6 +12,7 @@ __all__ = (
'CustomFieldChoiceSetType',
'CustomFieldType',
'CustomLinkType',
+ 'EventRuleType',
'ExportTemplateType',
'ImageAttachmentType',
'JournalEntryType',
@@ -128,3 +129,12 @@ class TagType(ObjectType):
)
class WebhookType(OrganizationalObjectType):
pass
+
+
+@strawberry.django.type(
+ models.EventRule,
+ exclude=['content_types',],
+ filters=EventRuleFilter
+)
+class EventRuleType(OrganizationalObjectType):
+ pass
diff --git a/netbox/extras/management/commands/reindex.py b/netbox/extras/management/commands/reindex.py
index 9a29c54f5..e072c220a 100644
--- a/netbox/extras/management/commands/reindex.py
+++ b/netbox/extras/management/commands/reindex.py
@@ -69,10 +69,7 @@ class Command(BaseCommand):
if not kwargs['lazy']:
self.stdout.write('Clearing cached values... ', ending='')
self.stdout.flush()
- content_types = [
- ContentType.objects.get_for_model(model) for model in indexers.keys()
- ]
- deleted_count = search_backend.clear(content_types)
+ deleted_count = search_backend.clear()
self.stdout.write(f'{deleted_count} entries deleted.')
# Index models
diff --git a/netbox/extras/management/commands/runscript.py b/netbox/extras/management/commands/runscript.py
index d9a9f41ae..609374378 100644
--- a/netbox/extras/management/commands/runscript.py
+++ b/netbox/extras/management/commands/runscript.py
@@ -11,9 +11,9 @@ from django.db import transaction
from core.choices import JobStatusChoices
from core.models import Job
from extras.api.serializers import ScriptOutputSerializer
-from extras.context_managers import change_logging
+from extras.context_managers import event_tracking
from extras.scripts import get_module_and_script
-from extras.signals import clear_webhooks
+from extras.signals import clear_events
from utilities.exceptions import AbortTransaction
from utilities.utils import NetBoxFakeRequest
@@ -37,7 +37,7 @@ class Command(BaseCommand):
def _run_script():
"""
Core script execution task. We capture this within a subfunction to allow for conditionally wrapping it with
- the change_logging context manager (which is bypassed if commit == False).
+ the event_tracking context manager (which is bypassed if commit == False).
"""
try:
try:
@@ -47,7 +47,7 @@ class Command(BaseCommand):
raise AbortTransaction()
except AbortTransaction:
script.log_info("Database changes have been reverted automatically.")
- clear_webhooks.send(request)
+ clear_events.send(request)
job.data = ScriptOutputSerializer(script).data
job.terminate()
except Exception as e:
@@ -57,9 +57,9 @@ class Command(BaseCommand):
)
script.log_info("Database changes have been reverted due to error.")
logger.error(f"Exception raised during script execution: {e}")
- clear_webhooks.send(request)
+ clear_events.send(request)
job.data = ScriptOutputSerializer(script).data
- job.terminate(status=JobStatusChoices.STATUS_ERRORED)
+ job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
logger.info(f"Script completed in {job.duration}")
@@ -114,7 +114,7 @@ class Command(BaseCommand):
# Create the job
job = Job.objects.create(
object=module,
- name=script.name,
+ name=script.class_name,
user=User.objects.filter(is_superuser=True).order_by('pk')[0],
job_id=uuid.uuid4()
)
@@ -136,9 +136,9 @@ class Command(BaseCommand):
logger.info(f"Running script (commit={commit})")
script.request = request
- # Execute the script. If commit is True, wrap it with the change_logging context manager to ensure we process
+ # Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
# change logging, webhooks, etc.
- with change_logging(request):
+ with event_tracking(request):
_run_script()
else:
logger.error('Data is not valid:')
diff --git a/netbox/extras/migrations/0001_squashed.py b/netbox/extras/migrations/0001_squashed.py
index 2fdcc07eb..6f1f77e53 100644
--- a/netbox/extras/migrations/0001_squashed.py
+++ b/netbox/extras/migrations/0001_squashed.py
@@ -88,7 +88,7 @@ class Migration(migrations.Migration):
('secret', models.CharField(blank=True, max_length=255)),
('ssl_verification', models.BooleanField(default=True)),
('ca_file_path', models.CharField(blank=True, max_length=4096, null=True)),
- ('content_types', models.ManyToManyField(limit_choices_to=extras.utils.FeatureQuery('webhooks'), related_name='webhooks', to='contenttypes.ContentType')),
+ ('content_types', models.ManyToManyField(related_name='webhooks', to='contenttypes.ContentType')),
],
options={
'ordering': ('name',),
@@ -151,7 +151,7 @@ class Migration(migrations.Migration):
('status', models.CharField(default='pending', max_length=30)),
('data', models.JSONField(blank=True, null=True)),
('job_id', models.UUIDField(unique=True)),
- ('obj_type', models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('jobs'), on_delete=django.db.models.deletion.CASCADE, related_name='job_results', to='contenttypes.contenttype')),
+ ('obj_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='job_results', to='contenttypes.contenttype')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
@@ -184,7 +184,7 @@ class Migration(migrations.Migration):
('mime_type', models.CharField(blank=True, max_length=50)),
('file_extension', models.CharField(blank=True, max_length=15)),
('as_attachment', models.BooleanField(default=True)),
- ('content_type', models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('export_templates'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
+ ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
],
options={
'ordering': ['content_type', 'name'],
@@ -201,7 +201,7 @@ class Migration(migrations.Migration):
('group_name', models.CharField(blank=True, max_length=50)),
('button_class', models.CharField(default='default', max_length=30)),
('new_window', models.BooleanField(default=False)),
- ('content_type', models.ForeignKey(limit_choices_to=extras.utils.FeatureQuery('custom_links'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
+ ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
],
options={
'ordering': ['group_name', 'weight', 'name'],
@@ -223,7 +223,7 @@ class Migration(migrations.Migration):
('validation_maximum', models.PositiveIntegerField(blank=True, null=True)),
('validation_regex', models.CharField(blank=True, max_length=500, validators=[utilities.validators.validate_regex])),
('choices', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, null=True, size=None)),
- ('content_types', models.ManyToManyField(limit_choices_to=extras.utils.FeatureQuery('custom_fields'), related_name='custom_fields', to='contenttypes.ContentType')),
+ ('content_types', models.ManyToManyField(related_name='custom_fields', to='contenttypes.ContentType')),
],
options={
'ordering': ['weight', 'name'],
diff --git a/netbox/extras/migrations/0094_tag_object_types.py b/netbox/extras/migrations/0094_tag_object_types.py
index 944ef64b2..8bb760980 100644
--- a/netbox/extras/migrations/0094_tag_object_types.py
+++ b/netbox/extras/migrations/0094_tag_object_types.py
@@ -1,5 +1,4 @@
from django.db import migrations, models
-import extras.utils
class Migration(migrations.Migration):
@@ -13,7 +12,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='tag',
name='object_types',
- field=models.ManyToManyField(blank=True, limit_choices_to=extras.utils.FeatureQuery('tags'), related_name='+', to='contenttypes.contenttype'),
+ field=models.ManyToManyField(blank=True, related_name='+', to='contenttypes.contenttype'),
),
migrations.RenameIndex(
model_name='taggeditem',
diff --git a/netbox/extras/migrations/0099_cachedvalue_ordering.py b/netbox/extras/migrations/0099_cachedvalue_ordering.py
new file mode 100644
index 000000000..242ffd983
--- /dev/null
+++ b/netbox/extras/migrations/0099_cachedvalue_ordering.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.6 on 2023-10-30 14:04
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0098_webhook_custom_field_data_webhook_tags'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='cachedvalue',
+ options={'ordering': ('weight', 'object_type', 'value', 'object_id')},
+ ),
+ ]
diff --git a/netbox/extras/migrations/0100_customfield_ui_attrs.py b/netbox/extras/migrations/0100_customfield_ui_attrs.py
new file mode 100644
index 000000000..a4a713a86
--- /dev/null
+++ b/netbox/extras/migrations/0100_customfield_ui_attrs.py
@@ -0,0 +1,41 @@
+from django.db import migrations, models
+
+
+def update_ui_attrs(apps, schema_editor):
+ """
+ Replicate legacy ui_visibility values to the new ui_visible and ui_editable fields.
+ """
+ CustomField = apps.get_model('extras', 'CustomField')
+
+ CustomField.objects.filter(ui_visibility='read-write').update(ui_visible='always', ui_editable='yes')
+ CustomField.objects.filter(ui_visibility='read-only').update(ui_visible='always', ui_editable='no')
+ CustomField.objects.filter(ui_visibility='hidden').update(ui_visible='hidden', ui_editable='hidden')
+ CustomField.objects.filter(ui_visibility='hidden-ifunset').update(ui_visible='if-set', ui_editable='yes')
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0099_cachedvalue_ordering'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='customfield',
+ name='ui_editable',
+ field=models.CharField(default='yes', max_length=50),
+ ),
+ migrations.AddField(
+ model_name='customfield',
+ name='ui_visible',
+ field=models.CharField(default='always', max_length=50),
+ ),
+ migrations.RunPython(
+ code=update_ui_attrs,
+ reverse_code=migrations.RunPython.noop
+ ),
+ migrations.RemoveField(
+ model_name='customfield',
+ name='ui_visibility',
+ ),
+ ]
diff --git a/netbox/extras/migrations/0101_eventrule.py b/netbox/extras/migrations/0101_eventrule.py
new file mode 100644
index 000000000..3d236c847
--- /dev/null
+++ b/netbox/extras/migrations/0101_eventrule.py
@@ -0,0 +1,146 @@
+import django.db.models.deletion
+import taggit.managers
+from django.contrib.contenttypes.models import ContentType
+from django.db import migrations, models
+
+import utilities.json
+from extras.choices import *
+
+
+def move_webhooks(apps, schema_editor):
+ Webhook = apps.get_model("extras", "Webhook")
+ EventRule = apps.get_model("extras", "EventRule")
+
+ webhook_ct = ContentType.objects.get_for_model(Webhook).pk
+ for webhook in Webhook.objects.all():
+ event = EventRule()
+
+ # Replicate attributes from Webhook instance
+ event.name = webhook.name
+ event.type_create = webhook.type_create
+ event.type_update = webhook.type_update
+ event.type_delete = webhook.type_delete
+ event.type_job_start = webhook.type_job_start
+ event.type_job_end = webhook.type_job_end
+ event.enabled = webhook.enabled
+ event.conditions = webhook.conditions
+
+ event.action_type = EventRuleActionChoices.WEBHOOK
+ event.action_object_type_id = webhook_ct
+ event.action_object_id = webhook.id
+ event.save()
+ event.content_types.add(*webhook.content_types.all())
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('extras', '0100_customfield_ui_attrs'),
+ ]
+
+ operations = [
+
+ # Create the EventRule model
+ migrations.CreateModel(
+ name='EventRule',
+ 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=150, unique=True)),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ('type_create', models.BooleanField(default=False)),
+ ('type_update', models.BooleanField(default=False)),
+ ('type_delete', models.BooleanField(default=False)),
+ ('type_job_start', models.BooleanField(default=False)),
+ ('type_job_end', models.BooleanField(default=False)),
+ ('enabled', models.BooleanField(default=True)),
+ ('conditions', models.JSONField(blank=True, null=True)),
+ ('action_type', models.CharField(default='webhook', max_length=30)),
+ ('action_object_id', models.PositiveBigIntegerField(blank=True, null=True)),
+ ('action_parameters', models.JSONField(blank=True, null=True)),
+ ('action_data', models.JSONField(blank=True, null=True)),
+ ('comments', models.TextField(blank=True)),
+ ],
+ options={
+ 'verbose_name': 'eventrule',
+ 'verbose_name_plural': 'eventrules',
+ 'ordering': ('name',),
+ },
+ ),
+ migrations.AddField(
+ model_name='eventrule',
+ name='action_object_type',
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='eventrule_actions',
+ to='contenttypes.contenttype',
+ ),
+ ),
+ migrations.AddField(
+ model_name='eventrule',
+ name='content_types',
+ field=models.ManyToManyField(related_name='eventrules', to='contenttypes.contenttype'),
+ ),
+ migrations.AddField(
+ model_name='eventrule',
+ name='tags',
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
+ ),
+ migrations.AddIndex(
+ model_name='eventrule',
+ index=models.Index(fields=['action_object_type', 'action_object_id'], name='extras_even_action__d9e2af_idx'),
+ ),
+
+ # Replicate Webhook data
+ migrations.RunPython(move_webhooks),
+
+ # Remove obsolete fields from Webhook
+ migrations.RemoveConstraint(
+ model_name='webhook',
+ name='extras_webhook_unique_payload_url_types',
+ ),
+ migrations.RemoveField(
+ model_name='webhook',
+ name='conditions',
+ ),
+ migrations.RemoveField(
+ model_name='webhook',
+ name='content_types',
+ ),
+ migrations.RemoveField(
+ model_name='webhook',
+ name='enabled',
+ ),
+ migrations.RemoveField(
+ model_name='webhook',
+ name='type_create',
+ ),
+ migrations.RemoveField(
+ model_name='webhook',
+ name='type_delete',
+ ),
+ migrations.RemoveField(
+ model_name='webhook',
+ name='type_job_end',
+ ),
+ migrations.RemoveField(
+ model_name='webhook',
+ name='type_job_start',
+ ),
+ migrations.RemoveField(
+ model_name='webhook',
+ name='type_update',
+ ),
+
+ # Add description field to Webhook
+ migrations.AddField(
+ model_name='webhook',
+ name='description',
+ field=models.CharField(blank=True, max_length=200),
+ ),
+ ]
diff --git a/netbox/extras/migrations/0102_move_configrevision.py b/netbox/extras/migrations/0102_move_configrevision.py
new file mode 100644
index 000000000..36eef1205
--- /dev/null
+++ b/netbox/extras/migrations/0102_move_configrevision.py
@@ -0,0 +1,39 @@
+from django.db import migrations
+
+
+def update_content_type(apps, schema_editor):
+ ContentType = apps.get_model('contenttypes', 'ContentType')
+
+ # Delete the new ContentType effected by the introduction of core.ConfigRevision
+ ContentType.objects.filter(app_label='core', model='configrevision').delete()
+
+ # Update the app label of the original ContentType for extras.ConfigRevision to ensure any foreign key
+ # references are preserved
+ ContentType.objects.filter(app_label='extras', model='configrevision').update(app_label='core')
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0101_eventrule'),
+ ]
+
+ operations = [
+ migrations.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.DeleteModel(
+ name='ConfigRevision',
+ ),
+ ],
+ database_operations=[
+ migrations.AlterModelTable(
+ name='ConfigRevision',
+ table='core_configrevision',
+ ),
+ ],
+ ),
+ migrations.RunPython(
+ code=update_content_type,
+ reverse_code=migrations.RunPython.noop
+ ),
+ ]
diff --git a/netbox/extras/migrations/0103_gfk_indexes.py b/netbox/extras/migrations/0103_gfk_indexes.py
new file mode 100644
index 000000000..2ccbdb2ff
--- /dev/null
+++ b/netbox/extras/migrations/0103_gfk_indexes.py
@@ -0,0 +1,37 @@
+# Generated by Django 4.2.7 on 2023-12-07 16:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0102_move_configrevision'),
+ ]
+
+ operations = [
+ migrations.AddIndex(
+ model_name='bookmark',
+ index=models.Index(fields=['object_type', 'object_id'], name='extras_book_object__2df6b4_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='imageattachment',
+ index=models.Index(fields=['content_type', 'object_id'], name='extras_imag_content_94728e_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='journalentry',
+ index=models.Index(fields=['assigned_object_type', 'assigned_object_id'], name='extras_jour_assigne_76510f_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='objectchange',
+ index=models.Index(fields=['changed_object_type', 'changed_object_id'], name='extras_obje_changed_927fe5_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='objectchange',
+ index=models.Index(fields=['related_object_type', 'related_object_id'], name='extras_obje_related_bfcdef_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='stagedchange',
+ index=models.Index(fields=['object_type', 'object_id'], name='extras_stag_object__4734d5_idx'),
+ ),
+ ]
diff --git a/netbox/extras/migrations/0104_stagedchange_remove_change_logging.py b/netbox/extras/migrations/0104_stagedchange_remove_change_logging.py
new file mode 100644
index 000000000..df962bbb8
--- /dev/null
+++ b/netbox/extras/migrations/0104_stagedchange_remove_change_logging.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.2.5 on 2023-12-08 16:03
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('extras', '0103_gfk_indexes'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='stagedchange',
+ name='created',
+ ),
+ migrations.RemoveField(
+ model_name='stagedchange',
+ name='last_updated',
+ ),
+ ]
diff --git a/netbox/extras/migrations/0105_customfield_min_max_values.py b/netbox/extras/migrations/0105_customfield_min_max_values.py
new file mode 100644
index 000000000..bcf3f97bd
--- /dev/null
+++ b/netbox/extras/migrations/0105_customfield_min_max_values.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.8 on 2023-12-27 20:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0104_stagedchange_remove_change_logging'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='customfield',
+ name='validation_maximum',
+ field=models.BigIntegerField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='customfield',
+ name='validation_minimum',
+ field=models.BigIntegerField(blank=True, null=True),
+ ),
+ ]
diff --git a/netbox/extras/models/change_logging.py b/netbox/extras/models/change_logging.py
index ac9c60998..0155849aa 100644
--- a/netbox/extras/models/change_logging.py
+++ b/netbox/extras/models/change_logging.py
@@ -1,10 +1,11 @@
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
+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 ContentType
from extras.choices import *
from ..querysets import ObjectChangeQuerySet
@@ -48,7 +49,7 @@ class ObjectChange(models.Model):
choices=ObjectChangeActionChoices
)
changed_object_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
on_delete=models.PROTECT,
related_name='+'
)
@@ -58,7 +59,7 @@ class ObjectChange(models.Model):
fk_field='changed_object_id'
)
related_object_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
on_delete=models.PROTECT,
related_name='+',
blank=True,
@@ -93,6 +94,10 @@ class ObjectChange(models.Model):
class Meta:
ordering = ['-time']
+ indexes = (
+ models.Index(fields=('changed_object_type', 'changed_object_id')),
+ models.Index(fields=('related_object_type', 'related_object_id')),
+ )
verbose_name = _('object change')
verbose_name_plural = _('object changes')
@@ -104,6 +109,17 @@ class ObjectChange(models.Model):
self.user_name
)
+ def clean(self):
+ super().clean()
+
+ # Validate the assigned object type
+ if self.changed_object_type not in ContentType.objects.with_feature('change_logging'):
+ raise ValidationError(
+ _("Change logging is not supported for this object type ({type}).").format(
+ type=self.changed_object_type
+ )
+ )
+
def save(self, *args, **kwargs):
# Record the user's name and the object's representation as static strings
@@ -119,3 +135,7 @@ class ObjectChange(models.Model):
def get_action_color(self):
return ObjectChangeActionChoices.colors.get(self.action)
+
+ @property
+ def has_changes(self):
+ return self.prechange_data != self.postchange_data
diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py
index 47e8dcd82..425c1386a 100644
--- a/netbox/extras/models/configs.py
+++ b/netbox/extras/models/configs.py
@@ -146,7 +146,7 @@ class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
# Verify that JSON data is provided as an object
if type(self.data) is not dict:
raise ValidationError(
- {'data': _('JSON data must be in object form. Example: {"foo": 123}')}
+ {'data': _('JSON data must be in object form. Example:') + ' {"foo": 123}'}
)
def sync_data(self):
@@ -202,7 +202,7 @@ class ConfigContextModel(models.Model):
# Verify that JSON data is provided as an object
if self.local_context_data and type(self.local_context_data) is not dict:
raise ValidationError(
- {'local_context_data': _('JSON data must be in object form. Example: {"foo": 123}')}
+ {'local_context_data': _('JSON data must be in object form. Example:') + ' {"foo": 123}'}
)
@@ -260,12 +260,14 @@ class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLog
_context = dict()
# Populate the default template context with NetBox model classes, namespaced by app
- # TODO: Devise a canonical mechanism for identifying the models to include (see #13427)
- for app, model_names in registry['model_features']['custom_fields'].items():
+ for app, model_names in registry['models'].items():
_context.setdefault(app, {})
for model_name in model_names:
- model = apps.get_registered_model(app, model_name)
- _context[app][model.__name__] = model
+ try:
+ model = apps.get_registered_model(app, model_name)
+ _context[app][model.__name__] = model
+ except LookupError:
+ pass
# Add the provided context data, if any
if context is not None:
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index ac68855a0..e78d1af23 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -5,18 +5,16 @@ from datetime import datetime, date
import django_filters
from django import forms
from django.conf import settings
-from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField
from django.core.validators import RegexValidator, ValidationError
from django.db import models
from django.urls import reverse
-from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
+from core.models import ContentType
from extras.choices import *
from extras.data import CHOICE_SETS
-from extras.utils import FeatureQuery
from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, ExportTemplatesMixin
from netbox.search import FieldTypes
@@ -28,6 +26,7 @@ from utilities.forms.fields import (
from utilities.forms.utils import add_blank_choice
from utilities.forms.widgets import APISelect, APISelectMultiple, DatePicker, DateTimePicker
from utilities.querysets import RestrictedQuerySet
+from utilities.templatetags.builtins.filters import render_markdown
from utilities.validators import validate_regex
__all__ = (
@@ -56,12 +55,20 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
content_type = ContentType.objects.get_for_model(model._meta.concrete_model)
return self.get_queryset().filter(content_types=content_type)
+ def get_defaults_for_model(self, model):
+ """
+ Return a dictionary of serialized default values for all CustomFields applicable to the given model.
+ """
+ custom_fields = self.get_for_model(model).filter(default__isnull=False)
+ return {
+ cf.name: cf.default for cf in custom_fields
+ }
+
class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
content_types = models.ManyToManyField(
- to=ContentType,
+ to='contenttypes.ContentType',
related_name='custom_fields',
- limit_choices_to=FeatureQuery('custom_fields'),
help_text=_('The object(s) to which this field applies.')
)
type = models.CharField(
@@ -72,7 +79,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
help_text=_('The type of data this custom field holds')
)
object_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
on_delete=models.PROTECT,
blank=True,
null=True,
@@ -149,13 +156,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
verbose_name=_('display weight'),
help_text=_('Fields with higher weights appear lower in a form.')
)
- validation_minimum = models.IntegerField(
+ validation_minimum = models.BigIntegerField(
blank=True,
null=True,
verbose_name=_('minimum value'),
help_text=_('Minimum allowed value (for numeric fields)')
)
- validation_maximum = models.IntegerField(
+ validation_maximum = models.BigIntegerField(
blank=True,
null=True,
verbose_name=_('maximum value'),
@@ -179,12 +186,19 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
blank=True,
null=True
)
- ui_visibility = models.CharField(
+ ui_visible = models.CharField(
max_length=50,
- choices=CustomFieldVisibilityChoices,
- default=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
- verbose_name=_('UI visibility'),
- help_text=_('Specifies the visibility of custom field in the UI')
+ choices=CustomFieldUIVisibleChoices,
+ default=CustomFieldUIVisibleChoices.ALWAYS,
+ verbose_name=_('UI visible'),
+ help_text=_('Specifies whether the custom field is displayed in the UI')
+ )
+ ui_editable = models.CharField(
+ max_length=50,
+ choices=CustomFieldUIEditableChoices,
+ default=CustomFieldUIEditableChoices.YES,
+ verbose_name=_('UI editable'),
+ help_text=_('Specifies whether the custom field value can be edited in the UI')
)
is_cloneable = models.BooleanField(
default=False,
@@ -197,7 +211,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
clone_fields = (
'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight',
'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
- 'choice_set', 'ui_visibility', 'is_cloneable',
+ 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
)
class Meta:
@@ -219,7 +233,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
super().__init__(*args, **kwargs)
# Cache instance's original name so we can check later whether it has changed
- self._name = self.name
+ self._name = self.__dict__.get('name')
@property
def search_type(self):
@@ -231,6 +245,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
return self.choice_set.choices
return []
+ def get_ui_visible_color(self):
+ return CustomFieldUIVisibleChoices.colors.get(self.ui_visible)
+
+ def get_ui_editable_color(self):
+ return CustomFieldUIEditableChoices.colors.get(self.ui_editable)
+
+ def get_choice_label(self, value):
+ if not hasattr(self, '_choice_map'):
+ self._choice_map = dict(self.choices)
+ return self._choice_map.get(value, value)
+
def populate_initial_data(self, content_types):
"""
Populate initial custom field data upon either a) the creation of a new CustomField, or
@@ -281,8 +306,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
except ValidationError as err:
raise ValidationError({
'default': _(
- 'Invalid default value "{default}": {message}'
- ).format(default=self.default, message=self.message)
+ 'Invalid default value "{value}": {error}'
+ ).format(value=self.default, error=err.message)
})
# Minimum/maximum values can be set only for numeric fields
@@ -317,14 +342,6 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
'choice_set': _("Choices may be set only on selection fields.")
})
- # A selection field's default (if any) must be present in its available choices
- if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.default and self.default not in self.choices:
- raise ValidationError({
- 'default': _(
- "The specified default value ({default}) is not listed as an available choice."
- ).format(default=self.default)
- })
-
# Object fields must define an object_type; other fields must not
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
if not self.object_type:
@@ -334,8 +351,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
elif self.object_type:
raise ValidationError({
'object_type': _(
- "{type_display} fields may not define an object type.")
- .format(type_display=self.get_type_display())
+ "{type} fields may not define an object type.")
+ .format(type=self.get_type_display())
})
def serialize(self, value):
@@ -384,7 +401,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
- enforce_visibility: Honor the value of CustomField.ui_visibility. Set to False for filtering.
+ enforce_visibility: Honor the value of CustomField.ui_visible. Set to False for filtering.
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
"""
initial = self.default if set_initial else None
@@ -506,13 +523,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
field.model = self
field.label = str(self)
if self.description:
- field.help_text = escape(self.description)
+ field.help_text = render_markdown(self.description)
# Annotate read-only fields
- if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
+ if enforce_visibility and self.ui_editable != CustomFieldUIEditableChoices.YES:
field.disabled = True
- prepend = ' ' if field.help_text else ''
- field.help_text += f'{prepend} ' + _('Field is set to read-only.')
return field
@@ -564,8 +579,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Multiselect
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
- filter_class = filters.MultiValueCharFilter
- kwargs['lookup_expr'] = 'has_key'
+ filter_class = filters.MultiValueArrayFilter
# Object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
@@ -650,19 +664,22 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Validate selected choice
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
- if value not in [c[0] for c in self.choices]:
+ if value not in self.choice_set.values:
raise ValidationError(
- _("Invalid choice ({value}). Available choices are: {choices}").format(
- value=value, choices=', '.join(self.choices)
+ _("Invalid choice ({value}) for choice set {choiceset}.").format(
+ value=value,
+ choiceset=self.choice_set
)
)
# Validate all selected choices
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
- if not set(value).issubset([c[0] for c in self.choices]):
+ if not set(value).issubset(self.choice_set.values):
raise ValidationError(
- _("Invalid choice(s) ({invalid_choices}). Available choices are: {available_choices}").format(
- invalid_choices=', '.join(value), available_choices=', '.join(self.choices))
+ _("Invalid choice(s) ({value}) for choice set {choiceset}.").format(
+ value=value,
+ choiceset=self.choice_set
+ )
)
# Validate selected object
@@ -747,6 +764,13 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
def choices_count(self):
return len(self.choices)
+ @property
+ def values(self):
+ """
+ Returns an iterator of the valid choice values.
+ """
+ return (x[0] for x in self.choices)
+
def clean(self):
if not self.base_choices and not self.extra_choices:
raise ValidationError(_("Must define base or extra choices."))
diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py
index 54832b3ba..778d7b68d 100644
--- a/netbox/extras/models/models.py
+++ b/netbox/extras/models/models.py
@@ -1,11 +1,8 @@
import json
import urllib.parse
-from django.contrib import admin
from django.conf import settings
-from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
-from django.core.cache import cache
+from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.core.validators import ValidationError
from django.db import models
from django.http import HttpResponse
@@ -15,10 +12,11 @@ from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _
from rest_framework.utils.encoders import JSONEncoder
+from core.models import ContentType
from extras.choices import *
from extras.conditions import ConditionSet
from extras.constants import *
-from extras.utils import FeatureQuery, image_upload
+from extras.utils import image_upload
from netbox.config import get_config
from netbox.models import ChangeLoggedModel
from netbox.models.features import (
@@ -29,8 +27,8 @@ from utilities.utils import clean_html, dict_to_querydict, render_jinja2
__all__ = (
'Bookmark',
- 'ConfigRevision',
'CustomLink',
+ 'EventRule',
'ExportTemplate',
'ImageAttachment',
'JournalEntry',
@@ -39,24 +37,28 @@ __all__ = (
)
-class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
+class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
"""
- A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
- delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
- Each Webhook can be limited to firing only on certain actions or certain object types.
+ An EventRule defines an action to be taken automatically in response to a specific set of events, such as when a
+ specific type of object is created, modified, or deleted. The action to be taken might entail transmitting a
+ webhook or executing a custom script.
"""
content_types = models.ManyToManyField(
- to=ContentType,
- related_name='webhooks',
+ to='contenttypes.ContentType',
+ related_name='eventrules',
verbose_name=_('object types'),
- limit_choices_to=FeatureQuery('webhooks'),
- help_text=_("The object(s) to which this Webhook applies.")
+ help_text=_("The object(s) to which this rule applies.")
)
name = models.CharField(
verbose_name=_('name'),
max_length=150,
unique=True
)
+ description = models.CharField(
+ verbose_name=_('description'),
+ max_length=200,
+ blank=True
+ )
type_create = models.BooleanField(
verbose_name=_('on create'),
default=False,
@@ -82,6 +84,111 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo
default=False,
help_text=_("Triggers when a job for a matching object terminates.")
)
+ enabled = models.BooleanField(
+ verbose_name=_('enabled'),
+ default=True
+ )
+ conditions = models.JSONField(
+ verbose_name=_('conditions'),
+ blank=True,
+ null=True,
+ help_text=_("A set of conditions which determine whether the event will be generated.")
+ )
+
+ # Action to take
+ action_type = models.CharField(
+ max_length=30,
+ choices=EventRuleActionChoices,
+ default=EventRuleActionChoices.WEBHOOK,
+ verbose_name=_('action type')
+ )
+ action_object_type = models.ForeignKey(
+ to='contenttypes.ContentType',
+ related_name='eventrule_actions',
+ on_delete=models.CASCADE
+ )
+ action_object_id = models.PositiveBigIntegerField(
+ blank=True,
+ null=True
+ )
+ action_object = GenericForeignKey(
+ ct_field='action_object_type',
+ fk_field='action_object_id'
+ )
+ action_parameters = models.JSONField(
+ blank=True,
+ null=True
+ )
+ action_data = models.JSONField(
+ verbose_name=_('data'),
+ blank=True,
+ null=True,
+ help_text=_("Additional data to pass to the action object")
+ )
+ comments = models.TextField(
+ verbose_name=_('comments'),
+ blank=True
+ )
+
+ class Meta:
+ ordering = ('name',)
+ indexes = (
+ models.Index(fields=('action_object_type', 'action_object_id')),
+ )
+ verbose_name = _('event rule')
+ verbose_name_plural = _('event rules')
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('extras:eventrule', args=[self.pk])
+
+ 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:
+ ConditionSet(self.conditions)
+ except ValueError as e:
+ raise ValidationError({'conditions': e})
+
+ def eval_conditions(self, data):
+ """
+ Test whether the given data meets the conditions of the event rule (if any). Return True
+ if met or no conditions are specified.
+ """
+ if not self.conditions:
+ return True
+
+ return ConditionSet(self.conditions).eval(data)
+
+
+class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
+ """
+ A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
+ delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
+ Each Webhook can be limited to firing only on certain actions or certain object types.
+ """
+ name = models.CharField(
+ verbose_name=_('name'),
+ max_length=150,
+ unique=True
+ )
+ description = models.CharField(
+ verbose_name=_('description'),
+ max_length=200,
+ blank=True
+ )
payload_url = models.CharField(
max_length=500,
verbose_name=_('URL'),
@@ -90,10 +197,6 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo
"processing is supported with the same context as the request body."
)
)
- enabled = models.BooleanField(
- verbose_name=_('enabled'),
- default=True
- )
http_method = models.CharField(
max_length=30,
choices=WebhookHttpMethodChoices,
@@ -136,12 +239,6 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo
"digest of the payload body using the secret as the key. The secret is not transmitted in the request."
)
)
- conditions = models.JSONField(
- verbose_name=_('conditions'),
- blank=True,
- null=True,
- help_text=_("A set of conditions which determine whether the webhook will be generated.")
- )
ssl_verification = models.BooleanField(
default=True,
verbose_name=_('SSL verification'),
@@ -156,15 +253,14 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo
"The specific CA certificate file to use for SSL verification. Leave blank to use the system defaults."
)
)
+ events = GenericRelation(
+ EventRule,
+ content_type_field='action_object_type',
+ object_id_field='action_object_id'
+ )
class Meta:
ordering = ('name',)
- constraints = (
- models.UniqueConstraint(
- fields=('payload_url', 'type_create', 'type_update', 'type_delete'),
- name='%(app_label)s_%(class)s_unique_payload_url_types'
- ),
- )
verbose_name = _('webhook')
verbose_name_plural = _('webhooks')
@@ -181,20 +277,6 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo
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.")
- )
-
- if self.conditions:
- try:
- ConditionSet(self.conditions)
- except ValueError as e:
- raise ValidationError({'conditions': e})
-
# CA file path requires SSL verification enabled
if not self.ssl_verification and self.ca_file_path:
raise ValidationError({
@@ -236,7 +318,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
code to be rendered with an object as context.
"""
content_types = models.ManyToManyField(
- to=ContentType,
+ to='contenttypes.ContentType',
related_name='custom_links',
help_text=_('The object type(s) to which this link applies.')
)
@@ -316,7 +398,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
text = clean_html(text, allowed_schemes)
# Sanitize link
- link = urllib.parse.quote(link, safe='/:?&=%+[]@#,;')
+ link = urllib.parse.quote(link, safe='/:?&=%+[]@#,;!')
# Verify link scheme is allowed
result = urllib.parse.urlparse(link)
@@ -332,7 +414,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
content_types = models.ManyToManyField(
- to=ContentType,
+ to='contenttypes.ContentType',
related_name='export_templates',
help_text=_('The object type(s) to which this template applies.')
)
@@ -441,7 +523,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
A set of predefined keyword parameters that can be reused to filter for specific objects.
"""
content_types = models.ManyToManyField(
- to=ContentType,
+ to='contenttypes.ContentType',
related_name='saved_filters',
help_text=_('The object type(s) to which this filter applies.')
)
@@ -521,7 +603,7 @@ class ImageAttachment(ChangeLoggedModel):
An uploaded image which is associated with an object.
"""
content_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
on_delete=models.CASCADE
)
object_id = models.PositiveBigIntegerField()
@@ -552,6 +634,9 @@ class ImageAttachment(ChangeLoggedModel):
class Meta:
ordering = ('name', 'pk') # name may be non-unique
+ indexes = (
+ models.Index(fields=('content_type', 'object_id')),
+ )
verbose_name = _('image attachment')
verbose_name_plural = _('image attachments')
@@ -561,6 +646,15 @@ class ImageAttachment(ChangeLoggedModel):
filename = self.image.name.rsplit('/', 1)[-1]
return filename.split('_', 2)[2]
+ def clean(self):
+ super().clean()
+
+ # Validate the assigned object type
+ if self.content_type not in ContentType.objects.with_feature('image_attachments'):
+ raise ValidationError(
+ _("Image attachments cannot be assigned to this object type ({type}).").format(type=self.content_type)
+ )
+
def delete(self, *args, **kwargs):
_name = self.image.name
@@ -606,7 +700,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
might record a new journal entry when a device undergoes maintenance, or when a prefix is expanded.
"""
assigned_object_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
on_delete=models.CASCADE
)
assigned_object_id = models.PositiveBigIntegerField()
@@ -632,6 +726,9 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
class Meta:
ordering = ('-created',)
+ indexes = (
+ models.Index(fields=('assigned_object_type', 'assigned_object_id')),
+ )
verbose_name = _('journal entry')
verbose_name_plural = _('journal entries')
@@ -645,9 +742,8 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
def clean(self):
super().clean()
- # Prevent the creation of journal entries on unsupported models
- permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
- if self.assigned_object_type not in permitted_types:
+ # Validate the assigned object type
+ if self.assigned_object_type not in ContentType.objects.with_feature('journaling'):
raise ValidationError(
_("Journaling is not supported for this object type ({type}).").format(type=self.assigned_object_type)
)
@@ -665,7 +761,7 @@ class Bookmark(models.Model):
auto_now_add=True
)
object_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
on_delete=models.PROTECT
)
object_id = models.PositiveBigIntegerField()
@@ -682,6 +778,9 @@ class Bookmark(models.Model):
class Meta:
ordering = ('created', 'pk')
+ indexes = (
+ models.Index(fields=('object_type', 'object_id')),
+ )
constraints = (
models.UniqueConstraint(
fields=('object_type', 'object_id', 'user'),
@@ -696,52 +795,11 @@ class Bookmark(models.Model):
return str(self.object)
return super().__str__()
+ def clean(self):
+ super().clean()
-class ConfigRevision(models.Model):
- """
- An atomic revision of NetBox's configuration.
- """
- created = models.DateTimeField(
- verbose_name=_('created'),
- auto_now_add=True
- )
- comment = models.CharField(
- verbose_name=_('comment'),
- max_length=200,
- blank=True
- )
- data = models.JSONField(
- blank=True,
- null=True,
- verbose_name=_('configuration data')
- )
-
- objects = RestrictedQuerySet.as_manager()
-
- class Meta:
- ordering = ['-created']
- verbose_name = _('config revision')
- verbose_name_plural = _('config revisions')
-
- def __str__(self):
- return f'Config revision #{self.pk} ({self.created})'
-
- def __getattr__(self, item):
- if item in self.data:
- return self.data[item]
- return super().__getattribute__(item)
-
- def get_absolute_url(self):
- return reverse('extras:configrevision', args=[self.pk])
-
- def activate(self):
- """
- Cache the configuration data.
- """
- cache.set('config', self.data, None)
- cache.set('config_version', self.pk, None)
- activate.alters_data = True
-
- @admin.display(boolean=True)
- def is_active(self):
- return cache.get('config_version') == self.pk
+ # Validate the assigned object type
+ if self.object_type not in ContentType.objects.with_feature('bookmarks'):
+ raise ValidationError(
+ _("Bookmarks cannot be assigned to this object type ({type}).").format(type=self.object_type)
+ )
diff --git a/netbox/extras/models/reports.py b/netbox/extras/models/reports.py
index 223d679bd..f6228ef24 100644
--- a/netbox/extras/models/reports.py
+++ b/netbox/extras/models/reports.py
@@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _
from core.choices import ManagedFileRootPathChoices
from core.models import ManagedFile
from extras.utils import is_report
-from netbox.models.features import JobsMixin, WebhooksMixin
+from netbox.models.features import JobsMixin, EventRulesMixin
from utilities.querysets import RestrictedQuerySet
from .mixins import PythonModuleMixin
@@ -21,7 +21,7 @@ __all__ = (
)
-class Report(WebhooksMixin, models.Model):
+class Report(EventRulesMixin, models.Model):
"""
Dummy model used to generate permissions for reports. Does not exist in the database.
"""
diff --git a/netbox/extras/models/scripts.py b/netbox/extras/models/scripts.py
index 122f56f20..93275acda 100644
--- a/netbox/extras/models/scripts.py
+++ b/netbox/extras/models/scripts.py
@@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _
from core.choices import ManagedFileRootPathChoices
from core.models import ManagedFile
from extras.utils import is_script
-from netbox.models.features import JobsMixin, WebhooksMixin
+from netbox.models.features import JobsMixin, EventRulesMixin
from utilities.querysets import RestrictedQuerySet
from .mixins import PythonModuleMixin
@@ -21,7 +21,7 @@ __all__ = (
logger = logging.getLogger('netbox.data_backends')
-class Script(WebhooksMixin, models.Model):
+class Script(EventRulesMixin, models.Model):
"""
Dummy model used to generate permissions for custom scripts. Does not exist in the database.
"""
diff --git a/netbox/extras/models/search.py b/netbox/extras/models/search.py
index debe4c648..9ba779642 100644
--- a/netbox/extras/models/search.py
+++ b/netbox/extras/models/search.py
@@ -1,10 +1,12 @@
import uuid
-from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.translation import gettext_lazy as _
+from netbox.search.utils import get_indexer
+from netbox.registry import registry
from utilities.fields import RestrictedGenericForeignKey
+from utilities.utils import content_type_identifier
from ..fields import CachedValueField
__all__ = (
@@ -24,7 +26,7 @@ class CachedValue(models.Model):
editable=False
)
object_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
on_delete=models.CASCADE,
related_name='+'
)
@@ -49,10 +51,28 @@ class CachedValue(models.Model):
default=1000
)
+ _netbox_private = True
+
class Meta:
- ordering = ('weight', 'object_type', 'object_id')
+ ordering = ('weight', 'object_type', 'value', 'object_id')
verbose_name = _('cached value')
verbose_name_plural = _('cached values')
def __str__(self):
return f'{self.object_type} {self.object_id}: {self.field}={self.value}'
+
+ @property
+ def display_attrs(self):
+ """
+ Render any display attributes associated with this search result.
+ """
+ indexer = get_indexer(self.object_type)
+ attrs = {}
+ for attr in indexer.display_attrs:
+ name = self.object._meta.get_field(attr).verbose_name
+ if value := getattr(self.object, attr):
+ if display_func := getattr(self.object, f'get_{attr}_display', None):
+ attrs[name] = display_func()
+ else:
+ attrs[name] = value
+ return attrs
diff --git a/netbox/extras/models/staging.py b/netbox/extras/models/staging.py
index b0df9e26e..f15d8d470 100644
--- a/netbox/extras/models/staging.py
+++ b/netbox/extras/models/staging.py
@@ -2,12 +2,12 @@ import logging
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
from django.db import models, transaction
from django.utils.translation import gettext_lazy as _
from extras.choices import ChangeActionChoices
from netbox.models import ChangeLoggedModel
+from netbox.models.features import *
from utilities.utils import deserialize_object
__all__ = (
@@ -55,7 +55,7 @@ class Branch(ChangeLoggedModel):
self.staged_changes.all().delete()
-class StagedChange(ChangeLoggedModel):
+class StagedChange(CustomValidationMixin, EventRulesMixin, models.Model):
"""
The prepared creation, modification, or deletion of an object to be applied to the active database at a
future point.
@@ -71,7 +71,7 @@ class StagedChange(ChangeLoggedModel):
choices=ChangeActionChoices
)
object_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
on_delete=models.CASCADE,
related_name='+'
)
@@ -91,6 +91,9 @@ class StagedChange(ChangeLoggedModel):
class Meta:
ordering = ('pk',)
+ indexes = (
+ models.Index(fields=('object_type', 'object_id')),
+ )
verbose_name = _('staged change')
verbose_name_plural = _('staged changes')
diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py
index f4ba5ea64..3aba6df60 100644
--- a/netbox/extras/models/tags.py
+++ b/netbox/extras/models/tags.py
@@ -1,13 +1,10 @@
from django.conf import settings
-from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from taggit.models import TagBase, GenericTaggedItemBase
-from extras.utils import FeatureQuery
from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, ExportTemplatesMixin
from utilities.choices import ColorChoices
@@ -37,9 +34,8 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
blank=True,
)
object_types = models.ManyToManyField(
- to=ContentType,
+ to='contenttypes.ContentType',
related_name='+',
- limit_choices_to=FeatureQuery('tags'),
blank=True,
help_text=_("The object type(s) to which this this tag can be applied.")
)
@@ -75,6 +71,8 @@ class TaggedItem(GenericTaggedItemBase):
on_delete=models.CASCADE
)
+ _netbox_private = True
+
class Meta:
indexes = [models.Index(fields=["content_type", "object_id"])]
verbose_name = _('tagged item')
diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py
index 8736a3197..31ea1ce09 100644
--- a/netbox/extras/plugins/__init__.py
+++ b/netbox/extras/plugins/__init__.py
@@ -1,147 +1,9 @@
-import collections
-from importlib import import_module
-
-from django.apps import AppConfig
-from django.core.exceptions import ImproperlyConfigured
-from django.utils.module_loading import import_string
-from packaging import version
-
-from netbox.registry import registry
-from netbox.search import register_search
from .navigation import *
from .registration import *
from .templates import *
-
-# Initialize plugin registry
-registry['plugins'].update({
- 'graphql_schemas': [],
- 'menus': [],
- 'menu_items': {},
- 'preferences': {},
- 'template_extensions': collections.defaultdict(list),
-})
-
-DEFAULT_RESOURCE_PATHS = {
- 'search_indexes': 'search.indexes',
- 'graphql_schema': 'graphql.schema',
- 'menu': 'navigation.menu',
- 'menu_items': 'navigation.menu_items',
- 'template_extensions': 'template_content.template_extensions',
- 'user_preferences': 'preferences.preferences',
-}
+from .utils import *
+from netbox.plugins import PluginConfig
-#
-# Plugin AppConfig class
-#
-
-class PluginConfig(AppConfig):
- """
- Subclass of Django's built-in AppConfig class, to be used for NetBox plugins.
- """
- # Plugin metadata
- author = ''
- author_email = ''
- description = ''
- version = ''
-
- # Root URL path under /plugins. If not set, the plugin's label will be used.
- base_url = None
-
- # Minimum/maximum compatible versions of NetBox
- min_version = None
- max_version = None
-
- # Default configuration parameters
- default_settings = {}
-
- # Mandatory configuration parameters
- required_settings = []
-
- # Middleware classes provided by the plugin
- middleware = []
-
- # Django-rq queues dedicated to the plugin
- queues = []
-
- # Django apps to append to INSTALLED_APPS when plugin requires them.
- django_apps = []
-
- # Optional plugin resources
- search_indexes = None
- graphql_schema = None
- menu = None
- menu_items = None
- template_extensions = None
- user_preferences = None
-
- def _load_resource(self, name):
- # Import from the configured path, if defined.
- if path := getattr(self, name, None):
- return import_string(f"{self.__module__}.{path}")
-
- # Fall back to the resource's default path. Return None if the module has not been provided.
- default_path = f'{self.__module__}.{DEFAULT_RESOURCE_PATHS[name]}'
- default_module, resource_name = default_path.rsplit('.', 1)
- try:
- module = import_module(default_module)
- return getattr(module, resource_name, None)
- except ModuleNotFoundError:
- pass
-
- def ready(self):
- plugin_name = self.name.rsplit('.', 1)[-1]
-
- # Register search extensions (if defined)
- search_indexes = self._load_resource('search_indexes') or []
- for idx in search_indexes:
- register_search(idx)
-
- # Register template content (if defined)
- if template_extensions := self._load_resource('template_extensions'):
- register_template_extensions(template_extensions)
-
- # Register navigation menu and/or menu items (if defined)
- if menu := self._load_resource('menu'):
- register_menu(menu)
- if menu_items := self._load_resource('menu_items'):
- register_menu_items(self.verbose_name, menu_items)
-
- # Register GraphQL schema (if defined)
- if graphql_schema := self._load_resource('graphql_schema'):
- register_graphql_schema(graphql_schema)
-
- # Register user preferences (if defined)
- if user_preferences := self._load_resource('user_preferences'):
- register_user_preferences(plugin_name, user_preferences)
-
- @classmethod
- def validate(cls, user_config, netbox_version):
-
- # Enforce version constraints
- current_version = version.parse(netbox_version)
- if cls.min_version is not None:
- min_version = version.parse(cls.min_version)
- if current_version < min_version:
- raise ImproperlyConfigured(
- f"Plugin {cls.__module__} requires NetBox minimum version {cls.min_version}."
- )
- if cls.max_version is not None:
- max_version = version.parse(cls.max_version)
- if current_version > max_version:
- raise ImproperlyConfigured(
- f"Plugin {cls.__module__} requires NetBox maximum version {cls.max_version}."
- )
-
- # Verify required configuration settings
- for setting in cls.required_settings:
- if setting not in user_config:
- raise ImproperlyConfigured(
- f"Plugin {cls.__module__} requires '{setting}' to be present in the PLUGINS_CONFIG section of "
- f"configuration.py."
- )
-
- # Apply default configuration values
- for setting, value in cls.default_settings.items():
- if setting not in user_config:
- user_config[setting] = value
+# TODO: Remove in v4.0
+warnings.warn(f"{__name__} is deprecated. Import from netbox.plugins instead.", DeprecationWarning)
diff --git a/netbox/extras/plugins/navigation.py b/netbox/extras/plugins/navigation.py
index 288a78512..08d1baa54 100644
--- a/netbox/extras/plugins/navigation.py
+++ b/netbox/extras/plugins/navigation.py
@@ -1,71 +1,7 @@
-from netbox.navigation import MenuGroup
-from utilities.choices import ButtonColorChoices
-from django.utils.text import slugify
+import warnings
-__all__ = (
- 'PluginMenu',
- 'PluginMenuButton',
- 'PluginMenuItem',
-)
+from netbox.plugins.navigation import *
-class PluginMenu:
- icon_class = 'mdi mdi-puzzle'
-
- def __init__(self, label, groups, icon_class=None):
- self.label = label
- self.groups = [
- MenuGroup(label, items) for label, items in groups
- ]
- if icon_class is not None:
- self.icon_class = icon_class
-
- @property
- def name(self):
- return slugify(self.label)
-
-
-class PluginMenuItem:
- """
- This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
- specifying additional link buttons that appear to the right of the item in the van menu.
-
- Links are specified as Django reverse URL strings.
- Buttons are each specified as a list of PluginMenuButton instances.
- """
- permissions = []
- buttons = []
-
- def __init__(self, link, link_text, permissions=None, buttons=None):
- self.link = link
- self.link_text = link_text
- if permissions is not None:
- if type(permissions) not in (list, tuple):
- raise TypeError("Permissions must be passed as a tuple or list.")
- self.permissions = permissions
- if buttons is not None:
- if type(buttons) not in (list, tuple):
- raise TypeError("Buttons must be passed as a tuple or list.")
- self.buttons = buttons
-
-
-class PluginMenuButton:
- """
- This class represents a button within a PluginMenuItem. Note that button colors should come from
- ButtonColorChoices.
- """
- color = ButtonColorChoices.DEFAULT
- permissions = []
-
- def __init__(self, link, title, icon_class, color=None, permissions=None):
- self.link = link
- self.title = title
- self.icon_class = icon_class
- if permissions is not None:
- if type(permissions) not in (list, tuple):
- raise TypeError("Permissions must be passed as a tuple or list.")
- self.permissions = permissions
- if color is not None:
- if color not in ButtonColorChoices.values():
- raise ValueError("Button color must be a choice within ButtonColorChoices.")
- self.color = color
+# TODO: Remove in v4.0
+warnings.warn(f"{__name__} is deprecated. Import from netbox.plugins instead.", DeprecationWarning)
diff --git a/netbox/extras/plugins/registration.py b/netbox/extras/plugins/registration.py
index 5b7e58172..8d2d85573 100644
--- a/netbox/extras/plugins/registration.py
+++ b/netbox/extras/plugins/registration.py
@@ -1,64 +1,7 @@
-import inspect
+import warnings
-from netbox.registry import registry
-from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem
-from .templates import PluginTemplateExtension
-
-__all__ = (
- 'register_graphql_schema',
- 'register_menu',
- 'register_menu_items',
- 'register_template_extensions',
- 'register_user_preferences',
-)
+from netbox.plugins.registration import *
-def register_template_extensions(class_list):
- """
- Register a list of PluginTemplateExtension classes
- """
- # Validation
- for template_extension in class_list:
- if not inspect.isclass(template_extension):
- raise TypeError(f"PluginTemplateExtension class {template_extension} was passed as an instance!")
- if not issubclass(template_extension, PluginTemplateExtension):
- raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!")
- if template_extension.model is None:
- raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!")
-
- registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
-
-
-def register_menu(menu):
- if not isinstance(menu, PluginMenu):
- raise TypeError(f"{menu} must be an instance of extras.plugins.PluginMenu")
- registry['plugins']['menus'].append(menu)
-
-
-def register_menu_items(section_name, class_list):
- """
- Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
- """
- # Validation
- for menu_link in class_list:
- if not isinstance(menu_link, PluginMenuItem):
- raise TypeError(f"{menu_link} must be an instance of extras.plugins.PluginMenuItem")
- for button in menu_link.buttons:
- if not isinstance(button, PluginMenuButton):
- raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton")
-
- registry['plugins']['menu_items'][section_name] = class_list
-
-
-def register_graphql_schema(graphql_schema):
- """
- Register a GraphQL schema class for inclusion in NetBox's GraphQL API.
- """
- registry['plugins']['graphql_schemas'].append(graphql_schema)
-
-
-def register_user_preferences(plugin_name, preferences):
- """
- Register a list of user preferences defined by a plugin.
- """
- registry['plugins']['preferences'][plugin_name] = preferences
+# TODO: Remove in v4.0
+warnings.warn(f"{__name__} is deprecated. Import from netbox.plugins instead.", DeprecationWarning)
diff --git a/netbox/extras/plugins/templates.py b/netbox/extras/plugins/templates.py
index e9b9a9dca..0e09f33d2 100644
--- a/netbox/extras/plugins/templates.py
+++ b/netbox/extras/plugins/templates.py
@@ -1,73 +1,7 @@
-from django.template.loader import get_template
+import warnings
-__all__ = (
- 'PluginTemplateExtension',
-)
+from netbox.plugins.templates import *
-class PluginTemplateExtension:
- """
- This class is used to register plugin content to be injected into core NetBox templates. It contains methods
- that are overridden by plugin authors to return template content.
-
- The `model` attribute on the class defines the which model detail page this class renders content for. It
- should be set as a string in the form '.'. render() provides the following context data:
-
- * object - The object being viewed
- * request - The current request
- * settings - Global NetBox settings
- * config - Plugin-specific configuration parameters
- """
- model = None
-
- def __init__(self, context):
- self.context = context
-
- def render(self, template_name, extra_context=None):
- """
- Convenience method for rendering the specified Django template using the default context data. An additional
- context dictionary may be passed as `extra_context`.
- """
- if extra_context is None:
- extra_context = {}
- elif not isinstance(extra_context, dict):
- raise TypeError("extra_context must be a dictionary")
-
- return get_template(template_name).render({**self.context, **extra_context})
-
- def left_page(self):
- """
- Content that will be rendered on the left of 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 right_page(self):
- """
- Content that will be rendered on the right of 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 full_width_page(self):
- """
- Content that will be rendered within the full width of 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 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
+# TODO: Remove in v4.0
+warnings.warn(f"{__name__} is deprecated. Import from netbox.plugins instead.", DeprecationWarning)
diff --git a/netbox/extras/plugins/urls.py b/netbox/extras/plugins/urls.py
index 2f237f56a..8b24e8fd2 100644
--- a/netbox/extras/plugins/urls.py
+++ b/netbox/extras/plugins/urls.py
@@ -1,41 +1,7 @@
-from importlib import import_module
+import warnings
-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 netbox.plugins.urls import *
-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'),
- path('installed-plugins/', views.InstalledPluginsAPIView.as_view(), name='plugins-list')
-]
-plugin_admin_patterns = [
- path('installed-plugins/', staff_member_required(views.InstalledPluginsAdminView.as_view()), name='plugins_list')
-]
-
-# Register base/API URL patterns for each plugin
-for plugin_path in settings.PLUGINS:
- plugin = import_module(plugin_path)
- plugin_name = plugin_path.split('.')[-1]
- app = apps.get_app_config(plugin_name)
- base_url = getattr(app, 'base_url') or app.label
-
- # Check if the plugin specifies any base URLs
- if module_has_submodule(plugin, 'urls'):
- urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns")
- plugin_patterns.append(
- path(f"{base_url}/", include((urlpatterns, app.label)))
- )
-
- # Check if the plugin specifies any API URLs
- if module_has_submodule(plugin, 'api.urls'):
- urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns")
- plugin_api_patterns.append(
- path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
- )
+# TODO: Remove in v4.0
+warnings.warn(f"{__name__} is deprecated. Import from netbox.plugins instead.", DeprecationWarning)
diff --git a/netbox/extras/plugins/utils.py b/netbox/extras/plugins/utils.py
index c260f156d..15ae018d1 100644
--- a/netbox/extras/plugins/utils.py
+++ b/netbox/extras/plugins/utils.py
@@ -1,37 +1,7 @@
-from django.apps import apps
-from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
+import warnings
-__all__ = (
- 'get_installed_plugins',
- 'get_plugin_config',
-)
+from netbox.plugins.utils import *
-def get_installed_plugins():
- """
- Return a dictionary mapping the names of installed plugins to their versions.
- """
- plugins = {}
- for plugin_name in settings.PLUGINS:
- plugin_name = plugin_name.rsplit('.', 1)[-1]
- plugin_config = apps.get_app_config(plugin_name)
- plugins[plugin_name] = getattr(plugin_config, 'version', None)
-
- return dict(sorted(plugins.items()))
-
-
-def get_plugin_config(plugin_name, parameter, default=None):
- """
- Return the value of the specified plugin configuration parameter.
-
- Args:
- plugin_name: The name of the plugin
- parameter: The name of the configuration parameter
- default: The value to return if the parameter is not defined (default: None)
- """
- try:
- plugin_config = settings.PLUGINS_CONFIG[plugin_name]
- return plugin_config.get(parameter, default)
- except KeyError:
- raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.")
+# TODO: Remove in v4.0
+warnings.warn(f"{__name__} is deprecated. Import from netbox.plugins instead.", DeprecationWarning)
diff --git a/netbox/extras/plugins/views.py b/netbox/extras/plugins/views.py
index 5971f78ef..505742e6b 100644
--- a/netbox/extras/plugins/views.py
+++ b/netbox/extras/plugins/views.py
@@ -1,89 +1,7 @@
-from collections import OrderedDict
+import warnings
-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
-from rest_framework.reverse import reverse
-from rest_framework.views import APIView
+from netbox.plugins.views import *
-class InstalledPluginsAdminView(View):
- """
- Admin view for listing all installed plugins
- """
- def get(self, request):
- plugins = [apps.get_app_config(plugin) for plugin in settings.PLUGINS]
- return render(request, 'extras/admin/plugins_list.html', {
- 'plugins': plugins,
- })
-
-
-@extend_schema(exclude=True)
-class InstalledPluginsAPIView(APIView):
- """
- API view for listing all installed plugins
- """
- permission_classes = [permissions.IsAdminUser]
- _ignore_model_permissions = True
- schema = None
-
- def get_view_name(self):
- return "Installed Plugins"
-
- @staticmethod
- def _get_plugin_data(plugin_app_config):
- return {
- 'name': plugin_app_config.verbose_name,
- 'package': plugin_app_config.name,
- 'author': plugin_app_config.author,
- 'author_email': plugin_app_config.author_email,
- 'description': plugin_app_config.description,
- 'version': plugin_app_config.version
- }
-
- def get(self, request, format=None):
- return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS])
-
-
-@extend_schema(exclude=True)
-class PluginsAPIRootView(APIView):
- _ignore_model_permissions = True
- schema = None
-
- def get_view_name(self):
- return "Plugins"
-
- @staticmethod
- def _get_plugin_entry(plugin, app_config, request, format):
- # Check if the plugin specifies any API URLs
- api_app_name = f'{app_config.name}-api'
- try:
- entry = (getattr(app_config, 'base_url', app_config.label), reverse(
- f"plugins-api:{api_app_name}:api-root",
- request=request,
- format=format
- ))
- except NoReverseMatch:
- # The plugin does not include an api-root url
- entry = None
-
- return entry
-
- def get(self, request, format=None):
-
- entries = []
- for plugin in settings.PLUGINS:
- app_config = apps.get_app_config(plugin)
- entry = self._get_plugin_entry(plugin, app_config, request, format)
- if entry is not None:
- entries.append(entry)
-
- return Response(OrderedDict((
- ('installed-plugins', reverse('plugins-api:plugins-list', request=request, format=format)),
- *entries
- )))
+# TODO: Remove in v4.0
+warnings.warn(f"{__name__} is deprecated. Import from netbox.plugins instead.", DeprecationWarning)
diff --git a/netbox/extras/reports.py b/netbox/extras/reports.py
index 6af81a9d9..90641cc84 100644
--- a/netbox/extras/reports.py
+++ b/netbox/extras/reports.py
@@ -23,7 +23,7 @@ logger = logging.getLogger(__name__)
def get_module_and_report(module_name, report_name):
module = ReportModule.objects.get(file_path=f'{module_name}.py')
- report = module.reports.get(report_name)
+ report = module.reports.get(report_name)()
return module, report
@@ -40,8 +40,8 @@ def run_report(job, *args, **kwargs):
try:
report.run(job)
- except Exception:
- job.terminate(status=JobStatusChoices.STATUS_ERRORED)
+ except Exception as e:
+ job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
logging.error(f"Error during execution of report {job.name}")
finally:
# Schedule the next job if an interval has been set
@@ -106,8 +106,6 @@ class Report(object):
'failure': 0,
'log': [],
}
- if not test_methods:
- raise Exception("A report must contain at least one test method.")
self.test_methods = test_methods
@classproperty
@@ -137,6 +135,13 @@ class Report(object):
def source(self):
return inspect.getsource(self.__class__)
+ @property
+ def is_valid(self):
+ """
+ Indicates whether the report can be run.
+ """
+ return bool(self.test_methods)
+
#
# Logging methods
#
@@ -225,7 +230,7 @@ class Report(object):
stacktrace = traceback.format_exc()
self.log_failure(None, f"An exception occurred: {type(e).__name__}: {e} {stacktrace} ")
logger.error(f"Exception raised during report execution: {e}")
- job.terminate(status=JobStatusChoices.STATUS_ERRORED)
+ job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
# Perform any post-run tasks
self.post_run()
diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py
index 9fa31db31..f28465547 100644
--- a/netbox/extras/scripts.py
+++ b/netbox/extras/scripts.py
@@ -17,13 +17,13 @@ from core.models import Job
from extras.api.serializers import ScriptOutputSerializer
from extras.choices import LogLevelChoices
from extras.models import ScriptModule
-from extras.signals import clear_webhooks
+from extras.signals import clear_events
from ipam.formfields import IPAddressFormField, IPNetworkFormField
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
from utilities.exceptions import AbortScript, AbortTransaction
from utilities.forms import add_blank_choice
from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField
-from .context_managers import change_logging
+from .context_managers import event_tracking
from .forms import ScriptForm
__all__ = (
@@ -401,23 +401,23 @@ class BaseScript:
def log_debug(self, message):
self.logger.log(logging.DEBUG, message)
- self.log.append((LogLevelChoices.LOG_DEFAULT, message))
+ self.log.append((LogLevelChoices.LOG_DEFAULT, str(message)))
def log_success(self, message):
self.logger.log(logging.INFO, message) # No syslog equivalent for SUCCESS
- self.log.append((LogLevelChoices.LOG_SUCCESS, message))
+ self.log.append((LogLevelChoices.LOG_SUCCESS, str(message)))
def log_info(self, message):
self.logger.log(logging.INFO, message)
- self.log.append((LogLevelChoices.LOG_INFO, message))
+ self.log.append((LogLevelChoices.LOG_INFO, str(message)))
def log_warning(self, message):
self.logger.log(logging.WARNING, message)
- self.log.append((LogLevelChoices.LOG_WARNING, message))
+ self.log.append((LogLevelChoices.LOG_WARNING, str(message)))
def log_failure(self, message):
self.logger.log(logging.ERROR, message)
- self.log.append((LogLevelChoices.LOG_FAILURE, message))
+ self.log.append((LogLevelChoices.LOG_FAILURE, str(message)))
# Convenience functions
@@ -472,10 +472,16 @@ def get_module_and_script(module_name, script_name):
return module, script
-def run_script(data, request, job, commit=True, **kwargs):
+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()
@@ -486,9 +492,10 @@ def run_script(data, request, job, commit=True, **kwargs):
logger.info(f"Running script (commit={commit})")
# Add files to form data
- files = request.FILES
- for field_name, fileobj in files.items():
- data[field_name] = fileobj
+ 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
@@ -496,7 +503,7 @@ def run_script(data, request, job, commit=True, **kwargs):
def _run_script():
"""
Core script execution task. We capture this within a subfunction to allow for conditionally wrapping it with
- the change_logging context manager (which is bypassed if commit == False).
+ the event_tracking context manager (which is bypassed if commit == False).
"""
try:
try:
@@ -506,7 +513,8 @@ def run_script(data, request, job, commit=True, **kwargs):
raise AbortTransaction()
except AbortTransaction:
script.log_info("Database changes have been reverted automatically.")
- clear_webhooks.send(request)
+ if request:
+ clear_events.send(request)
job.data = ScriptOutputSerializer(script).data
job.terminate()
except Exception as e:
@@ -519,15 +527,16 @@ def run_script(data, request, job, commit=True, **kwargs):
logger.error(f"Exception raised during script execution: {e}")
script.log_info("Database changes have been reverted due to error.")
job.data = ScriptOutputSerializer(script).data
- job.terminate(status=JobStatusChoices.STATUS_ERRORED)
- clear_webhooks.send(request)
+ 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 change_logging context manager to ensure we process
- # change logging, webhooks, etc.
+ # 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 change_logging(request):
+ with event_tracking(request):
_run_script()
else:
_run_script()
diff --git a/netbox/extras/search.py b/netbox/extras/search.py
index da4aa1c84..fff59fa77 100644
--- a/netbox/extras/search.py
+++ b/netbox/extras/search.py
@@ -9,3 +9,14 @@ class JournalEntryIndex(SearchIndex):
('comments', 5000),
)
category = 'Journal'
+ display_attrs = ('kind', 'created_by')
+
+
+@register_search
+class WebhookEntryIndex(SearchIndex):
+ model = models.Webhook
+ fields = (
+ ('name', 100),
+ ('description', 500),
+ )
+ display_attrs = ('description',)
diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py
index d6550309f..798a9f442 100644
--- a/netbox/extras/signals.py
+++ b/netbox/extras/signals.py
@@ -2,25 +2,31 @@ import importlib
import logging
from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ValidationError
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.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.validators import CustomValidator
from netbox.config import get_config
-from netbox.context import current_request, webhooks_queue
+from netbox.context import current_request, events_queue
from netbox.signals import post_clean
from utilities.exceptions import AbortRequest
from .choices import ObjectChangeActionChoices
-from .models import ConfigRevision, CustomField, ObjectChange, TaggedItem
-from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
+from .events import enqueue_object, get_snapshots, serialize_for_event
+from .models import CustomField, ObjectChange, TaggedItem
#
# Change logging/webhooks
#
-# Define a custom signal that can be sent to clear any queued webhooks
-clear_webhooks = Signal()
+# Define a custom signal that can be sent to clear any queued events
+clear_events = Signal()
def is_same_object(instance, webhook_data, request_id):
@@ -63,30 +69,30 @@ def handle_changed_object(sender, instance, **kwargs):
return
# Record an ObjectChange if applicable
- if hasattr(instance, 'to_objectchange'):
- if m2m_changed:
- ObjectChange.objects.filter(
- changed_object_type=ContentType.objects.get_for_model(instance),
- changed_object_id=instance.pk,
- request_id=request.id
- ).update(
- postchange_data=instance.to_objectchange(action).postchange_data
- )
- else:
- objectchange = instance.to_objectchange(action)
+ if m2m_changed:
+ ObjectChange.objects.filter(
+ changed_object_type=ContentType.objects.get_for_model(instance),
+ changed_object_id=instance.pk,
+ request_id=request.id
+ ).update(
+ postchange_data=instance.to_objectchange(action).postchange_data
+ )
+ else:
+ objectchange = instance.to_objectchange(action)
+ if objectchange and objectchange.has_changes:
objectchange.user = request.user
objectchange.request_id = request.id
objectchange.save()
# If this is an M2M change, update the previously queued webhook (from post_save)
- queue = webhooks_queue.get()
+ queue = events_queue.get()
if m2m_changed and queue and is_same_object(instance, queue[-1], request.id):
instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments
- queue[-1]['data'] = serialize_for_webhook(instance)
+ queue[-1]['data'] = serialize_for_event(instance)
queue[-1]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange']
else:
enqueue_object(queue, instance, request.user, request.id, action)
- webhooks_queue.set(queue)
+ events_queue.set(queue)
# Increment metric counters
if action == ObjectChangeActionChoices.ACTION_CREATE:
@@ -115,22 +121,22 @@ def handle_deleted_object(sender, instance, **kwargs):
objectchange.save()
# Enqueue webhooks
- queue = webhooks_queue.get()
+ queue = events_queue.get()
enqueue_object(queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
- webhooks_queue.set(queue)
+ events_queue.set(queue)
# Increment metric counters
model_deletes.labels(instance._meta.model_name).inc()
-@receiver(clear_webhooks)
-def clear_webhook_queue(sender, **kwargs):
+@receiver(clear_events)
+def clear_events_queue(sender, **kwargs):
"""
- Delete any queued webhooks (e.g. because of an aborted bulk transaction)
+ Delete any queued events (e.g. because of an aborted bulk transaction)
"""
- logger = logging.getLogger('webhooks')
- logger.info(f"Clearing {len(webhooks_queue.get())} queued webhooks ({sender})")
- webhooks_queue.set([])
+ logger = logging.getLogger('events')
+ logger.info(f"Clearing {len(events_queue.get())} queued events ({sender})")
+ events_queue.set([])
#
@@ -178,11 +184,7 @@ m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_type
# Custom validation
#
-@receiver(post_clean)
-def run_custom_validators(sender, instance, **kwargs):
- config = get_config()
- model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
- validators = config.CUSTOM_VALIDATORS.get(model_name, [])
+def run_validators(instance, validators):
for validator in validators:
@@ -198,16 +200,27 @@ def run_custom_validators(sender, instance, **kwargs):
validator(instance)
-#
-# Dynamic configuration
-#
+@receiver(post_clean)
+def run_save_validators(sender, instance, **kwargs):
+ model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
+ validators = get_config().CUSTOM_VALIDATORS.get(model_name, [])
-@receiver(post_save, sender=ConfigRevision)
-def update_config(sender, instance, **kwargs):
- """
- Update the cached NetBox configuration when a new ConfigRevision is created.
- """
- instance.activate()
+ run_validators(instance, validators)
+
+
+@receiver(pre_delete)
+def run_delete_validators(sender, instance, **kwargs):
+ 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
+ )
+ )
#
@@ -226,3 +239,25 @@ def validate_assigned_tags(sender, instance, action, model, pk_set, **kwargs):
for tag in model.objects.filter(pk__in=pk_set, object_types__isnull=False).prefetch_related('object_types'):
if ct not in tag.object_types.all():
raise AbortRequest(f"Tag {tag} cannot be assigned to {ct.model} objects.")
+
+
+#
+# Event rules
+#
+
+@receiver(job_start)
+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, content_types=sender.object_type)
+ process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_START, sender.data, sender.user.username)
+
+
+@receiver(job_end)
+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, content_types=sender.object_type)
+ process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_END, sender.data, sender.user.username)
diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py
index 9e14a2d27..8482c5e24 100644
--- a/netbox/extras/tables/tables.py
+++ b/netbox/extras/tables/tables.py
@@ -11,11 +11,11 @@ from .template_code import *
__all__ = (
'BookmarkTable',
'ConfigContextTable',
- 'ConfigRevisionTable',
'ConfigTemplateTable',
'CustomFieldChoiceSetTable',
'CustomFieldTable',
'CustomLinkTable',
+ 'EventRuleTable',
'ExportTemplateTable',
'ImageAttachmentTable',
'JournalEntryTable',
@@ -34,31 +34,6 @@ IMAGEATTACHMENT_IMAGE = '''
{% endif %}
'''
-REVISION_BUTTONS = """
-{% if not record.is_active %}
-
-
-
-{% endif %}
-"""
-
-
-class ConfigRevisionTable(NetBoxTable):
- is_active = columns.BooleanColumn(
- verbose_name=_('Is Active'),
- )
- actions = columns.ActionsColumn(
- actions=('delete',),
- extra_buttons=REVISION_BUTTONS
- )
-
- class Meta(NetBoxTable.Meta):
- model = ConfigRevision
- fields = (
- 'pk', 'id', 'is_active', 'created', 'comment',
- )
- default_columns = ('pk', 'id', 'is_active', 'created', 'comment')
-
class CustomFieldTable(NetBoxTable):
name = tables.Column(
@@ -71,8 +46,11 @@ class CustomFieldTable(NetBoxTable):
required = columns.BooleanColumn(
verbose_name=_('Required')
)
- ui_visibility = columns.ChoiceFieldColumn(
- verbose_name=_('UI Visibility')
+ ui_visible = columns.ChoiceFieldColumn(
+ verbose_name=_('Visible')
+ )
+ ui_editable = columns.ChoiceFieldColumn(
+ verbose_name=_('Editable')
)
description = columns.MarkdownColumn(
verbose_name=_('Description')
@@ -94,8 +72,8 @@ class CustomFieldTable(NetBoxTable):
model = CustomField
fields = (
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description',
- 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight', 'choice_set', 'choices',
- 'created', 'last_updated',
+ 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set',
+ 'choices', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
@@ -273,6 +251,36 @@ class WebhookTable(NetBoxTable):
verbose_name=_('Name'),
linkify=True
)
+ ssl_validation = columns.BooleanColumn(
+ verbose_name=_('SSL Validation')
+ )
+ tags = columns.TagColumn(
+ url_name='extras:webhook_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = Webhook
+ fields = (
+ 'pk', 'id', 'name', 'http_method', 'payload_url', 'http_content_type', 'secret', 'ssl_verification',
+ 'ca_file_path', 'description', 'tags', 'created', 'last_updated',
+ )
+ default_columns = (
+ 'pk', 'name', 'http_method', 'payload_url', 'description',
+ )
+
+
+class EventRuleTable(NetBoxTable):
+ name = tables.Column(
+ verbose_name=_('Name'),
+ linkify=True
+ )
+ action_type = tables.Column(
+ verbose_name=_('Type'),
+ )
+ action_object = tables.Column(
+ linkify=True,
+ verbose_name=_('Object'),
+ )
content_types = columns.ContentTypesColumn(
verbose_name=_('Content Types'),
)
@@ -294,23 +302,20 @@ class WebhookTable(NetBoxTable):
type_job_end = columns.BooleanColumn(
verbose_name=_('Job End')
)
- ssl_validation = columns.BooleanColumn(
- verbose_name=_('SSL Validation')
- )
tags = columns.TagColumn(
url_name='extras:webhook_list'
)
class Meta(NetBoxTable.Meta):
- model = Webhook
+ model = EventRule
fields = (
- 'pk', 'id', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete',
- 'type_job_start', 'type_job_end', 'http_method', 'payload_url', 'secret', 'ssl_validation', 'ca_file_path',
- 'tags', 'created', 'last_updated',
+ 'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'action_object', 'content_types',
+ 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'tags', 'created',
+ 'last_updated',
)
default_columns = (
- 'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'type_job_start',
- 'type_job_end', 'http_method', 'payload_url',
+ 'pk', 'name', 'enabled', 'action_type', 'action_object', 'content_types', 'type_create', 'type_update',
+ 'type_delete', 'type_job_start', 'type_job_end',
)
diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py
index 255457f21..93be2d2c4 100644
--- a/netbox/extras/tests/test_api.py
+++ b/netbox/extras/tests/test_api.py
@@ -8,6 +8,7 @@ from rest_framework import status
from core.choices import ManagedFileRootPathChoices
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
+from extras.choices import *
from extras.models import *
from extras.reports import Report
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
@@ -32,53 +33,119 @@ class WebhookTest(APIViewTestCases.APIViewTestCase):
brief_fields = ['display', 'id', 'name', 'url']
create_data = [
{
- 'content_types': ['dcim.device', 'dcim.devicetype'],
'name': 'Webhook 4',
- 'type_create': True,
'payload_url': 'http://example.com/?4',
},
{
- 'content_types': ['dcim.device', 'dcim.devicetype'],
'name': 'Webhook 5',
- 'type_update': True,
'payload_url': 'http://example.com/?5',
},
{
- 'content_types': ['dcim.device', 'dcim.devicetype'],
'name': 'Webhook 6',
- 'type_delete': True,
'payload_url': 'http://example.com/?6',
},
]
bulk_update_data = {
+ 'description': 'New description',
'ssl_verification': False,
}
@classmethod
def setUpTestData(cls):
- site_ct = ContentType.objects.get_for_model(Site)
- rack_ct = ContentType.objects.get_for_model(Rack)
webhooks = (
Webhook(
name='Webhook 1',
- type_create=True,
payload_url='http://example.com/?1',
),
Webhook(
name='Webhook 2',
- type_update=True,
payload_url='http://example.com/?1',
),
Webhook(
name='Webhook 3',
- type_delete=True,
payload_url='http://example.com/?1',
),
)
Webhook.objects.bulk_create(webhooks)
- for webhook in webhooks:
- webhook.content_types.add(site_ct, rack_ct)
+
+
+class EventRuleTest(APIViewTestCases.APIViewTestCase):
+ model = EventRule
+ brief_fields = ['display', 'id', 'name', 'url']
+ bulk_update_data = {
+ 'enabled': False,
+ 'description': 'New description',
+ }
+ update_data = {
+ 'name': 'Event Rule X',
+ 'enabled': False,
+ 'description': 'New description',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+ webhooks = (
+ Webhook(
+ name='Webhook 1',
+ payload_url='http://example.com/?1',
+ ),
+ Webhook(
+ name='Webhook 2',
+ payload_url='http://example.com/?1',
+ ),
+ Webhook(
+ name='Webhook 3',
+ payload_url='http://example.com/?1',
+ ),
+ Webhook(
+ name='Webhook 4',
+ payload_url='http://example.com/?1',
+ ),
+ Webhook(
+ name='Webhook 5',
+ payload_url='http://example.com/?1',
+ ),
+ Webhook(
+ name='Webhook 6',
+ payload_url='http://example.com/?1',
+ ),
+ )
+ 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.objects.bulk_create(event_rules)
+
+ cls.create_data = [
+ {
+ 'name': 'EventRule 4',
+ 'content_types': ['dcim.device', 'dcim.devicetype'],
+ 'type_create': True,
+ 'action_type': EventRuleActionChoices.WEBHOOK,
+ 'action_object_type': 'extras.webhook',
+ 'action_object_id': webhooks[3].pk,
+ },
+ {
+ 'name': 'EventRule 5',
+ 'content_types': ['dcim.device', 'dcim.devicetype'],
+ 'type_create': True,
+ 'action_type': EventRuleActionChoices.WEBHOOK,
+ 'action_object_type': 'extras.webhook',
+ 'action_object_id': webhooks[4].pk,
+ },
+ {
+ 'name': 'EventRule 6',
+ 'content_types': ['dcim.device', 'dcim.devicetype'],
+ 'type_create': True,
+ 'action_type': EventRuleActionChoices.WEBHOOK,
+ 'action_object_type': 'extras.webhook',
+ 'action_object_id': webhooks[5].pk,
+ },
+ ]
class CustomFieldTest(APIViewTestCases.APIViewTestCase):
diff --git a/netbox/extras/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py
index 34fd72b2b..e144c5dee 100644
--- a/netbox/extras/tests/test_changelog.py
+++ b/netbox/extras/tests/test_changelog.py
@@ -1,4 +1,5 @@
from django.contrib.contenttypes.models import ContentType
+from django.test import override_settings
from django.urls import reverse
from rest_framework import status
@@ -207,6 +208,66 @@ class ChangeLogViewTest(ModelViewTestCase):
self.assertEqual(objectchange.prechange_data['slug'], sites[0].slug)
self.assertEqual(objectchange.postchange_data, None)
+ @override_settings(CHANGELOG_SKIP_EMPTY_CHANGES=False)
+ def test_update_object_change(self):
+ # Create a Site
+ site = Site.objects.create(
+ name='Site 1',
+ slug='site-1',
+ status=SiteStatusChoices.STATUS_PLANNED,
+ custom_field_data={
+ 'cf1': None,
+ 'cf2': None
+ }
+ )
+
+ # Update it with the same field values
+ form_data = {
+ 'name': site.name,
+ 'slug': site.slug,
+ 'status': SiteStatusChoices.STATUS_PLANNED,
+ }
+ request = {
+ 'path': self._get_url('edit', instance=site),
+ 'data': post_data(form_data),
+ }
+ self.add_permissions('dcim.change_site', 'extras.view_tag')
+ response = self.client.post(**request)
+ self.assertHttpStatus(response, 302)
+
+ # Check that an ObjectChange record has been created
+ self.assertEqual(ObjectChange.objects.count(), 1)
+
+ @override_settings(CHANGELOG_SKIP_EMPTY_CHANGES=True)
+ def test_update_object_nochange(self):
+ # Create a Site
+ site = Site.objects.create(
+ name='Site 1',
+ slug='site-1',
+ status=SiteStatusChoices.STATUS_PLANNED,
+ custom_field_data={
+ 'cf1': None,
+ 'cf2': None
+ }
+ )
+
+ # Update it with the same field values
+ form_data = {
+ 'name': site.name,
+ 'slug': site.slug,
+ 'status': SiteStatusChoices.STATUS_PLANNED,
+ }
+ request = {
+ 'path': self._get_url('edit', instance=site),
+ 'data': post_data(form_data),
+ }
+ self.add_permissions('dcim.change_site', 'extras.view_tag')
+ response = self.client.post(**request)
+ self.assertHttpStatus(response, 302)
+
+ # Check that no ObjectChange records have been created
+ self.assertEqual(ObjectChange.objects.count(), 0)
+
class ChangeLogAPITest(APITestCase):
diff --git a/netbox/extras/tests/test_custom_validation.py b/netbox/extras/tests/test_custom_validation.py
new file mode 100644
index 000000000..e375b49f5
--- /dev/null
+++ b/netbox/extras/tests/test_custom_validation.py
@@ -0,0 +1,265 @@
+from django.test import TestCase
+from django.test import override_settings
+
+from circuits.api.serializers import ProviderSerializer
+from circuits.forms import ProviderForm
+from circuits.models import Provider
+from ipam.models import ASN, RIR
+from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
+from utilities.testing import APITestCase, ModelViewTestCase, create_tags, post_data
+
+
+class ModelFormCustomValidationTest(TestCase):
+
+ @override_settings(CUSTOM_VALIDATORS={
+ 'circuits.provider': [
+ {'tags': {'required': True}}
+ ]
+ })
+ def test_tags_validation(self):
+ """
+ Check that custom validation rules work for tag assignment.
+ """
+ data = {
+ 'name': 'Provider 1',
+ 'slug': 'provider-1',
+ }
+ form = ProviderForm(data)
+ self.assertFalse(form.is_valid())
+
+ tags = create_tags('Tag1', 'Tag2', 'Tag3')
+ data['tags'] = [tag.pk for tag in tags]
+ form = ProviderForm(data)
+ self.assertTrue(form.is_valid())
+
+ @override_settings(CUSTOM_VALIDATORS={
+ 'circuits.provider': [
+ {'asns': {'required': True}}
+ ]
+ })
+ def test_m2m_validation(self):
+ """
+ Check that custom validation rules work for many-to-many fields.
+ """
+ data = {
+ 'name': 'Provider 1',
+ 'slug': 'provider-1',
+ }
+ form = ProviderForm(data)
+ self.assertFalse(form.is_valid())
+
+ rir = RIR.objects.create(name='RIR 1', slug='rir-1')
+ asns = ASN.objects.bulk_create((
+ ASN(rir=rir, asn=65001),
+ ASN(rir=rir, asn=65002),
+ ASN(rir=rir, asn=65003),
+ ))
+ data['asns'] = [asn.pk for asn in asns]
+ form = ProviderForm(data)
+ self.assertTrue(form.is_valid())
+
+
+class BulkEditCustomValidationTest(ModelViewTestCase):
+ model = Provider
+
+ @classmethod
+ def setUpTestData(cls):
+ rir = RIR.objects.create(name='RIR 1', slug='rir-1')
+ asns = ASN.objects.bulk_create((
+ ASN(rir=rir, asn=65001),
+ ASN(rir=rir, asn=65002),
+ ASN(rir=rir, asn=65003),
+ ))
+
+ providers = (
+ Provider(name='Provider 1', slug='provider-1'),
+ Provider(name='Provider 2', slug='provider-2'),
+ Provider(name='Provider 3', slug='provider-3'),
+ )
+ Provider.objects.bulk_create(providers)
+ for provider in providers:
+ provider.asns.set(asns)
+
+ @override_settings(CUSTOM_VALIDATORS={
+ 'circuits.provider': [
+ {'asns': {'required': True}}
+ ]
+ })
+ def test_bulk_edit_without_m2m(self):
+ """
+ Check that custom validation rules do not interfere with bulk editing.
+ """
+ data = {
+ 'pk': list(Provider.objects.values_list('pk', flat=True)),
+ '_apply': '',
+ 'description': 'New description',
+ }
+ self.add_permissions(
+ 'circuits.view_provider',
+ 'circuits.change_provider',
+ )
+
+ # Bulk edit the description without changing ASN assignments
+ request = {
+ 'path': self._get_url('bulk_edit'),
+ 'data': post_data(data),
+ }
+ response = self.client.post(**request)
+ self.assertHttpStatus(response, 302)
+ self.assertEqual(
+ Provider.objects.filter(description=data['description']).count(),
+ len(data['pk'])
+ )
+
+ @override_settings(CUSTOM_VALIDATORS={
+ 'circuits.provider': [
+ {'asns': {'required': True}}
+ ]
+ })
+ def test_bulk_edit_m2m(self):
+ """
+ Test that custom validation rules are enforced during bulk editing.
+ """
+ data = {
+ 'pk': list(Provider.objects.values_list('pk', flat=True)),
+ '_apply': '',
+ 'description': 'New description',
+ }
+ self.add_permissions(
+ 'circuits.view_provider',
+ 'circuits.change_provider',
+ 'ipam.view_asn',
+ )
+
+ # Change the ASN assignments
+ asn = ASN.objects.first()
+ data['asns'] = [asn.pk]
+ request = {
+ 'path': self._get_url('bulk_edit'),
+ 'data': post_data(data),
+ }
+ response = self.client.post(**request)
+ self.assertHttpStatus(response, 302)
+ for provider in Provider.objects.all():
+ self.assertEqual(len(provider.asns.all()), 1)
+
+ # Attempt to remove the ASN assignments
+ data.pop('asns')
+ data['_nullify'] = 'asns'
+ request = {
+ 'path': self._get_url('bulk_edit'),
+ 'data': post_data(data),
+ }
+ response = self.client.post(**request)
+ self.assertHttpStatus(response, 200)
+ for provider in Provider.objects.all():
+ self.assertTrue(provider.asns.exists())
+
+
+class BulkImportCustomValidationTest(ModelViewTestCase):
+ model = Provider
+
+ @classmethod
+ def setUpTestData(cls):
+ create_tags('Tag1', 'Tag2', 'Tag3')
+
+ @override_settings(CUSTOM_VALIDATORS={
+ 'circuits.provider': [
+ {'tags': {'required': True}}
+ ]
+ })
+ def test_bulk_import_invalid(self):
+ """
+ Test that custom validation rules are enforced during bulk import.
+ """
+ csv_data = (
+ "name,slug",
+ "Provider 1,provider-1",
+ "Provider 2,provider-2",
+ "Provider 3,provider-3",
+ )
+ data = {
+ 'data': '\n'.join(csv_data),
+ 'format': ImportFormatChoices.CSV,
+ 'csv_delimiter': CSVDelimiterChoices.COMMA,
+ }
+ self.add_permissions(
+ 'circuits.view_provider',
+ 'circuits.add_provider',
+ 'extras.view_tag',
+ )
+
+ # Attempt to import providers without tags
+ request = {
+ 'path': self._get_url('import'),
+ 'data': post_data(data),
+ }
+ response = self.client.post(**request)
+ self.assertHttpStatus(response, 200)
+ self.assertFalse(Provider.objects.exists())
+
+ # Import providers successfully with tag assignments
+ csv_data = (
+ "name,slug,tags",
+ "Provider 1,provider-1,tag1",
+ "Provider 2,provider-2,tag2",
+ "Provider 3,provider-3,tag3",
+ )
+ data['data'] = '\n'.join(csv_data)
+ request = {
+ 'path': self._get_url('import'),
+ 'data': post_data(data),
+ }
+ response = self.client.post(**request)
+ self.assertHttpStatus(response, 302)
+ self.assertTrue(Provider.objects.exists())
+
+
+class APISerializerCustomValidationTest(APITestCase):
+
+ @override_settings(CUSTOM_VALIDATORS={
+ 'circuits.provider': [
+ {'tags': {'required': True}}
+ ]
+ })
+ def test_tags_validation(self):
+ """
+ Check that custom validation rules work for tag assignment.
+ """
+ data = {
+ 'name': 'Provider 1',
+ 'slug': 'provider-1',
+ }
+ serializer = ProviderSerializer(data=data)
+ self.assertFalse(serializer.is_valid())
+
+ tags = create_tags('Tag1', 'Tag2', 'Tag3')
+ data['tags'] = [tag.pk for tag in tags]
+ serializer = ProviderSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+
+ @override_settings(CUSTOM_VALIDATORS={
+ 'circuits.provider': [
+ {'asns': {'required': True}}
+ ]
+ })
+ def test_m2m_validation(self):
+ """
+ Check that custom validation rules work for many-to-many fields.
+ """
+ data = {
+ 'name': 'Provider 1',
+ 'slug': 'provider-1',
+ }
+ serializer = ProviderSerializer(data=data)
+ self.assertFalse(serializer.is_valid())
+
+ rir = RIR.objects.create(name='RIR 1', slug='rir-1')
+ asns = ASN.objects.bulk_create((
+ ASN(rir=rir, asn=65001),
+ ASN(rir=rir, asn=65002),
+ ASN(rir=rir, asn=65003),
+ ))
+ data['asns'] = [asn.pk for asn in asns]
+ serializer = ProviderSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py
index 019aef235..574452a81 100644
--- a/netbox/extras/tests/test_customfields.py
+++ b/netbox/extras/tests/test_customfields.py
@@ -12,6 +12,7 @@ from dcim.models import Manufacturer, Rack, Site
from extras.choices import *
from extras.models import CustomField, CustomFieldChoiceSet
from ipam.models import VLAN
+from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
from utilities.testing import APITestCase, TestCase
from virtualization.models import VirtualMachine
@@ -427,6 +428,97 @@ class CustomFieldTest(TestCase):
self.assertNotIn('field1', site.custom_field_data)
self.assertEqual(site.custom_field_data['field2'], FIELD_DATA)
+ def test_default_value_validation(self):
+ choiceset = CustomFieldChoiceSet.objects.create(
+ name="Test Choice Set",
+ extra_choices=(
+ ('choice1', 'Choice 1'),
+ ('choice2', 'Choice 2'),
+ )
+ )
+ site = Site.objects.create(name='Site 1', slug='site-1')
+ object_type = ContentType.objects.get_for_model(Site)
+
+ # Text
+ CustomField(name='test', type='text', required=True, default="Default text").full_clean()
+
+ # Integer
+ CustomField(name='test', type='integer', required=True, default=1).full_clean()
+ with self.assertRaises(ValidationError):
+ CustomField(name='test', type='integer', required=True, default='xxx').full_clean()
+
+ # Boolean
+ CustomField(name='test', type='boolean', required=True, default=True).full_clean()
+ with self.assertRaises(ValidationError):
+ CustomField(name='test', type='boolean', required=True, default='xxx').full_clean()
+
+ # Date
+ CustomField(name='test', type='date', required=True, default="2023-02-25").full_clean()
+ with self.assertRaises(ValidationError):
+ CustomField(name='test', type='date', required=True, default='xxx').full_clean()
+
+ # Datetime
+ CustomField(name='test', type='datetime', required=True, default="2023-02-25 02:02:02").full_clean()
+ with self.assertRaises(ValidationError):
+ CustomField(name='test', type='datetime', required=True, default='xxx').full_clean()
+
+ # URL
+ CustomField(name='test', type='url', required=True, default="https://www.netbox.dev").full_clean()
+
+ # JSON
+ CustomField(name='test', type='json', required=True, default='{"test": "object"}').full_clean()
+
+ # Selection
+ CustomField(name='test', type='select', required=True, choice_set=choiceset, default='choice1').full_clean()
+ with self.assertRaises(ValidationError):
+ CustomField(name='test', type='select', required=True, choice_set=choiceset, default='xxx').full_clean()
+
+ # Multi-select
+ CustomField(
+ name='test',
+ type='multiselect',
+ required=True,
+ choice_set=choiceset,
+ default=['choice1'] # Single default choice
+ ).full_clean()
+ CustomField(
+ name='test',
+ type='multiselect',
+ required=True,
+ choice_set=choiceset,
+ default=['choice1', 'choice2'] # Multiple default choices
+ ).full_clean()
+ with self.assertRaises(ValidationError):
+ CustomField(
+ name='test',
+ type='multiselect',
+ required=True,
+ choice_set=choiceset,
+ default=['xxx']
+ ).full_clean()
+
+ # Object
+ CustomField(name='test', type='object', required=True, object_type=object_type, default=site.pk).full_clean()
+ with self.assertRaises(ValidationError):
+ CustomField(name='test', type='object', required=True, object_type=object_type, default="xxx").full_clean()
+
+ # Multi-object
+ CustomField(
+ name='test',
+ type='multiobject',
+ required=True,
+ object_type=object_type,
+ default=[site.pk]
+ ).full_clean()
+ with self.assertRaises(ValidationError):
+ CustomField(
+ name='test',
+ type='multiobject',
+ required=True,
+ object_type=object_type,
+ default=["xxx"]
+ ).full_clean()
+
class CustomFieldManagerTest(TestCase):
@@ -1085,7 +1177,11 @@ class CustomFieldImportTest(TestCase):
)
csv_data = '\n'.join(','.join(row) for row in data)
- response = self.client.post(reverse('dcim:site_import'), {'data': csv_data, 'format': 'csv'})
+ response = self.client.post(reverse('dcim:site_import'), {
+ 'data': csv_data,
+ 'format': ImportFormatChoices.CSV,
+ 'csv_delimiter': CSVDelimiterChoices.AUTO,
+ })
self.assertEqual(response.status_code, 302)
self.assertEqual(Site.objects.count(), 3)
@@ -1233,7 +1329,7 @@ class CustomFieldModelFilterTest(TestCase):
choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1',
- extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'), ('x', 'X'))
+ extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
)
# Integer filtering
@@ -1339,7 +1435,7 @@ class CustomFieldModelFilterTest(TestCase):
'cf7': 'http://a.example.com',
'cf8': 'http://a.example.com',
'cf9': 'A',
- 'cf10': ['A', 'X'],
+ 'cf10': ['A', 'B'],
'cf11': manufacturers[0].pk,
'cf12': [manufacturers[0].pk, manufacturers[3].pk],
}),
@@ -1353,7 +1449,7 @@ class CustomFieldModelFilterTest(TestCase):
'cf7': 'http://b.example.com',
'cf8': 'http://b.example.com',
'cf9': 'B',
- 'cf10': ['B', 'X'],
+ 'cf10': ['B', 'C'],
'cf11': manufacturers[1].pk,
'cf12': [manufacturers[1].pk, manufacturers[3].pk],
}),
@@ -1367,7 +1463,7 @@ class CustomFieldModelFilterTest(TestCase):
'cf7': 'http://c.example.com',
'cf8': 'http://c.example.com',
'cf9': 'C',
- 'cf10': ['C', 'X'],
+ 'cf10': None,
'cf11': manufacturers[2].pk,
'cf12': [manufacturers[2].pk, manufacturers[3].pk],
}),
@@ -1435,8 +1531,9 @@ class CustomFieldModelFilterTest(TestCase):
self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
def test_filter_multiselect(self):
- self.assertEqual(self.filterset({'cf_cf10': ['A', 'B']}, self.queryset).qs.count(), 2)
- self.assertEqual(self.filterset({'cf_cf10': ['X']}, self.queryset).qs.count(), 3)
+ self.assertEqual(self.filterset({'cf_cf10': ['A']}, self.queryset).qs.count(), 1)
+ self.assertEqual(self.filterset({'cf_cf10': ['A', 'C']}, self.queryset).qs.count(), 2)
+ self.assertEqual(self.filterset({'cf_cf10': ['null']}, self.queryset).qs.count(), 1)
def test_filter_object(self):
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)
diff --git a/netbox/extras/tests/test_customvalidator.py b/netbox/extras/tests/test_customvalidation.py
similarity index 64%
rename from netbox/extras/tests/test_customvalidator.py
rename to netbox/extras/tests/test_customvalidation.py
index 0fe507b67..d74ad599b 100644
--- a/netbox/extras/tests/test_customvalidator.py
+++ b/netbox/extras/tests/test_customvalidation.py
@@ -1,10 +1,13 @@
from django.conf import settings
from django.core.exceptions import ValidationError
+from django.db import transaction
from django.test import TestCase, override_settings
from ipam.models import ASN, RIR
+from dcim.choices import SiteStatusChoices
from dcim.models import Site
from extras.validators import CustomValidator
+from utilities.exceptions import AbortRequest
class MyValidator(CustomValidator):
@@ -14,6 +17,20 @@ class MyValidator(CustomValidator):
self.fail("Name must be foo!")
+eq_validator = CustomValidator({
+ 'asn': {
+ 'eq': 100
+ }
+})
+
+
+neq_validator = CustomValidator({
+ 'asn': {
+ 'neq': 100
+ }
+})
+
+
min_validator = CustomValidator({
'asn': {
'min': 65000
@@ -77,6 +94,18 @@ class CustomValidatorTest(TestCase):
validator = settings.CUSTOM_VALIDATORS['ipam.asn'][0]
self.assertIsInstance(validator, CustomValidator)
+ @override_settings(CUSTOM_VALIDATORS={'ipam.asn': [eq_validator]})
+ def test_eq(self):
+ ASN(asn=100, rir=RIR.objects.first()).clean()
+ with self.assertRaises(ValidationError):
+ ASN(asn=99, rir=RIR.objects.first()).clean()
+
+ @override_settings(CUSTOM_VALIDATORS={'ipam.asn': [neq_validator]})
+ def test_neq(self):
+ ASN(asn=99, rir=RIR.objects.first()).clean()
+ with self.assertRaises(ValidationError):
+ ASN(asn=100, rir=RIR.objects.first()).clean()
+
@override_settings(CUSTOM_VALIDATORS={'ipam.asn': [min_validator]})
def test_min(self):
with self.assertRaises(ValidationError):
@@ -147,7 +176,7 @@ class CustomValidatorConfigTest(TestCase):
@override_settings(
CUSTOM_VALIDATORS={
'dcim.site': (
- 'extras.tests.test_customvalidator.MyValidator',
+ 'extras.tests.test_customvalidation.MyValidator',
)
}
)
@@ -159,3 +188,62 @@ class CustomValidatorConfigTest(TestCase):
Site(name='foo', slug='foo').clean()
with self.assertRaises(ValidationError):
Site(name='bar', slug='bar').clean()
+
+
+class ProtectionRulesConfigTest(TestCase):
+
+ @override_settings(
+ PROTECTION_RULES={
+ 'dcim.site': [
+ {'status': {'eq': SiteStatusChoices.STATUS_DECOMMISSIONING}}
+ ]
+ }
+ )
+ def test_plain_data(self):
+ """
+ Test custom validator configuration using plain data (as opposed to a CustomValidator
+ class)
+ """
+ # Create a site with a protected status
+ site = Site(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE)
+ site.save()
+
+ # Try to delete it
+ with self.assertRaises(AbortRequest):
+ with transaction.atomic():
+ site.delete()
+
+ # Change its status to an allowed value
+ site.status = SiteStatusChoices.STATUS_DECOMMISSIONING
+ site.save()
+
+ # Deletion should now succeed
+ site.delete()
+
+ @override_settings(
+ PROTECTION_RULES={
+ 'dcim.site': (
+ 'extras.tests.test_customvalidation.MyValidator',
+ )
+ }
+ )
+ def test_dotted_path(self):
+ """
+ Test custom validator configuration using a dotted path (string) reference to a
+ CustomValidator class.
+ """
+ # Create a site with a protected name
+ site = Site(name='bar', slug='bar')
+ site.save()
+
+ # Try to delete it
+ with self.assertRaises(AbortRequest):
+ with transaction.atomic():
+ site.delete()
+
+ # Change the name to an allowed value
+ site.name = site.slug = 'foo'
+ site.save()
+
+ # Deletion should now succeed
+ site.delete()
diff --git a/netbox/extras/tests/test_webhooks.py b/netbox/extras/tests/test_event_rules.py
similarity index 72%
rename from netbox/extras/tests/test_webhooks.py
rename to netbox/extras/tests/test_event_rules.py
index ef7637765..549c33478 100644
--- a/netbox/extras/tests/test_webhooks.py
+++ b/netbox/extras/tests/test_event_rules.py
@@ -3,22 +3,21 @@ import uuid
from unittest.mock import patch
import django_rq
+from dcim.choices import SiteStatusChoices
+from dcim.models import Site
from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponse
from django.urls import reverse
+from extras.choices import EventRuleActionChoices, ObjectChangeActionChoices
+from extras.events import enqueue_object, flush_events, serialize_for_event
+from extras.models import EventRule, Tag, Webhook
+from extras.webhooks import generate_signature, send_webhook
from requests import Session
from rest_framework import status
-
-from dcim.choices import SiteStatusChoices
-from dcim.models import Site
-from extras.choices import ObjectChangeActionChoices
-from extras.models import Tag, Webhook
-from extras.webhooks import enqueue_object, flush_webhooks, generate_signature, serialize_for_webhook
-from extras.webhooks_worker import eval_conditions, process_webhook
from utilities.testing import APITestCase
-class WebhookTest(APITestCase):
+class EventRuleTest(APITestCase):
def setUp(self):
super().setUp()
@@ -35,12 +34,37 @@ class WebhookTest(APITestCase):
DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING'
webhooks = Webhook.objects.bulk_create((
- Webhook(name='Webhook 1', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers='X-Foo: Bar'),
- Webhook(name='Webhook 2', type_update=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET),
- Webhook(name='Webhook 3', type_delete=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET),
+ Webhook(name='Webhook 1', payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers='X-Foo: Bar'),
+ Webhook(name='Webhook 2', payload_url=DUMMY_URL, secret=DUMMY_SECRET),
+ Webhook(name='Webhook 3', payload_url=DUMMY_URL, secret=DUMMY_SECRET),
))
- for webhook in webhooks:
- webhook.content_types.set([site_ct])
+
+ ct = ContentType.objects.get(app_label='extras', model='webhook')
+ event_rules = EventRule.objects.bulk_create((
+ EventRule(
+ name='Webhook Event 1',
+ type_create=True,
+ action_type=EventRuleActionChoices.WEBHOOK,
+ action_object_type=ct,
+ action_object_id=webhooks[0].id
+ ),
+ EventRule(
+ name='Webhook Event 2',
+ type_update=True,
+ action_type=EventRuleActionChoices.WEBHOOK,
+ action_object_type=ct,
+ action_object_id=webhooks[0].id
+ ),
+ EventRule(
+ name='Webhook Event 3',
+ type_delete=True,
+ action_type=EventRuleActionChoices.WEBHOOK,
+ action_object_type=ct,
+ action_object_id=webhooks[0].id
+ ),
+ ))
+ for event_rule in event_rules:
+ event_rule.content_types.set([site_ct])
Tag.objects.bulk_create((
Tag(name='Foo', slug='foo'),
@@ -48,7 +72,42 @@ class WebhookTest(APITestCase):
Tag(name='Baz', slug='baz'),
))
- def test_enqueue_webhook_create(self):
+ def test_eventrule_conditions(self):
+ """
+ Test evaluation of EventRule conditions.
+ """
+ event_rule = EventRule(
+ name='Event Rule 1',
+ type_create=True,
+ type_update=True,
+ conditions={
+ 'and': [
+ {
+ 'attr': 'status.value',
+ 'value': 'active',
+ }
+ ]
+ }
+ )
+
+ # Create a Site to evaluate
+ site = Site.objects.create(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_STAGING)
+ data = serialize_for_event(site)
+
+ # Evaluate the conditions (status='staging')
+ self.assertFalse(event_rule.eval_conditions(data))
+
+ # Change the site's status
+ site.status = SiteStatusChoices.STATUS_ACTIVE
+ data = serialize_for_event(site)
+
+ # Evaluate the conditions (status='active')
+ self.assertTrue(event_rule.eval_conditions(data))
+
+ def test_single_create_process_eventrule(self):
+ """
+ Check that creating an object with an applicable EventRule queues a background task for the rule's action.
+ """
# Create an object via the REST API
data = {
'name': 'Site 1',
@@ -65,10 +124,10 @@ class WebhookTest(APITestCase):
self.assertEqual(Site.objects.count(), 1)
self.assertEqual(Site.objects.first().tags.count(), 2)
- # Verify that a job was queued for the object creation webhook
+ # 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['webhook'], Webhook.objects.get(type_create=True))
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_create=True))
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_CREATE)
self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], response.data['id'])
@@ -76,7 +135,11 @@ class WebhookTest(APITestCase):
self.assertEqual(job.kwargs['snapshots']['postchange']['name'], 'Site 1')
self.assertEqual(job.kwargs['snapshots']['postchange']['tags'], ['Bar', 'Foo'])
- def test_enqueue_webhook_bulk_create(self):
+ def test_bulk_create_process_eventrule(self):
+ """
+ Check that bulk creating multiple objects with an applicable EventRule queues a background task for each
+ new object.
+ """
# Create multiple objects via the REST API
data = [
{
@@ -111,10 +174,10 @@ class WebhookTest(APITestCase):
self.assertEqual(Site.objects.count(), 3)
self.assertEqual(Site.objects.first().tags.count(), 2)
- # Verify that a webhook was queued for each object
+ # 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['webhook'], Webhook.objects.get(type_create=True))
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_create=True))
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_CREATE)
self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], response.data[i]['id'])
@@ -122,7 +185,10 @@ class WebhookTest(APITestCase):
self.assertEqual(job.kwargs['snapshots']['postchange']['name'], response.data[i]['name'])
self.assertEqual(job.kwargs['snapshots']['postchange']['tags'], ['Bar', 'Foo'])
- def test_enqueue_webhook_update(self):
+ def test_single_update_process_eventrule(self):
+ """
+ Check that updating an object with an applicable EventRule queues a background task for the rule's action.
+ """
site = Site.objects.create(name='Site 1', slug='site-1')
site.tags.set(Tag.objects.filter(name__in=['Foo', 'Bar']))
@@ -139,10 +205,10 @@ class WebhookTest(APITestCase):
response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
- # Verify that a job was queued for the object update webhook
+ # 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['webhook'], Webhook.objects.get(type_update=True))
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_update=True))
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_UPDATE)
self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], site.pk)
@@ -152,7 +218,11 @@ class WebhookTest(APITestCase):
self.assertEqual(job.kwargs['snapshots']['postchange']['name'], 'Site X')
self.assertEqual(job.kwargs['snapshots']['postchange']['tags'], ['Baz'])
- def test_enqueue_webhook_bulk_update(self):
+ def test_bulk_update_process_eventrule(self):
+ """
+ Check that bulk updating multiple objects with an applicable EventRule queues a background task for each
+ updated object.
+ """
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
@@ -191,10 +261,10 @@ class WebhookTest(APITestCase):
response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
- # Verify that a job was queued for the object update webhook
+ # 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['webhook'], Webhook.objects.get(type_update=True))
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_update=True))
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_UPDATE)
self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], data[i]['id'])
@@ -204,7 +274,10 @@ class WebhookTest(APITestCase):
self.assertEqual(job.kwargs['snapshots']['postchange']['name'], response.data[i]['name'])
self.assertEqual(job.kwargs['snapshots']['postchange']['tags'], ['Baz'])
- def test_enqueue_webhook_delete(self):
+ def test_single_delete_process_eventrule(self):
+ """
+ Check that deleting an object with an applicable EventRule queues a background task for the rule's action.
+ """
site = Site.objects.create(name='Site 1', slug='site-1')
site.tags.set(Tag.objects.filter(name__in=['Foo', 'Bar']))
@@ -214,17 +287,21 @@ class WebhookTest(APITestCase):
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
- # Verify that a job was queued for the object update webhook
+ # 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['webhook'], Webhook.objects.get(type_delete=True))
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_delete=True))
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_DELETE)
self.assertEqual(job.kwargs['model_name'], 'site')
self.assertEqual(job.kwargs['data']['id'], site.pk)
self.assertEqual(job.kwargs['snapshots']['prechange']['name'], 'Site 1')
self.assertEqual(job.kwargs['snapshots']['prechange']['tags'], ['Bar', 'Foo'])
- def test_enqueue_webhook_bulk_delete(self):
+ def test_bulk_delete_process_eventrule(self):
+ """
+ Check that bulk deleting multiple objects with an applicable EventRule queues a background task for each
+ deleted object.
+ """
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
@@ -243,49 +320,17 @@ class WebhookTest(APITestCase):
response = self.client.delete(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
- # Verify that a job was queued for the object update webhook
+ # 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['webhook'], Webhook.objects.get(type_delete=True))
+ self.assertEqual(job.kwargs['event_rule'], EventRule.objects.get(type_delete=True))
self.assertEqual(job.kwargs['event'], ObjectChangeActionChoices.ACTION_DELETE)
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)
self.assertEqual(job.kwargs['snapshots']['prechange']['tags'], ['Bar', 'Foo'])
- def test_webhook_conditions(self):
- # Create a conditional Webhook
- webhook = Webhook(
- name='Conditional Webhook',
- type_create=True,
- type_update=True,
- payload_url='http://localhost:9000/',
- conditions={
- 'and': [
- {
- 'attr': 'status.value',
- 'value': 'active',
- }
- ]
- }
- )
-
- # Create a Site to evaluate
- site = Site.objects.create(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_STAGING)
- data = serialize_for_webhook(site)
-
- # Evaluate the conditions (status='staging')
- self.assertFalse(eval_conditions(webhook, data))
-
- # Change the site's status
- site.status = SiteStatusChoices.STATUS_ACTIVE
- data = serialize_for_webhook(site)
-
- # Evaluate the conditions (status='active')
- self.assertTrue(eval_conditions(webhook, data))
-
- def test_webhooks_worker(self):
-
+ def test_send_webhook(self):
request_id = uuid.uuid4()
def dummy_send(_, request, **kwargs):
@@ -293,7 +338,8 @@ class WebhookTest(APITestCase):
A dummy implementation of Session.send() to be used for testing.
Always returns a 200 HTTP response.
"""
- webhook = Webhook.objects.get(type_create=True)
+ event = EventRule.objects.get(type_create=True)
+ webhook = event.action_object
signature = generate_signature(request.body, webhook.secret)
# Validate the outgoing request headers
@@ -322,11 +368,11 @@ class WebhookTest(APITestCase):
request_id=request_id,
action=ObjectChangeActionChoices.ACTION_CREATE
)
- flush_webhooks(webhooks_queue)
+ flush_events(webhooks_queue)
# Retrieve the job from queue
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:
- process_webhook(**job.kwargs)
+ send_webhook(**job.kwargs)
diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py
index c558a0467..ef8aedcbd 100644
--- a/netbox/extras/tests/test_filtersets.py
+++ b/netbox/extras/tests/test_filtersets.py
@@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from circuits.models import Provider
+from core.choices import ManagedFileRootPathChoices
from dcim.filtersets import SiteFilterSet
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
from dcim.models import Location
@@ -40,7 +41,9 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
required=True,
weight=100,
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE,
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE
+ ui_visible=CustomFieldUIVisibleChoices.ALWAYS,
+ ui_editable=CustomFieldUIEditableChoices.YES,
+ description='foobar1'
),
CustomField(
name='Custom Field 2',
@@ -48,7 +51,9 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
required=False,
weight=200,
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT,
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY
+ ui_visible=CustomFieldUIVisibleChoices.IF_SET,
+ ui_editable=CustomFieldUIEditableChoices.NO,
+ description='foobar2'
),
CustomField(
name='Custom Field 3',
@@ -56,7 +61,9 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
required=False,
weight=300,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
+ ui_visible=CustomFieldUIVisibleChoices.HIDDEN,
+ ui_editable=CustomFieldUIEditableChoices.HIDDEN,
+ description='foobar3'
),
CustomField(
name='Custom Field 4',
@@ -64,7 +71,8 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
required=False,
weight=400,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN,
+ ui_visible=CustomFieldUIVisibleChoices.HIDDEN,
+ ui_editable=CustomFieldUIEditableChoices.HIDDEN,
choice_set=choice_sets[0]
),
CustomField(
@@ -73,7 +81,8 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
required=False,
weight=500,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN,
+ ui_visible=CustomFieldUIVisibleChoices.HIDDEN,
+ ui_editable=CustomFieldUIEditableChoices.HIDDEN,
choice_set=choice_sets[1]
),
)
@@ -84,6 +93,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
custom_fields[3].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device'))
custom_fields[4].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device'))
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Custom Field 1', 'Custom Field 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -106,8 +119,12 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
params = {'filter_logic': CustomFieldFilterLogicChoices.FILTER_LOOSE}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
- def test_ui_visibility(self):
- params = {'ui_visibility': CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE}
+ def test_ui_visible(self):
+ params = {'ui_visible': CustomFieldUIVisibleChoices.ALWAYS}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_ui_editable(self):
+ params = {'ui_editable': CustomFieldUIEditableChoices.YES}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_choice_set(self):
@@ -116,6 +133,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
params = {'choice_set_id': CustomFieldChoiceSet.objects.values_list('pk', flat=True)}
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)
+
class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests):
queryset = CustomFieldChoiceSet.objects.all()
@@ -124,12 +145,16 @@ class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests):
@classmethod
def setUpTestData(cls):
choice_sets = (
- CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['A', 'B', 'C']),
- CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['D', 'E', 'F']),
- CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['G', 'H', 'I']),
+ CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['A', 'B', 'C'], description='foobar1'),
+ CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['D', 'E', 'F'], description='foobar2'),
+ CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['G', 'H', 'I'], description='foobar3'),
)
CustomFieldChoiceSet.objects.bulk_create(choice_sets)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Choice Set 1', 'Choice Set 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -138,6 +163,10 @@ class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests):
params = {'choice': ['A', 'D']}
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)
+
class WebhookTestCase(TestCase, BaseFilterSetTests):
queryset = Webhook.objects.all()
@@ -150,82 +179,196 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
webhooks = (
Webhook(
name='Webhook 1',
- type_create=True,
- type_update=False,
- type_delete=False,
- type_job_start=False,
- type_job_end=False,
payload_url='http://example.com/?1',
- enabled=True,
http_method='GET',
ssl_verification=True,
+ description='foobar1'
),
Webhook(
name='Webhook 2',
- type_create=False,
- type_update=True,
- type_delete=False,
- type_job_start=False,
- type_job_end=False,
payload_url='http://example.com/?2',
- enabled=True,
http_method='POST',
ssl_verification=True,
+ description='foobar2'
),
Webhook(
name='Webhook 3',
- type_create=False,
- type_update=False,
- type_delete=True,
- type_job_start=False,
- type_job_end=False,
payload_url='http://example.com/?3',
- enabled=False,
http_method='PATCH',
ssl_verification=False,
+ description='foobar3'
),
Webhook(
name='Webhook 4',
- type_create=False,
- type_update=False,
- type_delete=False,
- type_job_start=True,
- type_job_end=False,
payload_url='http://example.com/?4',
- enabled=False,
http_method='PATCH',
ssl_verification=False,
),
Webhook(
name='Webhook 5',
- type_create=False,
- type_update=False,
- type_delete=False,
- type_job_start=False,
- type_job_end=True,
payload_url='http://example.com/?5',
- enabled=False,
http_method='PATCH',
ssl_verification=False,
),
)
Webhook.objects.bulk_create(webhooks)
- webhooks[0].content_types.add(content_types[0])
- webhooks[1].content_types.add(content_types[1])
- webhooks[2].content_types.add(content_types[2])
- webhooks[3].content_types.add(content_types[3])
- webhooks[4].content_types.add(content_types[4])
+
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_name(self):
params = {'name': ['Webhook 1', 'Webhook 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_http_method(self):
+ params = {'http_method': ['GET', 'POST']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_ssl_verification(self):
+ params = {'ssl_verification': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
+class EventRuleTestCase(TestCase, BaseFilterSetTests):
+ queryset = EventRule.objects.all()
+ filterset = EventRuleFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ content_types = ContentType.objects.filter(
+ model__in=['region', 'site', 'rack', 'location', 'device']
+ )
+
+ webhooks = (
+ Webhook(
+ name='Webhook 1',
+ payload_url='http://example.com/?1',
+ ),
+ Webhook(
+ name='Webhook 2',
+ payload_url='http://example.com/?2',
+ ),
+ Webhook(
+ name='Webhook 3',
+ payload_url='http://example.com/?3',
+ ),
+ )
+ Webhook.objects.bulk_create(webhooks)
+
+ scripts = (
+ ScriptModule(
+ file_root=ManagedFileRootPathChoices.SCRIPTS,
+ file_path='/var/tmp/script1.py'
+ ),
+ ScriptModule(
+ file_root=ManagedFileRootPathChoices.SCRIPTS,
+ file_path='/var/tmp/script2.py'
+ ),
+ )
+ ScriptModule.objects.bulk_create(scripts)
+
+ event_rules = (
+ EventRule(
+ 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,
+ action_type=EventRuleActionChoices.WEBHOOK,
+ description='foobar1'
+ ),
+ EventRule(
+ 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,
+ action_type=EventRuleActionChoices.WEBHOOK,
+ description='foobar2'
+ ),
+ EventRule(
+ 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,
+ action_type=EventRuleActionChoices.WEBHOOK,
+ description='foobar3'
+ ),
+ EventRule(
+ 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,
+ 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,
+ action_type=EventRuleActionChoices.SCRIPT,
+ ),
+ )
+ EventRule.objects.bulk_create(event_rules)
+ event_rules[0].content_types.add(content_types[0])
+ event_rules[1].content_types.add(content_types[1])
+ event_rules[2].content_types.add(content_types[2])
+ event_rules[3].content_types.add(content_types[3])
+ event_rules[4].content_types.add(content_types[4])
+
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_name(self):
+ params = {'name': ['Event Rule 1', 'Event Rule 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_content_types(self):
params = {'content_types': 'dcim.region'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'content_type_id': [ContentType.objects.get_for_model(Region).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ def test_action_type(self):
+ params = {'action_type': [EventRuleActionChoices.WEBHOOK]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ params = {'action_type': [EventRuleActionChoices.SCRIPT]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_enabled(self):
+ params = {'enabled': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ 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)
@@ -246,18 +389,6 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
params = {'type_job_end': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
- def test_enabled(self):
- params = {'enabled': True}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
- def test_http_method(self):
- params = {'http_method': ['GET', 'POST']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
- def test_ssl_verification(self):
- params = {'ssl_verification': True}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
class CustomLinkTestCase(TestCase, BaseFilterSetTests):
queryset = CustomLink.objects.all()
@@ -297,6 +428,10 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
for i, custom_link in enumerate(custom_links):
custom_link.content_types.set([content_types[i]])
+ def test_q(self):
+ params = {'q': 'Custom Link 1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Custom Link 1', 'Custom Link 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -347,7 +482,8 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
weight=100,
enabled=True,
shared=True,
- parameters={'status': ['active']}
+ parameters={'status': ['active']},
+ description='foobar1'
),
SavedFilter(
name='Saved Filter 2',
@@ -356,7 +492,8 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
weight=200,
enabled=True,
shared=True,
- parameters={'status': ['planned']}
+ parameters={'status': ['planned']},
+ description='foobar2'
),
SavedFilter(
name='Saved Filter 3',
@@ -365,13 +502,18 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
weight=300,
enabled=False,
shared=False,
- parameters={'status': ['retired']}
+ parameters={'status': ['retired']},
+ description='foobar3'
),
)
SavedFilter.objects.bulk_create(saved_filters)
for i, savedfilter in enumerate(saved_filters):
savedfilter.content_types.set([content_types[i]])
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Saved Filter 1', 'Saved Filter 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -380,6 +522,10 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
params = {'slug': ['saved-filter-1', 'saved-filter-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_content_types(self):
params = {'content_types': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@@ -423,8 +569,6 @@ class BookmarkTestCase(TestCase, BaseFilterSetTests):
@classmethod
def setUpTestData(cls):
- content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
-
users = (
User(username='User 1'),
User(username='User 2'),
@@ -505,6 +649,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
for i, et in enumerate(export_templates):
et.content_types.set([content_types[i]])
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Export Template 1', 'Export Template 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -578,6 +726,10 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
)
ImageAttachment.objects.bulk_create(image_attachments)
+ def test_q(self):
+ params = {'q': 'Attachment 1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Image Attachment 1', 'Image Attachment 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -630,41 +782,45 @@ class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests):
assigned_object=sites[0],
created_by=users[0],
kind=JournalEntryKindChoices.KIND_INFO,
- comments='New journal entry'
+ comments='foobar1'
),
JournalEntry(
assigned_object=sites[0],
created_by=users[1],
kind=JournalEntryKindChoices.KIND_SUCCESS,
- comments='New journal entry'
+ comments='foobar2'
),
JournalEntry(
assigned_object=sites[1],
created_by=users[2],
kind=JournalEntryKindChoices.KIND_WARNING,
- comments='New journal entry'
+ comments='foobar3'
),
JournalEntry(
assigned_object=racks[0],
created_by=users[0],
kind=JournalEntryKindChoices.KIND_INFO,
- comments='New journal entry'
+ comments='foobar4'
),
JournalEntry(
assigned_object=racks[0],
created_by=users[1],
kind=JournalEntryKindChoices.KIND_SUCCESS,
- comments='New journal entry'
+ comments='foobar5'
),
JournalEntry(
assigned_object=racks[1],
created_by=users[2],
kind=JournalEntryKindChoices.KIND_WARNING,
- comments='New journal entry'
+ comments='foobar6'
),
)
JournalEntry.objects.bulk_create(journal_entries)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_created_by(self):
users = User.objects.filter(username__in=['Alice', 'Bob'])
params = {'created_by': [users[0].username, users[1].username]}
@@ -800,9 +956,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
for i in range(0, 3):
is_active = bool(i % 2)
c = ConfigContext.objects.create(
- name='Config Context {}'.format(i + 1),
+ name=f"Config Context {i + 1}",
is_active=is_active,
- data='{"foo": 123}'
+ data='{"foo": 123}',
+ description=f"foobar{i + 1}"
)
c.regions.set([regions[i]])
c.site_groups.set([site_groups[i]])
@@ -818,6 +975,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
c.tenants.set([tenants[i]])
c.tags.set([tags[i]])
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Config Context 1', 'Config Context 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -828,6 +989,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'is_active': False}
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_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -929,6 +1094,10 @@ class ConfigTemplateTestCase(TestCase, BaseFilterSetTests):
)
ConfigTemplate.objects.bulk_create(config_templates)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Config Template 1', 'Config Template 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -965,6 +1134,10 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
site.tags.set([tags[0]])
provider.tags.set([tags[1]])
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Tag 1', 'Tag 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1076,6 +1249,10 @@ class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
)
ObjectChange.objects.bulk_create(object_changes)
+ def test_q(self):
+ params = {'q': 'Site 1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
def test_user(self):
params = {'user_id': User.objects.filter(username__in=['user1', 'user2']).values_list('pk', flat=True)}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
@@ -1109,11 +1286,13 @@ class ChangeLoggedFilterSetTestCase(TestCase):
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)
# Simulate *creation* changelog records for two of the sites
request_id = uuid.uuid4()
+ cls.create_request_id = request_id
objectchanges = (
ObjectChange(
changed_object_type=content_type,
@@ -1132,6 +1311,7 @@ class ChangeLoggedFilterSetTestCase(TestCase):
# Simulate *update* changelog records for two of the sites
request_id = uuid.uuid4()
+ cls.update_request_id = request_id
objectchanges = (
ObjectChange(
changed_object_type=content_type,
@@ -1148,14 +1328,36 @@ class ChangeLoggedFilterSetTestCase(TestCase):
)
ObjectChange.objects.bulk_create(objectchanges)
+ # Simulate *create* and *update* changelog records for two of the sites
+ request_id = uuid.uuid4()
+ cls.create_update_request_id = request_id
+ objectchanges = (
+ ObjectChange(
+ changed_object_type=content_type,
+ changed_object_id=sites[2].pk,
+ action=ObjectChangeActionChoices.ACTION_CREATE,
+ request_id=request_id
+ ),
+ ObjectChange(
+ changed_object_type=content_type,
+ changed_object_id=sites[3].pk,
+ action=ObjectChangeActionChoices.ACTION_UPDATE,
+ request_id=request_id
+ ),
+ )
+ ObjectChange.objects.bulk_create(objectchanges)
+
def test_created_by_request(self):
- request_id = ObjectChange.objects.filter(action=ObjectChangeActionChoices.ACTION_CREATE).first().request_id
- params = {'created_by_request': request_id}
+ params = {'created_by_request': self.create_request_id}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- self.assertEqual(self.queryset.count(), 3)
+ self.assertEqual(self.queryset.count(), 4)
def test_updated_by_request(self):
- request_id = ObjectChange.objects.filter(action=ObjectChangeActionChoices.ACTION_UPDATE).first().request_id
- params = {'updated_by_request': request_id}
+ params = {'updated_by_request': self.update_request_id}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- self.assertEqual(self.queryset.count(), 3)
+ self.assertEqual(self.queryset.count(), 4)
+
+ def test_modified_by_request(self):
+ 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)
diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py
index 01ef9a2a6..d720560e4 100644
--- a/netbox/extras/tests/test_views.py
+++ b/netbox/extras/tests/test_views.py
@@ -1,4 +1,3 @@
-import json
import urllib.parse
import uuid
@@ -6,12 +5,11 @@ from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
-from dcim.models import Site
+from dcim.models import DeviceType, Manufacturer, Site
from extras.choices import *
from extras.models import *
from utilities.testing import ViewTestCases, TestCase
-
User = get_user_model()
@@ -50,15 +48,16 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'default': None,
'weight': 200,
'required': True,
- 'ui_visibility': CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
+ 'ui_visible': CustomFieldUIVisibleChoices.ALWAYS,
+ 'ui_editable': CustomFieldUIEditableChoices.YES,
}
cls.csv_data = (
- 'name,label,type,content_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visibility',
- 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},read-write',
- 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,read-write',
- 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,read-write',
- 'field7,Field 7,object,dcim.site,dcim.region,100,4000,exact,,,,,read-write',
+ 'name,label,type,content_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable',
+ 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},always,yes',
+ 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes',
+ 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes',
+ 'field7,Field 7,object,dcim.site,dcim.region,100,4000,exact,,,,,always,yes',
)
cls.csv_update_data = (
@@ -93,19 +92,24 @@ class CustomFieldChoiceSetTestCase(ViewTestCases.PrimaryObjectViewTestCase):
name='Choice Set 3',
extra_choices=(('C1', 'Choice 1'), ('C2', 'Choice 2'), ('C3', 'Choice 3'))
),
+ CustomFieldChoiceSet(
+ name='Choice Set 4',
+ extra_choices=(('D1', 'Choice 1'), ('D2', 'Choice 2'), ('D3', 'Choice 3'))
+ ),
)
CustomFieldChoiceSet.objects.bulk_create(choice_sets)
cls.form_data = {
'name': 'Choice Set X',
- 'extra_choices': '\n'.join(['X1,Choice 1', 'X2,Choice 2', 'X3,Choice 3'])
+ 'extra_choices': '\n'.join(['X1:Choice 1', 'X2:Choice 2', 'X3:Choice 3'])
}
cls.csv_data = (
'name,extra_choices',
- 'Choice Set 4,"D1,D2,D3"',
- 'Choice Set 5,"E1,E2,E3"',
- 'Choice Set 6,"F1,F2,F3"',
+ 'Choice Set 5,"D1,D2,D3"',
+ 'Choice Set 6,"E1,E2,E3"',
+ 'Choice Set 7,"F1,F2,F3"',
+ 'Choice Set 8,"F1:L1,F2:L2,F3:L3"',
)
cls.csv_update_data = (
@@ -113,12 +117,20 @@ class CustomFieldChoiceSetTestCase(ViewTestCases.PrimaryObjectViewTestCase):
f'{choice_sets[0].pk},"A,B,C"',
f'{choice_sets[1].pk},"A,B,C"',
f'{choice_sets[2].pk},"A,B,C"',
+ f'{choice_sets[3].pk},"A:L1,B:L2,C:L3"',
)
cls.bulk_edit_data = {
'description': 'New description',
}
+ # This is here as extra_choices field splits on colon, but is returned
+ # from DB as comma separated.
+ def assertInstanceEqual(self, instance, data, exclude=None, api=False):
+ if 'extra_choices' in data:
+ data['extra_choices'] = data['extra_choices'].replace(':', ',')
+ return super().assertInstanceEqual(instance, data, exclude, api)
+
class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = CustomLink
@@ -335,48 +347,94 @@ class WebhookTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@classmethod
def setUpTestData(cls):
- site_ct = ContentType.objects.get_for_model(Site)
webhooks = (
- Webhook(name='Webhook 1', payload_url='http://example.com/?1', type_create=True, http_method='POST'),
- Webhook(name='Webhook 2', payload_url='http://example.com/?2', type_create=True, http_method='POST'),
- Webhook(name='Webhook 3', payload_url='http://example.com/?3', type_create=True, http_method='POST'),
+ Webhook(name='Webhook 1', payload_url='http://example.com/?1', http_method='POST'),
+ Webhook(name='Webhook 2', payload_url='http://example.com/?2', http_method='POST'),
+ Webhook(name='Webhook 3', payload_url='http://example.com/?3', http_method='POST'),
)
for webhook in webhooks:
webhook.save()
- webhook.content_types.add(site_ct)
cls.form_data = {
'name': 'Webhook X',
+ 'payload_url': 'http://example.com/?x',
+ 'http_method': 'GET',
+ 'http_content_type': 'application/foo',
+ 'description': 'My webhook',
+ }
+
+ cls.csv_data = (
+ "name,payload_url,http_method,http_content_type,description",
+ "Webhook 4,http://example.com/?4,GET,application/json,Foo",
+ "Webhook 5,http://example.com/?5,GET,application/json,Bar",
+ "Webhook 6,http://example.com/?6,GET,application/json,Baz",
+ )
+
+ cls.csv_update_data = (
+ "id,name,description",
+ f"{webhooks[0].pk},Webhook 7,Foo",
+ f"{webhooks[1].pk},Webhook 8,Bar",
+ f"{webhooks[2].pk},Webhook 9,Baz",
+ )
+
+ cls.bulk_edit_data = {
+ 'http_method': 'GET',
+ }
+
+
+class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = EventRule
+
+ @classmethod
+ def setUpTestData(cls):
+
+ webhooks = (
+ Webhook(name='Webhook 1', payload_url='http://example.com/?1', http_method='POST'),
+ Webhook(name='Webhook 2', payload_url='http://example.com/?2', http_method='POST'),
+ Webhook(name='Webhook 3', payload_url='http://example.com/?3', http_method='POST'),
+ )
+ for webhook in webhooks:
+ webhook.save()
+
+ site_ct = ContentType.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]),
+ )
+ for event in event_rules:
+ event.save()
+ event.content_types.add(site_ct)
+
+ webhook_ct = ContentType.objects.get_for_model(Webhook)
+ cls.form_data = {
+ 'name': 'Event X',
'content_types': [site_ct.pk],
'type_create': False,
'type_update': True,
'type_delete': True,
- 'payload_url': 'http://example.com/?x',
- 'http_method': 'GET',
- 'http_content_type': 'application/foo',
'conditions': None,
+ 'action_type': 'webhook',
+ 'action_object_type': webhook_ct.pk,
+ 'action_object_id': webhooks[0].pk,
+ 'action_choice': webhooks[0],
+ 'description': 'New description',
}
cls.csv_data = (
- "name,content_types,type_create,payload_url,http_method,http_content_type",
- "Webhook 4,dcim.site,True,http://example.com/?4,GET,application/json",
- "Webhook 5,dcim.site,True,http://example.com/?5,GET,application/json",
- "Webhook 6,dcim.site,True,http://example.com/?6,GET,application/json",
+ "name,content_types,type_create,action_type,action_object",
+ "Webhook 4,dcim.site,True,webhook,Webhook 1",
)
cls.csv_update_data = (
"id,name",
- f"{webhooks[0].pk},Webhook 7",
- f"{webhooks[1].pk},Webhook 8",
- f"{webhooks[2].pk},Webhook 9",
+ f"{event_rules[0].pk},Event 7",
+ f"{event_rules[1].pk},Event 8",
+ f"{event_rules[2].pk},Event 9",
)
cls.bulk_edit_data = {
- 'enabled': False,
- 'type_create': False,
'type_update': True,
- 'type_delete': True,
- 'http_method': 'GET',
}
@@ -434,7 +492,8 @@ class ConfigContextTestCase(
@classmethod
def setUpTestData(cls):
- site = Site.objects.create(name='Site 1', slug='site-1')
+ manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+ devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
# Create three ConfigContexts
for i in range(1, 4):
@@ -443,7 +502,7 @@ class ConfigContextTestCase(
data={'foo': i}
)
configcontext.save()
- configcontext.sites.add(site)
+ configcontext.device_types.add(devicetype)
cls.form_data = {
'name': 'Config Context X',
@@ -451,11 +510,12 @@ class ConfigContextTestCase(
'description': 'A new config context',
'is_active': True,
'regions': [],
- 'sites': [site.pk],
+ 'sites': [],
'roles': [],
'platforms': [],
'tenant_groups': [],
'tenants': [],
+ 'device_types': [devicetype.id],
'tags': [],
'data': '{"foo": 123}',
}
diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py
index fd95186e4..0a1786f1f 100644
--- a/netbox/extras/urls.py
+++ b/netbox/extras/urls.py
@@ -61,6 +61,14 @@ urlpatterns = [
path('webhooks/delete/', views.WebhookBulkDeleteView.as_view(), name='webhook_bulk_delete'),
path('webhooks//', include(get_model_urls('extras', 'webhook'))),
+ # Event rules
+ path('event-rules/', views.EventRuleListView.as_view(), name='eventrule_list'),
+ path('event-rules/add/', views.EventRuleEditView.as_view(), name='eventrule_add'),
+ path('event-rules/import/', views.EventRuleBulkImportView.as_view(), name='eventrule_import'),
+ path('event-rules/edit/', views.EventRuleBulkEditView.as_view(), name='eventrule_bulk_edit'),
+ path('event-rules/delete/', views.EventRuleBulkDeleteView.as_view(), name='eventrule_bulk_delete'),
+ path('event-rules//', include(get_model_urls('extras', 'eventrule'))),
+
# Tags
path('tags/', views.TagListView.as_view(), name='tag_list'),
path('tags/add/', views.TagEditView.as_view(), name='tag_add'),
@@ -98,13 +106,6 @@ urlpatterns = [
path('journal-entries/import/', views.JournalEntryBulkImportView.as_view(), name='journalentry_import'),
path('journal-entries//', include(get_model_urls('extras', 'journalentry'))),
- # Config revisions
- path('config-revisions/', views.ConfigRevisionListView.as_view(), name='configrevision_list'),
- path('config-revisions/add/', views.ConfigRevisionEditView.as_view(), name='configrevision_add'),
- path('config-revisions/delete/', views.ConfigRevisionBulkDeleteView.as_view(), name='configrevision_bulk_delete'),
- path('config-revisions//restore/', views.ConfigRevisionRestoreView.as_view(), name='configrevision_restore'),
- path('config-revisions//', include(get_model_urls('extras', 'configrevision'))),
-
# Change logging
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
path('changelog//', include(get_model_urls('extras', 'objectchange'))),
diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py
index 23892e098..c6b2de188 100644
--- a/netbox/extras/utils.py
+++ b/netbox/extras/utils.py
@@ -1,5 +1,3 @@
-from django.db.models import Q
-from django.utils.deconstruct import deconstructible
from taggit.managers import _TaggableManager
from netbox.registry import registry
@@ -31,29 +29,6 @@ def image_upload(instance, filename):
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
-@deconstructible
-class FeatureQuery:
- """
- Helper class that delays evaluation of the registry contents for the functionality store
- until it has been populated.
- """
- def __init__(self, feature):
- self.feature = feature
-
- def __call__(self):
- return self.get_query()
-
- def get_query(self):
- """
- Given an extras feature, return a Q object for content type lookup
- """
- query = Q()
- for app_label, models in registry['model_features'][self.feature].items():
- query |= Q(app_label=app_label, model__in=models)
-
- return query
-
-
def register_features(model, features):
"""
Register model features in the application registry.
@@ -67,6 +42,10 @@ def register_features(model, features):
f"{feature} is not a valid model feature! Valid keys are: {registry['model_features'].keys()}"
)
+ # Register public models
+ if not getattr(model, '_netbox_private', False):
+ registry['models'][app_label].add(model_name)
+
def is_script(obj):
"""
diff --git a/netbox/extras/validators.py b/netbox/extras/validators.py
index 686c9b032..35f61958c 100644
--- a/netbox/extras/validators.py
+++ b/netbox/extras/validators.py
@@ -1,15 +1,38 @@
-from django.core.exceptions import ValidationError
from django.core import validators
+from django.core.exceptions import ValidationError
+from django.utils.translation import gettext_lazy as _
# NOTE: As this module may be imported by configuration.py, we cannot import
# anything from NetBox itself.
+class IsEqualValidator(validators.BaseValidator):
+ """
+ Employed by CustomValidator to require a specific value.
+ """
+ message = _("Ensure this value is equal to %(limit_value)s.")
+ code = "is_equal"
+
+ def compare(self, a, b):
+ return a != b
+
+
+class IsNotEqualValidator(validators.BaseValidator):
+ """
+ Employed by CustomValidator to exclude a specific value.
+ """
+ message = _("Ensure this value does not equal %(limit_value)s.")
+ code = "is_not_equal"
+
+ def compare(self, a, b):
+ return a == b
+
+
class IsEmptyValidator:
"""
Employed by CustomValidator to enforce required fields.
"""
- message = "This field must be empty."
+ message = _("This field must be empty.")
code = 'is_empty'
def __init__(self, enforce=True):
@@ -24,7 +47,7 @@ class IsNotEmptyValidator:
"""
Employed by CustomValidator to enforce prohibited fields.
"""
- message = "This field must not be empty."
+ message = _("This field must not be empty.")
code = 'not_empty'
def __init__(self, enforce=True):
@@ -50,6 +73,8 @@ class CustomValidator:
:param validation_rules: A dictionary mapping object attributes to validation rules
"""
VALIDATORS = {
+ 'eq': IsEqualValidator,
+ 'neq': IsNotEqualValidator,
'min': validators.MinValueValidator,
'max': validators.MaxValueValidator,
'min_length': validators.MinLengthValidator,
@@ -66,8 +91,7 @@ class CustomValidator:
def __call__(self, instance):
# Validate instance attributes per validation rules
for attr_name, rules in self.validation_rules.items():
- assert hasattr(instance, attr_name), f"Invalid attribute '{attr_name}' for {instance.__class__.__name__}"
- attr = getattr(instance, attr_name)
+ attr = self._getattr(instance, attr_name)
for descriptor, value in rules.items():
validator = self.get_validator(descriptor, value)
try:
@@ -79,6 +103,26 @@ class CustomValidator:
# Execute custom validation logic (if any)
self.validate(instance)
+ @staticmethod
+ def _getattr(instance, name):
+ # Attempt to resolve many-to-many fields to their stored values
+ m2m_fields = [f.name for f in instance._meta.local_many_to_many]
+ if name in m2m_fields:
+ if name in getattr(instance, '_m2m_values', []):
+ return instance._m2m_values[name]
+ if instance.pk:
+ return list(getattr(instance, name).all())
+ return []
+
+ # Raise a ValidationError for unknown attributes
+ if not hasattr(instance, name):
+ raise ValidationError(_('Invalid attribute "{name}" for {model}').format(
+ name=name,
+ model=instance.__class__.__name__
+ ))
+
+ return getattr(instance, name)
+
def get_validator(self, descriptor, value):
"""
Instantiate and return the appropriate validator based on the descriptor given. For
diff --git a/netbox/extras/views.py b/netbox/extras/views.py
index a9636cc9e..a3dd7f193 100644
--- a/netbox/extras/views.py
+++ b/netbox/extras/views.py
@@ -15,7 +15,7 @@ from core.models import Job
from core.tables import JobTable
from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class
-from netbox.config import get_config, PARAMS
+from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic
from utilities.forms import ConfirmationForm, get_field_value
from utilities.htmx import is_htmx
@@ -46,6 +46,21 @@ class CustomFieldListView(generic.ObjectListView):
class CustomFieldView(generic.ObjectView):
queryset = CustomField.objects.select_related('choice_set')
+ def get_extra_context(self, request, instance):
+ related_models = ()
+
+ for content_type in instance.content_types.all():
+ related_models += (
+ content_type.model_class().objects.restrict(request.user, 'view').exclude(
+ Q(**{f'custom_field_data__{instance.name}': ''}) |
+ Q(**{f'custom_field_data__{instance.name}': None})
+ ),
+ )
+
+ return {
+ 'related_models': related_models
+ }
+
@register_model_view(CustomField, 'edit')
class CustomFieldEditView(generic.ObjectEditView):
@@ -195,7 +210,10 @@ class ExportTemplateListView(generic.ObjectListView):
filterset_form = forms.ExportTemplateFilterForm
table = tables.ExportTemplateTable
template_name = 'extras/exporttemplate_list.html'
- actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_sync')
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_sync': {'sync'},
+ }
@register_model_view(ExportTemplate)
@@ -377,6 +395,51 @@ class WebhookBulkDeleteView(generic.BulkDeleteView):
table = tables.WebhookTable
+#
+# Event Rules
+#
+
+class EventRuleListView(generic.ObjectListView):
+ queryset = EventRule.objects.all()
+ filterset = filtersets.EventRuleFilterSet
+ filterset_form = forms.EventRuleFilterForm
+ table = tables.EventRuleTable
+
+
+@register_model_view(EventRule)
+class EventRuleView(generic.ObjectView):
+ queryset = EventRule.objects.all()
+
+
+@register_model_view(EventRule, 'edit')
+class EventRuleEditView(generic.ObjectEditView):
+ queryset = EventRule.objects.all()
+ form = forms.EventRuleForm
+
+
+@register_model_view(EventRule, 'delete')
+class EventRuleDeleteView(generic.ObjectDeleteView):
+ queryset = EventRule.objects.all()
+
+
+class EventRuleBulkImportView(generic.BulkImportView):
+ queryset = EventRule.objects.all()
+ model_form = forms.EventRuleImportForm
+
+
+class EventRuleBulkEditView(generic.BulkEditView):
+ queryset = EventRule.objects.all()
+ filterset = filtersets.EventRuleFilterSet
+ table = tables.EventRuleTable
+ form = forms.EventRuleBulkEditForm
+
+
+class EventRuleBulkDeleteView(generic.BulkDeleteView):
+ queryset = EventRule.objects.all()
+ filterset = filtersets.EventRuleFilterSet
+ table = tables.EventRuleTable
+
+
#
# Tags
#
@@ -457,7 +520,12 @@ class ConfigContextListView(generic.ObjectListView):
filterset_form = forms.ConfigContextFilterForm
table = tables.ConfigContextTable
template_name = 'extras/configcontext_list.html'
- actions = ('add', 'bulk_edit', 'bulk_delete', 'bulk_sync')
+ actions = {
+ 'add': {'add'},
+ 'bulk_edit': {'change'},
+ 'bulk_delete': {'delete'},
+ 'bulk_sync': {'sync'},
+ }
@register_model_view(ConfigContext)
@@ -561,7 +629,10 @@ class ConfigTemplateListView(generic.ObjectListView):
filterset_form = forms.ConfigTemplateFilterForm
table = tables.ConfigTemplateTable
template_name = 'extras/configtemplate_list.html'
- actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_sync')
+ actions = {
+ **DEFAULT_ACTION_PERMISSIONS,
+ 'bulk_sync': {'sync'},
+ }
@register_model_view(ConfigTemplate)
@@ -612,7 +683,9 @@ class ObjectChangeListView(generic.ObjectListView):
filterset_form = forms.ObjectChangeFilterForm
table = tables.ObjectChangeTable
template_name = 'extras/objectchange_list.html'
- actions = ('export',)
+ actions = {
+ 'export': {'view'},
+ }
@register_model_view(ObjectChange)
@@ -678,7 +751,9 @@ class ImageAttachmentListView(generic.ObjectListView):
filterset = filtersets.ImageAttachmentFilterSet
filterset_form = forms.ImageAttachmentFilterForm
table = tables.ImageAttachmentTable
- actions = ('export',)
+ actions = {
+ 'export': {'view'},
+ }
@register_model_view(ImageAttachment, 'edit')
@@ -721,7 +796,12 @@ class JournalEntryListView(generic.ObjectListView):
filterset = filtersets.JournalEntryFilterSet
filterset_form = forms.JournalEntryFilterForm
table = tables.JournalEntryTable
- actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
+ actions = {
+ 'import': {'add'},
+ 'export': {'view'},
+ 'bulk_edit': {'change'},
+ 'bulk_delete': {'delete'},
+ }
@register_model_view(JournalEntry)
@@ -963,6 +1043,10 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
})
+def get_report_module(module, request):
+ return get_object_or_404(ReportModule.objects.restrict(request.user), file_path__regex=f"^{module}\\.")
+
+
class ReportView(ContentTypePermissionRequiredMixin, View):
"""
Display a single Report and its associated Job (if any).
@@ -971,7 +1055,7 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
return 'extras.view_report'
def get(self, request, module, name):
- module = get_object_or_404(ReportModule.objects.restrict(request.user), file_path__startswith=module)
+ module = get_report_module(module, request)
report = module.reports[name]()
object_type = ContentType.objects.get(app_label='extras', model='reportmodule')
@@ -992,7 +1076,7 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
if not request.user.has_perm('extras.run_report'):
return HttpResponseForbidden()
- module = get_object_or_404(ReportModule.objects.restrict(request.user), file_path__startswith=module)
+ module = get_report_module(module, request)
report = module.reports[name]()
form = ReportForm(request.POST, scheduling_enabled=report.scheduling_enabled)
@@ -1031,7 +1115,7 @@ class ReportSourceView(ContentTypePermissionRequiredMixin, View):
return 'extras.view_report'
def get(self, request, module, name):
- module = get_object_or_404(ReportModule.objects.restrict(request.user), file_path__startswith=module)
+ module = get_report_module(module, request)
report = module.reports[name]()
return render(request, 'extras/report/source.html', {
@@ -1047,14 +1131,14 @@ class ReportJobsView(ContentTypePermissionRequiredMixin, View):
return 'extras.view_report'
def get(self, request, module, name):
- module = get_object_or_404(ReportModule.objects.restrict(request.user), file_path__startswith=module)
+ module = get_report_module(module, request)
report = module.reports[name]()
object_type = ContentType.objects.get(app_label='extras', model='reportmodule')
jobs = Job.objects.filter(
object_type=object_type,
object_id=module.pk,
- name=report.name
+ name=report.class_name
)
jobs_table = JobTable(
@@ -1136,13 +1220,17 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
})
+def get_script_module(module, request):
+ return get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__regex=f"^{module}\\.")
+
+
class ScriptView(ContentTypePermissionRequiredMixin, View):
def get_required_permission(self):
return 'extras.view_script'
def get(self, request, module, name):
- module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__startswith=module)
+ module = get_script_module(module, request)
script = module.scripts[name]()
form = script.as_form(initial=normalize_querydict(request.GET))
@@ -1166,7 +1254,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, View):
if not request.user.has_perm('extras.run_script'):
return HttpResponseForbidden()
- module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__startswith=module)
+ module = get_script_module(module, request)
script = module.scripts[name]()
form = script.as_form(request.POST, request.FILES)
@@ -1203,7 +1291,7 @@ class ScriptSourceView(ContentTypePermissionRequiredMixin, View):
return 'extras.view_script'
def get(self, request, module, name):
- module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__startswith=module)
+ module = get_script_module(module, request)
script = module.scripts[name]()
return render(request, 'extras/script/source.html', {
@@ -1219,7 +1307,7 @@ class ScriptJobsView(ContentTypePermissionRequiredMixin, View):
return 'extras.view_script'
def get(self, request, module, name):
- module = get_object_or_404(ScriptModule.objects.restrict(request.user), file_path__startswith=module)
+ module = get_script_module(module, request)
script = module.scripts[name]()
object_type = ContentType.objects.get(app_label='extras', model='scriptmodule')
@@ -1272,74 +1360,6 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, View):
})
-#
-# Config Revisions
-#
-
-class ConfigRevisionListView(generic.ObjectListView):
- queryset = ConfigRevision.objects.all()
- filterset = filtersets.ConfigRevisionFilterSet
- filterset_form = forms.ConfigRevisionFilterForm
- table = tables.ConfigRevisionTable
-
-
-@register_model_view(ConfigRevision)
-class ConfigRevisionView(generic.ObjectView):
- queryset = ConfigRevision.objects.all()
-
-
-class ConfigRevisionEditView(generic.ObjectEditView):
- queryset = ConfigRevision.objects.all()
- form = forms.ConfigRevisionForm
-
-
-@register_model_view(ConfigRevision, 'delete')
-class ConfigRevisionDeleteView(generic.ObjectDeleteView):
- queryset = ConfigRevision.objects.all()
-
-
-class ConfigRevisionBulkDeleteView(generic.BulkDeleteView):
- queryset = ConfigRevision.objects.all()
- filterset = filtersets.ConfigRevisionFilterSet
- table = tables.ConfigRevisionTable
-
-
-class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
-
- def get_required_permission(self):
- return 'extras.configrevision_edit'
-
- def get(self, request, pk):
- candidate_config = get_object_or_404(ConfigRevision, pk=pk)
-
- # Get the current ConfigRevision
- config_version = get_config().version
- current_config = ConfigRevision.objects.filter(pk=config_version).first()
-
- params = []
- for param in PARAMS:
- params.append((
- param.name,
- current_config.data.get(param.name, None),
- candidate_config.data.get(param.name, None)
- ))
-
- return render(request, 'extras/configrevision_restore.html', {
- 'object': candidate_config,
- 'params': params,
- })
-
- def post(self, request, pk):
- if not request.user.has_perm('extras.configrevision_edit'):
- return HttpResponseForbidden()
-
- candidate_config = get_object_or_404(ConfigRevision, pk=pk)
- candidate_config.activate()
- messages.success(request, f"Restored configuration revision #{pk}")
-
- return redirect(candidate_config.get_absolute_url())
-
-
#
# Markdown
#
diff --git a/netbox/extras/webhooks.py b/netbox/extras/webhooks.py
index 1fc869ee8..53ec161d7 100644
--- a/netbox/extras/webhooks.py
+++ b/netbox/extras/webhooks.py
@@ -1,46 +1,15 @@
import hashlib
import hmac
+import logging
-from django.contrib.contenttypes.models import ContentType
-from django.utils import timezone
-from django_rq import get_queue
+import requests
+from django.conf import settings
+from django_rq import job
+from jinja2.exceptions import TemplateError
-from netbox.config import get_config
-from netbox.constants import RQ_QUEUE_DEFAULT
-from netbox.registry import registry
-from utilities.api import get_serializer_for_model
-from utilities.rqworker import get_rq_retry
-from utilities.utils import serialize_object
-from .choices import *
-from .models import Webhook
+from .constants import WEBHOOK_EVENT_TYPES
-
-def serialize_for_webhook(instance):
- """
- Return a serialized representation of the given instance suitable for use in a webhook.
- """
- serializer_class = get_serializer_for_model(instance.__class__)
- serializer_context = {
- 'request': None,
- }
- serializer = serializer_class(instance, context=serializer_context)
-
- return serializer.data
-
-
-def get_snapshots(instance, action):
- snapshots = {
- 'prechange': getattr(instance, '_prechange_snapshot', None),
- 'postchange': None,
- }
- if action != ObjectChangeActionChoices.ACTION_DELETE:
- # Use model's serialize_object() method if defined; fall back to serialize_object() utility function
- if hasattr(instance, 'serialize_object'):
- snapshots['postchange'] = instance.serialize_object()
- else:
- snapshots['postchange'] = serialize_object(instance)
-
- return snapshots
+logger = logging.getLogger('netbox.webhooks')
def generate_signature(request_body, secret):
@@ -55,68 +24,77 @@ def generate_signature(request_body, secret):
return hmac_prep.hexdigest()
-def enqueue_object(queue, instance, user, request_id, action):
+@job('default')
+def send_webhook(event_rule, model_name, event, data, timestamp, username, request_id=None, snapshots=None):
"""
- Enqueue a serialized representation of a created/updated/deleted object for the processing of
- webhooks once the request has completed.
+ Make a POST request to the defined Webhook
"""
- # Determine whether this type of object supports webhooks
- app_label = instance._meta.app_label
- model_name = instance._meta.model_name
- if model_name not in registry['model_features']['webhooks'].get(app_label, []):
- return
+ webhook = event_rule.action_object
- queue.append({
- 'content_type': ContentType.objects.get_for_model(instance),
- 'object_id': instance.pk,
- 'event': action,
- 'data': serialize_for_webhook(instance),
- 'snapshots': get_snapshots(instance, action),
- 'username': user.username,
- 'request_id': request_id
- })
-
-
-def flush_webhooks(queue):
- """
- Flush a list of object representation to RQ for webhook processing.
- """
- rq_queue_name = get_config().QUEUE_MAPPINGS.get('webhook', RQ_QUEUE_DEFAULT)
- rq_queue = get_queue(rq_queue_name)
- webhooks_cache = {
- 'type_create': {},
- 'type_update': {},
- 'type_delete': {},
+ # Prepare context data for headers & body templates
+ context = {
+ 'event': WEBHOOK_EVENT_TYPES[event],
+ 'timestamp': timestamp,
+ 'model': model_name,
+ 'username': username,
+ 'request_id': request_id,
+ 'data': data,
}
+ if snapshots:
+ context.update({
+ 'snapshots': snapshots
+ })
- for data in queue:
+ # Build the headers for the HTTP request
+ headers = {
+ 'Content-Type': webhook.http_content_type,
+ }
+ try:
+ headers.update(webhook.render_headers(context))
+ except (TemplateError, ValueError) as e:
+ logger.error(f"Error parsing HTTP headers for webhook {webhook}: {e}")
+ raise e
- action_flag = {
- ObjectChangeActionChoices.ACTION_CREATE: 'type_create',
- ObjectChangeActionChoices.ACTION_UPDATE: 'type_update',
- ObjectChangeActionChoices.ACTION_DELETE: 'type_delete',
- }[data['event']]
- content_type = data['content_type']
+ # Render the request body
+ try:
+ body = webhook.render_body(context)
+ except TemplateError as e:
+ logger.error(f"Error rendering request body for webhook {webhook}: {e}")
+ raise e
- # Cache applicable Webhooks
- if content_type not in webhooks_cache[action_flag]:
- webhooks_cache[action_flag][content_type] = Webhook.objects.filter(
- **{action_flag: True},
- content_types=content_type,
- enabled=True
- )
- webhooks = webhooks_cache[action_flag][content_type]
+ # Prepare the HTTP request
+ params = {
+ 'method': webhook.http_method,
+ 'url': webhook.render_payload_url(context),
+ 'headers': headers,
+ 'data': body.encode('utf8'),
+ }
+ logger.info(
+ f"Sending {params['method']} request to {params['url']} ({context['model']} {context['event']})"
+ )
+ logger.debug(params)
+ try:
+ prepared_request = requests.Request(**params).prepare()
+ except requests.exceptions.RequestException as e:
+ logger.error(f"Error forming HTTP request: {e}")
+ raise e
- for webhook in webhooks:
- rq_queue.enqueue(
- "extras.webhooks_worker.process_webhook",
- webhook=webhook,
- model_name=content_type.model,
- event=data['event'],
- data=data['data'],
- snapshots=data['snapshots'],
- timestamp=str(timezone.now()),
- username=data['username'],
- request_id=data['request_id'],
- retry=get_rq_retry()
- )
+ # If a secret key is defined, sign the request with a hash of the key and its content
+ if webhook.secret != '':
+ prepared_request.headers['X-Hook-Signature'] = generate_signature(prepared_request.body, webhook.secret)
+
+ # Send the request
+ with requests.Session() as session:
+ session.verify = webhook.ssl_verification
+ if webhook.ca_file_path:
+ session.verify = webhook.ca_file_path
+ response = session.send(prepared_request, proxies=settings.HTTP_PROXIES)
+
+ if 200 <= response.status_code <= 299:
+ logger.info(f"Request succeeded; response status {response.status_code}")
+ return f"Status {response.status_code} returned, webhook successfully processed."
+ else:
+ logger.warning(f"Request failed; response status {response.status_code}: {response.content}")
+ raise requests.exceptions.RequestException(
+ f"Status {response.status_code} returned with content '{response.content}', webhook FAILED to process."
+ )
diff --git a/netbox/extras/webhooks_worker.py b/netbox/extras/webhooks_worker.py
index 438231b7e..77535fafa 100644
--- a/netbox/extras/webhooks_worker.py
+++ b/netbox/extras/webhooks_worker.py
@@ -1,105 +1,10 @@
-import logging
+import warnings
-import requests
-from django.conf import settings
-from django_rq import job
-from jinja2.exceptions import TemplateError
-
-from .conditions import ConditionSet
-from .constants import WEBHOOK_EVENT_TYPES
-from .webhooks import generate_signature
-
-logger = logging.getLogger('netbox.webhooks_worker')
+from .webhooks import send_webhook as process_webhook
-def eval_conditions(webhook, data):
- """
- Test whether the given data meets the conditions of the webhook (if any). Return True
- if met or no conditions are specified.
- """
- if not webhook.conditions:
- return True
-
- logger.debug(f'Evaluating webhook conditions: {webhook.conditions}')
- if ConditionSet(webhook.conditions).eval(data):
- return True
-
- return False
-
-
-@job('default')
-def process_webhook(webhook, model_name, event, data, timestamp, username, request_id=None, snapshots=None):
- """
- Make a POST request to the defined Webhook
- """
- # Evaluate webhook conditions (if any)
- if not eval_conditions(webhook, data):
- return
-
- # Prepare context data for headers & body templates
- context = {
- 'event': WEBHOOK_EVENT_TYPES[event],
- 'timestamp': timestamp,
- 'model': model_name,
- 'username': username,
- 'request_id': request_id,
- 'data': data,
- }
- if snapshots:
- context.update({
- 'snapshots': snapshots
- })
-
- # Build the headers for the HTTP request
- headers = {
- 'Content-Type': webhook.http_content_type,
- }
- try:
- headers.update(webhook.render_headers(context))
- except (TemplateError, ValueError) as e:
- logger.error(f"Error parsing HTTP headers for webhook {webhook}: {e}")
- raise e
-
- # Render the request body
- try:
- body = webhook.render_body(context)
- except TemplateError as e:
- logger.error(f"Error rendering request body for webhook {webhook}: {e}")
- raise e
-
- # Prepare the HTTP request
- params = {
- 'method': webhook.http_method,
- 'url': webhook.render_payload_url(context),
- 'headers': headers,
- 'data': body.encode('utf8'),
- }
- logger.info(
- f"Sending {params['method']} request to {params['url']} ({context['model']} {context['event']})"
- )
- logger.debug(params)
- try:
- prepared_request = requests.Request(**params).prepare()
- except requests.exceptions.RequestException as e:
- logger.error(f"Error forming HTTP request: {e}")
- raise e
-
- # If a secret key is defined, sign the request with a hash of the key and its content
- if webhook.secret != '':
- prepared_request.headers['X-Hook-Signature'] = generate_signature(prepared_request.body, webhook.secret)
-
- # Send the request
- with requests.Session() as session:
- session.verify = webhook.ssl_verification
- if webhook.ca_file_path:
- session.verify = webhook.ca_file_path
- response = session.send(prepared_request, proxies=settings.HTTP_PROXIES)
-
- if 200 <= response.status_code <= 299:
- logger.info(f"Request succeeded; response status {response.status_code}")
- return f"Status {response.status_code} returned, webhook successfully processed."
- else:
- logger.warning(f"Request failed; response status {response.status_code}: {response.content}")
- raise requests.exceptions.RequestException(
- f"Status {response.status_code} returned with content '{response.content}', webhook FAILED to process."
- )
+# TODO: Remove in v4.0
+warnings.warn(
+ f"webhooks_worker.process_webhook has been moved to webhooks.send_webhook.",
+ DeprecationWarning
+)
diff --git a/netbox/ipam/api/field_serializers.py b/netbox/ipam/api/field_serializers.py
index d44d8b7d4..d12530a60 100644
--- a/netbox/ipam/api/field_serializers.py
+++ b/netbox/ipam/api/field_serializers.py
@@ -1,21 +1,18 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
-from ipam import models
from netaddr import AddrFormatError, IPNetwork
-__all__ = [
+__all__ = (
'IPAddressField',
-]
+ 'IPNetworkField',
+)
-#
-# IP address field
-#
-
class IPAddressField(serializers.CharField):
- """IPAddressField with mask"""
-
+ """
+ An IPv4 or IPv6 address with optional mask
+ """
default_error_messages = {
'invalid': _('Enter a valid IPv4 or IPv6 address with optional mask.'),
}
@@ -24,7 +21,27 @@ class IPAddressField(serializers.CharField):
try:
return IPNetwork(data)
except AddrFormatError:
- raise serializers.ValidationError("Invalid IP address format: {}".format(data))
+ raise serializers.ValidationError(_("Invalid IP address format: {data}").format(data))
+ except (TypeError, ValueError) as e:
+ raise serializers.ValidationError(e)
+
+ def to_representation(self, value):
+ return str(value)
+
+
+class IPNetworkField(serializers.CharField):
+ """
+ An IPv4 or IPv6 prefix
+ """
+ default_error_messages = {
+ 'invalid': _('Enter a valid IPv4 or IPv6 prefix and mask in CIDR notation.'),
+ }
+
+ def to_internal_value(self, data):
+ try:
+ return IPNetwork(data)
+ except AddrFormatError:
+ raise serializers.ValidationError(_("Invalid IP prefix format: {data}").format(data))
except (TypeError, ValueError) as e:
raise serializers.ValidationError(e)
diff --git a/netbox/ipam/api/nested_serializers.py b/netbox/ipam/api/nested_serializers.py
index 9e150e2cb..17d8d74a7 100644
--- a/netbox/ipam/api/nested_serializers.py
+++ b/netbox/ipam/api/nested_serializers.py
@@ -2,7 +2,6 @@ from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from ipam import models
-from ipam.models.l2vpn import L2VPNTermination, L2VPN
from netbox.api.serializers import WritableNestedSerializer
from .field_serializers import IPAddressField
@@ -14,8 +13,6 @@ __all__ = [
'NestedFHRPGroupAssignmentSerializer',
'NestedIPAddressSerializer',
'NestedIPRangeSerializer',
- 'NestedL2VPNSerializer',
- 'NestedL2VPNTerminationSerializer',
'NestedPrefixSerializer',
'NestedRIRSerializer',
'NestedRoleSerializer',
@@ -223,28 +220,3 @@ class NestedServiceSerializer(WritableNestedSerializer):
class Meta:
model = models.Service
fields = ['id', 'url', 'display', 'name', 'protocol', 'ports']
-
-#
-# L2VPN
-#
-
-
-class NestedL2VPNSerializer(WritableNestedSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpn-detail')
-
- class Meta:
- model = L2VPN
- fields = [
- 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type'
- ]
-
-
-class NestedL2VPNTerminationSerializer(WritableNestedSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpntermination-detail')
- l2vpn = NestedL2VPNSerializer()
-
- class Meta:
- model = L2VPNTermination
- fields = [
- 'id', 'url', 'display', 'l2vpn'
- ]
diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py
index c2cf38fe7..33aa55a93 100644
--- a/netbox/ipam/api/serializers.py
+++ b/netbox/ipam/api/serializers.py
@@ -12,8 +12,9 @@ from netbox.constants import NESTED_SERIALIZER_PREFIX
from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
+from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
+from .field_serializers import IPAddressField, IPNetworkField
from .nested_serializers import *
-from .field_serializers import IPAddressField
#
@@ -138,7 +139,7 @@ class AggregateSerializer(NetBoxModelSerializer):
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
rir = NestedRIRSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
- prefix = serializers.CharField()
+ prefix = IPNetworkField()
class Meta:
model = Aggregate
@@ -146,7 +147,6 @@ class AggregateSerializer(NetBoxModelSerializer):
'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'comments',
'tags', 'custom_fields', 'created', 'last_updated',
]
- read_only_fields = ['family']
#
@@ -306,7 +306,7 @@ class PrefixSerializer(NetBoxModelSerializer):
role = NestedRoleSerializer(required=False, allow_null=True)
children = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(read_only=True)
- prefix = serializers.CharField()
+ prefix = IPNetworkField()
class Meta:
model = Prefix
@@ -315,7 +315,6 @@ class PrefixSerializer(NetBoxModelSerializer):
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children',
'_depth',
]
- read_only_fields = ['family']
class PrefixLengthSerializer(serializers.Serializer):
@@ -386,7 +385,6 @@ class IPRangeSerializer(NetBoxModelSerializer):
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
- read_only_fields = ['family']
#
@@ -482,54 +480,3 @@ class ServiceSerializer(NetBoxModelSerializer):
'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'ports', 'protocol', 'ipaddresses',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
-
-#
-# L2VPN
-#
-
-
-class L2VPNSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpn-detail')
- type = ChoiceField(choices=L2VPNTypeChoices, required=False)
- import_targets = SerializedPKRelatedField(
- queryset=RouteTarget.objects.all(),
- serializer=NestedRouteTargetSerializer,
- required=False,
- many=True
- )
- export_targets = SerializedPKRelatedField(
- queryset=RouteTarget.objects.all(),
- serializer=NestedRouteTargetSerializer,
- required=False,
- many=True
- )
- tenant = NestedTenantSerializer(required=False, allow_null=True)
-
- class Meta:
- model = L2VPN
- fields = [
- 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets',
- 'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated'
- ]
-
-
-class L2VPNTerminationSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpntermination-detail')
- l2vpn = NestedL2VPNSerializer()
- assigned_object_type = ContentTypeField(
- queryset=ContentType.objects.all()
- )
- assigned_object = serializers.SerializerMethodField(read_only=True)
-
- class Meta:
- model = L2VPNTermination
- fields = [
- 'id', 'url', 'display', 'l2vpn', 'assigned_object_type', 'assigned_object_id',
- 'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
- ]
-
- @extend_schema_field(serializers.JSONField(allow_null=True))
- def get_assigned_object(self, instance):
- serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
- context = {'request': self.context['request']}
- return serializer(instance.assigned_object, context=context).data
diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py
index 442fd2240..bae9d8048 100644
--- a/netbox/ipam/api/urls.py
+++ b/netbox/ipam/api/urls.py
@@ -23,8 +23,6 @@ router.register('vlan-groups', views.VLANGroupViewSet)
router.register('vlans', views.VLANViewSet)
router.register('service-templates', views.ServiceTemplateViewSet)
router.register('services', views.ServiceViewSet)
-router.register('l2vpns', views.L2VPNViewSet)
-router.register('l2vpn-terminations', views.L2VPNTerminationViewSet)
app_name = 'ipam-api'
diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py
index da6463e23..4439e82b4 100644
--- a/netbox/ipam/api/views.py
+++ b/netbox/ipam/api/views.py
@@ -1,3 +1,5 @@
+from copy import deepcopy
+
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django.shortcuts import get_object_or_404
@@ -14,7 +16,6 @@ from circuits.models import Provider
from dcim.models import Site
from ipam import filtersets
from ipam.models import *
-from ipam.models import L2VPN, L2VPNTermination
from ipam.utils import get_next_available_prefix
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.api.viewsets.mixins import ObjectValidationMixin
@@ -178,18 +179,6 @@ class ServiceViewSet(NetBoxModelViewSet):
filterset_class = filtersets.ServiceFilterSet
-class L2VPNViewSet(NetBoxModelViewSet):
- queryset = L2VPN.objects.prefetch_related('import_targets', 'export_targets', 'tenant', 'tags')
- serializer_class = serializers.L2VPNSerializer
- filterset_class = filtersets.L2VPNFilterSet
-
-
-class L2VPNTerminationViewSet(NetBoxModelViewSet):
- queryset = L2VPNTermination.objects.prefetch_related('assigned_object')
- serializer_class = serializers.L2VPNTerminationSerializer
- filterset_class = filtersets.L2VPNTerminationFilterSet
-
-
#
# Views
#
@@ -266,6 +255,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView):
# Normalize request data to a list of objects
requested_objects = request.data if isinstance(request.data, list) else [request.data]
+ limit = len(requested_objects)
# Serialize and validate the request data
serializer = self.write_serializer_class(data=requested_objects, many=True, context={
@@ -279,7 +269,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView):
)
with advisory_lock(ADVISORY_LOCK_KEYS[self.advisory_lock_key]):
- available_objects = self.get_available_objects(parent)
+ available_objects = self.get_available_objects(parent, limit)
# Determine if the requested number of objects is available
if not self.check_sufficient_available(serializer.validated_data, available_objects):
@@ -289,7 +279,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView):
)
# Prepare object data for deserialization
- requested_objects = self.prep_object_data(serializer.validated_data, available_objects, parent)
+ requested_objects = self.prep_object_data(deepcopy(requested_objects), available_objects, parent)
# Initialize the serializer with a list or a single object depending on what was requested
serializer_class = get_serializer_for_model(self.queryset.model)
diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py
index 436cbd040..017fd0430 100644
--- a/netbox/ipam/choices.py
+++ b/netbox/ipam/choices.py
@@ -172,52 +172,3 @@ class ServiceProtocolChoices(ChoiceSet):
(PROTOCOL_UDP, 'UDP'),
(PROTOCOL_SCTP, 'SCTP'),
)
-
-
-class L2VPNTypeChoices(ChoiceSet):
- TYPE_VPLS = 'vpls'
- TYPE_VPWS = 'vpws'
- TYPE_EPL = 'epl'
- TYPE_EVPL = 'evpl'
- TYPE_EPLAN = 'ep-lan'
- TYPE_EVPLAN = 'evp-lan'
- TYPE_EPTREE = 'ep-tree'
- TYPE_EVPTREE = 'evp-tree'
- TYPE_VXLAN = 'vxlan'
- TYPE_VXLAN_EVPN = 'vxlan-evpn'
- TYPE_MPLS_EVPN = 'mpls-evpn'
- TYPE_PBB_EVPN = 'pbb-evpn'
-
- CHOICES = (
- ('VPLS', (
- (TYPE_VPWS, 'VPWS'),
- (TYPE_VPLS, 'VPLS'),
- )),
- ('VXLAN', (
- (TYPE_VXLAN, 'VXLAN'),
- (TYPE_VXLAN_EVPN, 'VXLAN-EVPN'),
- )),
- ('L2VPN E-VPN', (
- (TYPE_MPLS_EVPN, 'MPLS EVPN'),
- (TYPE_PBB_EVPN, 'PBB EVPN'),
- )),
- ('E-Line', (
- (TYPE_EPL, 'EPL'),
- (TYPE_EVPL, 'EVPL'),
- )),
- ('E-LAN', (
- (TYPE_EPLAN, 'Ethernet Private LAN'),
- (TYPE_EVPLAN, 'Ethernet Virtual Private LAN'),
- )),
- ('E-Tree', (
- (TYPE_EPTREE, 'Ethernet Private Tree'),
- (TYPE_EVPTREE, 'Ethernet Virtual Private Tree'),
- )),
- )
-
- P2P = (
- TYPE_VPWS,
- TYPE_EPL,
- TYPE_EPLAN,
- TYPE_EPTREE
- )
diff --git a/netbox/ipam/constants.py b/netbox/ipam/constants.py
index f26fce2b5..6dffd3287 100644
--- a/netbox/ipam/constants.py
+++ b/netbox/ipam/constants.py
@@ -86,9 +86,3 @@ VLANGROUP_SCOPE_TYPES = (
# 16-bit port number
SERVICE_PORT_MIN = 1
SERVICE_PORT_MAX = 65535
-
-L2VPN_ASSIGNMENT_MODELS = Q(
- Q(app_label='dcim', model='interface') |
- Q(app_label='ipam', model='vlan') |
- Q(app_label='virtualization', model='vminterface')
-)
diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py
index 9b57cb273..404baf71b 100644
--- a/netbox/ipam/filtersets.py
+++ b/netbox/ipam/filtersets.py
@@ -4,8 +4,8 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.translation import gettext as _
-from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
from netaddr.core import AddrFormatError
from dcim.models import Device, Interface, Region, Site, SiteGroup
@@ -15,6 +15,7 @@ from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
)
from virtualization.models import VirtualMachine, VMInterface
+from vpn.models import L2VPN
from .choices import *
from .models import *
@@ -26,9 +27,8 @@ __all__ = (
'FHRPGroupFilterSet',
'IPAddressFilterSet',
'IPRangeFilterSet',
- 'L2VPNFilterSet',
- 'L2VPNTerminationFilterSet',
'PrefixFilterSet',
+ 'PrimaryIPFilterSet',
'RIRFilterSet',
'RoleFilterSet',
'RouteTargetFilterSet',
@@ -266,7 +266,8 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
)
mask_length = MultiValueNumberFilter(
field_name='prefix',
- lookup_expr='net_mask_length'
+ lookup_expr='net_mask_length',
+ label=_('Mask length')
)
mask_length__gte = django_filters.NumberFilter(
field_name='prefix',
@@ -467,6 +468,10 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
choices=IPRangeStatusChoices,
null_value=None
)
+ parent = MultiValueCharFilter(
+ method='search_by_parent',
+ label=_('Parent prefix'),
+ )
class Meta:
model = IPRange
@@ -501,6 +506,18 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
except ValidationError:
return queryset.none()
+ def search_by_parent(self, queryset, name, value):
+ if not value:
+ return queryset
+ q = Q()
+ for prefix in value:
+ try:
+ query = str(netaddr.IPNetwork(prefix.strip()).cidr)
+ q |= Q(start_address__net_host_contained=query, end_address__net_host_contained=query)
+ except (AddrFormatError, ValueError):
+ return queryset.none()
+ return queryset.filter(q)
+
class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
family = django_filters.NumberFilter(
@@ -515,9 +532,18 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
method='filter_address',
label=_('Address'),
)
- mask_length = django_filters.NumberFilter(
- method='filter_mask_length',
- label=_('Mask length'),
+ mask_length = MultiValueNumberFilter(
+ field_name='address',
+ lookup_expr='net_mask_length',
+ label=_('Mask length')
+ )
+ mask_length__gte = django_filters.NumberFilter(
+ field_name='address',
+ lookup_expr='net_mask_length__gte'
+ )
+ mask_length__lte = django_filters.NumberFilter(
+ field_name='address',
+ lookup_expr='net_mask_length__lte'
)
vrf_id = django_filters.ModelMultipleChoiceFilter(
queryset=VRF.objects.all(),
@@ -661,11 +687,6 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
except ValidationError:
return queryset.none()
- def filter_mask_length(self, queryset, name, value):
- if not value:
- return queryset
- return queryset.filter(address__net_mask_length=value)
-
@extend_schema_field(OpenApiTypes.STR)
def filter_present_in_vrf(self, queryset, name, vrf):
if vrf is None:
@@ -737,7 +758,7 @@ class FHRPGroupFilterSet(NetBoxModelFilterSet):
class Meta:
model = FHRPGroup
- fields = ['id', 'group_id', 'name', 'auth_key']
+ fields = ['id', 'group_id', 'name', 'auth_key', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -928,6 +949,10 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
choices=VLANStatusChoices,
null_value=None
)
+ available_at_site = django_filters.ModelChoiceFilter(
+ queryset=Site.objects.all(),
+ method='get_for_site'
+ )
available_on_device = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
method='get_for_device'
@@ -962,6 +987,10 @@ class VLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
pass
return queryset.filter(qs_filter)
+ @extend_schema_field(OpenApiTypes.STR)
+ def get_for_site(self, queryset, name, value):
+ return queryset.get_for_site(value)
+
@extend_schema_field(OpenApiTypes.STR)
def get_for_device(self, queryset, name, value):
return queryset.get_for_device(value)
@@ -979,12 +1008,15 @@ class ServiceTemplateFilterSet(NetBoxModelFilterSet):
class Meta:
model = ServiceTemplate
- fields = ['id', 'name', 'protocol']
+ fields = ['id', 'name', 'protocol', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
- qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
+ qs_filter = (
+ Q(name__icontains=value) |
+ Q(description__icontains=value)
+ )
return queryset.filter(qs_filter)
@@ -1037,177 +1069,17 @@ class ServiceFilterSet(NetBoxModelFilterSet):
return queryset.filter(qs_filter)
-#
-# L2VPN
-#
-
-class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
- type = django_filters.MultipleChoiceFilter(
- choices=L2VPNTypeChoices,
- null_value=None
+class PrimaryIPFilterSet(django_filters.FilterSet):
+ """
+ An inheritable FilterSet for models which support primary IP assignment.
+ """
+ primary_ip4_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='primary_ip4',
+ queryset=IPAddress.objects.all(),
+ label=_('Primary IPv4 (ID)'),
)
- import_target_id = django_filters.ModelMultipleChoiceFilter(
- field_name='import_targets',
- queryset=RouteTarget.objects.all(),
- label=_('Import target'),
+ primary_ip6_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='primary_ip6',
+ queryset=IPAddress.objects.all(),
+ label=_('Primary IPv6 (ID)'),
)
- import_target = django_filters.ModelMultipleChoiceFilter(
- field_name='import_targets__name',
- queryset=RouteTarget.objects.all(),
- to_field_name='name',
- label=_('Import target (name)'),
- )
- export_target_id = django_filters.ModelMultipleChoiceFilter(
- field_name='export_targets',
- queryset=RouteTarget.objects.all(),
- label=_('Export target'),
- )
- export_target = django_filters.ModelMultipleChoiceFilter(
- field_name='export_targets__name',
- queryset=RouteTarget.objects.all(),
- to_field_name='name',
- label=_('Export target (name)'),
- )
-
- class Meta:
- model = L2VPN
- fields = ['id', 'identifier', 'name', 'slug', 'type', 'description']
-
- def search(self, queryset, name, value):
- if not value.strip():
- return queryset
- qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
- try:
- qs_filter |= Q(identifier=int(value))
- except ValueError:
- pass
- return queryset.filter(qs_filter)
-
-
-class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
- l2vpn_id = django_filters.ModelMultipleChoiceFilter(
- queryset=L2VPN.objects.all(),
- label=_('L2VPN (ID)'),
- )
- l2vpn = django_filters.ModelMultipleChoiceFilter(
- field_name='l2vpn__slug',
- queryset=L2VPN.objects.all(),
- to_field_name='slug',
- label=_('L2VPN (slug)'),
- )
- region = MultiValueCharFilter(
- method='filter_region',
- field_name='slug',
- label=_('Region (slug)'),
- )
- region_id = MultiValueNumberFilter(
- method='filter_region',
- field_name='pk',
- label=_('Region (ID)'),
- )
- site = MultiValueCharFilter(
- method='filter_site',
- field_name='slug',
- label=_('Site (slug)'),
- )
- site_id = MultiValueNumberFilter(
- method='filter_site',
- field_name='pk',
- label=_('Site (ID)'),
- )
- device = django_filters.ModelMultipleChoiceFilter(
- field_name='interface__device__name',
- queryset=Device.objects.all(),
- to_field_name='name',
- label=_('Device (name)'),
- )
- device_id = django_filters.ModelMultipleChoiceFilter(
- field_name='interface__device',
- queryset=Device.objects.all(),
- label=_('Device (ID)'),
- )
- virtual_machine = django_filters.ModelMultipleChoiceFilter(
- field_name='vminterface__virtual_machine__name',
- queryset=VirtualMachine.objects.all(),
- to_field_name='name',
- label=_('Virtual machine (name)'),
- )
- virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
- field_name='vminterface__virtual_machine',
- queryset=VirtualMachine.objects.all(),
- label=_('Virtual machine (ID)'),
- )
- interface = django_filters.ModelMultipleChoiceFilter(
- field_name='interface__name',
- queryset=Interface.objects.all(),
- to_field_name='name',
- label=_('Interface (name)'),
- )
- interface_id = django_filters.ModelMultipleChoiceFilter(
- field_name='interface',
- queryset=Interface.objects.all(),
- label=_('Interface (ID)'),
- )
- vminterface = django_filters.ModelMultipleChoiceFilter(
- field_name='vminterface__name',
- queryset=VMInterface.objects.all(),
- to_field_name='name',
- label=_('VM interface (name)'),
- )
- vminterface_id = django_filters.ModelMultipleChoiceFilter(
- field_name='vminterface',
- queryset=VMInterface.objects.all(),
- label=_('VM Interface (ID)'),
- )
- vlan = django_filters.ModelMultipleChoiceFilter(
- field_name='vlan__name',
- queryset=VLAN.objects.all(),
- to_field_name='name',
- label=_('VLAN (name)'),
- )
- vlan_vid = django_filters.NumberFilter(
- field_name='vlan__vid',
- label=_('VLAN number (1-4094)'),
- )
- vlan_id = django_filters.ModelMultipleChoiceFilter(
- field_name='vlan',
- queryset=VLAN.objects.all(),
- label=_('VLAN (ID)'),
- )
- assigned_object_type = ContentTypeFilter()
-
- class Meta:
- model = L2VPNTermination
- fields = ('id', 'assigned_object_type_id')
-
- def search(self, queryset, name, value):
- if not value.strip():
- return queryset
- qs_filter = Q(l2vpn__name__icontains=value)
- return queryset.filter(qs_filter)
-
- def filter_assigned_object(self, queryset, name, value):
- qs = queryset.filter(
- Q(**{'{}__in'.format(name): value})
- )
- return qs
-
- def filter_site(self, queryset, name, value):
- qs = queryset.filter(
- Q(
- Q(**{'vlan__site__{}__in'.format(name): value}) |
- Q(**{'interface__device__site__{}__in'.format(name): value}) |
- Q(**{'vminterface__virtual_machine__site__{}__in'.format(name): value})
- )
- )
- return qs
-
- def filter_region(self, queryset, name, value):
- qs = queryset.filter(
- Q(
- Q(**{'vlan__site__region__{}__in'.format(name): value}) |
- Q(**{'interface__device__site__region__{}__in'.format(name): value}) |
- Q(**{'vminterface__virtual_machine__site__region__{}__in'.format(name): value})
- )
- )
- return qs
diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py
index 548d01afa..bf4825be9 100644
--- a/netbox/ipam/forms/bulk_edit.py
+++ b/netbox/ipam/forms/bulk_edit.py
@@ -1,7 +1,8 @@
from django import forms
+from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
-from dcim.models import Region, Site, SiteGroup
+from dcim.models import Location, Rack, Region, Site, SiteGroup
from ipam.choices import *
from ipam.constants import *
from ipam.models import *
@@ -10,9 +11,10 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice
from utilities.forms.fields import (
- CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
+ CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
)
from utilities.forms.widgets import BulkEditNullBooleanSelect
+from virtualization.models import Cluster, ClusterGroup
__all__ = (
'AggregateBulkEditForm',
@@ -21,8 +23,6 @@ __all__ = (
'FHRPGroupBulkEditForm',
'IPAddressBulkEditForm',
'IPRangeBulkEditForm',
- 'L2VPNBulkEditForm',
- 'L2VPNTerminationBulkEditForm',
'PrefixBulkEditForm',
'RIRBulkEditForm',
'RoleBulkEditForm',
@@ -407,11 +407,6 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
- site = DynamicModelChoiceField(
- label=_('Site'),
- queryset=Site.objects.all(),
- required=False
- )
min_vid = forms.IntegerField(
min_value=VLAN_VID_MIN,
max_value=VLAN_VID_MAX,
@@ -429,12 +424,84 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
max_length=200,
required=False
)
+ scope_type = ContentTypeChoiceField(
+ label=_('Scope type'),
+ queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
+ required=False
+ )
+ scope_id = forms.IntegerField(
+ required=False,
+ widget=forms.HiddenInput()
+ )
+ region = DynamicModelChoiceField(
+ label=_('Region'),
+ queryset=Region.objects.all(),
+ required=False
+ )
+ sitegroup = DynamicModelChoiceField(
+ queryset=SiteGroup.objects.all(),
+ required=False,
+ label=_('Site group')
+ )
+ site = DynamicModelChoiceField(
+ label=_('Site'),
+ queryset=Site.objects.all(),
+ required=False,
+ query_params={
+ 'region_id': '$region',
+ 'group_id': '$sitegroup',
+ }
+ )
+ location = DynamicModelChoiceField(
+ label=_('Location'),
+ queryset=Location.objects.all(),
+ required=False,
+ 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,
+ label=_('Cluster group')
+ )
+ cluster = DynamicModelChoiceField(
+ label=_('Cluster'),
+ queryset=Cluster.objects.all(),
+ required=False,
+ query_params={
+ 'group_id': '$clustergroup',
+ }
+ )
model = VLANGroup
fieldsets = (
(None, ('site', 'min_vid', 'max_vid', 'description')),
+ (_('Scope'), ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
)
- nullable_fields = ('site', 'description')
+ nullable_fields = ('description',)
+
+ 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
+ if scope_obj := self.cleaned_data.get(scope_field):
+ self.cleaned_data['scope_id'] = scope_obj.pk
+ self.changed_data.append('scope_id')
+ else:
+ self.cleaned_data.pop('scope_type')
+ self.changed_data.remove('scope_type')
class VLANBulkEditForm(NetBoxModelBulkEditForm):
@@ -527,32 +594,3 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
model = Service
-
-
-class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
- type = forms.ChoiceField(
- label=_('Type'),
- choices=add_blank_choice(L2VPNTypeChoices),
- required=False
- )
- tenant = DynamicModelChoiceField(
- label=_('Tenant'),
- queryset=Tenant.objects.all(),
- required=False
- )
- description = forms.CharField(
- label=_('Description'),
- max_length=200,
- required=False
- )
- comments = CommentField()
-
- model = L2VPN
- fieldsets = (
- (None, ('type', 'tenant', 'description')),
- )
- nullable_fields = ('tenant', 'description', 'comments')
-
-
-class L2VPNTerminationBulkEditForm(NetBoxModelBulkEditForm):
- model = L2VPN
diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py
index ec0812593..0627a6765 100644
--- a/netbox/ipam/forms/bulk_import.py
+++ b/netbox/ipam/forms/bulk_import.py
@@ -1,6 +1,5 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface, Site
@@ -21,8 +20,6 @@ __all__ = (
'FHRPGroupImportForm',
'IPAddressImportForm',
'IPRangeImportForm',
- 'L2VPNImportForm',
- 'L2VPNTerminationImportForm',
'PrefixImportForm',
'RIRImportForm',
'RoleImportForm',
@@ -507,94 +504,25 @@ class ServiceImportForm(NetBoxModelImportForm):
choices=ServiceProtocolChoices,
help_text=_('IP protocol')
)
+ ipaddresses = CSVModelMultipleChoiceField(
+ queryset=IPAddress.objects.all(),
+ required=False,
+ to_field_name='address',
+ help_text=_('IP Address'),
+ )
class Meta:
model = Service
- fields = ('device', 'virtual_machine', 'name', 'protocol', 'ports', 'description', 'comments', 'tags')
+ fields = (
+ 'device', 'virtual_machine', 'ipaddresses', 'name', 'protocol', 'ports', 'description', 'comments', 'tags',
+ )
-
-class L2VPNImportForm(NetBoxModelImportForm):
- tenant = CSVModelChoiceField(
- label=_('Tenant'),
- queryset=Tenant.objects.all(),
- required=False,
- to_field_name='name',
- )
- type = CSVChoiceField(
- label=_('Type'),
- choices=L2VPNTypeChoices,
- help_text=_('L2VPN type')
- )
-
- class Meta:
- model = L2VPN
- fields = ('identifier', 'name', 'slug', 'tenant', 'type', 'description',
- 'comments', 'tags')
-
-
-class L2VPNTerminationImportForm(NetBoxModelImportForm):
- l2vpn = CSVModelChoiceField(
- queryset=L2VPN.objects.all(),
- required=True,
- to_field_name='name',
- label=_('L2VPN'),
- )
- device = CSVModelChoiceField(
- label=_('Device'),
- queryset=Device.objects.all(),
- required=False,
- to_field_name='name',
- help_text=_('Parent device (for interface)')
- )
- virtual_machine = CSVModelChoiceField(
- label=_('Virtual machine'),
- queryset=VirtualMachine.objects.all(),
- required=False,
- to_field_name='name',
- help_text=_('Parent virtual machine (for interface)')
- )
- interface = CSVModelChoiceField(
- label=_('Interface'),
- queryset=Interface.objects.none(), # Can also refer to VMInterface
- required=False,
- to_field_name='name',
- help_text=_('Assigned interface (device or VM)')
- )
- vlan = CSVModelChoiceField(
- label=_('VLAN'),
- queryset=VLAN.objects.all(),
- required=False,
- to_field_name='name',
- help_text=_('Assigned VLAN')
- )
-
- class Meta:
- model = L2VPNTermination
- fields = ('l2vpn', 'device', 'virtual_machine', 'interface', 'vlan', 'tags')
-
- def __init__(self, data=None, *args, **kwargs):
- super().__init__(data, *args, **kwargs)
-
- if data:
-
- # Limit interface queryset by device or VM
- if data.get('device'):
- self.fields['interface'].queryset = Interface.objects.filter(
- **{f"device__{self.fields['device'].to_field_name}": data['device']}
- )
- elif data.get('virtual_machine'):
- self.fields['interface'].queryset = VMInterface.objects.filter(
- **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
+ def clean_ipaddresses(self):
+ parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
+ for ip_address in self.cleaned_data['ipaddresses']:
+ if not ip_address.assigned_object or getattr(ip_address.assigned_object, 'parent_object') != parent:
+ raise forms.ValidationError(
+ _("{ip} is not assigned to this device/VM.").format(ip=ip_address)
)
- def clean(self):
- super().clean()
-
- if self.cleaned_data.get('device') and self.cleaned_data.get('virtual_machine'):
- raise ValidationError(_('Cannot import device and VM interface terminations simultaneously.'))
- if not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
- raise ValidationError(_('Each termination must specify either an interface or a VLAN.'))
- if self.cleaned_data.get('interface') and self.cleaned_data.get('vlan'):
- raise ValidationError(_('Cannot assign both an interface and a VLAN.'))
-
- self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')
+ return self.cleaned_data['ipaddresses']
diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py
index e4e967f81..e6acdb012 100644
--- a/netbox/ipam/forms/filtersets.py
+++ b/netbox/ipam/forms/filtersets.py
@@ -1,5 +1,4 @@
from django import forms
-from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
@@ -9,10 +8,9 @@ from ipam.models import *
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
-from utilities.forms.fields import (
- ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
-)
+from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
from virtualization.models import VirtualMachine
+from vpn.models import L2VPN
__all__ = (
'AggregateFilterForm',
@@ -21,8 +19,6 @@ __all__ = (
'FHRPGroupFilterForm',
'IPAddressFilterForm',
'IPRangeFilterForm',
- 'L2VPNFilterForm',
- 'L2VPNTerminationFilterForm',
'PrefixFilterForm',
'RIRFilterForm',
'RoleFilterForm',
@@ -295,11 +291,12 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPAddress
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
- (_('Attributes'), ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
+ (_('Attributes'), ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name')),
(_('VRF'), ('vrf_id', 'present_in_vrf_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Device/VM'), ('device_id', 'virtual_machine_id')),
)
+ selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
parent = forms.CharField(
required=False,
widget=forms.TextInput(
@@ -357,6 +354,10 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
+ dns_name = forms.CharField(
+ required=False,
+ label=_('DNS Name')
+ )
tag = TagFilterField(model)
@@ -448,6 +449,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
(_('Attributes'), ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
)
+ selector_fields = ('filter_id', 'q', 'site_id')
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
@@ -519,91 +521,19 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
class ServiceFilterForm(ServiceTemplateFilterForm):
model = Service
- tag = TagFilterField(model)
-
-
-class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
- model = L2VPN
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
- (_('Attributes'), ('type', 'import_target_id', 'export_target_id')),
- (_('Tenant'), ('tenant_group_id', 'tenant_id')),
- )
- type = forms.ChoiceField(
- label=_('Type'),
- choices=add_blank_choice(L2VPNTypeChoices),
- required=False
- )
- import_target_id = DynamicModelMultipleChoiceField(
- queryset=RouteTarget.objects.all(),
- required=False,
- label=_('Import targets')
- )
- export_target_id = DynamicModelMultipleChoiceField(
- queryset=RouteTarget.objects.all(),
- required=False,
- label=_('Export targets')
- )
- tag = TagFilterField(model)
-
-
-class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
- model = L2VPNTermination
- fieldsets = (
- (None, ('filter_id', 'l2vpn_id',)),
- (_('Assigned Object'), (
- 'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
- )),
- )
- l2vpn_id = DynamicModelChoiceField(
- queryset=L2VPN.objects.all(),
- required=False,
- label=_('L2VPN')
- )
- assigned_object_type_id = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS),
- required=False,
- label=_('Assigned Object Type'),
- limit_choices_to=L2VPN_ASSIGNMENT_MODELS
- )
- region_id = DynamicModelMultipleChoiceField(
- queryset=Region.objects.all(),
- required=False,
- label=_('Region')
- )
- site_id = DynamicModelMultipleChoiceField(
- queryset=Site.objects.all(),
- required=False,
- null_option='None',
- query_params={
- 'region_id': '$region_id'
- },
- label=_('Site')
+ (_('Attributes'), ('protocol', 'port')),
+ (_('Assignment'), ('device_id', 'virtual_machine_id')),
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
- null_option='None',
- query_params={
- 'site_id': '$site_id'
- },
- label=_('Device')
- )
- vlan_id = DynamicModelMultipleChoiceField(
- queryset=VLAN.objects.all(),
- required=False,
- null_option='None',
- query_params={
- 'site_id': '$site_id'
- },
- label=_('VLAN')
+ label=_('Device'),
)
virtual_machine_id = DynamicModelMultipleChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
- null_option='None',
- query_params={
- 'site_id': '$site_id'
- },
- label=_('Virtual Machine')
+ label=_('Virtual Machine'),
)
+ tag = TagFilterField(model)
diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py
index c466e279f..6c445ef27 100644
--- a/netbox/ipam/forms/model_forms.py
+++ b/netbox/ipam/forms/model_forms.py
@@ -29,8 +29,6 @@ __all__ = (
'IPAddressBulkAddForm',
'IPAddressForm',
'IPRangeForm',
- 'L2VPNForm',
- 'L2VPNTerminationForm',
'PrefixForm',
'RIRForm',
'RoleForm',
@@ -215,6 +213,9 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
queryset=VLAN.objects.all(),
required=False,
selector=True,
+ query_params={
+ 'site_id': '$site',
+ },
label=_('VLAN'),
)
role = DynamicModelChoiceField(
@@ -351,7 +352,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
})
elif selected_objects:
assigned_object = self.cleaned_data[selected_objects[0]]
- if self.instance.pk and self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object:
+ if self.instance.pk and self.instance.assigned_object and self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object:
raise ValidationError(
_("Cannot reassign IP address while it is designated as the primary IP for the parent object")
)
@@ -369,14 +370,14 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
# Do not allow assigning a network ID or broadcast address to an interface.
if interface and (address := self.cleaned_data.get('address')):
if address.ip == address.network:
- msg = _("{address} is a network ID, which may not be assigned to an interface.").format(address=address)
+ msg = _("{ip} is a network ID, which may not be assigned to an interface.").format(ip=address.ip)
if address.version == 4 and address.prefixlen not in (31, 32):
raise ValidationError(msg)
if address.version == 6 and address.prefixlen not in (127, 128):
raise ValidationError(msg)
if address.version == 4 and address.ip == address.broadcast and address.prefixlen not in (31, 32):
- msg = _("{address} is a broadcast address, which may not be assigned to an interface.").format(
- address=address
+ msg = _("{ip} is a broadcast address, which may not be assigned to an interface.").format(
+ ip=address.ip
)
raise ValidationError(msg)
@@ -728,7 +729,7 @@ class ServiceCreateForm(ServiceForm):
class Meta(ServiceForm.Meta):
fields = [
'device', 'virtual_machine', 'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description',
- 'tags',
+ 'comments', 'tags',
]
def __init__(self, *args, **kwargs):
@@ -751,97 +752,3 @@ class ServiceCreateForm(ServiceForm):
self.cleaned_data['description'] = service_template.description
elif not all(self.cleaned_data[f] for f in ('name', 'protocol', 'ports')):
raise forms.ValidationError("Must specify name, protocol, and port(s) if not using a service template.")
-
-
-#
-# L2VPN
-#
-
-
-class L2VPNForm(TenancyForm, NetBoxModelForm):
- slug = SlugField()
- import_targets = DynamicModelMultipleChoiceField(
- label=_('Import targets'),
- queryset=RouteTarget.objects.all(),
- required=False
- )
- export_targets = DynamicModelMultipleChoiceField(
- label=_('Export targets'),
- queryset=RouteTarget.objects.all(),
- required=False
- )
- comments = CommentField()
-
- fieldsets = (
- (_('L2VPN'), ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
- (_('Route Targets'), ('import_targets', 'export_targets')),
- (_('Tenancy'), ('tenant_group', 'tenant')),
- )
-
- class Meta:
- model = L2VPN
- fields = (
- 'name', 'slug', 'type', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description',
- 'comments', 'tags'
- )
-
-
-class L2VPNTerminationForm(NetBoxModelForm):
- l2vpn = DynamicModelChoiceField(
- queryset=L2VPN.objects.all(),
- required=True,
- query_params={},
- label=_('L2VPN'),
- fetch_trigger='open'
- )
- vlan = DynamicModelChoiceField(
- queryset=VLAN.objects.all(),
- required=False,
- selector=True,
- label=_('VLAN')
- )
- interface = DynamicModelChoiceField(
- label=_('Interface'),
- queryset=Interface.objects.all(),
- required=False,
- selector=True
- )
- vminterface = DynamicModelChoiceField(
- queryset=VMInterface.objects.all(),
- required=False,
- selector=True,
- label=_('Interface')
- )
-
- class Meta:
- model = L2VPNTermination
- fields = ('l2vpn', )
-
- def __init__(self, *args, **kwargs):
- instance = kwargs.get('instance')
- initial = kwargs.get('initial', {}).copy()
-
- if instance:
- if type(instance.assigned_object) is Interface:
- initial['interface'] = instance.assigned_object
- elif type(instance.assigned_object) is VLAN:
- initial['vlan'] = instance.assigned_object
- elif type(instance.assigned_object) is VMInterface:
- initial['vminterface'] = instance.assigned_object
- kwargs['initial'] = initial
-
- super().__init__(*args, **kwargs)
-
- def clean(self):
- super().clean()
-
- interface = self.cleaned_data.get('interface')
- vminterface = self.cleaned_data.get('vminterface')
- vlan = self.cleaned_data.get('vlan')
-
- if not (interface or vminterface or vlan):
- raise ValidationError(_('A termination must specify an interface or VLAN.'))
- if len([x for x in (interface, vminterface, vlan) if x]) > 1:
- raise ValidationError(_('A termination can only have one terminating object (an interface or VLAN).'))
-
- self.instance.assigned_object = interface or vminterface or vlan
diff --git a/netbox/ipam/graphql/schema.py b/netbox/ipam/graphql/schema.py
index 596b5eb78..6627c540e 100644
--- a/netbox/ipam/graphql/schema.py
+++ b/netbox/ipam/graphql/schema.py
@@ -1,9 +1,8 @@
import graphene
+
from ipam import models
-from utilities.graphql_optimizer import gql_query_optimizer
-
from netbox.graphql.fields import ObjectField, ObjectListField
-
+from utilities.graphql_optimizer import gql_query_optimizer
from .types import *
@@ -38,18 +37,6 @@ class IPAMQuery(graphene.ObjectType):
def resolve_ip_range_list(root, info, **kwargs):
return gql_query_optimizer(models.IPRange.objects.all(), info)
- l2vpn = ObjectField(L2VPNType)
- l2vpn_list = ObjectListField(L2VPNType)
-
- def resolve_l2vpn_list(root, info, **kwargs):
- return gql_query_optimizer(models.L2VPN.objects.all(), info)
-
- l2vpn_termination = ObjectField(L2VPNTerminationType)
- l2vpn_termination_list = ObjectListField(L2VPNTerminationType)
-
- def resolve_l2vpn_termination_list(root, info, **kwargs):
- return gql_query_optimizer(models.L2VPNTermination.objects.all(), info)
-
prefix = ObjectField(PrefixType)
prefix_list = ObjectListField(PrefixType)
diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py
index 6e834512e..b4350f9f2 100644
--- a/netbox/ipam/graphql/types.py
+++ b/netbox/ipam/graphql/types.py
@@ -1,6 +1,5 @@
import graphene
-from extras.graphql.mixins import ContactsMixin
from ipam import filtersets, models
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
@@ -13,8 +12,6 @@ __all__ = (
'FHRPGroupAssignmentType',
'IPAddressType',
'IPRangeType',
- 'L2VPNType',
- 'L2VPNTerminationType',
'PrefixType',
'RIRType',
'RoleType',
@@ -188,19 +185,3 @@ class VRFType(NetBoxObjectType):
model = models.VRF
fields = '__all__'
filterset_class = filtersets.VRFFilterSet
-
-
-class L2VPNType(ContactsMixin, NetBoxObjectType):
- class Meta:
- model = models.L2VPN
- fields = '__all__'
- filtersets_class = filtersets.L2VPNFilterSet
-
-
-class L2VPNTerminationType(NetBoxObjectType):
- assigned_object = graphene.Field('ipam.graphql.gfk_mixins.L2VPNAssignmentType')
-
- class Meta:
- model = models.L2VPNTermination
- exclude = ('assigned_object_type', 'assigned_object_id')
- filtersets_class = filtersets.L2VPNTerminationFilterSet
diff --git a/netbox/ipam/migrations/0068_move_l2vpn.py b/netbox/ipam/migrations/0068_move_l2vpn.py
new file mode 100644
index 000000000..b1a059de1
--- /dev/null
+++ b/netbox/ipam/migrations/0068_move_l2vpn.py
@@ -0,0 +1,64 @@
+from django.db import migrations
+
+
+def update_content_types(apps, schema_editor):
+ ContentType = apps.get_model('contenttypes', 'ContentType')
+
+ # Delete the new ContentTypes effected by the new models in the vpn app
+ ContentType.objects.filter(app_label='vpn', model='l2vpn').delete()
+ ContentType.objects.filter(app_label='vpn', model='l2vpntermination').delete()
+
+ # Update the app labels of the original ContentTypes for ipam.L2VPN and ipam.L2VPNTermination to ensure
+ # that any foreign key references are preserved
+ ContentType.objects.filter(app_label='ipam', model='l2vpn').update(app_label='vpn')
+ ContentType.objects.filter(app_label='ipam', model='l2vpntermination').update(app_label='vpn')
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ipam', '0067_ipaddress_index_host'),
+ ]
+
+ operations = [
+ migrations.RemoveConstraint(
+ model_name='l2vpntermination',
+ name='ipam_l2vpntermination_assigned_object',
+ ),
+ migrations.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.RemoveField(
+ model_name='l2vpntermination',
+ name='assigned_object_type',
+ ),
+ migrations.RemoveField(
+ model_name='l2vpntermination',
+ name='l2vpn',
+ ),
+ migrations.RemoveField(
+ model_name='l2vpntermination',
+ name='tags',
+ ),
+ migrations.DeleteModel(
+ name='L2VPN',
+ ),
+ migrations.DeleteModel(
+ name='L2VPNTermination',
+ ),
+ ],
+ database_operations=[
+ migrations.AlterModelTable(
+ name='L2VPN',
+ table='vpn_l2vpn',
+ ),
+ migrations.AlterModelTable(
+ name='L2VPNTermination',
+ table='vpn_l2vpntermination',
+ ),
+ ],
+ ),
+ migrations.RunPython(
+ code=update_content_types,
+ reverse_code=migrations.RunPython.noop
+ ),
+ ]
diff --git a/netbox/ipam/migrations/0069_gfk_indexes.py b/netbox/ipam/migrations/0069_gfk_indexes.py
new file mode 100644
index 000000000..75c016102
--- /dev/null
+++ b/netbox/ipam/migrations/0069_gfk_indexes.py
@@ -0,0 +1,25 @@
+# Generated by Django 4.2.7 on 2023-12-07 16:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ipam', '0068_move_l2vpn'),
+ ]
+
+ operations = [
+ migrations.AddIndex(
+ model_name='fhrpgroupassignment',
+ index=models.Index(fields=['interface_type', 'interface_id'], name='ipam_fhrpgr_interfa_2acc3f_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='ipaddress',
+ index=models.Index(fields=['assigned_object_type', 'assigned_object_id'], name='ipam_ipaddr_assigne_890ab8_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='vlangroup',
+ index=models.Index(fields=['scope_type', 'scope_id'], name='ipam_vlangr_scope_t_9da557_idx'),
+ ),
+ ]
diff --git a/netbox/ipam/models/__init__.py b/netbox/ipam/models/__init__.py
index a00919ee0..0d0b3d6ac 100644
--- a/netbox/ipam/models/__init__.py
+++ b/netbox/ipam/models/__init__.py
@@ -3,27 +3,5 @@ from .asns import *
from .fhrp import *
from .vrfs import *
from .ip import *
-from .l2vpn import *
from .services import *
from .vlans import *
-
-__all__ = (
- 'ASN',
- 'ASNRange',
- 'Aggregate',
- 'IPAddress',
- 'IPRange',
- 'FHRPGroup',
- 'FHRPGroupAssignment',
- 'L2VPN',
- 'L2VPNTermination',
- 'Prefix',
- 'RIR',
- 'Role',
- 'RouteTarget',
- 'Service',
- 'ServiceTemplate',
- 'VLAN',
- 'VLANGroup',
- 'VRF',
-)
diff --git a/netbox/ipam/models/fhrp.py b/netbox/ipam/models/fhrp.py
index 5d355102f..c3a7084b6 100644
--- a/netbox/ipam/models/fhrp.py
+++ b/netbox/ipam/models/fhrp.py
@@ -1,13 +1,12 @@
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
-from django.contrib.contenttypes.models import ContentType
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 netbox.models import ChangeLoggedModel, PrimaryModel
from ipam.choices import *
from ipam.constants import *
+from netbox.models import ChangeLoggedModel, PrimaryModel
__all__ = (
'FHRPGroup',
@@ -78,7 +77,7 @@ class FHRPGroup(PrimaryModel):
class FHRPGroupAssignment(ChangeLoggedModel):
interface_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
on_delete=models.CASCADE
)
interface_id = models.PositiveBigIntegerField()
@@ -102,6 +101,9 @@ class FHRPGroupAssignment(ChangeLoggedModel):
class Meta:
ordering = ('-priority', 'pk')
+ indexes = (
+ models.Index(fields=('interface_type', 'interface_id')),
+ )
constraints = (
models.UniqueConstraint(
fields=('interface_type', 'interface_id', 'group'),
diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py
index 553f5eb92..01e2ed1c7 100644
--- a/netbox/ipam/models/ip.py
+++ b/netbox/ipam/models/ip.py
@@ -1,6 +1,5 @@
import netaddr
from django.contrib.contenttypes.fields import GenericForeignKey
-from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F
@@ -9,6 +8,7 @@ from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
+from core.models import ContentType
from ipam.choices import *
from ipam.constants import *
from ipam.fields import IPNetworkField, IPAddressField
@@ -140,8 +140,11 @@ class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
if covering_aggregates:
raise ValidationError({
'prefix': _(
- "Aggregates cannot overlap. {} is already covered by an existing aggregate ({})."
- ).format(self.prefix, covering_aggregates[0])
+ "Aggregates cannot overlap. {prefix} is already covered by an existing aggregate ({aggregate})."
+ ).format(
+ prefix=self.prefix,
+ aggregate=covering_aggregates[0]
+ )
})
# Ensure that the aggregate being added does not cover an existing aggregate
@@ -150,8 +153,11 @@ class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
covered_aggregates = covered_aggregates.exclude(pk=self.pk)
if covered_aggregates:
raise ValidationError({
- 'prefix': _("Aggregates cannot overlap. {} covers an existing aggregate ({}).").format(
- self.prefix, covered_aggregates[0]
+ 'prefix': _(
+ "Prefixes cannot overlap aggregates. {prefix} covers an existing aggregate ({aggregate})."
+ ).format(
+ prefix=self.prefix,
+ aggregate=covered_aggregates[0]
)
})
@@ -290,8 +296,8 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
super().__init__(*args, **kwargs)
# Cache the original prefix and VRF so we can check if they have changed on post_save
- self._prefix = self.prefix
- self._vrf_id = self.vrf_id
+ self._prefix = self.__dict__.get('prefix')
+ self._vrf_id = self.__dict__.get('vrf_id')
def __str__(self):
return str(self.prefix)
@@ -314,10 +320,11 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
duplicate_prefixes = self.get_duplicates()
if duplicate_prefixes:
+ table = _("VRF {vrf}").format(vrf=self.vrf) if self.vrf else _("global table")
raise ValidationError({
- 'prefix': _("Duplicate prefix found in {}: {}").format(
- _("VRF {}").format(self.vrf) if self.vrf else _("global table"),
- duplicate_prefixes.first(),
+ 'prefix': _("Duplicate prefix found in {table}: {prefix}").format(
+ table=table,
+ prefix=duplicate_prefixes.first(),
)
})
@@ -554,25 +561,13 @@ class IPRange(PrimaryModel):
# Check that start & end IP versions match
if self.start_address.version != self.end_address.version:
raise ValidationError({
- 'end_address': _(
- "Ending address version (IPv{end_address_version}) does not match starting address "
- "(IPv{start_address_version})"
- ).format(
- end_address_version=self.end_address.version,
- start_address_version=self.start_address.version
- )
+ 'end_address': _("Starting and ending IP address versions must match")
})
# Check that the start & end IP prefix lengths match
if self.start_address.prefixlen != self.end_address.prefixlen:
raise ValidationError({
- 'end_address': _(
- "Ending address mask (/{end_address_prefixlen}) does not match starting address mask "
- "(/{start_address_prefixlen})"
- ).format(
- end_address_prefixlen=self.end_address.prefixlen,
- start_address_prefixlen=self.start_address.prefixlen
- )
+ 'end_address': _("Starting and ending IP address masks must match")
})
# Check that the ending address is greater than the starting address
@@ -745,7 +740,7 @@ class IPAddress(PrimaryModel):
help_text=_('The functional role of this IP')
)
assigned_object_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
limit_choices_to=IPADDRESS_ASSIGNMENT_MODELS,
on_delete=models.PROTECT,
related_name='+',
@@ -785,15 +780,23 @@ class IPAddress(PrimaryModel):
class Meta:
ordering = ('address', 'pk') # address may be non-unique
- indexes = [
+ indexes = (
models.Index(Cast(Host('address'), output_field=IPAddressField()), name='ipam_ipaddress_host'),
- ]
+ models.Index(fields=('assigned_object_type', 'assigned_object_id')),
+ )
verbose_name = _('IP address')
verbose_name_plural = _('IP addresses')
def __str__(self):
return str(self.address)
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Denote the original assigned object (if any) for validation in clean()
+ 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])
@@ -848,13 +851,32 @@ class IPAddress(PrimaryModel):
self.role not in IPADDRESS_ROLES_NONUNIQUE or
any(dip.role not in IPADDRESS_ROLES_NONUNIQUE for dip in duplicate_ips)
):
+ table = _("VRF {vrf}").format(vrf=self.vrf) if self.vrf else _("global table")
raise ValidationError({
- 'address': _("Duplicate IP address found in {}: {}").format(
- _("VRF {}").format(self.vrf) if self.vrf else _("global table"),
- duplicate_ips.first(),
+ 'address': _("Duplicate IP address found in {table}: {ipaddress}").format(
+ table=table,
+ ipaddress=duplicate_ips.first(),
)
})
+ if self._original_assigned_object_id and self._original_assigned_object_type_id:
+ parent = getattr(self.assigned_object, 'parent_object', None)
+ ct = ContentType.objects.get_for_id(self._original_assigned_object_type_id)
+ original_assigned_object = ct.get_object_for_this_type(pk=self._original_assigned_object_id)
+ original_parent = getattr(original_assigned_object, 'parent_object', None)
+
+ # can't use is_primary_ip as self.assigned_object might be changed
+ is_primary = False
+ if self.family == 4 and hasattr(original_parent, 'primary_ip4') and original_parent.primary_ip4_id == self.pk:
+ is_primary = True
+ if self.family == 6 and hasattr(original_parent, 'primary_ip6') and original_parent.primary_ip6_id == self.pk:
+ is_primary = True
+
+ if is_primary and (parent != original_parent):
+ raise ValidationError(
+ _("Cannot reassign IP address while it is designated as the primary IP for the parent object")
+ )
+
# Validate IP status selection
if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6:
raise ValidationError({
@@ -892,7 +914,7 @@ class IPAddress(PrimaryModel):
def is_oob_ip(self):
if self.assigned_object:
parent = getattr(self.assigned_object, 'parent_object', None)
- if parent.oob_ip_id == self.pk:
+ if hasattr(parent, 'oob_ip') and parent.oob_ip_id == self.pk:
return True
return False
@@ -900,9 +922,9 @@ class IPAddress(PrimaryModel):
def is_primary_ip(self):
if self.assigned_object:
parent = getattr(self.assigned_object, 'parent_object', None)
- if self.family == 4 and parent.primary_ip4_id == self.pk:
+ if self.family == 4 and hasattr(parent, 'primary_ip4') and parent.primary_ip4_id == self.pk:
return True
- if self.family == 6 and parent.primary_ip6_id == self.pk:
+ if self.family == 6 and hasattr(parent, 'primary_ip6') and parent.primary_ip6_id == self.pk:
return True
return False
diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py
index 927d01fa5..7434bd0b4 100644
--- a/netbox/ipam/models/vlans.py
+++ b/netbox/ipam/models/vlans.py
@@ -1,5 +1,4 @@
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
-from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@@ -32,7 +31,7 @@ class VLANGroup(OrganizationalModel):
max_length=100
)
scope_type = models.ForeignKey(
- to=ContentType,
+ to='contenttypes.ContentType',
on_delete=models.CASCADE,
limit_choices_to=Q(model__in=VLANGROUP_SCOPE_TYPES),
blank=True,
@@ -69,6 +68,9 @@ class VLANGroup(OrganizationalModel):
class Meta:
ordering = ('name', 'pk') # Name may be non-unique
+ indexes = (
+ models.Index(fields=('scope_type', 'scope_id')),
+ )
constraints = (
models.UniqueConstraint(
fields=('scope_type', 'scope_id', 'name'),
@@ -118,6 +120,12 @@ class VLANGroup(OrganizationalModel):
return available_vids[0]
return None
+ def get_child_vlans(self):
+ """
+ Return all VLANs within this group.
+ """
+ return VLAN.objects.filter(group=self).order_by('vid')
+
class VLAN(PrimaryModel):
"""
@@ -178,9 +186,8 @@ class VLAN(PrimaryModel):
null=True,
help_text=_("The primary function of this VLAN")
)
-
l2vpn_terminations = GenericRelation(
- to='ipam.L2VPNTermination',
+ to='vpn.L2VPNTermination',
content_type_field='assigned_object_type',
object_id_field='assigned_object_id',
related_query_name='vlan'
@@ -218,18 +225,18 @@ class VLAN(PrimaryModel):
# Validate VLAN group (if assigned)
if self.group and self.site and self.group.scope != self.site:
- raise ValidationError({
- 'group': _(
+ raise ValidationError(
+ _(
"VLAN is assigned to group {group} (scope: {scope}); cannot also assign to site {site}."
).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 {min_vid} and {max_vid} for VLANs in group {group}"
- ).format(min_vid=self.group.min_vid, max_vid=self.group.max_vid, group=self.group)
+ "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)
})
def get_status_color(self):
diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py
index 39da0c3a2..2ff8a8b6e 100644
--- a/netbox/ipam/querysets.py
+++ b/netbox/ipam/querysets.py
@@ -69,6 +69,35 @@ class VLANGroupQuerySet(RestrictedQuerySet):
class VLANQuerySet(RestrictedQuerySet):
+ def get_for_site(self, site):
+ """
+ Return all VLANs in the specified site
+ """
+ from .models import VLANGroup
+ q = Q()
+ q |= Q(
+ scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'),
+ scope_id=site.pk
+ )
+
+ if site.region:
+ q |= Q(
+ scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'),
+ scope_id__in=site.region.get_ancestors(include_self=True)
+ )
+ if site.group:
+ q |= Q(
+ scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
+ scope_id__in=site.group.get_ancestors(include_self=True)
+ )
+
+ return self.filter(
+ Q(group__in=VLANGroup.objects.filter(q)) |
+ Q(site=site) |
+ Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
+ Q(group__isnull=True, site__isnull=True) # Global VLANs
+ )
+
def get_for_device(self, device):
"""
Return all VLANs available to the specified Device.
diff --git a/netbox/ipam/search.py b/netbox/ipam/search.py
index 4d97bf5f0..a1cddbb1a 100644
--- a/netbox/ipam/search.py
+++ b/netbox/ipam/search.py
@@ -1,5 +1,5 @@
-from . import models
from netbox.search import SearchIndex, register_search
+from . import models
@register_search
@@ -11,6 +11,7 @@ class AggregateIndex(SearchIndex):
('date_added', 2000),
('comments', 5000),
)
+ display_attrs = ('rir', 'tenant', 'description')
@register_search
@@ -20,6 +21,7 @@ class ASNIndex(SearchIndex):
('asn', 100),
('description', 500),
)
+ display_attrs = ('rir', 'tenant', 'description')
@register_search
@@ -28,6 +30,7 @@ class ASNRangeIndex(SearchIndex):
fields = (
('description', 500),
)
+ display_attrs = ('rir', 'tenant', 'description')
@register_search
@@ -39,6 +42,7 @@ class FHRPGroupIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('protocol', 'auth_type', 'description')
@register_search
@@ -50,6 +54,7 @@ class IPAddressIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
@register_search
@@ -61,17 +66,7 @@ class IPRangeIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
-
-
-@register_search
-class L2VPNIndex(SearchIndex):
- model = models.L2VPN
- fields = (
- ('name', 100),
- ('slug', 110),
- ('description', 500),
- ('comments', 5000),
- )
+ display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
@register_search
@@ -82,6 +77,7 @@ class PrefixIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description')
@register_search
@@ -92,6 +88,7 @@ class RIRIndex(SearchIndex):
('slug', 110),
('description', 500),
)
+ display_attrs = ('description',)
@register_search
@@ -102,6 +99,7 @@ class RoleIndex(SearchIndex):
('slug', 110),
('description', 500),
)
+ display_attrs = ('description',)
@register_search
@@ -112,6 +110,7 @@ class RouteTargetIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('tenant', 'description')
@register_search
@@ -122,6 +121,7 @@ class ServiceIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('device', 'virtual_machine', 'description')
@register_search
@@ -132,6 +132,7 @@ class ServiceTemplateIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('description',)
@register_search
@@ -143,6 +144,7 @@ class VLANIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('site', 'group', 'tenant', 'status', 'role', 'description')
@register_search
@@ -154,6 +156,7 @@ class VLANGroupIndex(SearchIndex):
('description', 500),
('max_vid', 2000),
)
+ display_attrs = ('scope_type', 'min_vid', 'max_vid', 'description')
@register_search
@@ -165,3 +168,4 @@ class VRFIndex(SearchIndex):
('description', 500),
('comments', 5000),
)
+ display_attrs = ('rd', 'tenant', 'description')
diff --git a/netbox/ipam/signals.py b/netbox/ipam/signals.py
index 2a985c294..3b36b561f 100644
--- a/netbox/ipam/signals.py
+++ b/netbox/ipam/signals.py
@@ -56,8 +56,12 @@ def clear_primary_ip(instance, **kwargs):
"""
field_name = f'primary_ip{instance.family}'
if device := Device.objects.filter(**{field_name: instance}).first():
+ device.snapshot()
+ setattr(device, field_name, None)
device.save()
if virtualmachine := VirtualMachine.objects.filter(**{field_name: instance}).first():
+ virtualmachine.snapshot()
+ setattr(virtualmachine, field_name, None)
virtualmachine.save()
@@ -67,4 +71,6 @@ def clear_oob_ip(instance, **kwargs):
When an IPAddress is deleted, trigger save() on any Devices for which it was a OOB IP.
"""
if device := Device.objects.filter(oob_ip=instance).first():
+ device.snapshot()
+ device.oob_ip = None
device.save()
diff --git a/netbox/ipam/tables/__init__.py b/netbox/ipam/tables/__init__.py
index 7d04a5fea..95676b82c 100644
--- a/netbox/ipam/tables/__init__.py
+++ b/netbox/ipam/tables/__init__.py
@@ -1,7 +1,6 @@
from .asn import *
from .fhrp import *
from .ip import *
-from .l2vpn import *
from .services import *
from .vlans import *
from .vrfs import *
diff --git a/netbox/ipam/tables/asn.py b/netbox/ipam/tables/asn.py
index 6bb15523e..bbe38dc1a 100644
--- a/netbox/ipam/tables/asn.py
+++ b/netbox/ipam/tables/asn.py
@@ -48,6 +48,7 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable):
asn_asdot = tables.Column(
accessor=tables.A('asn_asdot'),
linkify=True,
+ order_by=tables.A('asn'),
verbose_name=_('ASDOT')
)
site_count = columns.LinkedCountColumn(
diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py
index 24d219ca0..cb633e162 100644
--- a/netbox/ipam/tests/test_api.py
+++ b/netbox/ipam/tests/test_api.py
@@ -659,6 +659,62 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase):
)
IPAddress.objects.bulk_create(ip_addresses)
+ def test_assign_object(self):
+ """
+ Test the creation of available IP addresses within a parent IP range.
+ """
+ site = Site.objects.create(name='Site 1')
+ manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
+ device_type = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
+ role = DeviceRole.objects.create(name='Switch')
+ device1 = Device.objects.create(
+ name='Device 1',
+ site=site,
+ device_type=device_type,
+ role=role,
+ status='active'
+ )
+ interface1 = Interface.objects.create(name='Interface 1', device=device1, type='1000baset')
+ interface2 = Interface.objects.create(name='Interface 2', device=device1, type='1000baset')
+ device2 = Device.objects.create(
+ name='Device 2',
+ site=site,
+ device_type=device_type,
+ role=role,
+ status='active'
+ )
+ interface3 = Interface.objects.create(name='Interface 3', device=device2, type='1000baset')
+
+ ip_addresses = (
+ IPAddress(address=IPNetwork('192.168.0.4/24'), assigned_object=interface1),
+ IPAddress(address=IPNetwork('192.168.1.4/24')),
+ )
+ IPAddress.objects.bulk_create(ip_addresses)
+
+ ip1 = ip_addresses[0]
+ ip1.assigned_object = interface1
+ 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')
+
+ # assign to same parent
+ data = {
+ 'assigned_object_id': interface2.pk
+ }
+ response = self.client.patch(url, data, format='json', **self.header)
+ self.assertHttpStatus(response, status.HTTP_200_OK)
+
+ # assign to same different parent - should error
+ data = {
+ 'assigned_object_id': interface3.pk
+ }
+ response = self.client.patch(url, data, format='json', **self.header)
+ self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
+
class FHRPGroupTest(APIViewTestCases.APIViewTestCase):
model = FHRPGroup
@@ -1044,96 +1100,3 @@ class ServiceTest(APIViewTestCases.APIViewTestCase):
'ports': [6],
},
]
-
-
-class L2VPNTest(APIViewTestCases.APIViewTestCase):
- model = L2VPN
- brief_fields = ['display', 'id', 'identifier', 'name', 'slug', 'type', 'url']
- create_data = [
- {
- 'name': 'L2VPN 4',
- 'slug': 'l2vpn-4',
- 'type': 'vxlan',
- 'identifier': 33343344
- },
- {
- 'name': 'L2VPN 5',
- 'slug': 'l2vpn-5',
- 'type': 'vxlan',
- 'identifier': 33343345
- },
- {
- 'name': 'L2VPN 6',
- 'slug': 'l2vpn-6',
- 'type': 'vpws',
- 'identifier': 33343346
- },
- ]
- bulk_update_data = {
- 'description': 'New description',
- }
-
- @classmethod
- def setUpTestData(cls):
-
- l2vpns = (
- L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
- L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
- L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
- )
- L2VPN.objects.bulk_create(l2vpns)
-
-
-class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase):
- model = L2VPNTermination
- brief_fields = ['display', 'id', 'l2vpn', 'url']
-
- @classmethod
- def setUpTestData(cls):
-
- vlans = (
- VLAN(name='VLAN 1', vid=651),
- VLAN(name='VLAN 2', vid=652),
- VLAN(name='VLAN 3', vid=653),
- VLAN(name='VLAN 4', vid=654),
- VLAN(name='VLAN 5', vid=655),
- VLAN(name='VLAN 6', vid=656),
- VLAN(name='VLAN 7', vid=657)
- )
- VLAN.objects.bulk_create(vlans)
-
- l2vpns = (
- L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
- L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
- L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
- )
- L2VPN.objects.bulk_create(l2vpns)
-
- l2vpnterminations = (
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
- )
- L2VPNTermination.objects.bulk_create(l2vpnterminations)
-
- cls.create_data = [
- {
- 'l2vpn': l2vpns[0].pk,
- 'assigned_object_type': 'ipam.vlan',
- 'assigned_object_id': vlans[3].pk,
- },
- {
- 'l2vpn': l2vpns[0].pk,
- 'assigned_object_type': 'ipam.vlan',
- 'assigned_object_id': vlans[4].pk,
- },
- {
- 'l2vpn': l2vpns[0].pk,
- 'assigned_object_type': 'ipam.vlan',
- 'assigned_object_id': vlans[5].pk,
- },
- ]
-
- cls.bulk_update_data = {
- 'l2vpn': l2vpns[2].pk
- }
diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py
index 4adff78ef..bb4f50c21 100644
--- a/netbox/ipam/tests/test_filtersets.py
+++ b/netbox/ipam/tests/test_filtersets.py
@@ -7,10 +7,9 @@ from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Man
from ipam.choices import *
from ipam.filtersets import *
from ipam.models import *
+from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
-from tenancy.models import Tenant, TenantGroup
-from rest_framework import serializers
class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
@@ -40,7 +39,7 @@ class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
tenant=None,
start=65000,
end=65009,
- description='aaa'
+ description='foobar1'
),
ASNRange(
name='ASN Range 2',
@@ -49,7 +48,7 @@ class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
tenant=tenants[0],
start=65010,
end=65019,
- description='bbb'
+ description='foobar2'
),
ASNRange(
name='ASN Range 3',
@@ -58,11 +57,15 @@ class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
tenant=tenants[1],
start=65020,
end=65029,
- description='ccc'
+ description='foobar3'
),
)
ASNRange.objects.bulk_create(asn_ranges)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['ASN Range 1', 'ASN Range 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -90,7 +93,7 @@ class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
- params = {'description': ['aaa', 'bbb']}
+ params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -124,9 +127,9 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
asns = (
- ASN(asn=65001, rir=rirs[0], tenant=tenants[0], description='aaa'),
- ASN(asn=65002, rir=rirs[1], tenant=tenants[1], description='bbb'),
- ASN(asn=65003, rir=rirs[2], tenant=tenants[2], description='ccc'),
+ ASN(asn=65001, rir=rirs[0], tenant=tenants[0], description='foobar1'),
+ ASN(asn=65002, rir=rirs[1], tenant=tenants[1], description='foobar2'),
+ ASN(asn=65003, rir=rirs[2], tenant=tenants[2], description='foobar3'),
ASN(asn=4200000000, rir=rirs[0], tenant=tenants[0]),
ASN(asn=4200000001, rir=rirs[1], tenant=tenants[1]),
ASN(asn=4200000002, rir=rirs[2], tenant=tenants[2]),
@@ -140,6 +143,10 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
asns[4].sites.set([sites[1]])
asns[5].sites.set([sites[2]])
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_asn(self):
params = {'asn': [65001, 4200000000]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -166,7 +173,7 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_description(self):
- params = {'description': ['aaa', 'bbb']}
+ params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -215,6 +222,10 @@ class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
vrfs[2].import_targets.add(route_targets[2])
vrfs[2].export_targets.add(route_targets[2])
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['VRF 1', 'VRF 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -311,6 +322,10 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
vrfs[1].import_targets.add(route_targets[4], route_targets[5])
vrfs[1].export_targets.add(route_targets[6], route_targets[7])
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['65000:1001', '65000:1002', '65000:1003']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
@@ -356,15 +371,19 @@ class RIRTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
rirs = (
- RIR(name='RIR 1', slug='rir-1', is_private=False, description='A'),
- RIR(name='RIR 2', slug='rir-2', is_private=False, description='B'),
- RIR(name='RIR 3', slug='rir-3', is_private=False, description='C'),
- RIR(name='RIR 4', slug='rir-4', is_private=True, description='D'),
- RIR(name='RIR 5', slug='rir-5', is_private=True, description='E'),
- RIR(name='RIR 6', slug='rir-6', is_private=True, description='F'),
+ RIR(name='RIR 1', slug='rir-1', is_private=False, description='foobar1'),
+ RIR(name='RIR 2', slug='rir-2', is_private=False, description='foobar2'),
+ RIR(name='RIR 3', slug='rir-3', is_private=False, description='foobar3'),
+ RIR(name='RIR 4', slug='rir-4', is_private=True),
+ RIR(name='RIR 5', slug='rir-5', is_private=True),
+ RIR(name='RIR 6', slug='rir-6', is_private=True),
)
RIR.objects.bulk_create(rirs)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['RIR 1', 'RIR 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -374,7 +393,7 @@ class RIRTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
- params = {'description': ['A', 'B']}
+ params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_is_private(self):
@@ -423,6 +442,10 @@ class AggregateTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Aggregate.objects.bulk_create(aggregates)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_family(self):
params = {'family': '4'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
@@ -476,6 +499,10 @@ class RoleTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Role.objects.bulk_create(roles)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Role 1', 'Role 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -580,6 +607,10 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
for prefix in prefixes:
prefix.save()
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_family(self):
params = {'family': '6'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
@@ -628,8 +659,12 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_mask_length(self):
- params = {'mask_length': ['24']}
+ params = {'mask_length': [24]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ params = {'mask_length__gte': 32}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
+ params = {'mask_length__lte': 24}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
def test_vrf(self):
vrfs = VRF.objects.all()[:2]
@@ -742,17 +777,87 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
ip_ranges = (
- IPRange(start_address='10.0.1.100/24', end_address='10.0.1.199/24', size=100, vrf=None, tenant=None, role=None, status=IPRangeStatusChoices.STATUS_ACTIVE, description='foobar1'),
- IPRange(start_address='10.0.2.100/24', end_address='10.0.2.199/24', size=100, vrf=vrfs[0], tenant=tenants[0], role=roles[0], status=IPRangeStatusChoices.STATUS_ACTIVE, description='foobar2'),
- IPRange(start_address='10.0.3.100/24', end_address='10.0.3.199/24', size=100, vrf=vrfs[1], tenant=tenants[1], role=roles[1], status=IPRangeStatusChoices.STATUS_DEPRECATED),
- IPRange(start_address='10.0.4.100/24', end_address='10.0.4.199/24', size=100, vrf=vrfs[2], tenant=tenants[2], role=roles[2], status=IPRangeStatusChoices.STATUS_RESERVED),
- IPRange(start_address='2001:db8:0:1::1/64', end_address='2001:db8:0:1::100/64', size=100, vrf=None, tenant=None, role=None, status=IPRangeStatusChoices.STATUS_ACTIVE),
- IPRange(start_address='2001:db8:0:2::1/64', end_address='2001:db8:0:2::100/64', size=100, vrf=vrfs[0], tenant=tenants[0], role=roles[0], status=IPRangeStatusChoices.STATUS_ACTIVE),
- IPRange(start_address='2001:db8:0:3::1/64', end_address='2001:db8:0:3::100/64', size=100, vrf=vrfs[1], tenant=tenants[1], role=roles[1], status=IPRangeStatusChoices.STATUS_DEPRECATED),
- IPRange(start_address='2001:db8:0:4::1/64', end_address='2001:db8:0:4::100/64', size=100, vrf=vrfs[2], tenant=tenants[2], role=roles[2], status=IPRangeStatusChoices.STATUS_RESERVED),
+ IPRange(
+ start_address='10.0.1.100/24',
+ end_address='10.0.1.199/24',
+ size=100,
+ vrf=None,
+ tenant=None,
+ role=None,
+ status=IPRangeStatusChoices.STATUS_ACTIVE,
+ description='foobar1'
+ ),
+ IPRange(
+ start_address='10.0.2.100/24',
+ end_address='10.0.2.199/24',
+ size=100,
+ vrf=vrfs[0],
+ tenant=tenants[0],
+ role=roles[0],
+ status=IPRangeStatusChoices.STATUS_ACTIVE,
+ description='foobar2'
+ ),
+ IPRange(
+ start_address='10.0.3.100/24',
+ end_address='10.0.3.199/24',
+ size=100,
+ vrf=vrfs[1],
+ tenant=tenants[1],
+ role=roles[1],
+ status=IPRangeStatusChoices.STATUS_DEPRECATED
+ ),
+ IPRange(
+ start_address='10.0.4.100/24',
+ end_address='10.0.4.199/24',
+ size=100,
+ vrf=vrfs[2],
+ tenant=tenants[2],
+ role=roles[2],
+ status=IPRangeStatusChoices.STATUS_RESERVED
+ ),
+ IPRange(
+ start_address='2001:db8:0:1::1/64',
+ end_address='2001:db8:0:1::100/64',
+ size=100,
+ vrf=None,
+ tenant=None,
+ role=None,
+ status=IPRangeStatusChoices.STATUS_ACTIVE
+ ),
+ IPRange(
+ start_address='2001:db8:0:2::1/64',
+ end_address='2001:db8:0:2::100/64',
+ size=100,
+ vrf=vrfs[0],
+ tenant=tenants[0],
+ role=roles[0],
+ status=IPRangeStatusChoices.STATUS_ACTIVE
+ ),
+ IPRange(
+ start_address='2001:db8:0:3::1/64',
+ end_address='2001:db8:0:3::100/64',
+ size=100,
+ vrf=vrfs[1],
+ tenant=tenants[1],
+ role=roles[1],
+ status=IPRangeStatusChoices.STATUS_DEPRECATED
+ ),
+ IPRange(
+ start_address='2001:db8:0:4::1/64',
+ end_address='2001:db8:0:4::100/64',
+ size=100,
+ vrf=vrfs[2],
+ tenant=tenants[2],
+ role=roles[2],
+ status=IPRangeStatusChoices.STATUS_RESERVED
+ ),
)
IPRange.objects.bulk_create(ip_ranges)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_family(self):
params = {'family': '6'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
@@ -807,6 +912,12 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_parent(self):
+ params = {'parent': ['10.0.1.0/24', '10.0.2.0/24']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'parent': ['10.0.1.0/25']} # Range 10.0.1.100-199 is not fully contained by 10.0.1.0/25
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
+
class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IPAddress.objects.all()
@@ -880,21 +991,111 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
ipaddresses = (
- IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar1'),
- IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
- IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
- IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
- IPAddress(address='10.0.0.5/24', tenant=None, vrf=None, assigned_object=fhrp_groups[0], status=IPAddressStatusChoices.STATUS_ACTIVE),
- IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
- IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar2'),
- IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
- IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
- IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
- IPAddress(address='2001:db8::5/64', tenant=None, vrf=None, assigned_object=fhrp_groups[1], status=IPAddressStatusChoices.STATUS_ACTIVE),
- IPAddress(address='2001:db8::1/65', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
+ IPAddress(
+ address='10.0.0.1/24',
+ tenant=None,
+ vrf=None,
+ assigned_object=None,
+ status=IPAddressStatusChoices.STATUS_ACTIVE,
+ dns_name='ipaddress-a',
+ description='foobar1'
+ ),
+ IPAddress(
+ address='10.0.0.2/24',
+ tenant=tenants[0],
+ vrf=vrfs[0],
+ assigned_object=interfaces[0],
+ status=IPAddressStatusChoices.STATUS_ACTIVE,
+ dns_name='ipaddress-b'
+ ),
+ IPAddress(
+ address='10.0.0.3/24',
+ tenant=tenants[1],
+ vrf=vrfs[1],
+ assigned_object=interfaces[1],
+ status=IPAddressStatusChoices.STATUS_RESERVED,
+ role=IPAddressRoleChoices.ROLE_VIP,
+ dns_name='ipaddress-c'
+ ),
+ IPAddress(
+ address='10.0.0.4/24',
+ tenant=tenants[2],
+ vrf=vrfs[2],
+ assigned_object=interfaces[2],
+ status=IPAddressStatusChoices.STATUS_DEPRECATED,
+ role=IPAddressRoleChoices.ROLE_SECONDARY,
+ dns_name='ipaddress-d'
+ ),
+ IPAddress(
+ address='10.0.0.5/24',
+ tenant=None,
+ vrf=None,
+ assigned_object=fhrp_groups[0],
+ status=IPAddressStatusChoices.STATUS_ACTIVE
+ ),
+ IPAddress(
+ address='10.0.0.1/25',
+ tenant=None,
+ vrf=None,
+ assigned_object=None,
+ status=IPAddressStatusChoices.STATUS_ACTIVE
+ ),
+ IPAddress(
+ address='2001:db8::1/64',
+ tenant=None,
+ vrf=None,
+ assigned_object=None,
+ status=IPAddressStatusChoices.STATUS_ACTIVE,
+ dns_name='ipaddress-a',
+ description='foobar2'
+ ),
+ IPAddress(
+ address='2001:db8::2/64',
+ tenant=tenants[0],
+ vrf=vrfs[0],
+ assigned_object=vminterfaces[0],
+ status=IPAddressStatusChoices.STATUS_ACTIVE,
+ dns_name='ipaddress-b'
+ ),
+ IPAddress(
+ address='2001:db8::3/64',
+ tenant=tenants[1],
+ vrf=vrfs[1],
+ assigned_object=vminterfaces[1],
+ status=IPAddressStatusChoices.STATUS_RESERVED,
+ role=IPAddressRoleChoices.ROLE_VIP,
+ dns_name='ipaddress-c'
+ ),
+ IPAddress(
+ address='2001:db8::4/64',
+ tenant=tenants[2],
+ vrf=vrfs[2],
+ assigned_object=vminterfaces[2],
+ status=IPAddressStatusChoices.STATUS_DEPRECATED,
+ role=IPAddressRoleChoices.ROLE_SECONDARY,
+ dns_name='ipaddress-d'
+ ),
+ IPAddress(
+ address='2001:db8::5/64',
+ tenant=None,
+ vrf=None,
+ assigned_object=fhrp_groups[1],
+ status=IPAddressStatusChoices.STATUS_ACTIVE
+ ),
+ IPAddress(
+ address='2001:db8::1/65',
+ tenant=None,
+ vrf=None,
+ assigned_object=None,
+ status=IPAddressStatusChoices.STATUS_ACTIVE
+ ),
)
IPAddress.objects.bulk_create(ipaddresses)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_family(self):
params = {'family': '4'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
@@ -949,8 +1150,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_mask_length(self):
- params = {'mask_length': '24'}
+ params = {'mask_length': [24]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
+ params = {'mask_length__gte': 64}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ params = {'mask_length__lte': 25}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_vrf(self):
vrfs = VRF.objects.all()[:2]
@@ -1042,15 +1247,36 @@ class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
IPAddress.objects.bulk_create(ip_addresses)
fhrp_groups = (
- FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=10, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT, auth_key='foo123'),
- FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3, group_id=20, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, auth_key='bar456', name='bar123'),
- FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP, group_id=30),
+ FHRPGroup(
+ protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2,
+ group_id=10,
+ auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT,
+ auth_key='foo123',
+ description='foobar1'
+ ),
+ FHRPGroup(
+ protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3,
+ group_id=20,
+ auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5,
+ auth_key='bar456',
+ name='bar123',
+ description='foobar2'
+ ),
+ FHRPGroup(
+ protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP,
+ group_id=30,
+ description='foobar3'
+ ),
)
FHRPGroup.objects.bulk_create(fhrp_groups)
fhrp_groups[0].ip_addresses.set([ip_addresses[0]])
fhrp_groups[1].ip_addresses.set([ip_addresses[1]])
fhrp_groups[2].ip_addresses.set([ip_addresses[2]])
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_protocol(self):
params = {'protocol': [FHRPGroupProtocolChoices.PROTOCOL_VRRP2, FHRPGroupProtocolChoices.PROTOCOL_VRRP3]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1071,6 +1297,10 @@ class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'name': ['bar123', ]}
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_related_ip(self):
# Create some regular IPs to query for related IPs
ipaddresses = (
@@ -1186,17 +1416,21 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
cluster.save()
vlan_groups = (
- VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=region, description='A'),
- VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sitegroup, description='B'),
- VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=site, description='C'),
- VLANGroup(name='VLAN Group 4', slug='vlan-group-4', scope=location, description='D'),
- VLANGroup(name='VLAN Group 5', slug='vlan-group-5', scope=rack, description='E'),
- VLANGroup(name='VLAN Group 6', slug='vlan-group-6', scope=clustergroup, description='F'),
- VLANGroup(name='VLAN Group 7', slug='vlan-group-7', scope=cluster, description='G'),
+ 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.objects.bulk_create(vlan_groups)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['VLAN Group 1', 'VLAN Group 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1206,7 +1440,7 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
- params = {'description': ['A', 'B']}
+ params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self):
@@ -1346,6 +1580,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
VLANGroup(name='VLAN Group 1', slug='vlan-group-1'),
VLANGroup(name='VLAN Group 2', slug='vlan-group-2'),
VLANGroup(name='VLAN Group 3', slug='vlan-group-3'),
+ VLANGroup(name='VLAN Group 4', slug='vlan-group-4'),
)
VLANGroup.objects.bulk_create(groups)
@@ -1402,11 +1637,18 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
VLAN(vid=301, name='VLAN 301', site=sites[5], group=groups[23], role=roles[2], tenant=tenants[2], status=VLANStatusChoices.STATUS_RESERVED),
VLAN(vid=302, name='VLAN 302', site=sites[5], group=groups[23], role=roles[2], tenant=tenants[2], status=VLANStatusChoices.STATUS_RESERVED),
+ # Create one globally available VLAN on a VLAN group
+ VLAN(vid=500, name='VLAN Group 1', group=groups[24]),
+
# Create one globally available VLAN
VLAN(vid=1000, name='Global VLAN'),
)
VLAN.objects.bulk_create(vlans)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['VLAN 101', 'VLAN 102']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1475,12 +1717,17 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_available_on_device(self):
device_id = Device.objects.first().pk
params = {'available_on_device': device_id}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) # 5 scoped + 1 global
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7) # 5 scoped + 1 global group + 1 global
def test_available_on_virtualmachine(self):
vm_id = VirtualMachine.objects.first().pk
params = {'available_on_virtualmachine': vm_id}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) # 5 scoped + 1 global
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7) # 5 scoped + 1 global group + 1 global
+
+ def test_available_at_site(self):
+ site_id = Site.objects.first().pk
+ params = {'available_at_site': site_id}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) # 4 scoped + 1 global group + 1 global
class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
@@ -1490,15 +1737,46 @@ class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod
def setUpTestData(cls):
service_templates = (
- ServiceTemplate(name='Service Template 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1001]),
- ServiceTemplate(name='Service Template 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1002]),
- ServiceTemplate(name='Service Template 3', protocol=ServiceProtocolChoices.PROTOCOL_UDP, ports=[1003]),
- ServiceTemplate(name='Service Template 4', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[2001]),
- ServiceTemplate(name='Service Template 5', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[2002]),
- ServiceTemplate(name='Service Template 6', protocol=ServiceProtocolChoices.PROTOCOL_UDP, ports=[2003]),
+ ServiceTemplate(
+ name='Service Template 1',
+ protocol=ServiceProtocolChoices.PROTOCOL_TCP,
+ ports=[1001],
+ description='foobar1'
+ ),
+ ServiceTemplate(
+ name='Service Template 2',
+ protocol=ServiceProtocolChoices.PROTOCOL_TCP,
+ ports=[1002],
+ description='foobar2'
+ ),
+ ServiceTemplate(
+ name='Service Template 3',
+ protocol=ServiceProtocolChoices.PROTOCOL_UDP,
+ ports=[1003],
+ description='foobar3'
+ ),
+ ServiceTemplate(
+ name='Service Template 4',
+ protocol=ServiceProtocolChoices.PROTOCOL_TCP,
+ ports=[2001]
+ ),
+ ServiceTemplate(
+ name='Service Template 5',
+ protocol=ServiceProtocolChoices.PROTOCOL_TCP,
+ ports=[2002]
+ ),
+ ServiceTemplate(
+ name='Service Template 6',
+ protocol=ServiceProtocolChoices.PROTOCOL_UDP,
+ ports=[2003]
+ ),
)
ServiceTemplate.objects.bulk_create(service_templates)
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Service Template 1', 'Service Template 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1511,6 +1789,10 @@ class ServiceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'port': '1001'}
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)
+
class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Service.objects.all()
@@ -1567,6 +1849,10 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
services[1].ipaddresses.add(ip_addresses[1])
services[2].ipaddresses.add(ip_addresses[2])
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
def test_name(self):
params = {'name': ['Service 1', 'Service 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -1603,163 +1889,3 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'ipaddress': [str(ips[0].address), str(ips[1].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-
-class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
- queryset = L2VPN.objects.all()
- filterset = L2VPNFilterSet
-
- @classmethod
- def setUpTestData(cls):
-
- route_targets = (
- RouteTarget(name='1:1'),
- RouteTarget(name='1:2'),
- RouteTarget(name='1:3'),
- RouteTarget(name='2:1'),
- RouteTarget(name='2:2'),
- RouteTarget(name='2:3'),
- )
- RouteTarget.objects.bulk_create(route_targets)
-
- l2vpns = (
- L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=65001),
- L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VPWS, identifier=65002),
- L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VPLS),
- )
- L2VPN.objects.bulk_create(l2vpns)
- l2vpns[0].import_targets.add(route_targets[0])
- l2vpns[1].import_targets.add(route_targets[1])
- l2vpns[2].import_targets.add(route_targets[2])
- l2vpns[0].export_targets.add(route_targets[3])
- l2vpns[1].export_targets.add(route_targets[4])
- l2vpns[2].export_targets.add(route_targets[5])
-
- def test_name(self):
- params = {'name': ['L2VPN 1', 'L2VPN 2']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
- def test_slug(self):
- params = {'slug': ['l2vpn-1', 'l2vpn-2']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
- def test_identifier(self):
- params = {'identifier': ['65001', '65002']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
- def test_type(self):
- params = {'type': [L2VPNTypeChoices.TYPE_VXLAN, L2VPNTypeChoices.TYPE_VPWS]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
- def test_import_targets(self):
- route_targets = RouteTarget.objects.filter(name__in=['1:1', '1:2'])
- params = {'import_target_id': [route_targets[0].pk, route_targets[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- params = {'import_target': [route_targets[0].name, route_targets[1].name]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
- def test_export_targets(self):
- route_targets = RouteTarget.objects.filter(name__in=['2:1', '2:2'])
- params = {'export_target_id': [route_targets[0].pk, route_targets[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- params = {'export_target': [route_targets[0].name, route_targets[1].name]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-
-class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
- queryset = L2VPNTermination.objects.all()
- filterset = L2VPNTerminationFilterSet
-
- @classmethod
- def setUpTestData(cls):
- device = create_test_device('Device 1')
- interfaces = (
- Interface(name='Interface 1', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
- Interface(name='Interface 2', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
- Interface(name='Interface 3', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
- )
- Interface.objects.bulk_create(interfaces)
-
- vm = create_test_virtualmachine('Virtual Machine 1')
- vminterfaces = (
- VMInterface(name='Interface 1', virtual_machine=vm),
- VMInterface(name='Interface 2', virtual_machine=vm),
- VMInterface(name='Interface 3', virtual_machine=vm),
- )
- VMInterface.objects.bulk_create(vminterfaces)
-
- vlans = (
- VLAN(name='VLAN 1', vid=101),
- VLAN(name='VLAN 2', vid=102),
- VLAN(name='VLAN 3', vid=103),
- )
- VLAN.objects.bulk_create(vlans)
-
- l2vpns = (
- L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=65001),
- L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=65002),
- L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD,
- )
- L2VPN.objects.bulk_create(l2vpns)
-
- l2vpnterminations = (
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
- L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vlans[1]),
- L2VPNTermination(l2vpn=l2vpns[2], assigned_object=vlans[2]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=interfaces[0]),
- L2VPNTermination(l2vpn=l2vpns[1], assigned_object=interfaces[1]),
- L2VPNTermination(l2vpn=l2vpns[2], assigned_object=interfaces[2]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vminterfaces[0]),
- L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vminterfaces[1]),
- L2VPNTermination(l2vpn=l2vpns[2], assigned_object=vminterfaces[2]),
- )
- L2VPNTermination.objects.bulk_create(l2vpnterminations)
-
- def test_l2vpn(self):
- l2vpns = L2VPN.objects.all()[:2]
- params = {'l2vpn_id': [l2vpns[0].pk, l2vpns[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
- params = {'l2vpn': [l2vpns[0].slug, l2vpns[1].slug]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
-
- def test_content_type(self):
- params = {'assigned_object_type_id': ContentType.objects.get(model='vlan').pk}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
-
- def test_interface(self):
- interfaces = Interface.objects.all()[:2]
- params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
- def test_vminterface(self):
- vminterfaces = VMInterface.objects.all()[:2]
- params = {'vminterface_id': [vminterfaces[0].pk, vminterfaces[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
- def test_vlan(self):
- vlans = VLAN.objects.all()[:2]
- params = {'vlan_id': [vlans[0].pk, vlans[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
- params = {'vlan': ['VLAN 1', 'VLAN 2']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
- def test_site(self):
- site = Site.objects.all().first()
- params = {'site_id': [site.pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
- params = {'site': ['site-1']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
-
- def test_device(self):
- device = Device.objects.all().first()
- params = {'device_id': [device.pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
- params = {'device': ['Device 1']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
-
- def test_virtual_machine(self):
- virtual_machine = VirtualMachine.objects.all().first()
- params = {'virtual_machine_id': [virtual_machine.pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
- params = {'virtual_machine': ['Virtual Machine 1']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py
index 06cd9b445..d0f42e8a6 100644
--- a/netbox/ipam/tests/test_models.py
+++ b/netbox/ipam/tests/test_models.py
@@ -1,10 +1,9 @@
-from netaddr import IPNetwork, IPSet
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
+from netaddr import IPNetwork, IPSet
-from dcim.models import Interface, Device, DeviceRole, DeviceType, Manufacturer, Site
-from ipam.choices import IPAddressRoleChoices, PrefixStatusChoices
-from ipam.models import Aggregate, IPAddress, IPRange, Prefix, RIR, VLAN, VLANGroup, VRF, L2VPN, L2VPNTermination
+from ipam.choices import *
+from ipam.models import *
class TestAggregate(TestCase):
@@ -233,7 +232,6 @@ class TestPrefix(TestCase):
duplicate_prefix = Prefix(prefix=IPNetwork('192.0.2.0/24'))
self.assertIsNone(duplicate_prefix.clean())
- @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
def test_duplicate_global_unique(self):
Prefix.objects.create(prefix=IPNetwork('192.0.2.0/24'))
duplicate_prefix = Prefix(prefix=IPNetwork('192.0.2.0/24'))
@@ -472,7 +470,6 @@ class TestIPAddress(TestCase):
duplicate_ip = IPAddress(address=IPNetwork('192.0.2.1/24'))
self.assertIsNone(duplicate_ip.clean())
- @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
def test_duplicate_global_unique(self):
IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'))
duplicate_ip = IPAddress(address=IPNetwork('192.0.2.1/24'))
@@ -490,19 +487,16 @@ class TestIPAddress(TestCase):
duplicate_ip = IPAddress(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
self.assertRaises(ValidationError, duplicate_ip.clean)
- @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
def test_duplicate_nonunique_nonrole_role(self):
IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'))
duplicate_ip = IPAddress(address=IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
self.assertRaises(ValidationError, duplicate_ip.clean)
- @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
def test_duplicate_nonunique_role_nonrole(self):
IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
duplicate_ip = IPAddress(address=IPNetwork('192.0.2.1/24'))
self.assertRaises(ValidationError, duplicate_ip.clean)
- @override_settings(ENFORCE_GLOBAL_UNIQUE=True)
def test_duplicate_nonunique_role(self):
IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), role=IPAddressRoleChoices.ROLE_VIP)
@@ -539,76 +533,3 @@ class TestVLANGroup(TestCase):
VLAN.objects.create(name='VLAN 104', vid=104, group=vlangroup)
self.assertEqual(vlangroup.get_next_available_vid(), 105)
-
-
-class TestL2VPNTermination(TestCase):
-
- @classmethod
- def setUpTestData(cls):
-
- site = Site.objects.create(name='Site 1')
- manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
- device_type = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
- role = DeviceRole.objects.create(name='Switch')
- device = Device.objects.create(
- name='Device 1',
- site=site,
- device_type=device_type,
- role=role,
- status='active'
- )
-
- interfaces = (
- Interface(name='Interface 1', device=device, type='1000baset'),
- Interface(name='Interface 2', device=device, type='1000baset'),
- Interface(name='Interface 3', device=device, type='1000baset'),
- Interface(name='Interface 4', device=device, type='1000baset'),
- Interface(name='Interface 5', device=device, type='1000baset'),
- )
-
- Interface.objects.bulk_create(interfaces)
-
- vlans = (
- VLAN(name='VLAN 1', vid=651),
- VLAN(name='VLAN 2', vid=652),
- VLAN(name='VLAN 3', vid=653),
- VLAN(name='VLAN 4', vid=654),
- VLAN(name='VLAN 5', vid=655),
- VLAN(name='VLAN 6', vid=656),
- VLAN(name='VLAN 7', vid=657)
- )
-
- VLAN.objects.bulk_create(vlans)
-
- l2vpns = (
- L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
- L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
- L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
- )
- L2VPN.objects.bulk_create(l2vpns)
-
- l2vpnterminations = (
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
- )
-
- L2VPNTermination.objects.bulk_create(l2vpnterminations)
-
- def test_duplicate_interface_terminations(self):
- device = Device.objects.first()
- interface = Interface.objects.filter(device=device).first()
- l2vpn = L2VPN.objects.first()
-
- L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=interface)
- duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=interface)
-
- self.assertRaises(ValidationError, duplicate.clean)
-
- def test_duplicate_vlan_terminations(self):
- vlan = Interface.objects.first()
- l2vpn = L2VPN.objects.first()
-
- L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=vlan)
- duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=vlan)
- self.assertRaises(ValidationError, duplicate.clean)
diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py
index afc97cc63..bc42341ba 100644
--- a/netbox/ipam/tests/test_views.py
+++ b/netbox/ipam/tests/test_views.py
@@ -4,11 +4,12 @@ from django.test import override_settings
from django.urls import reverse
from netaddr import IPNetwork
+from dcim.constants import InterfaceTypeChoices
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site, Interface
from ipam.choices import *
from ipam.models import *
from tenancy.models import Tenant
-from utilities.testing import ViewTestCases, create_test_device, create_tags
+from utilities.testing import ViewTestCases, create_tags
class ASNRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -911,6 +912,7 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
device = Device.objects.create(name='Device 1', site=site, device_type=devicetype, role=role)
+ interface = Interface.objects.create(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL)
services = (
Service(device=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[101]),
@@ -919,6 +921,12 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
Service.objects.bulk_create(services)
+ ip_addresses = (
+ IPAddress(assigned_object=interface, address='192.0.2.1/24'),
+ IPAddress(assigned_object=interface, address='192.0.2.2/24'),
+ )
+ IPAddress.objects.bulk_create(ip_addresses)
+
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
@@ -933,10 +941,10 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
- "device,name,protocol,ports,description",
- "Device 1,Service 1,tcp,1,First service",
- "Device 1,Service 2,tcp,2,Second service",
- "Device 1,Service 3,udp,3,Third service",
+ "device,name,protocol,ports,ipaddresses,description",
+ "Device 1,Service 1,tcp,1,192.0.2.1/24,First service",
+ "Device 1,Service 2,tcp,2,192.0.2.2/24,Second service",
+ "Device 1,Service 3,udp,3,,Third service",
)
cls.csv_update_data = (
@@ -978,142 +986,3 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
self.assertEqual(instance.protocol, service_template.protocol)
self.assertEqual(instance.ports, service_template.ports)
self.assertEqual(instance.description, service_template.description)
-
-
-class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
- model = L2VPN
-
- @classmethod
- def setUpTestData(cls):
- rts = (
- RouteTarget(name='64534:123'),
- RouteTarget(name='64534:321')
- )
- RouteTarget.objects.bulk_create(rts)
-
- l2vpns = (
- L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650001'),
- L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650002'),
- L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650003')
- )
- L2VPN.objects.bulk_create(l2vpns)
-
- cls.csv_data = (
- 'name,slug,type,identifier',
- 'L2VPN 5,l2vpn-5,vxlan,456',
- 'L2VPN 6,l2vpn-6,vxlan,444',
- )
-
- cls.csv_update_data = (
- 'id,name,description',
- f'{l2vpns[0].pk},L2VPN 7,New description 7',
- f'{l2vpns[1].pk},L2VPN 8,New description 8',
- )
-
- cls.bulk_edit_data = {
- 'description': 'New Description',
- }
-
- cls.form_data = {
- 'name': 'L2VPN 8',
- 'slug': 'l2vpn-8',
- 'type': L2VPNTypeChoices.TYPE_VXLAN,
- 'identifier': 123,
- 'description': 'Description',
- 'import_targets': [rts[0].pk],
- 'export_targets': [rts[1].pk]
- }
-
-
-class L2VPNTerminationTestCase(
- ViewTestCases.GetObjectViewTestCase,
- ViewTestCases.GetObjectChangelogViewTestCase,
- ViewTestCases.CreateObjectViewTestCase,
- ViewTestCases.EditObjectViewTestCase,
- ViewTestCases.DeleteObjectViewTestCase,
- ViewTestCases.ListObjectsViewTestCase,
- ViewTestCases.BulkImportObjectsViewTestCase,
- ViewTestCases.BulkDeleteObjectsViewTestCase,
-):
-
- model = L2VPNTermination
-
- @classmethod
- def setUpTestData(cls):
- device = create_test_device('Device 1')
- interface = Interface.objects.create(name='Interface 1', device=device, type='1000baset')
- l2vpns = (
- L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=650001),
- L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=650002),
- )
- L2VPN.objects.bulk_create(l2vpns)
-
- vlans = (
- VLAN(name='Vlan 1', vid=1001),
- VLAN(name='Vlan 2', vid=1002),
- VLAN(name='Vlan 3', vid=1003),
- VLAN(name='Vlan 4', vid=1004),
- VLAN(name='Vlan 5', vid=1005),
- VLAN(name='Vlan 6', vid=1006)
- )
- VLAN.objects.bulk_create(vlans)
-
- terminations = (
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
- )
- L2VPNTermination.objects.bulk_create(terminations)
-
- cls.form_data = {
- 'l2vpn': l2vpns[0].pk,
- 'device': device.pk,
- 'interface': interface.pk,
- }
-
- cls.csv_data = (
- "l2vpn,vlan",
- "L2VPN 1,Vlan 4",
- "L2VPN 1,Vlan 5",
- "L2VPN 1,Vlan 6",
- )
-
- cls.csv_update_data = (
- f"id,l2vpn",
- f"{terminations[0].pk},{l2vpns[0].name}",
- f"{terminations[1].pk},{l2vpns[0].name}",
- f"{terminations[2].pk},{l2vpns[0].name}",
- )
-
- cls.bulk_edit_data = {}
-
- # TODO: Fix L2VPNTerminationImportForm validation to support bulk updates
- def test_bulk_update_objects_with_permission(self):
- pass
-
- #
- # Custom assertions
- #
-
- # TODO: Remove this
- def assertInstanceEqual(self, instance, data, exclude=None, api=False):
- """
- Override parent
- """
- if exclude is None:
- exclude = []
-
- fields = [k for k in data.keys() if k not in exclude]
- model_dict = self.model_to_dict(instance, fields=fields, api=api)
-
- # Omit any dictionary keys which are not instance attributes or have been excluded
- relevant_data = {
- k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude
- }
-
- # Handle relations on the model
- for k, v in model_dict.items():
- if isinstance(v, object) and hasattr(v, 'first'):
- model_dict[k] = v.first().pk
-
- self.assertDictEqual(model_dict, relevant_data)
diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py
index 3bfe34b7b..61deeff4b 100644
--- a/netbox/ipam/urls.py
+++ b/netbox/ipam/urls.py
@@ -131,20 +131,4 @@ urlpatterns = [
path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
path('services//', include(get_model_urls('ipam', 'service'))),
-
- # L2VPN
- path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'),
- path('l2vpns/add/', views.L2VPNEditView.as_view(), name='l2vpn_add'),
- path('l2vpns/import/', views.L2VPNBulkImportView.as_view(), name='l2vpn_import'),
- path('l2vpns/edit/', views.L2VPNBulkEditView.as_view(), name='l2vpn_bulk_edit'),
- path('l2vpns/delete/', views.L2VPNBulkDeleteView.as_view(), name='l2vpn_bulk_delete'),
- path('l2vpns//', include(get_model_urls('ipam', 'l2vpn'))),
-
- # L2VPN terminations
- path('l2vpn-terminations/', views.L2VPNTerminationListView.as_view(), name='l2vpntermination_list'),
- path('l2vpn-terminations/add/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_add'),
- path('l2vpn-terminations/import/', views.L2VPNTerminationBulkImportView.as_view(), name='l2vpntermination_import'),
- path('l2vpn-terminations/edit/', views.L2VPNTerminationBulkEditView.as_view(), name='l2vpntermination_bulk_edit'),
- path('l2vpn-terminations/delete/', views.L2VPNTerminationBulkDeleteView.as_view(), name='l2vpntermination_bulk_delete'),
- path('l2vpn-terminations//', include(get_model_urls('ipam', 'l2vpntermination'))),
]
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index d8e4d8b47..1598f0321 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -1,7 +1,6 @@
from django.contrib.contenttypes.models import ContentType
-from django.db.models import F, Prefetch
+from django.db.models import Prefetch
from django.db.models.expressions import RawSQL
-from django.db.models.functions import Round
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext as _
@@ -10,7 +9,7 @@ from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet
from dcim.models import Interface, Site
from netbox.views import generic
-from tenancy.views import ObjectContactsView
+from utilities.tables import get_table_ordering
from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet
@@ -19,7 +18,6 @@ from . import filtersets, forms, tables
from .choices import PrefixStatusChoices
from .constants import *
from .models import *
-from .tables.l2vpn import L2VPNTable, L2VPNTerminationTable
from .utils import add_requested_prefixes, add_available_ipaddresses, add_available_vlans
@@ -220,7 +218,7 @@ class ASNRangeASNsView(generic.ObjectChildrenView):
tab = ViewTab(
label=_('ASNs'),
badge=lambda x: x.get_child_asns().count(),
- permission='ipam.view_asns',
+ permission='ipam.view_asn',
weight=500
)
@@ -606,7 +604,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
def prep_table_data(self, request, queryset, parent):
- if not request.GET.get('q') and not request.GET.get('sort'):
+ if not get_table_ordering(request, self.table):
return add_available_ipaddresses(parent.prefix, queryset, parent.is_pool)
return queryset
@@ -661,6 +659,26 @@ class IPRangeListView(generic.ObjectListView):
class IPRangeView(generic.ObjectView):
queryset = IPRange.objects.all()
+ def get_extra_context(self, request, instance):
+
+ # Parent prefixes table
+ parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
+ Q(prefix__net_contains_or_equals=str(instance.start_address.ip)),
+ Q(prefix__net_contains_or_equals=str(instance.end_address.ip)),
+ vrf=instance.vrf
+ ).prefetch_related(
+ 'site', 'role', 'tenant', 'vlan', 'role'
+ )
+ parent_prefixes_table = tables.PrefixTable(
+ list(parent_prefixes),
+ exclude=('vrf', 'utilization'),
+ orderable=False
+ )
+
+ return {
+ 'parent_prefixes_table': parent_prefixes_table,
+ }
+
@register_model_view(IPRange, 'ipaddresses', path='ip-addresses')
class IPRangeIPAddressesView(generic.ObjectChildrenView):
@@ -897,21 +915,8 @@ class VLANGroupView(generic.ObjectView):
(VLAN.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
)
- # TODO: Replace with embedded table
- vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related(
- Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)),
- 'tenant', 'site', 'role',
- ).order_by('vid')
- vlans = add_available_vlans(vlans, vlan_group=instance)
-
- vlans_table = tables.VLANTable(vlans, user=request.user, exclude=('group',))
- if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
- vlans_table.columns.show('pk')
- vlans_table.configure(request)
-
return {
'related_models': related_models,
- 'vlans_table': vlans_table,
}
@@ -944,6 +949,32 @@ class VLANGroupBulkDeleteView(generic.BulkDeleteView):
table = tables.VLANGroupTable
+@register_model_view(VLANGroup, 'vlans')
+class VLANGroupVLANsView(generic.ObjectChildrenView):
+ queryset = VLANGroup.objects.all()
+ child_model = VLAN
+ table = tables.VLANTable
+ filterset = filtersets.VLANFilterSet
+ template_name = 'generic/object_children.html'
+ tab = ViewTab(
+ label=_('VLANs'),
+ badge=lambda x: x.get_child_vlans().count(),
+ permission='ipam.view_vlan',
+ weight=500
+ )
+
+ def get_children(self, request, parent):
+ return parent.get_child_vlans().restrict(request.user, 'view').prefetch_related(
+ Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)),
+ 'tenant', 'site', 'role',
+ )
+
+ def prep_table_data(self, request, queryset, parent):
+ if not get_table_ordering(request, self.table):
+ return add_available_vlans(queryset, parent)
+ return queryset
+
+
#
# FHRP groups
#
@@ -1230,112 +1261,3 @@ class ServiceBulkDeleteView(generic.BulkDeleteView):
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
filterset = filtersets.ServiceFilterSet
table = tables.ServiceTable
-
-
-# L2VPN
-
-class L2VPNListView(generic.ObjectListView):
- queryset = L2VPN.objects.all()
- table = L2VPNTable
- filterset = filtersets.L2VPNFilterSet
- filterset_form = forms.L2VPNFilterForm
-
-
-@register_model_view(L2VPN)
-class L2VPNView(generic.ObjectView):
- queryset = L2VPN.objects.all()
-
- def get_extra_context(self, request, instance):
- import_targets_table = tables.RouteTargetTable(
- instance.import_targets.prefetch_related('tenant'),
- orderable=False
- )
- export_targets_table = tables.RouteTargetTable(
- instance.export_targets.prefetch_related('tenant'),
- orderable=False
- )
-
- return {
- 'import_targets_table': import_targets_table,
- 'export_targets_table': export_targets_table,
- }
-
-
-@register_model_view(L2VPN, 'edit')
-class L2VPNEditView(generic.ObjectEditView):
- queryset = L2VPN.objects.all()
- form = forms.L2VPNForm
-
-
-@register_model_view(L2VPN, 'delete')
-class L2VPNDeleteView(generic.ObjectDeleteView):
- queryset = L2VPN.objects.all()
-
-
-class L2VPNBulkImportView(generic.BulkImportView):
- queryset = L2VPN.objects.all()
- model_form = forms.L2VPNImportForm
-
-
-class L2VPNBulkEditView(generic.BulkEditView):
- queryset = L2VPN.objects.all()
- filterset = filtersets.L2VPNFilterSet
- table = tables.L2VPNTable
- form = forms.L2VPNBulkEditForm
-
-
-class L2VPNBulkDeleteView(generic.BulkDeleteView):
- queryset = L2VPN.objects.all()
- filterset = filtersets.L2VPNFilterSet
- table = tables.L2VPNTable
-
-
-@register_model_view(L2VPN, 'contacts')
-class L2VPNContactsView(ObjectContactsView):
- queryset = L2VPN.objects.all()
-
-
-#
-# L2VPN terminations
-#
-
-class L2VPNTerminationListView(generic.ObjectListView):
- queryset = L2VPNTermination.objects.all()
- table = L2VPNTerminationTable
- filterset = filtersets.L2VPNTerminationFilterSet
- filterset_form = forms.L2VPNTerminationFilterForm
-
-
-@register_model_view(L2VPNTermination)
-class L2VPNTerminationView(generic.ObjectView):
- queryset = L2VPNTermination.objects.all()
-
-
-@register_model_view(L2VPNTermination, 'edit')
-class L2VPNTerminationEditView(generic.ObjectEditView):
- queryset = L2VPNTermination.objects.all()
- form = forms.L2VPNTerminationForm
- template_name = 'ipam/l2vpntermination_edit.html'
-
-
-@register_model_view(L2VPNTermination, 'delete')
-class L2VPNTerminationDeleteView(generic.ObjectDeleteView):
- queryset = L2VPNTermination.objects.all()
-
-
-class L2VPNTerminationBulkImportView(generic.BulkImportView):
- queryset = L2VPNTermination.objects.all()
- model_form = forms.L2VPNTerminationImportForm
-
-
-class L2VPNTerminationBulkEditView(generic.BulkEditView):
- queryset = L2VPNTermination.objects.all()
- filterset = filtersets.L2VPNTerminationFilterSet
- table = tables.L2VPNTerminationTable
- form = forms.L2VPNTerminationBulkEditForm
-
-
-class L2VPNTerminationBulkDeleteView(generic.BulkDeleteView):
- queryset = L2VPNTermination.objects.all()
- filterset = filtersets.L2VPNTerminationFilterSet
- table = tables.L2VPNTerminationTable
diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py
index 347ed55bd..d6e43ea75 100644
--- a/netbox/netbox/api/fields.py
+++ b/netbox/netbox/api/fields.py
@@ -46,12 +46,13 @@ class ChoiceField(serializers.Field):
return super().validate_empty_values(data)
def to_representation(self, obj):
- if obj == '':
- return None
- return {
- 'value': obj,
- 'label': self._choices[obj],
- }
+ if obj != '':
+ # Use an empty string in place of the choice label if it cannot be resolved (i.e. because a previously
+ # configured choice has been removed from FIELD_CHOICES).
+ return {
+ 'value': obj,
+ 'label': self._choices.get(obj, ''),
+ }
def to_internal_value(self, data):
if data == '':
diff --git a/netbox/netbox/api/serializers/base.py b/netbox/netbox/api/serializers/base.py
index 5ee74bf8c..d513c8000 100644
--- a/netbox/netbox/api/serializers/base.py
+++ b/netbox/netbox/api/serializers/base.py
@@ -23,16 +23,16 @@ class ValidatedModelSerializer(BaseModelSerializer):
validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
"""
def validate(self, data):
-
- # Remove custom fields data and tags (if any) prior to model validation
attrs = data.copy()
+
+ # Remove custom field data (if any) prior to model validation
attrs.pop('custom_fields', None)
- attrs.pop('tags', None)
# Skip ManyToManyFields
- for field in self.Meta.model._meta.get_fields():
- if isinstance(field, ManyToManyField):
- attrs.pop(field.name, None)
+ m2m_values = {}
+ for field in self.Meta.model._meta.local_many_to_many:
+ if field.name in attrs:
+ m2m_values[field.name] = attrs.pop(field.name)
# Run clean() on an instance of the model
if self.instance is None:
@@ -41,6 +41,7 @@ class ValidatedModelSerializer(BaseModelSerializer):
instance = self.instance
for k, v in attrs.items():
setattr(instance, k, v)
+ instance._m2m_values = m2m_values
instance.full_clean()
return data
diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py
index 97f690762..cfbe82f14 100644
--- a/netbox/netbox/api/views.py
+++ b/netbox/netbox/api/views.py
@@ -11,7 +11,7 @@ from rest_framework.reverse import reverse
from rest_framework.views import APIView
from rq.worker import Worker
-from extras.plugins.utils import get_installed_plugins
+from netbox.plugins.utils import get_installed_plugins
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
@@ -39,6 +39,7 @@ class APIRootView(APIView):
'tenancy': reverse('tenancy-api:api-root', request=request, format=format),
'users': reverse('users-api:api-root', request=request, format=format),
'virtualization': reverse('virtualization-api:api-root', request=request, format=format),
+ 'vpn': reverse('vpn-api:api-root', request=request, format=format),
'wireless': reverse('wireless-api:api-root', request=request, format=format),
})
diff --git a/netbox/netbox/api/viewsets/__init__.py b/netbox/netbox/api/viewsets/__init__.py
index 5fe81b1f5..522bcf77b 100644
--- a/netbox/netbox/api/viewsets/__init__.py
+++ b/netbox/netbox/api/viewsets/__init__.py
@@ -2,7 +2,9 @@ import logging
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
-from django.db.models import ProtectedError
+from django.db.models import ProtectedError, RestrictedError
+from django_pglocks import advisory_lock
+from netbox.constants import ADVISORY_LOCK_KEYS
from rest_framework import mixins as drf_mixins
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
@@ -89,8 +91,11 @@ class NetBoxModelViewSet(
try:
return super().dispatch(request, *args, **kwargs)
- except ProtectedError as e:
- protected_objects = list(e.protected_objects)
+ except (ProtectedError, RestrictedError) as e:
+ if type(e) is ProtectedError:
+ protected_objects = list(e.protected_objects)
+ else:
+ protected_objects = list(e.restricted_objects)
msg = f'Unable to delete object. {len(protected_objects)} dependent objects were found: '
msg += ', '.join([f'{obj} ({obj.pk})' for obj in protected_objects])
logger.warning(msg)
@@ -157,3 +162,22 @@ class NetBoxModelViewSet(
logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")
return super().perform_destroy(instance)
+
+
+class MPTTLockedMixin:
+ """
+ Puts pglock on objects that derive from MPTTModel for parallel API calling.
+ Note: If adding this to a view, must add the model name to ADVISORY_LOCK_KEYS
+ """
+
+ def create(self, request, *args, **kwargs):
+ with advisory_lock(ADVISORY_LOCK_KEYS[self.queryset.model._meta.model_name]):
+ return super().create(request, *args, **kwargs)
+
+ def update(self, request, *args, **kwargs):
+ with advisory_lock(ADVISORY_LOCK_KEYS[self.queryset.model._meta.model_name]):
+ return super().update(request, *args, **kwargs)
+
+ def destroy(self, request, *args, **kwargs):
+ with advisory_lock(ADVISORY_LOCK_KEYS[self.queryset.model._meta.model_name]):
+ return super().destroy(request, *args, **kwargs)
diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py
index fde486fe9..a45e0bdda 100644
--- a/netbox/netbox/api/viewsets/mixins.py
+++ b/netbox/netbox/api/viewsets/mixins.py
@@ -56,8 +56,15 @@ class BriefModeMixin:
def get_queryset(self):
qs = super().get_queryset()
- # If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any)
if self.brief:
+ serializer_class = self.get_serializer_class()
+
+ # Clear any annotations for fields not present on the nested serializer
+ for annotation in list(qs.query.annotations.keys()):
+ if annotation not in serializer_class().fields:
+ qs.query.annotations.pop(annotation)
+
+ # Clear any prefetches from the queryset and append only brief_prefetch_fields (if any)
return qs.prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)
return qs
@@ -137,11 +144,14 @@ class BulkUpdateModelMixin:
}
]
"""
+ def get_bulk_update_queryset(self):
+ return self.get_queryset()
+
def bulk_update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
serializer = BulkOperationSerializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
- qs = self.get_queryset().filter(
+ qs = self.get_bulk_update_queryset().filter(
pk__in=[o['id'] for o in serializer.data]
)
@@ -184,10 +194,13 @@ class BulkDestroyModelMixin:
{"id": 456}
]
"""
+ def get_bulk_destroy_queryset(self):
+ return self.get_queryset()
+
def bulk_destroy(self, request, *args, **kwargs):
serializer = BulkOperationSerializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
- qs = self.get_queryset().filter(
+ qs = self.get_bulk_destroy_queryset().filter(
pk__in=[o['id'] for o in serializer.data]
)
diff --git a/netbox/netbox/config/__init__.py b/netbox/netbox/config/__init__.py
index a9a93636c..c536ceadb 100644
--- a/netbox/netbox/config/__init__.py
+++ b/netbox/netbox/config/__init__.py
@@ -74,7 +74,7 @@ class Config:
def _populate_from_db(self):
"""Cache data from latest ConfigRevision, then populate from cache"""
- from extras.models import ConfigRevision
+ from core.models import ConfigRevision
try:
revision = ConfigRevision.objects.last()
diff --git a/netbox/netbox/config/parameters.py b/netbox/netbox/config/parameters.py
index 9c613217c..54c9027cc 100644
--- a/netbox/netbox/config/parameters.py
+++ b/netbox/netbox/config/parameters.py
@@ -66,7 +66,7 @@ PARAMS = (
ConfigParam(
name='ENFORCE_GLOBAL_UNIQUE',
label=_('Globally unique IP space'),
- default=False,
+ default=True,
description=_("Enforce unique IP addressing within the global table"),
field=forms.BooleanField
),
@@ -102,7 +102,6 @@ PARAMS = (
description=_("Default voltage for powerfeeds"),
field=forms.IntegerField
),
-
ConfigParam(
name='POWERFEED_DEFAULT_AMPERAGE',
label=_('Powerfeed amperage'),
@@ -110,7 +109,6 @@ PARAMS = (
description=_("Default amperage for powerfeeds"),
field=forms.IntegerField
),
-
ConfigParam(
name='POWERFEED_DEFAULT_MAX_UTILIZATION',
label=_('Powerfeed max utilization'),
@@ -154,42 +152,17 @@ PARAMS = (
description=_("Custom validation rules (JSON)"),
field=forms.JSONField,
field_kwargs={
- 'widget': forms.Textarea(
- attrs={'class': 'vLargeTextField'}
- ),
+ 'widget': forms.Textarea(),
},
),
-
- # NAPALM
ConfigParam(
- name='NAPALM_USERNAME',
- label=_('NAPALM username'),
- default='',
- description=_("Username to use when connecting to devices via NAPALM")
- ),
- ConfigParam(
- name='NAPALM_PASSWORD',
- label=_('NAPALM password'),
- default='',
- description=_("Password to use when connecting to devices via NAPALM")
- ),
- ConfigParam(
- name='NAPALM_TIMEOUT',
- label=_('NAPALM timeout'),
- default=30,
- description=_("NAPALM connection timeout (in seconds)"),
- field=forms.IntegerField
- ),
- ConfigParam(
- name='NAPALM_ARGS',
- label=_('NAPALM arguments'),
+ name='PROTECTION_RULES',
+ label=_('Protection rules'),
default={},
- description=_("Additional arguments to pass when invoking a NAPALM driver (as JSON data)"),
+ description=_("Deletion protection rules (JSON)"),
field=forms.JSONField,
field_kwargs={
- 'widget': forms.Textarea(
- attrs={'class': 'vLargeTextField'}
- ),
+ 'widget': forms.Textarea(),
},
),
diff --git a/netbox/netbox/configuration_testing.py b/netbox/netbox/configuration_testing.py
index 18a3c2afa..cec05cabb 100644
--- a/netbox/netbox/configuration_testing.py
+++ b/netbox/netbox/configuration_testing.py
@@ -15,7 +15,7 @@ DATABASE = {
}
PLUGINS = [
- 'extras.tests.dummy_plugin',
+ 'netbox.tests.dummy_plugin',
]
REDIS = {
diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py
index d69edc69c..faddf8c21 100644
--- a/netbox/netbox/constants.py
+++ b/netbox/netbox/constants.py
@@ -11,8 +11,28 @@ RQ_QUEUE_LOW = 'low'
# When adding a new key, pick something arbitrary and unique so that it is easily searchable in
# query logs.
ADVISORY_LOCK_KEYS = {
+ # Available object locks
'available-prefixes': 100100,
'available-ips': 100200,
'available-vlans': 100300,
'available-asns': 100400,
+
+ # MPTT locks
+ 'region': 105100,
+ 'sitegroup': 105200,
+ 'location': 105300,
+ 'tenantgroup': 105400,
+ 'contactgroup': 105500,
+ 'wirelesslangroup': 105600,
+ 'inventoryitem': 105700,
+ 'inventoryitemtemplate': 105800,
+}
+
+# Default view action permission mapping
+DEFAULT_ACTION_PERMISSIONS = {
+ 'add': {'add'},
+ 'import': {'add'},
+ 'export': {'view'},
+ 'bulk_edit': {'change'},
+ 'bulk_delete': {'delete'},
}
diff --git a/netbox/netbox/context.py b/netbox/netbox/context.py
index 5461a4e94..56e41cb63 100644
--- a/netbox/netbox/context.py
+++ b/netbox/netbox/context.py
@@ -2,9 +2,9 @@ from contextvars import ContextVar
__all__ = (
'current_request',
- 'webhooks_queue',
+ 'events_queue',
)
current_request = ContextVar('current_request', default=None)
-webhooks_queue = ContextVar('webhooks_queue', default=[])
+events_queue = ContextVar('events_queue', default=[])
diff --git a/netbox/netbox/data_backends.py b/netbox/netbox/data_backends.py
new file mode 100644
index 000000000..d5bab75c1
--- /dev/null
+++ b/netbox/netbox/data_backends.py
@@ -0,0 +1,53 @@
+from contextlib import contextmanager
+from urllib.parse import urlparse
+
+__all__ = (
+ 'DataBackend',
+)
+
+
+class DataBackend:
+ """
+ A data backend represents a specific system of record for data, such as a git repository or Amazon S3 bucket.
+
+ Attributes:
+ name: The identifier under which this backend will be registered in NetBox
+ label: The human-friendly name for this backend
+ is_local: A boolean indicating whether this backend accesses local data
+ parameters: A dictionary mapping configuration form field names to their classes
+ sensitive_parameters: An iterable of field names for which the values should not be displayed to the user
+ """
+ is_local = False
+ parameters = {}
+ sensitive_parameters = []
+
+ # Prevent Django's template engine from calling the backend
+ # class when referenced via DataSource.backend_class
+ do_not_call_in_templates = True
+
+ def __init__(self, url, **kwargs):
+ self.url = url
+ self.params = kwargs
+ self.config = self.init_config()
+
+ def init_config(self):
+ """
+ A hook to initialize the instance's configuration. The data returned by this method is assigned to the
+ instance's `config` attribute upon initialization, which can be referenced by the `fetch()` method.
+ """
+ return
+
+ @property
+ def url_scheme(self):
+ return urlparse(self.url).scheme.lower()
+
+ @contextmanager
+ def fetch(self):
+ """
+ A context manager which performs the following:
+
+ 1. Handles all setup and synchronization
+ 2. Yields the local path at which data has been replicated
+ 3. Performs any necessary cleanup
+ """
+ raise NotImplemented()
diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py
index 9a2385c45..ebb98d15f 100644
--- a/netbox/netbox/filtersets.py
+++ b/netbox/netbox/filtersets.py
@@ -246,18 +246,22 @@ class ChangeLoggedModelFilterSet(BaseFilterSet):
updated_by_request = django_filters.UUIDFilter(
method='filter_by_request'
)
+ modified_by_request = django_filters.UUIDFilter(
+ method='filter_by_request'
+ )
def filter_by_request(self, queryset, name, value):
content_type = ContentType.objects.get_for_model(self.Meta.model)
action = {
- 'created_by_request': ObjectChangeActionChoices.ACTION_CREATE,
- 'updated_by_request': ObjectChangeActionChoices.ACTION_UPDATE,
+ 'created_by_request': Q(action=ObjectChangeActionChoices.ACTION_CREATE),
+ 'updated_by_request': Q(action=ObjectChangeActionChoices.ACTION_UPDATE),
+ 'modified_by_request': Q(action__in=[ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE]),
}.get(name)
request_id = value
pks = ObjectChange.objects.filter(
+ action,
changed_object_type=content_type,
- action=action,
- request_id=request_id
+ request_id=request_id,
).values_list('changed_object_id', flat=True)
return queryset.filter(pk__in=pks)
@@ -311,5 +315,6 @@ class OrganizationalModelFilterSet(NetBoxModelFilterSet):
return queryset
return queryset.filter(
models.Q(name__icontains=value) |
- models.Q(slug__icontains=value)
+ models.Q(slug__icontains=value) |
+ models.Q(description__icontains=value)
)
diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py
index c5dac90f7..0b0e2036e 100644
--- a/netbox/netbox/forms/base.py
+++ b/netbox/netbox/forms/base.py
@@ -3,11 +3,12 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
-from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
-from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin
+from extras.choices import *
from extras.models import CustomField, Tag
-from utilities.forms import BootstrapMixin, CSVModelForm, CheckLastUpdatedMixin
+from utilities.forms import CSVModelForm
from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.mixins import BootstrapMixin, CheckLastUpdatedMixin
+from .mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
__all__ = (
'NetBoxModelForm',
@@ -17,7 +18,7 @@ __all__ = (
)
-class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin, forms.ModelForm):
+class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm):
"""
Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
@@ -26,18 +27,6 @@ class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin,
the rendered form (optional). If not defined, the all fields will be rendered as a single section.
"""
fieldsets = ()
- tags = DynamicModelMultipleChoiceField(
- queryset=Tag.objects.all(),
- required=False,
- label=_('Tags'),
- )
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # Limit tags to those applicable to the object type
- if (ct := self._get_content_type()) and hasattr(self.fields['tags'].widget, 'add_query_param'):
- self.fields['tags'].widget.add_query_param('for_object_type_id', ct.pk)
def _get_content_type(self):
return ContentType.objects.get_for_model(self._meta.model)
@@ -68,6 +57,17 @@ class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin,
return super().clean()
+ def _post_clean(self):
+ """
+ Override BaseModelForm's _post_clean() to store many-to-many field values on the model instance.
+ """
+ self.instance._m2m_values = {}
+ for field in self.instance._meta.local_many_to_many:
+ if field.name in self.cleaned_data:
+ self.instance._m2m_values[field.name] = list(self.cleaned_data[field.name])
+
+ return super()._post_clean()
+
class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
"""
@@ -87,11 +87,9 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
)
def _get_custom_fields(self, content_type):
- return CustomField.objects.filter(content_types=content_type).filter(
- ui_visibility__in=[
- CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
- CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET,
- ]
+ return CustomField.objects.filter(
+ content_types=content_type,
+ ui_editable=CustomFieldUIEditableChoices.YES
)
def _get_form_field(self, customfield):
@@ -142,7 +140,8 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
def _extend_nullable_fields(self):
nullable_custom_fields = [
- name for name, customfield in self.custom_fields.items() if (not customfield.required and customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE)
+ name for name, customfield in self.custom_fields.items()
+ if (not customfield.required and customfield.ui_editable == CustomFieldUIEditableChoices.YES)
]
self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)
@@ -156,12 +155,16 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, SavedFiltersMi
model: The model class associated with the form
fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
the rendered form (optional). If not defined, the all fields will be rendered as a single section.
+ selector_fields: An iterable of names of fields to display by default when rendering the form as
+ a selector widget
"""
q = forms.CharField(
required=False,
label=_('Search')
)
+ selector_fields = ('filter_id', 'q')
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
diff --git a/netbox/extras/forms/mixins.py b/netbox/netbox/forms/mixins.py
similarity index 76%
rename from netbox/extras/forms/mixins.py
rename to netbox/netbox/forms/mixins.py
index be45f5211..d76eb56c8 100644
--- a/netbox/extras/forms/mixins.py
+++ b/netbox/netbox/forms/mixins.py
@@ -2,13 +2,14 @@ from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
-from extras.choices import CustomFieldVisibilityChoices
+from extras.choices import *
from extras.models import *
from utilities.forms.fields import DynamicModelMultipleChoiceField
__all__ = (
'CustomFieldsMixin',
'SavedFiltersMixin',
+ 'TagsMixin',
)
@@ -39,7 +40,7 @@ class CustomFieldsMixin:
def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type).exclude(
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
+ ui_editable=CustomFieldUIEditableChoices.HIDDEN
)
def _get_form_field(self, customfield):
@@ -50,9 +51,6 @@ class CustomFieldsMixin:
Append form fields for all CustomFields assigned to this object type.
"""
for customfield in self._get_custom_fields(self._get_content_type()):
- if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN:
- continue
-
field_name = f'cf_{customfield.name}'
self.fields[field_name] = self._get_form_field(customfield)
@@ -72,3 +70,19 @@ class SavedFiltersMixin(forms.Form):
'usable': True,
}
)
+
+
+class TagsMixin(forms.Form):
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
+ required=False,
+ label=_('Tags'),
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Limit tags to those applicable to the object type
+ content_type = ContentType.objects.get_for_model(self._meta.model)
+ if content_type and hasattr(self.fields['tags'].widget, 'add_query_param'):
+ self.fields['tags'].widget.add_query_param('for_object_type_id', content_type.pk)
diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py
index 02737b819..6a9e13d1c 100644
--- a/netbox/netbox/graphql/schema.py
+++ b/netbox/netbox/graphql/schema.py
@@ -3,12 +3,31 @@ from strawberry_django.optimizer import DjangoOptimizerExtension
from strawberry.schema.config import StrawberryConfig
from circuits.graphql.schema import CircuitsQuery
from users.graphql.schema import UsersQuery
+# from virtualization.graphql.schema import VirtualizationQuery
+# from vpn.graphql.schema import VPNQuery
+# from wireless.graphql.schema import WirelessQuery
@strawberry.type
class Query(CircuitsQuery, UsersQuery):
pass
+# class Query(
+# UsersQuery,
+# CircuitsQuery,
+# CoreQuery,
+# DCIMQuery,
+# ExtrasQuery,
+# IPAMQuery,
+# TenancyQuery,
+# VirtualizationQuery,
+# VPNQuery,
+# WirelessQuery,
+# *registry['plugins']['graphql_schemas'], # Append plugin schemas
+# graphene.ObjectType
+# ):
+# pass
+
schema = strawberry.Schema(
query=Query,
diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py
index 18f350fd7..cb7d2c8ba 100644
--- a/netbox/netbox/middleware.py
+++ b/netbox/netbox/middleware.py
@@ -10,7 +10,7 @@ from django.db import connection, ProgrammingError
from django.db.utils import InternalError
from django.http import Http404, HttpResponseRedirect
-from extras.context_managers import change_logging
+from extras.context_managers import event_tracking
from netbox.config import clear_config, get_config
from netbox.views import handler_500
from utilities.api import is_api_request, rest_api_server_error
@@ -42,8 +42,8 @@ class CoreMiddleware:
login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}'
return HttpResponseRedirect(login_url)
- # Enable the change_logging context manager and process the request.
- with change_logging(request):
+ # Enable the event_tracking context manager and process the request.
+ with event_tracking(request):
response = self.get_response(request)
# Attach the unique request ID as an HTTP header.
diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py
index 596357ea4..2c262b258 100644
--- a/netbox/netbox/models/__init__.py
+++ b/netbox/netbox/models/__init__.py
@@ -1,5 +1,6 @@
from django.conf import settings
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.utils.translation import gettext_lazy as _
@@ -29,7 +30,7 @@ class NetBoxFeatureSet(
ExportTemplatesMixin,
JournalingMixin,
TagsMixin,
- WebhooksMixin
+ EventRulesMixin
):
class Meta:
abstract = True
@@ -43,7 +44,7 @@ class NetBoxFeatureSet(
# Base model classes
#
-class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, WebhooksMixin, models.Model):
+class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, EventRulesMixin, models.Model):
"""
Base model for ancillary models; provides limited functionality for models which don't
support NetBox's full feature set.
@@ -85,11 +86,16 @@ class NetBoxModel(NetBoxFeatureSet, models.Model):
if ct_value and fk_value:
klass = getattr(self, field.ct_field).model_class()
- if not klass.objects.filter(pk=fk_value).exists():
+ try:
+ obj = klass.objects.get(pk=fk_value)
+ except ObjectDoesNotExist:
raise ValidationError({
field.fk_field: f"Related object not found using the provided value: {fk_value}."
})
+ # update the GFK field value
+ setattr(self, field.name, obj)
+
#
# NetBox internal base models
diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py
index cce265efc..0cba27318 100644
--- a/netbox/netbox/models/features.py
+++ b/netbox/netbox/models/features.py
@@ -3,7 +3,6 @@ from collections import defaultdict
from functools import cached_property
from django.contrib.contenttypes.fields import GenericRelation
-from django.contrib.contenttypes.models import ContentType
from django.core.validators import ValidationError
from django.db import models
from django.db.models.signals import class_prepared
@@ -13,8 +12,10 @@ from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager
from core.choices import JobStatusChoices
-from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
+from core.models import ContentType
+from extras.choices import *
from extras.utils import is_taggable, register_features
+from netbox.config import get_config
from netbox.registry import registry
from netbox.signals import post_clean
from utilities.json import CustomFieldJSONEncoder
@@ -35,7 +36,7 @@ __all__ = (
'JournalingMixin',
'SyncedDataMixin',
'TagsMixin',
- 'WebhooksMixin',
+ 'EventRulesMixin',
)
@@ -63,19 +64,26 @@ class ChangeLoggingMixin(models.Model):
class Meta:
abstract = True
- def serialize_object(self):
+ def serialize_object(self, exclude=None):
"""
Return a JSON representation of the instance. Models can override this method to replace or extend the default
serialization logic provided by the `serialize_object()` utility function.
+
+ Args:
+ exclude: An iterable of attribute names to omit from the serialized output
"""
- return serialize_object(self)
+ return serialize_object(self, exclude=exclude or [])
def snapshot(self):
"""
Save a snapshot of the object's current state in preparation for modification. The snapshot is saved as
`_prechange_snapshot` on the instance.
"""
- self._prechange_snapshot = self.serialize_object()
+ exclude_fields = []
+ if get_config().CHANGELOG_SKIP_EMPTY_CHANGES:
+ exclude_fields = ['last_updated',]
+
+ self._prechange_snapshot = self.serialize_object(exclude=exclude_fields)
snapshot.alters_data = True
def to_objectchange(self, action):
@@ -84,6 +92,11 @@ class ChangeLoggingMixin(models.Model):
by ChangeLoggingMiddleware.
"""
from extras.models import ObjectChange
+
+ exclude = []
+ if get_config().CHANGELOG_SKIP_EMPTY_CHANGES:
+ exclude = ['last_updated']
+
objectchange = ObjectChange(
changed_object=self,
object_repr=str(self)[:200],
@@ -92,7 +105,7 @@ class ChangeLoggingMixin(models.Model):
if hasattr(self, '_prechange_snapshot'):
objectchange.prechange_data = self._prechange_snapshot
if action in (ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE):
- objectchange.postchange_data = self.serialize_object()
+ objectchange.postchange_data = self.serialize_object(exclude=exclude)
return objectchange
@@ -205,12 +218,11 @@ class CustomFieldsMixin(models.Model):
for field in CustomField.objects.get_for_model(self):
value = self.custom_field_data.get(field.name)
- # Skip fields that are hidden if 'omit_hidden' is set
- if omit_hidden:
- if field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN:
- continue
- if field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET and not value:
- continue
+ # Skip hidden fields if 'omit_hidden' is True
+ if omit_hidden and field.ui_visible == CustomFieldUIVisibleChoices.HIDDEN:
+ continue
+ elif omit_hidden and field.ui_visible == CustomFieldUIVisibleChoices.IF_SET and not value:
+ continue
data[field] = field.deserialize(value)
@@ -232,12 +244,12 @@ class CustomFieldsMixin(models.Model):
from extras.models import CustomField
groups = defaultdict(dict)
visible_custom_fields = CustomField.objects.get_for_model(self).exclude(
- ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
+ ui_visible=CustomFieldUIVisibleChoices.HIDDEN
)
for cf in visible_custom_fields:
value = self.custom_field_data.get(cf.name)
- if value in (None, []) and cf.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET:
+ if value in (None, '', []) and cf.ui_visible == CustomFieldUIVisibleChoices.IF_SET:
continue
value = cf.deserialize(value)
groups[cf.group_name][cf] = value
@@ -401,9 +413,9 @@ class TagsMixin(models.Model):
abstract = True
-class WebhooksMixin(models.Model):
+class EventRulesMixin(models.Model):
"""
- Enables support for webhooks.
+ Enables support for event rules, which can be used to transmit webhooks or execute scripts automatically.
"""
class Meta:
abstract = True
@@ -556,7 +568,7 @@ FEATURES_MAP = {
'journaling': JournalingMixin,
'synced_data': SyncedDataMixin,
'tags': TagsMixin,
- 'webhooks': WebhooksMixin,
+ 'event_rules': EventRulesMixin,
}
registry['model_features'].update({
diff --git a/netbox/netbox/navigation/__init__.py b/netbox/netbox/navigation/__init__.py
index a05b1c495..4c7190bbb 100644
--- a/netbox/netbox/navigation/__init__.py
+++ b/netbox/netbox/navigation/__init__.py
@@ -34,6 +34,7 @@ class MenuItem:
link: str
link_text: str
permissions: Optional[Sequence[str]] = ()
+ staff_only: Optional[bool] = False
buttons: Optional[Sequence[MenuItemButton]] = ()
diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py
index 6b883c838..d4969386e 100644
--- a/netbox/netbox/navigation/menu.py
+++ b/netbox/netbox/navigation/menu.py
@@ -1,4 +1,4 @@
-from django.utils.translation import gettext as _
+from django.utils.translation import gettext_lazy as _
from netbox.registry import registry
from utilities.choices import ButtonColorChoices
@@ -195,15 +195,33 @@ IPAM_MENU = Menu(
),
)
-OVERLAY_MENU = Menu(
- label=_('Overlay'),
+VPN_MENU = Menu(
+ label=_('VPN'),
icon_class='mdi mdi-graph-outline',
groups=(
MenuGroup(
- label='L2VPNs',
+ label=_('Tunnels'),
items=(
- get_model_item('ipam', 'l2vpn', _('L2VPNs')),
- get_model_item('ipam', 'l2vpntermination', _('Terminations')),
+ get_model_item('vpn', 'tunnel', _('Tunnels')),
+ get_model_item('vpn', 'tunnelgroup', _('Tunnel Groups')),
+ get_model_item('vpn', 'tunneltermination', _('Tunnel Terminations')),
+ ),
+ ),
+ MenuGroup(
+ label=_('L2VPNs'),
+ items=(
+ get_model_item('vpn', 'l2vpn', _('L2VPNs')),
+ get_model_item('vpn', 'l2vpntermination', _('Terminations')),
+ ),
+ ),
+ MenuGroup(
+ label=_('Security'),
+ items=(
+ get_model_item('vpn', 'ikeproposal', _('IKE Proposals')),
+ get_model_item('vpn', 'ikepolicy', _('IKE Policies')),
+ get_model_item('vpn', 'ipsecproposal', _('IPSec Proposals')),
+ get_model_item('vpn', 'ipsecpolicy', _('IPSec Policies')),
+ get_model_item('vpn', 'ipsecprofile', _('IPSec Profiles')),
),
),
),
@@ -218,6 +236,7 @@ VIRTUALIZATION_MENU = Menu(
items=(
get_model_item('virtualization', 'virtualmachine', _('Virtual Machines')),
get_model_item('virtualization', 'vminterface', _('Interfaces')),
+ get_model_item('virtualization', 'virtualdisk', _('Virtual Disks')),
),
),
MenuGroup(
@@ -325,6 +344,7 @@ OPERATIONS_MENU = Menu(
label=_('Integrations'),
items=(
get_model_item('core', 'datasource', _('Data Sources')),
+ get_model_item('extras', 'eventrule', _('Event Rules')),
get_model_item('extras', 'webhook', _('Webhooks')),
),
),
@@ -360,6 +380,7 @@ ADMIN_MENU = Menu(
link=f'users:netboxuser_list',
link_text=_('Users'),
permissions=[f'auth.view_user'],
+ staff_only=True,
buttons=(
MenuItemButton(
link=f'users:netboxuser_add',
@@ -382,6 +403,7 @@ ADMIN_MENU = Menu(
link=f'users:netboxgroup_list',
link_text=_('Groups'),
permissions=[f'auth.view_group'],
+ staff_only=True,
buttons=(
MenuItemButton(
link=f'users:netboxgroup_add',
@@ -399,17 +421,36 @@ ADMIN_MENU = Menu(
)
)
),
- get_model_item('users', 'token', _('API Tokens')),
- get_model_item('users', 'objectpermission', _('Permissions'), actions=['add']),
+ MenuItem(
+ link=f'users:token_list',
+ link_text=_('API Tokens'),
+ permissions=[f'users.view_token'],
+ staff_only=True,
+ buttons=get_model_buttons('users', 'token')
+ ),
+ MenuItem(
+ link=f'users:objectpermission_list',
+ link_text=_('Permissions'),
+ permissions=[f'users.view_objectpermission'],
+ staff_only=True,
+ buttons=get_model_buttons('users', 'objectpermission', actions=['add'])
+ ),
),
),
MenuGroup(
label=_('Configuration'),
items=(
MenuItem(
- link='extras:configrevision_list',
+ link='core:config',
+ link_text=_('Current Config'),
+ permissions=['core.view_configrevision'],
+ staff_only=True
+ ),
+ MenuItem(
+ link='core:configrevision_list',
link_text=_('Config Revisions'),
- permissions=['extras.view_configrevision']
+ permissions=['core.view_configrevision'],
+ staff_only=True
),
),
),
@@ -422,7 +463,7 @@ MENUS = [
CONNECTIONS_MENU,
WIRELESS_MENU,
IPAM_MENU,
- OVERLAY_MENU,
+ VPN_MENU,
VIRTUALIZATION_MENU,
CIRCUITS_MENU,
POWER_MENU,
diff --git a/netbox/netbox/plugins/__init__.py b/netbox/netbox/plugins/__init__.py
new file mode 100644
index 000000000..8b6901b7a
--- /dev/null
+++ b/netbox/netbox/plugins/__init__.py
@@ -0,0 +1,156 @@
+import collections
+from importlib import import_module
+
+from django.apps import AppConfig
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.module_loading import import_string
+from packaging import version
+
+from netbox.registry import registry
+from netbox.search import register_search
+from netbox.utils import register_data_backend
+from .navigation import *
+from .registration import *
+from .templates import *
+from .utils import *
+
+# Initialize plugin registry
+registry['plugins'].update({
+ 'graphql_schemas': [],
+ 'menus': [],
+ 'menu_items': {},
+ 'preferences': {},
+ 'template_extensions': collections.defaultdict(list),
+})
+
+DEFAULT_RESOURCE_PATHS = {
+ 'search_indexes': 'search.indexes',
+ 'data_backends': 'data_backends.backends',
+ 'graphql_schema': 'graphql.schema',
+ 'menu': 'navigation.menu',
+ 'menu_items': 'navigation.menu_items',
+ 'template_extensions': 'template_content.template_extensions',
+ 'user_preferences': 'preferences.preferences',
+}
+
+
+#
+# Plugin AppConfig class
+#
+
+class PluginConfig(AppConfig):
+ """
+ Subclass of Django's built-in AppConfig class, to be used for NetBox plugins.
+ """
+ # Plugin metadata
+ author = ''
+ author_email = ''
+ description = ''
+ version = ''
+
+ # Root URL path under /plugins. If not set, the plugin's label will be used.
+ base_url = None
+
+ # Minimum/maximum compatible versions of NetBox
+ min_version = None
+ max_version = None
+
+ # Default configuration parameters
+ default_settings = {}
+
+ # Mandatory configuration parameters
+ required_settings = []
+
+ # Middleware classes provided by the plugin
+ middleware = []
+
+ # Django-rq queues dedicated to the plugin
+ queues = []
+
+ # Django apps to append to INSTALLED_APPS when plugin requires them.
+ django_apps = []
+
+ # Optional plugin resources
+ search_indexes = None
+ data_backends = None
+ graphql_schema = None
+ menu = None
+ menu_items = None
+ template_extensions = None
+ user_preferences = None
+
+ def _load_resource(self, name):
+ # Import from the configured path, if defined.
+ if path := getattr(self, name, None):
+ return import_string(f"{self.__module__}.{path}")
+
+ # Fall back to the resource's default path. Return None if the module has not been provided.
+ default_path = f'{self.__module__}.{DEFAULT_RESOURCE_PATHS[name]}'
+ default_module, resource_name = default_path.rsplit('.', 1)
+ try:
+ module = import_module(default_module)
+ return getattr(module, resource_name, None)
+ except ModuleNotFoundError:
+ pass
+
+ def ready(self):
+ plugin_name = self.name.rsplit('.', 1)[-1]
+
+ # Register search extensions (if defined)
+ search_indexes = self._load_resource('search_indexes') or []
+ for idx in search_indexes:
+ register_search(idx)
+
+ # Register data backends (if defined)
+ data_backends = self._load_resource('data_backends') or []
+ for backend in data_backends:
+ register_data_backend()(backend)
+
+ # Register template content (if defined)
+ if template_extensions := self._load_resource('template_extensions'):
+ register_template_extensions(template_extensions)
+
+ # Register navigation menu and/or menu items (if defined)
+ if menu := self._load_resource('menu'):
+ register_menu(menu)
+ if menu_items := self._load_resource('menu_items'):
+ register_menu_items(self.verbose_name, menu_items)
+
+ # Register GraphQL schema (if defined)
+ if graphql_schema := self._load_resource('graphql_schema'):
+ register_graphql_schema(graphql_schema)
+
+ # Register user preferences (if defined)
+ if user_preferences := self._load_resource('user_preferences'):
+ register_user_preferences(plugin_name, user_preferences)
+
+ @classmethod
+ def validate(cls, user_config, netbox_version):
+
+ # Enforce version constraints
+ current_version = version.parse(netbox_version)
+ if cls.min_version is not None:
+ min_version = version.parse(cls.min_version)
+ if current_version < min_version:
+ raise ImproperlyConfigured(
+ f"Plugin {cls.__module__} requires NetBox minimum version {cls.min_version}."
+ )
+ if cls.max_version is not None:
+ max_version = version.parse(cls.max_version)
+ if current_version > max_version:
+ raise ImproperlyConfigured(
+ f"Plugin {cls.__module__} requires NetBox maximum version {cls.max_version}."
+ )
+
+ # Verify required configuration settings
+ for setting in cls.required_settings:
+ if setting not in user_config:
+ raise ImproperlyConfigured(
+ f"Plugin {cls.__module__} requires '{setting}' to be present in the PLUGINS_CONFIG section of "
+ f"configuration.py."
+ )
+
+ # Apply default configuration values
+ for setting, value in cls.default_settings.items():
+ if setting not in user_config:
+ user_config[setting] = value
diff --git a/netbox/netbox/plugins/navigation.py b/netbox/netbox/plugins/navigation.py
new file mode 100644
index 000000000..2075c97b6
--- /dev/null
+++ b/netbox/netbox/plugins/navigation.py
@@ -0,0 +1,72 @@
+from netbox.navigation import MenuGroup
+from utilities.choices import ButtonColorChoices
+from django.utils.text import slugify
+
+__all__ = (
+ 'PluginMenu',
+ 'PluginMenuButton',
+ 'PluginMenuItem',
+)
+
+
+class PluginMenu:
+ icon_class = 'mdi mdi-puzzle'
+
+ def __init__(self, label, groups, icon_class=None):
+ self.label = label
+ self.groups = [
+ MenuGroup(label, items) for label, items in groups
+ ]
+ if icon_class is not None:
+ self.icon_class = icon_class
+
+ @property
+ def name(self):
+ return slugify(self.label)
+
+
+class PluginMenuItem:
+ """
+ This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
+ specifying additional link buttons that appear to the right of the item in the van menu.
+
+ Links are specified as Django reverse URL strings.
+ Buttons are each specified as a list of PluginMenuButton instances.
+ """
+ permissions = []
+ buttons = []
+
+ def __init__(self, link, link_text, staff_only=False, permissions=None, buttons=None):
+ self.link = link
+ self.link_text = link_text
+ self.staff_only = staff_only
+ if permissions is not None:
+ if type(permissions) not in (list, tuple):
+ raise TypeError("Permissions must be passed as a tuple or list.")
+ self.permissions = permissions
+ if buttons is not None:
+ if type(buttons) not in (list, tuple):
+ raise TypeError("Buttons must be passed as a tuple or list.")
+ self.buttons = buttons
+
+
+class PluginMenuButton:
+ """
+ This class represents a button within a PluginMenuItem. Note that button colors should come from
+ ButtonColorChoices.
+ """
+ color = ButtonColorChoices.DEFAULT
+ permissions = []
+
+ def __init__(self, link, title, icon_class, color=None, permissions=None):
+ self.link = link
+ self.title = title
+ self.icon_class = icon_class
+ if permissions is not None:
+ if type(permissions) not in (list, tuple):
+ raise TypeError("Permissions must be passed as a tuple or list.")
+ self.permissions = permissions
+ if color is not None:
+ if color not in ButtonColorChoices.values():
+ raise ValueError("Button color must be a choice within ButtonColorChoices.")
+ self.color = color
diff --git a/netbox/netbox/plugins/registration.py b/netbox/netbox/plugins/registration.py
new file mode 100644
index 000000000..3be538441
--- /dev/null
+++ b/netbox/netbox/plugins/registration.py
@@ -0,0 +1,64 @@
+import inspect
+
+from netbox.registry import registry
+from .navigation import PluginMenu, PluginMenuButton, PluginMenuItem
+from .templates import PluginTemplateExtension
+
+__all__ = (
+ 'register_graphql_schema',
+ 'register_menu',
+ 'register_menu_items',
+ 'register_template_extensions',
+ 'register_user_preferences',
+)
+
+
+def register_template_extensions(class_list):
+ """
+ Register a list of PluginTemplateExtension classes
+ """
+ # Validation
+ for template_extension in class_list:
+ if not inspect.isclass(template_extension):
+ raise TypeError(f"PluginTemplateExtension class {template_extension} was passed as an instance!")
+ if not issubclass(template_extension, PluginTemplateExtension):
+ raise TypeError(f"{template_extension} is not a subclass of netbox.plugins.PluginTemplateExtension!")
+ if template_extension.model is None:
+ raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!")
+
+ registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
+
+
+def register_menu(menu):
+ if not isinstance(menu, PluginMenu):
+ raise TypeError(f"{menu} must be an instance of netbox.plugins.PluginMenu")
+ registry['plugins']['menus'].append(menu)
+
+
+def register_menu_items(section_name, class_list):
+ """
+ Register a list of PluginMenuItem instances for a given menu section (e.g. plugin name)
+ """
+ # Validation
+ for menu_link in class_list:
+ if not isinstance(menu_link, PluginMenuItem):
+ raise TypeError(f"{menu_link} must be an instance of netbox.plugins.PluginMenuItem")
+ for button in menu_link.buttons:
+ if not isinstance(button, PluginMenuButton):
+ raise TypeError(f"{button} must be an instance of netbox.plugins.PluginMenuButton")
+
+ registry['plugins']['menu_items'][section_name] = class_list
+
+
+def register_graphql_schema(graphql_schema):
+ """
+ Register a GraphQL schema class for inclusion in NetBox's GraphQL API.
+ """
+ registry['plugins']['graphql_schemas'].append(graphql_schema)
+
+
+def register_user_preferences(plugin_name, preferences):
+ """
+ Register a list of user preferences defined by a plugin.
+ """
+ registry['plugins']['preferences'][plugin_name] = preferences
diff --git a/netbox/netbox/plugins/templates.py b/netbox/netbox/plugins/templates.py
new file mode 100644
index 000000000..e9b9a9dca
--- /dev/null
+++ b/netbox/netbox/plugins/templates.py
@@ -0,0 +1,73 @@
+from django.template.loader import get_template
+
+__all__ = (
+ 'PluginTemplateExtension',
+)
+
+
+class PluginTemplateExtension:
+ """
+ This class is used to register plugin content to be injected into core NetBox templates. It contains methods
+ that are overridden by plugin authors to return template content.
+
+ The `model` attribute on the class defines the which model detail page this class renders content for. It
+ should be set as a string in the form '.'. render() provides the following context data:
+
+ * object - The object being viewed
+ * request - The current request
+ * settings - Global NetBox settings
+ * config - Plugin-specific configuration parameters
+ """
+ model = None
+
+ def __init__(self, context):
+ self.context = context
+
+ def render(self, template_name, extra_context=None):
+ """
+ Convenience method for rendering the specified Django template using the default context data. An additional
+ context dictionary may be passed as `extra_context`.
+ """
+ if extra_context is None:
+ extra_context = {}
+ elif not isinstance(extra_context, dict):
+ raise TypeError("extra_context must be a dictionary")
+
+ return get_template(template_name).render({**self.context, **extra_context})
+
+ def left_page(self):
+ """
+ Content that will be rendered on the left of 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 right_page(self):
+ """
+ Content that will be rendered on the right of 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 full_width_page(self):
+ """
+ Content that will be rendered within the full width of 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 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
new file mode 100644
index 000000000..2f237f56a
--- /dev/null
+++ b/netbox/netbox/plugins/urls.py
@@ -0,0 +1,41 @@
+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'),
+ path('installed-plugins/', views.InstalledPluginsAPIView.as_view(), name='plugins-list')
+]
+plugin_admin_patterns = [
+ path('installed-plugins/', staff_member_required(views.InstalledPluginsAdminView.as_view()), name='plugins_list')
+]
+
+# Register base/API URL patterns for each plugin
+for plugin_path in settings.PLUGINS:
+ plugin = import_module(plugin_path)
+ plugin_name = plugin_path.split('.')[-1]
+ app = apps.get_app_config(plugin_name)
+ base_url = getattr(app, 'base_url') or app.label
+
+ # Check if the plugin specifies any base URLs
+ if module_has_submodule(plugin, 'urls'):
+ urlpatterns = import_string(f"{plugin_path}.urls.urlpatterns")
+ plugin_patterns.append(
+ path(f"{base_url}/", include((urlpatterns, app.label)))
+ )
+
+ # Check if the plugin specifies any API URLs
+ if module_has_submodule(plugin, 'api.urls'):
+ urlpatterns = import_string(f"{plugin_path}.api.urls.urlpatterns")
+ plugin_api_patterns.append(
+ path(f"{base_url}/", include((urlpatterns, f"{app.label}-api")))
+ )
diff --git a/netbox/netbox/plugins/utils.py b/netbox/netbox/plugins/utils.py
new file mode 100644
index 000000000..c260f156d
--- /dev/null
+++ b/netbox/netbox/plugins/utils.py
@@ -0,0 +1,37 @@
+from django.apps import apps
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+
+__all__ = (
+ 'get_installed_plugins',
+ 'get_plugin_config',
+)
+
+
+def get_installed_plugins():
+ """
+ Return a dictionary mapping the names of installed plugins to their versions.
+ """
+ plugins = {}
+ for plugin_name in settings.PLUGINS:
+ plugin_name = plugin_name.rsplit('.', 1)[-1]
+ plugin_config = apps.get_app_config(plugin_name)
+ plugins[plugin_name] = getattr(plugin_config, 'version', None)
+
+ return dict(sorted(plugins.items()))
+
+
+def get_plugin_config(plugin_name, parameter, default=None):
+ """
+ Return the value of the specified plugin configuration parameter.
+
+ Args:
+ plugin_name: The name of the plugin
+ parameter: The name of the configuration parameter
+ default: The value to return if the parameter is not defined (default: None)
+ """
+ try:
+ plugin_config = settings.PLUGINS_CONFIG[plugin_name]
+ return plugin_config.get(parameter, default)
+ except KeyError:
+ raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.")
diff --git a/netbox/netbox/plugins/views.py b/netbox/netbox/plugins/views.py
new file mode 100644
index 000000000..5971f78ef
--- /dev/null
+++ b/netbox/netbox/plugins/views.py
@@ -0,0 +1,89 @@
+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
+from rest_framework.reverse import reverse
+from rest_framework.views import APIView
+
+
+class InstalledPluginsAdminView(View):
+ """
+ Admin view for listing all installed plugins
+ """
+ def get(self, request):
+ plugins = [apps.get_app_config(plugin) for plugin in settings.PLUGINS]
+ return render(request, 'extras/admin/plugins_list.html', {
+ 'plugins': plugins,
+ })
+
+
+@extend_schema(exclude=True)
+class InstalledPluginsAPIView(APIView):
+ """
+ API view for listing all installed plugins
+ """
+ permission_classes = [permissions.IsAdminUser]
+ _ignore_model_permissions = True
+ schema = None
+
+ def get_view_name(self):
+ return "Installed Plugins"
+
+ @staticmethod
+ def _get_plugin_data(plugin_app_config):
+ return {
+ 'name': plugin_app_config.verbose_name,
+ 'package': plugin_app_config.name,
+ 'author': plugin_app_config.author,
+ 'author_email': plugin_app_config.author_email,
+ 'description': plugin_app_config.description,
+ 'version': plugin_app_config.version
+ }
+
+ def get(self, request, format=None):
+ return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS])
+
+
+@extend_schema(exclude=True)
+class PluginsAPIRootView(APIView):
+ _ignore_model_permissions = True
+ schema = None
+
+ def get_view_name(self):
+ return "Plugins"
+
+ @staticmethod
+ def _get_plugin_entry(plugin, app_config, request, format):
+ # Check if the plugin specifies any API URLs
+ api_app_name = f'{app_config.name}-api'
+ try:
+ entry = (getattr(app_config, 'base_url', app_config.label), reverse(
+ f"plugins-api:{api_app_name}:api-root",
+ request=request,
+ format=format
+ ))
+ except NoReverseMatch:
+ # The plugin does not include an api-root url
+ entry = None
+
+ return entry
+
+ def get(self, request, format=None):
+
+ entries = []
+ for plugin in settings.PLUGINS:
+ app_config = apps.get_app_config(plugin)
+ entry = self._get_plugin_entry(plugin, app_config, request, format)
+ if entry is not None:
+ entries.append(entry)
+
+ return Response(OrderedDict((
+ ('installed-plugins', reverse('plugins-api:plugins-list', request=request, format=format)),
+ *entries
+ )))
diff --git a/netbox/netbox/preferences.py b/netbox/netbox/preferences.py
index 5ef216259..9a6fe490c 100644
--- a/netbox/netbox/preferences.py
+++ b/netbox/netbox/preferences.py
@@ -1,4 +1,6 @@
+from django.conf import settings
from django.utils.translation import gettext as _
+
from netbox.registry import registry
from users.preferences import UserPreference
from utilities.paginator import EnhancedPaginator
@@ -16,11 +18,18 @@ PREFERENCES = {
'ui.colormode': UserPreference(
label=_('Color mode'),
choices=(
- ('light', 'Light'),
- ('dark', 'Dark'),
+ ('light', _('Light')),
+ ('dark', _('Dark')),
),
default='light',
),
+ 'locale.language': UserPreference(
+ label=_('Language'),
+ choices=(
+ ('', _('Auto')),
+ *settings.LANGUAGES,
+ )
+ ),
'pagination.per_page': UserPreference(
label=_('Page length'),
choices=get_page_lengths(),
@@ -30,9 +39,9 @@ PREFERENCES = {
'pagination.placement': UserPreference(
label=_('Paginator placement'),
choices=(
- ('bottom', 'Bottom'),
- ('top', 'Top'),
- ('both', 'Both'),
+ ('bottom', _('Bottom')),
+ ('top', _('Top')),
+ ('both', _('Both')),
),
description=_('Where the paginator controls will be displayed relative to a table'),
default='bottom'
diff --git a/netbox/netbox/registry.py b/netbox/netbox/registry.py
index 21a869001..ad8c18dcf 100644
--- a/netbox/netbox/registry.py
+++ b/netbox/netbox/registry.py
@@ -25,8 +25,10 @@ registry = Registry({
'data_backends': dict(),
'denormalized_fields': collections.defaultdict(list),
'model_features': dict(),
+ 'models': collections.defaultdict(set),
'plugins': dict(),
'search': dict(),
+ 'tables': collections.defaultdict(dict),
'views': collections.defaultdict(dict),
'widgets': dict(),
})
diff --git a/netbox/netbox/search/__init__.py b/netbox/netbox/search/__init__.py
index 6d53e9a97..590188f21 100644
--- a/netbox/netbox/search/__init__.py
+++ b/netbox/netbox/search/__init__.py
@@ -33,10 +33,12 @@ class SearchIndex:
category: The label of the group under which this indexer is categorized (for form field display). If none,
the name of the model's app will be used.
fields: An iterable of two-tuples defining the model fields to be indexed and the weight associated with each.
+ display_attrs: An iterable of additional object attributes to include when displaying search results.
"""
model = None
category = None
fields = ()
+ display_attrs = ()
@staticmethod
def get_field_type(instance, field_name):
diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py
index 4487b6bb8..1fb23a37c 100644
--- a/netbox/netbox/search/backends.py
+++ b/netbox/netbox/search/backends.py
@@ -3,7 +3,8 @@ from collections import defaultdict
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
-from django.db.models import F, Window, Q
+from django.db.models import F, Window, Q, prefetch_related_objects
+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
@@ -13,7 +14,7 @@ from netaddr.core import AddrFormatError
from extras.models import CachedValue, CustomField
from netbox.registry import registry
from utilities.querysets import RestrictedPrefetch
-from utilities.utils import title
+from utilities.utils import content_type_identifier, title
from . import FieldTypes, LookupTypes, get_indexer
DEFAULT_LOOKUP_TYPE = LookupTypes.PARTIAL
@@ -103,17 +104,17 @@ class CachedValueSearchBackend(SearchBackend):
def search(self, value, user=None, object_types=None, lookup=DEFAULT_LOOKUP_TYPE):
+ # Build the filter used to find relevant CachedValue records
query_filter = Q(**{f'value__{lookup}': value})
-
if object_types:
+ # Limit results by object type
query_filter &= Q(object_type__in=object_types)
-
if lookup in (LookupTypes.STARTSWITH, LookupTypes.ENDSWITH):
- # Partial string matches are valid only on string values
+ # "Starts/ends with" matches are valid only on string values
query_filter &= Q(type=FieldTypes.STRING)
-
- if lookup == LookupTypes.PARTIAL:
+ elif lookup == LookupTypes.PARTIAL:
try:
+ # If the value looks like an IP address, add an extra match for CIDR values
address = str(netaddr.IPNetwork(value.strip()).cidr)
query_filter |= Q(type=FieldTypes.CIDR) & Q(value__net_contains_or_equals=address)
except (AddrFormatError, ValueError):
@@ -129,6 +130,12 @@ class CachedValueSearchBackend(SearchBackend):
)
)[:MAX_RESULTS]
+ # Gather all ContentTypes present in the search results (used for prefetching related
+ # objects). This must be done before generating the final results list, which returns
+ # a RawQuerySet.
+ content_type_ids = set(queryset.values_list('object_type', flat=True))
+ content_types = ContentType.objects.filter(pk__in=content_type_ids)
+
# Construct a Prefetch to pre-fetch only those related objects for which the
# user has permission to view.
if user:
@@ -144,12 +151,34 @@ class CachedValueSearchBackend(SearchBackend):
params
)
+ # Iterate through each ContentType represented in the search results and prefetch any
+ # related objects necessary to render the prescribed display attributes (display_attrs).
+ for ct in content_types:
+ model = ct.model_class()
+ indexer = registry['search'].get(content_type_identifier(ct))
+ if not (display_attrs := getattr(indexer, 'display_attrs', None)):
+ continue
+
+ # Add ForeignKey fields to prefetch list
+ prefetch_fields = []
+ for attr in display_attrs:
+ field = model._meta.get_field(attr)
+ if type(field) is ForeignKey:
+ prefetch_fields.append(f'object__{attr}')
+
+ # Compile a list of all CachedValues referencing this object type, and prefetch
+ # any related objects
+ if prefetch_fields:
+ objects = [r for r in results if r.object_type == ct]
+ prefetch_related_objects(objects, *prefetch_fields)
+
# Omit any results pertaining to an object the user does not have permission to view
ret = []
for r in results:
if r.object is not None:
r.name = str(r.object)
ret.append(r)
+
return ret
def cache(self, instances, indexer=None, remove_existing=True):
diff --git a/netbox/netbox/search/utils.py b/netbox/netbox/search/utils.py
new file mode 100644
index 000000000..824fbfb3d
--- /dev/null
+++ b/netbox/netbox/search/utils.py
@@ -0,0 +1,14 @@
+from netbox.registry import registry
+from utilities.utils import content_type_identifier
+
+__all__ = (
+ 'get_indexer',
+)
+
+
+def get_indexer(content_type):
+ """
+ Return the registered search indexer for the given ContentType.
+ """
+ ct_identifier = content_type_identifier(content_type)
+ return registry['search'].get(ct_identifier)
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index f28449b12..33a75b8eb 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -9,23 +9,26 @@ import warnings
from urllib.parse import urlencode, urlsplit
import django
-import sentry_sdk
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 extras.plugins import PluginConfig
-from sentry_sdk.integrations.django import DjangoIntegration
+from django.utils.translation import gettext_lazy as _
+try:
+ import sentry_sdk
+except ModuleNotFoundError:
+ pass
from netbox.config import PARAMS
from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
+from netbox.plugins import PluginConfig
#
# Environment setup
#
-VERSION = '3.6-beta2'
+VERSION = '3.7-beta1'
# Hostname
HOSTNAME = platform.node()
@@ -39,8 +42,6 @@ if sys.version_info < (3, 8):
f"NetBox requires Python 3.8 or later. (Currently installed: Python {platform.python_version()})"
)
-DEFAULT_SENTRY_DSN = 'https://198cf560b29d4054ab8e583a1d10ea58@o1242133.ingest.sentry.io/6396485'
-
#
# Configuration import
#
@@ -95,6 +96,7 @@ CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
CSRF_COOKIE_SECURE = getattr(configuration, 'CSRF_COOKIE_SECURE', False)
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
+DATA_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'DATA_UPLOAD_MAX_MEMORY_SIZE', 2621440)
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
DEBUG = getattr(configuration, 'DEBUG', False)
@@ -114,6 +116,9 @@ DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', {
DEVELOPER = getattr(configuration, 'DEVELOPER', 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', (
+ 'extras.events.process_event_queue',
+))
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)
@@ -157,7 +162,7 @@ RQ_RETRY_MAX = getattr(configuration, 'RQ_RETRY_MAX', 0)
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend')
SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
-SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', DEFAULT_SENTRY_DSN)
+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_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE', 0)
@@ -173,6 +178,7 @@ STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
ENABLE_LOCALIZATION = getattr(configuration, 'ENABLE_LOCALIZATION', False)
+CHANGELOG_SKIP_EMPTY_CHANGES = getattr(configuration, 'CHANGELOG_SKIP_EMPTY_CHANGES', True)
# Check for hard-coded dynamic config parameters
for param in PARAMS:
@@ -355,6 +361,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
+ 'django.forms',
'corsheaders',
'debug_toolbar',
'graphiql_debug_toolbar',
@@ -377,6 +384,7 @@ INSTALLED_APPS = [
'users',
'utilities',
'virtualization',
+ 'vpn',
'wireless',
'django_rq', # Must come after extras to allow overriding management commands
'drf_spectacular',
@@ -430,6 +438,9 @@ TEMPLATES = [
},
]
+# This allows us to override Django's stock form widget templates
+FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
+
# Set up authentication backends
if type(REMOTE_AUTH_BACKEND) not in (list, tuple):
REMOTE_AUTH_BACKEND = [REMOTE_AUTH_BACKEND]
@@ -496,6 +507,10 @@ AUTH_EXEMPT_PATHS = (
# 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,
+ LOGOUT_REDIRECT_URL
)
SERIALIZATION_MODULES = {
@@ -508,12 +523,12 @@ SERIALIZATION_MODULES = {
#
if SENTRY_ENABLED:
+ try:
+ from sentry_sdk.integrations.django import DjangoIntegration
+ except ModuleNotFoundError:
+ raise ImproperlyConfigured("SENTRY_ENABLED is True but the sentry-sdk package is not installed.")
if not SENTRY_DSN:
raise ImproperlyConfigured("SENTRY_ENABLED is True but SENTRY_DSN has not been defined.")
- # If using the default DSN, force sampling rates
- if SENTRY_DSN == DEFAULT_SENTRY_DSN:
- SENTRY_SAMPLE_RATE = 1.0
- SENTRY_TRACES_SAMPLE_RATE = 0
# Initialize the SDK
sentry_sdk.init(
dsn=SENTRY_DSN,
@@ -528,9 +543,6 @@ if SENTRY_ENABLED:
# Assign any configured tags
for k, v in SENTRY_TAGS.items():
sentry_sdk.set_tag(k, v)
- # If using the default DSN, append a unique deployment ID tag for error correlation
- if SENTRY_DSN == DEFAULT_SENTRY_DSN:
- sentry_sdk.set_tag('netbox.deployment_id', DEPLOYMENT_ID)
#
@@ -665,7 +677,7 @@ GRAPHENE = {
#
-# Django RQ (Webhooks backend)
+# Django RQ (events backend)
#
if TASKS_REDIS_USING_SENTINEL:
@@ -710,6 +722,14 @@ RQ_QUEUES.update({
# Localization
#
+LANGUAGES = (
+ ('en', _('English')),
+ ('es', _('Spanish')),
+ ('fr', _('French')),
+ ('pt', _('Portuguese')),
+ ('ru', _('Russian')),
+)
+
LOCALE_PATHS = (
BASE_DIR + '/translations',
)
diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py
index 6d6eeb97e..d2cd0a0d4 100644
--- a/netbox/netbox/tables/columns.py
+++ b/netbox/netbox/tables/columns.py
@@ -4,6 +4,7 @@ from urllib.parse import quote
import django_tables2 as tables
from django.conf import settings
+from django.contrib.auth.context_processors import auth
from django.contrib.auth.models import AnonymousUser
from django.db.models import DateField, DateTimeField
from django.template import Context, Template
@@ -482,8 +483,10 @@ class CustomFieldColumn(tables.Column):
return mark_safe(' ')
if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
return mark_safe(f'{escape(value)} ')
+ if self.customfield.type == CustomFieldTypeChoices.TYPE_SELECT:
+ return self.customfield.get_choice_label(value)
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
- return ', '.join(v for v in value)
+ return ', '.join(self.customfield.get_choice_label(v) for v in value)
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
return mark_safe(', '.join(
self._linkify_item(obj) for obj in self.customfield.deserialize(value)
@@ -517,24 +520,32 @@ class CustomLinkColumn(tables.Column):
super().__init__(*args, **kwargs)
- def render(self, record):
- try:
- rendered = self.customlink.render({
- 'object': record,
+ def _render_customlink(self, record, table):
+ context = {
+ 'object': record,
+ 'debug': settings.DEBUG,
+ }
+ if request := getattr(table, 'context', {}).get('request'):
+ # If the request is available, include it as context
+ context.update({
+ 'request': request,
+ **auth(request),
})
- if rendered:
+
+ return self.customlink.render(context)
+
+ def render(self, record, table, **kwargs):
+ try:
+ if rendered := self._render_customlink(record, table):
return mark_safe(f'{rendered["text"]} ')
except Exception as e:
error_text = _('Error')
return mark_safe(f' {error_text} ')
return ''
- def value(self, record):
+ def value(self, record, table, **kwargs):
try:
- rendered = self.customlink.render({
- 'object': record,
- })
- if rendered:
+ if rendered := self._render_customlink(record, table):
return rendered['link']
except Exception:
pass
diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py
index 52ff69aa9..495e56991 100644
--- a/netbox/netbox/tables/tables.py
+++ b/netbox/netbox/tables/tables.py
@@ -1,3 +1,5 @@
+from copy import deepcopy
+
import django_tables2 as tables
from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.fields import GenericForeignKey
@@ -10,11 +12,13 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django_tables2.data import TableQuerysetData
+from extras.choices import *
from extras.models import CustomField, CustomLink
-from extras.choices import CustomFieldVisibilityChoices
+from netbox.registry import registry
from netbox.tables import columns
from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.utils import get_viewname, highlight_string, title
+from .template_code import *
__all__ = (
'BaseTable',
@@ -119,7 +123,7 @@ class BaseTable(tables.Table):
@property
def available_columns(self):
- return self._get_columns(visible=False)
+ return sorted(self._get_columns(visible=False))
@property
def selected_columns(self):
@@ -190,12 +194,17 @@ class NetBoxTable(BaseTable):
if extra_columns is None:
extra_columns = []
+ if registered_columns := registry['tables'].get(self.__class__):
+ extra_columns.extend([
+ # Create a copy to avoid modifying the original Column
+ (name, deepcopy(column)) for name, column in registered_columns.items()
+ ])
+
# Add custom field & custom link columns
content_type = ContentType.objects.get_for_model(self._meta.model)
custom_fields = CustomField.objects.filter(
content_types=content_type
- ).exclude(ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN)
-
+ ).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN)
extra_columns.extend([
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
])
@@ -236,6 +245,10 @@ class SearchTable(tables.Table):
value = tables.Column(
verbose_name=_('Value'),
)
+ attrs = columns.TemplateColumn(
+ template_code=SEARCH_RESULT_ATTRS,
+ verbose_name=_('Attributes')
+ )
trim_length = 30
diff --git a/netbox/netbox/tables/template_code.py b/netbox/netbox/tables/template_code.py
new file mode 100644
index 000000000..60bfda0c9
--- /dev/null
+++ b/netbox/netbox/tables/template_code.py
@@ -0,0 +1,18 @@
+SEARCH_RESULT_ATTRS = """
+{% for name, value in record.display_attrs.items %}
+ 40 %} data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ value }}"{% endif %}
+ >
+ {{ name|bettertitle }}:
+ {% with url=value.get_absolute_url %}
+ {% if url %}{% endif %}
+ {% if value|length > 40 %}
+ {{ value|truncatechars:"40" }}
+ {% else %}
+ {{ value }}
+ {% endif %}
+ {% if url %} {% endif %}
+ {% endwith %}
+
+{% endfor %}
+"""
diff --git a/netbox/extras/tests/dummy_plugin/__init__.py b/netbox/netbox/tests/dummy_plugin/__init__.py
similarity index 72%
rename from netbox/extras/tests/dummy_plugin/__init__.py
rename to netbox/netbox/tests/dummy_plugin/__init__.py
index 83baf064f..3ade8f9df 100644
--- a/netbox/extras/tests/dummy_plugin/__init__.py
+++ b/netbox/netbox/tests/dummy_plugin/__init__.py
@@ -1,8 +1,8 @@
-from extras.plugins import PluginConfig
+from netbox.plugins import PluginConfig
class DummyPluginConfig(PluginConfig):
- name = 'extras.tests.dummy_plugin'
+ name = 'netbox.tests.dummy_plugin'
verbose_name = 'Dummy plugin'
version = '0.0'
description = 'For testing purposes only'
@@ -10,7 +10,7 @@ class DummyPluginConfig(PluginConfig):
min_version = '1.0'
max_version = '9.0'
middleware = [
- 'extras.tests.dummy_plugin.middleware.DummyMiddleware'
+ 'netbox.tests.dummy_plugin.middleware.DummyMiddleware'
]
queues = [
'testing-low',
diff --git a/netbox/extras/tests/dummy_plugin/admin.py b/netbox/netbox/tests/dummy_plugin/admin.py
similarity index 100%
rename from netbox/extras/tests/dummy_plugin/admin.py
rename to netbox/netbox/tests/dummy_plugin/admin.py
diff --git a/netbox/extras/tests/dummy_plugin/api/serializers.py b/netbox/netbox/tests/dummy_plugin/api/serializers.py
similarity index 76%
rename from netbox/extras/tests/dummy_plugin/api/serializers.py
rename to netbox/netbox/tests/dummy_plugin/api/serializers.py
index 101786168..239d7d998 100644
--- a/netbox/extras/tests/dummy_plugin/api/serializers.py
+++ b/netbox/netbox/tests/dummy_plugin/api/serializers.py
@@ -1,5 +1,5 @@
from rest_framework.serializers import ModelSerializer
-from extras.tests.dummy_plugin.models import DummyModel
+from netbox.tests.dummy_plugin.models import DummyModel
class DummySerializer(ModelSerializer):
diff --git a/netbox/extras/tests/dummy_plugin/api/urls.py b/netbox/netbox/tests/dummy_plugin/api/urls.py
similarity index 100%
rename from netbox/extras/tests/dummy_plugin/api/urls.py
rename to netbox/netbox/tests/dummy_plugin/api/urls.py
diff --git a/netbox/extras/tests/dummy_plugin/api/views.py b/netbox/netbox/tests/dummy_plugin/api/views.py
similarity index 78%
rename from netbox/extras/tests/dummy_plugin/api/views.py
rename to netbox/netbox/tests/dummy_plugin/api/views.py
index 1977ec2af..58f221285 100644
--- a/netbox/extras/tests/dummy_plugin/api/views.py
+++ b/netbox/netbox/tests/dummy_plugin/api/views.py
@@ -1,5 +1,5 @@
from rest_framework.viewsets import ModelViewSet
-from extras.tests.dummy_plugin.models import DummyModel
+from netbox.tests.dummy_plugin.models import DummyModel
from .serializers import DummySerializer
diff --git a/netbox/netbox/tests/dummy_plugin/data_backends.py b/netbox/netbox/tests/dummy_plugin/data_backends.py
new file mode 100644
index 000000000..9b63e51c6
--- /dev/null
+++ b/netbox/netbox/tests/dummy_plugin/data_backends.py
@@ -0,0 +1,18 @@
+from contextlib import contextmanager
+
+from netbox.data_backends import DataBackend
+
+
+class DummyBackend(DataBackend):
+ name = 'dummy'
+ label = 'Dummy'
+ is_local = True
+
+ @contextmanager
+ def fetch(self):
+ yield '/tmp'
+
+
+backends = (
+ DummyBackend,
+)
diff --git a/netbox/extras/tests/dummy_plugin/graphql.py b/netbox/netbox/tests/dummy_plugin/graphql.py
similarity index 100%
rename from netbox/extras/tests/dummy_plugin/graphql.py
rename to netbox/netbox/tests/dummy_plugin/graphql.py
diff --git a/netbox/extras/tests/dummy_plugin/middleware.py b/netbox/netbox/tests/dummy_plugin/middleware.py
similarity index 100%
rename from netbox/extras/tests/dummy_plugin/middleware.py
rename to netbox/netbox/tests/dummy_plugin/middleware.py
diff --git a/netbox/extras/tests/dummy_plugin/migrations/0001_initial.py b/netbox/netbox/tests/dummy_plugin/migrations/0001_initial.py
similarity index 100%
rename from netbox/extras/tests/dummy_plugin/migrations/0001_initial.py
rename to netbox/netbox/tests/dummy_plugin/migrations/0001_initial.py
diff --git a/netbox/extras/tests/dummy_plugin/migrations/__init__.py b/netbox/netbox/tests/dummy_plugin/migrations/__init__.py
similarity index 100%
rename from netbox/extras/tests/dummy_plugin/migrations/__init__.py
rename to netbox/netbox/tests/dummy_plugin/migrations/__init__.py
diff --git a/netbox/extras/tests/dummy_plugin/models.py b/netbox/netbox/tests/dummy_plugin/models.py
similarity index 100%
rename from netbox/extras/tests/dummy_plugin/models.py
rename to netbox/netbox/tests/dummy_plugin/models.py
diff --git a/netbox/extras/tests/dummy_plugin/navigation.py b/netbox/netbox/tests/dummy_plugin/navigation.py
similarity index 90%
rename from netbox/extras/tests/dummy_plugin/navigation.py
rename to netbox/netbox/tests/dummy_plugin/navigation.py
index a9157b368..4e7bb4be8 100644
--- a/netbox/extras/tests/dummy_plugin/navigation.py
+++ b/netbox/netbox/tests/dummy_plugin/navigation.py
@@ -1,5 +1,5 @@
from django.utils.translation import gettext as _
-from extras.plugins import PluginMenu, PluginMenuButton, PluginMenuItem
+from netbox.plugins.navigation import PluginMenu, PluginMenuButton, PluginMenuItem
items = (
diff --git a/netbox/extras/tests/dummy_plugin/preferences.py b/netbox/netbox/tests/dummy_plugin/preferences.py
similarity index 100%
rename from netbox/extras/tests/dummy_plugin/preferences.py
rename to netbox/netbox/tests/dummy_plugin/preferences.py
diff --git a/netbox/extras/tests/dummy_plugin/search.py b/netbox/netbox/tests/dummy_plugin/search.py
similarity index 100%
rename from netbox/extras/tests/dummy_plugin/search.py
rename to netbox/netbox/tests/dummy_plugin/search.py
diff --git a/netbox/netbox/tests/dummy_plugin/tables.py b/netbox/netbox/tests/dummy_plugin/tables.py
new file mode 100644
index 000000000..0f1e823d7
--- /dev/null
+++ b/netbox/netbox/tests/dummy_plugin/tables.py
@@ -0,0 +1,11 @@
+import django_tables2 as tables
+
+from dcim.tables import SiteTable
+from utilities.tables import register_table_column
+
+mycol = tables.Column(
+ verbose_name='My column',
+ accessor=tables.A('description')
+)
+
+register_table_column(mycol, 'foo', SiteTable)
diff --git a/netbox/extras/tests/dummy_plugin/template_content.py b/netbox/netbox/tests/dummy_plugin/template_content.py
similarity index 88%
rename from netbox/extras/tests/dummy_plugin/template_content.py
rename to netbox/netbox/tests/dummy_plugin/template_content.py
index 364768a22..b63338f2f 100644
--- a/netbox/extras/tests/dummy_plugin/template_content.py
+++ b/netbox/netbox/tests/dummy_plugin/template_content.py
@@ -1,4 +1,4 @@
-from extras.plugins import PluginTemplateExtension
+from netbox.plugins.templates import PluginTemplateExtension
class SiteContent(PluginTemplateExtension):
diff --git a/netbox/extras/tests/dummy_plugin/urls.py b/netbox/netbox/tests/dummy_plugin/urls.py
similarity index 100%
rename from netbox/extras/tests/dummy_plugin/urls.py
rename to netbox/netbox/tests/dummy_plugin/urls.py
diff --git a/netbox/extras/tests/dummy_plugin/views.py b/netbox/netbox/tests/dummy_plugin/views.py
similarity index 88%
rename from netbox/extras/tests/dummy_plugin/views.py
rename to netbox/netbox/tests/dummy_plugin/views.py
index 8713102c5..03a83b585 100644
--- a/netbox/extras/tests/dummy_plugin/views.py
+++ b/netbox/netbox/tests/dummy_plugin/views.py
@@ -4,6 +4,8 @@ from django.views.generic import View
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
class DummyModelsView(View):
diff --git a/netbox/netbox/tests/test_config.py b/netbox/netbox/tests/test_config.py
index db401cf0c..f8c892363 100644
--- a/netbox/netbox/tests/test_config.py
+++ b/netbox/netbox/tests/test_config.py
@@ -2,7 +2,7 @@ from django.conf import settings
from django.core.cache import cache
from django.test import override_settings, TestCase
-from extras.models import ConfigRevision
+from core.models import ConfigRevision
from netbox.config import clear_config, get_config
diff --git a/netbox/netbox/tests/test_import.py b/netbox/netbox/tests/test_import.py
index 73f775bd7..bd07886e8 100644
--- a/netbox/netbox/tests/test_import.py
+++ b/netbox/netbox/tests/test_import.py
@@ -3,7 +3,7 @@ from django.test import override_settings
from dcim.models import *
from users.models import ObjectPermission
-from utilities.choices import ImportFormatChoices
+from utilities.choices import CSVDelimiterChoices, ImportFormatChoices
from utilities.testing import ModelViewTestCase, create_tags
@@ -17,6 +17,36 @@ class CSVImportTestCase(ModelViewTestCase):
def _get_csv_data(self, csv_data):
return '\n'.join(csv_data)
+ def test_invalid_headers(self):
+ """
+ Test that import form validation fails when an unknown CSV header is present.
+ """
+ self.add_permissions('dcim.add_region')
+
+ csv_data = [
+ 'name,slug,INVALIDHEADER',
+ 'Region 1,region-1,abc',
+ 'Region 2,region-2,def',
+ 'Region 3,region-3,ghi',
+ ]
+ data = {
+ 'format': ImportFormatChoices.CSV,
+ 'data': self._get_csv_data(csv_data),
+ 'csv_delimiter': CSVDelimiterChoices.AUTO,
+ }
+
+ # Form validation should fail with invalid header present
+ self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200)
+ self.assertEqual(Region.objects.count(), 0)
+
+ # Correct the CSV header name
+ csv_data[0] = 'name,slug,description'
+ data['data'] = self._get_csv_data(csv_data)
+
+ # Validation should succeed
+ self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
+ self.assertEqual(Region.objects.count(), 3)
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_valid_tags(self):
csv_data = (
@@ -30,6 +60,7 @@ class CSVImportTestCase(ModelViewTestCase):
data = {
'format': ImportFormatChoices.CSV,
'data': self._get_csv_data(csv_data),
+ 'csv_delimiter': CSVDelimiterChoices.AUTO,
}
# Assign model-level permission
diff --git a/netbox/extras/tests/test_plugins.py b/netbox/netbox/tests/test_plugins.py
similarity index 79%
rename from netbox/extras/tests/test_plugins.py
rename to netbox/netbox/tests/test_plugins.py
index 42dde43fd..40bf8b0ea 100644
--- a/netbox/extras/tests/test_plugins.py
+++ b/netbox/netbox/tests/test_plugins.py
@@ -5,22 +5,23 @@ from django.core.exceptions import ImproperlyConfigured
from django.test import Client, TestCase, override_settings
from django.urls import reverse
-from extras.plugins import PluginMenu
-from extras.tests.dummy_plugin import config as dummy_config
-from extras.plugins.utils import get_plugin_config
+from netbox.tests.dummy_plugin import config as dummy_config
+from netbox.tests.dummy_plugin.data_backends import DummyBackend
+from netbox.plugins.navigation import PluginMenu
+from netbox.plugins.utils import get_plugin_config
from netbox.graphql.schema import Query
from netbox.registry import registry
-@skipIf('extras.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS")
+@skipIf('netbox.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS")
class PluginTest(TestCase):
def test_config(self):
- self.assertIn('extras.tests.dummy_plugin.DummyPluginConfig', settings.INSTALLED_APPS)
+ self.assertIn('netbox.tests.dummy_plugin.DummyPluginConfig', settings.INSTALLED_APPS)
def test_models(self):
- from extras.tests.dummy_plugin.models import DummyModel
+ from netbox.tests.dummy_plugin.models import DummyModel
# Test saving an instance
instance = DummyModel(name='Instance 1', number=100)
@@ -92,10 +93,20 @@ class PluginTest(TestCase):
"""
Check that plugin TemplateExtensions are registered.
"""
- from extras.tests.dummy_plugin.template_content import SiteContent
+ from netbox.tests.dummy_plugin.template_content import SiteContent
self.assertIn(SiteContent, registry['plugins']['template_extensions']['dcim.site'])
+ def test_registered_columns(self):
+ """
+ Check that a plugin can register a custom column on a core model table.
+ """
+ from dcim.models import Site
+ from dcim.tables import SiteTable
+
+ table = SiteTable(Site.objects.all())
+ self.assertIn('foo', table.columns.names())
+
def test_user_preferences(self):
"""
Check that plugin UserPreferences are registered.
@@ -109,15 +120,22 @@ class PluginTest(TestCase):
"""
Check that plugin middleware is registered.
"""
- self.assertIn('extras.tests.dummy_plugin.middleware.DummyMiddleware', settings.MIDDLEWARE)
+ self.assertIn('netbox.tests.dummy_plugin.middleware.DummyMiddleware', settings.MIDDLEWARE)
+
+ def test_data_backends(self):
+ """
+ Check registered data backends.
+ """
+ self.assertIn('dummy', registry['data_backends'])
+ self.assertIs(registry['data_backends']['dummy'], DummyBackend)
def test_queues(self):
"""
Check that plugin queues are registered with the accurate name.
"""
- self.assertIn('extras.tests.dummy_plugin.testing-low', settings.RQ_QUEUES)
- self.assertIn('extras.tests.dummy_plugin.testing-medium', settings.RQ_QUEUES)
- self.assertIn('extras.tests.dummy_plugin.testing-high', settings.RQ_QUEUES)
+ self.assertIn('netbox.tests.dummy_plugin.testing-low', settings.RQ_QUEUES)
+ self.assertIn('netbox.tests.dummy_plugin.testing-medium', settings.RQ_QUEUES)
+ self.assertIn('netbox.tests.dummy_plugin.testing-high', settings.RQ_QUEUES)
def test_min_version(self):
"""
@@ -170,17 +188,17 @@ class PluginTest(TestCase):
"""
Validate the registration and operation of plugin-provided GraphQL schemas.
"""
- from extras.tests.dummy_plugin.graphql import DummyQuery
+ from netbox.tests.dummy_plugin.graphql import DummyQuery
self.assertIn(DummyQuery, registry['plugins']['graphql_schemas'])
self.assertTrue(issubclass(Query, DummyQuery))
- @override_settings(PLUGINS_CONFIG={'extras.tests.dummy_plugin': {'foo': 123}})
+ @override_settings(PLUGINS_CONFIG={'netbox.tests.dummy_plugin': {'foo': 123}})
def test_get_plugin_config(self):
"""
Validate that get_plugin_config() returns config parameters correctly.
"""
- plugin = 'extras.tests.dummy_plugin'
+ plugin = 'netbox.tests.dummy_plugin'
self.assertEqual(get_plugin_config(plugin, 'foo'), 123)
self.assertEqual(get_plugin_config(plugin, 'bar'), None)
self.assertEqual(get_plugin_config(plugin, 'bar', default=456), 456)
diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py
index 4ed180535..6776f6dde 100644
--- a/netbox/netbox/urls.py
+++ b/netbox/netbox/urls.py
@@ -6,9 +6,10 @@ from django.views.static import serve
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from account.views import LoginView, LogoutView
-from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
from netbox.api.views import APIRootView, StatusView
from netbox.graphql.schema import schema
+from netbox.graphql.views import GraphQLView
+from netbox.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
from strawberry.django.views import GraphQLView
from .admin import admin_site
@@ -33,6 +34,7 @@ _patterns = [
path('tenancy/', include('tenancy.urls')),
path('users/', include('users.urls')),
path('virtualization/', include('virtualization.urls')),
+ path('vpn/', include('vpn.urls')),
path('wireless/', include('wireless.urls')),
# Current user views
@@ -51,6 +53,7 @@ _patterns = [
path('api/tenancy/', include('tenancy.api.urls')),
path('api/users/', include('users.api.urls')),
path('api/virtualization/', include('virtualization.api.urls')),
+ path('api/vpn/', include('vpn.api.urls')),
path('api/wireless/', include('wireless.api.urls')),
path('api/status/', StatusView.as_view(), name='api-status'),
diff --git a/netbox/netbox/utils.py b/netbox/netbox/utils.py
new file mode 100644
index 000000000..f27d1b5f7
--- /dev/null
+++ b/netbox/netbox/utils.py
@@ -0,0 +1,26 @@
+from netbox.registry import registry
+
+__all__ = (
+ 'get_data_backend_choices',
+ 'register_data_backend',
+)
+
+
+def get_data_backend_choices():
+ return [
+ (None, '---------'),
+ *[
+ (name, cls.label) for name, cls in registry['data_backends'].items()
+ ]
+ ]
+
+
+def register_data_backend():
+ """
+ Decorator for registering a DataBackend class.
+ """
+ def _wrapper(cls):
+ registry['data_backends'][cls.name] = cls
+ return cls
+
+ return _wrapper
diff --git a/netbox/netbox/views/errors.py b/netbox/netbox/views/errors.py
index a81d45cb5..a0f783ed6 100644
--- a/netbox/netbox/views/errors.py
+++ b/netbox/netbox/views/errors.py
@@ -9,9 +9,8 @@ from django.template.exceptions import TemplateDoesNotExist
from django.views.decorators.csrf import requires_csrf_token
from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
from django.views.generic import View
-from sentry_sdk import capture_message
-from extras.plugins.utils import get_installed_plugins
+from netbox.plugins.utils import get_installed_plugins
__all__ = (
'handler_404',
@@ -34,7 +33,9 @@ def handler_404(request, exception):
"""
Wrap Django's default 404 handler to enable Sentry reporting.
"""
- capture_message("Page not found", level="error")
+ if settings.SENTRY_ENABLED:
+ from sentry_sdk import capture_message
+ capture_message("Page not found", level="error")
return page_not_found(request, exception)
diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py
index 43a55e453..615db6181 100644
--- a/netbox/netbox/views/generic/bulk_views.py
+++ b/netbox/netbox/views/generic/bulk_views.py
@@ -3,10 +3,11 @@ import re
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
+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.http import HttpResponse
@@ -16,7 +17,7 @@ from django.utils.safestring import mark_safe
from django_tables2.export import TableExport
from extras.models import ExportTemplate
-from extras.signals import clear_webhooks
+from extras.signals import clear_events
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
@@ -47,9 +48,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
Attributes:
filterset: A django-filter FilterSet that is applied to the queryset
filterset_form: The form class used to render filter options
- actions: Supported actions for the model. When adding custom actions, bulk action names must
- be prefixed with `bulk_`. Default actions: add, import, export, bulk_edit, bulk_delete
- action_perms: A dictionary mapping supported actions to a set of permissions required for each
+ actions: A mapping of supported actions to their required permissions. When adding custom actions, bulk
+ action names must be prefixed with `bulk_`. (See ActionsMixin.)
"""
template_name = 'generic/object_list.html'
filterset = None
@@ -278,7 +278,7 @@ class BulkCreateView(GetReturnURLMixin, BaseMultiObjectView):
except (AbortRequest, PermissionsViolation) as e:
logger.debug(e.message)
form.add_error(None, e.message)
- clear_webhooks.send(sender=self)
+ clear_events.send(sender=self)
else:
logger.debug("Form validation failed")
@@ -393,6 +393,10 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
form.add_error('data', f"Row {i}: Object with ID {object_id} does not exist")
raise ValidationError('')
+ # Take a snapshot for change logging
+ if instance.pk and hasattr(instance, 'snapshot'):
+ instance.snapshot()
+
# Instantiate the model form for the object
model_form_kwargs = {
'data': record,
@@ -465,16 +469,16 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
messages.success(request, msg)
view_name = get_viewname(model, action='list')
- results_url = f"{reverse(view_name)}?created_by_request={request.id}"
+ results_url = f"{reverse(view_name)}?modified_by_request={request.id}"
return redirect(results_url)
except (AbortTransaction, ValidationError):
- clear_webhooks.send(sender=self)
+ clear_events.send(sender=self)
except (AbortRequest, PermissionsViolation) as e:
logger.debug(e.message)
form.add_error(None, e.message)
- clear_webhooks.send(sender=self)
+ clear_events.send(sender=self)
else:
logger.debug("Form validation failed")
@@ -519,9 +523,11 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
model_field = self.queryset.model._meta.get_field(name)
if isinstance(model_field, (ManyToManyField, ManyToManyRel)):
m2m_fields[name] = model_field
+ elif isinstance(model_field, GenericRel):
+ # Ignore generic relations (these may be used for other purposes in the form)
+ continue
else:
model_fields[name] = model_field
-
except FieldDoesNotExist:
# This form field is used to modify a field rather than set its value directly
model_fields[name] = None
@@ -550,6 +556,14 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
elif name in form.changed_data:
obj.custom_field_data[cf_name] = customfield.serialize(form.cleaned_data[name])
+ # Store M2M values for validation
+ obj._m2m_values = {}
+ for field in obj._meta.local_many_to_many:
+ if value := form.cleaned_data.get(field.name):
+ obj._m2m_values[field.name] = list(value)
+ elif field.name in nullified_fields:
+ obj._m2m_values[field.name] = []
+
obj.full_clean()
obj.save()
updated_objects.append(obj)
@@ -625,12 +639,12 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
except ValidationError as e:
messages.error(self.request, ", ".join(e.messages))
- clear_webhooks.send(sender=self)
+ clear_events.send(sender=self)
except (AbortRequest, PermissionsViolation) as e:
logger.debug(e.message)
form.add_error(None, e.message)
- clear_webhooks.send(sender=self)
+ clear_events.send(sender=self)
else:
logger.debug("Form validation failed")
@@ -726,7 +740,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
except (AbortRequest, PermissionsViolation) as e:
logger.debug(e.message)
form.add_error(None, e.message)
- clear_webhooks.send(sender=self)
+ clear_events.send(sender=self)
else:
form = self.form(initial={'pk': request.POST.getlist('pk')})
@@ -795,14 +809,15 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView):
queryset = self.queryset.filter(pk__in=pk_list)
deleted_count = queryset.count()
try:
- for obj in queryset:
- # Take a snapshot of change-logged models
- if hasattr(obj, 'snapshot'):
- obj.snapshot()
- obj.delete()
+ with transaction.atomic():
+ for obj in queryset:
+ # Take a snapshot of change-logged models
+ if hasattr(obj, 'snapshot'):
+ obj.snapshot()
+ obj.delete()
- except ProtectedError as e:
- logger.info("Caught ProtectedError while attempting to delete objects")
+ except (ProtectedError, RestrictedError) as e:
+ logger.info(f"Caught {type(e)} while attempting to delete objects")
handle_protectederror(queryset, request, e)
return redirect(self.get_return_url(request))
@@ -919,12 +934,12 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
raise PermissionsViolation
except IntegrityError:
- clear_webhooks.send(sender=self)
+ clear_events.send(sender=self)
except (AbortRequest, PermissionsViolation) as e:
logger.debug(e.message)
form.add_error(None, e.message)
- clear_webhooks.send(sender=self)
+ clear_events.send(sender=self)
if not form.errors:
msg = "Added {} {} to {} {}.".format(
diff --git a/netbox/netbox/views/generic/mixins.py b/netbox/netbox/views/generic/mixins.py
index a55f01509..d01c534bb 100644
--- a/netbox/netbox/views/generic/mixins.py
+++ b/netbox/netbox/views/generic/mixins.py
@@ -1,5 +1,6 @@
-from collections import defaultdict
+import warnings
+from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from utilities.permissions import get_permission_for_model
__all__ = (
@@ -9,13 +10,15 @@ __all__ = (
class ActionsMixin:
- actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete')
- action_perms = defaultdict(set, **{
- 'add': {'add'},
- 'import': {'add'},
- 'bulk_edit': {'change'},
- 'bulk_delete': {'delete'},
- })
+ """
+ Maps action names to the set of required permissions for each. Object list views reference this mapping to
+ determine whether to render the applicable button for each action: The button will be rendered only if the user
+ possesses the specified permission(s).
+
+ Standard actions include: add, import, export, bulk_edit, and bulk_delete. Some views extend this default map
+ with custom actions, such as bulk_sync.
+ """
+ actions = DEFAULT_ACTION_PERMISSIONS
def get_permitted_actions(self, user, model=None):
"""
@@ -23,11 +26,43 @@ class ActionsMixin:
"""
model = model or self.queryset.model
- return [
- action for action in self.actions if user.has_perms([
- get_permission_for_model(model, name) for name in self.action_perms[action]
- ])
- ]
+ # TODO: Remove backward compatibility in Netbox v4.0
+ # Determine how permissions are being mapped to actions for the view
+ if hasattr(self, 'action_perms'):
+ # Backward compatibility for <3.7
+ permissions_map = self.action_perms
+ warnings.warn(
+ "Setting action_perms on views is deprecated and will be removed in NetBox v4.0. Use actions instead.",
+ DeprecationWarning
+ )
+ elif type(self.actions) is dict:
+ # New actions format (3.7+)
+ permissions_map = self.actions
+ else:
+ # actions is still defined as a list or tuple (<3.7) but no custom mapping is defined; use the old
+ # default mapping
+ permissions_map = {
+ 'add': {'add'},
+ 'import': {'add'},
+ 'bulk_edit': {'change'},
+ 'bulk_delete': {'delete'},
+ }
+ warnings.warn(
+ "View actions should be defined as a dictionary mapping. Support for the legacy list format will be "
+ "removed in NetBox v4.0.",
+ DeprecationWarning
+ )
+
+ # Resolve required permissions for each action
+ permitted_actions = []
+ for action in self.actions:
+ required_permissions = [
+ get_permission_for_model(model, name) for name in permissions_map.get(action, set())
+ ]
+ if not required_permissions or user.has_perms(required_permissions):
+ permitted_actions.append(action)
+
+ return permitted_actions
class TableMixin:
diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py
index 99d8ff540..90b6e9495 100644
--- a/netbox/netbox/views/generic/object_views.py
+++ b/netbox/netbox/views/generic/object_views.py
@@ -1,15 +1,18 @@
import logging
+from collections import defaultdict
from copy import deepcopy
from django.contrib import messages
-from django.db import transaction
-from django.db.models import ProtectedError
+from django.db import router, transaction
+from django.db.models import ProtectedError, RestrictedError
+from django.db.models.deletion import Collector
+from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
-from extras.signals import clear_webhooks
+from extras.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
@@ -83,9 +86,8 @@ 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
- actions: Supported actions for the model. When adding custom actions, bulk action names must
- be prefixed with `bulk_`. Default actions: add, import, export, bulk_edit, bulk_delete
- action_perms: A dictionary mapping supported actions to a set of permissions required for each
+ 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
@@ -298,7 +300,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
except (AbortRequest, PermissionsViolation) as e:
logger.debug(e.message)
form.add_error(None, e.message)
- clear_webhooks.send(sender=self)
+ clear_events.send(sender=self)
else:
logger.debug("Form validation failed")
@@ -320,6 +322,40 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'delete')
+ def _get_dependent_objects(self, obj):
+ """
+ Returns a dictionary mapping of dependent objects (organized by model) which will be deleted as a result of
+ deleting the requested object.
+
+ Args:
+ obj: The object to return dependent objects for
+ """
+ using = router.db_for_write(obj._meta.model)
+ collector = Collector(using=using)
+ collector.collect([obj])
+
+ # Compile a mapping of models to instances
+ dependent_objects = defaultdict(list)
+ for model, instance in collector.instances_with_model():
+ # Omit the root object
+ if instance != obj:
+ dependent_objects[model].append(instance)
+
+ return dict(dependent_objects)
+
+ def _handle_protected_objects(self, obj, protected_objects, request, exc):
+ """
+ Handle a ProtectedError or RestrictedError exception raised while attempt to resolve dependent objects.
+ """
+ handle_protectederror(protected_objects, request, exc)
+
+ if is_htmx(request):
+ return HttpResponse(headers={
+ 'HX-Redirect': obj.get_absolute_url(),
+ })
+ else:
+ return redirect(obj.get_absolute_url())
+
#
# Request handlers
#
@@ -334,6 +370,13 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
obj = self.get_object(**kwargs)
form = ConfirmationForm(initial=request.GET)
+ try:
+ dependent_objects = self._get_dependent_objects(obj)
+ except ProtectedError as e:
+ return self._handle_protected_objects(obj, e.protected_objects, request, e)
+ except RestrictedError as e:
+ return self._handle_protected_objects(obj, e.restricted_objects, request, e)
+
# If this is an HTMX request, return only the rendered deletion form as modal content
if is_htmx(request):
viewname = get_viewname(self.queryset.model, action='delete')
@@ -343,6 +386,7 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
'object_type': self.queryset.model._meta.verbose_name,
'form': form,
'form_url': form_url,
+ 'dependent_objects': dependent_objects,
**self.get_extra_context(request, obj),
})
@@ -350,6 +394,7 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
'object': obj,
'form': form,
'return_url': self.get_return_url(request, obj),
+ 'dependent_objects': dependent_objects,
**self.get_extra_context(request, obj),
})
@@ -374,8 +419,8 @@ class ObjectDeleteView(GetReturnURLMixin, BaseObjectView):
try:
obj.delete()
- except ProtectedError as e:
- logger.info("Caught ProtectedError while attempting to delete object")
+ except (ProtectedError, RestrictedError) as e:
+ logger.info(f"Caught {type(e)} while attempting to delete objects")
handle_protectederror([obj], request, e)
return redirect(obj.get_absolute_url())
@@ -502,7 +547,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
except (AbortRequest, PermissionsViolation) as e:
logger.debug(e.message)
form.add_error(None, e.message)
- clear_webhooks.send(sender=self)
+ clear_events.send(sender=self)
return render(request, self.template_name, {
'object': instance,
diff --git a/netbox/project-static/dist/graphiql.css b/netbox/project-static/dist/graphiql.css
index a20e480d3..267856f34 100644
--- a/netbox/project-static/dist/graphiql.css
+++ b/netbox/project-static/dist/graphiql.css
@@ -1 +1 @@
-.graphiql-container,.graphiql-container button,.graphiql-container input{color:#141823;font-family:system,-apple-system,San Francisco,".SFNSDisplay-Regular",Segoe UI,Segoe,Segoe WP,Helvetica Neue,helvetica,Lucida Grande,arial,sans-serif;font-size:14px}.graphiql-container{display:flex;flex-direction:row;height:100%;margin:0;overflow:hidden;width:100%}.graphiql-container .editorWrap{display:flex;flex-direction:column;flex:1;overflow-x:hidden}.graphiql-container .title{font-size:18px}.graphiql-container .title em{font-family:georgia;font-size:19px}.graphiql-container .topBarWrap{display:flex;flex-direction:row}.graphiql-container .topBar{align-items:center;background:linear-gradient(#f7f7f7,#e2e2e2);border-bottom:1px solid #d0d0d0;cursor:default;display:flex;flex-direction:row;flex:1;height:34px;overflow-y:visible;padding:7px 14px 6px;user-select:none}.graphiql-container .toolbar{overflow-x:visible;display:flex}.graphiql-container .docExplorerShow,.graphiql-container .historyShow{background:linear-gradient(#f7f7f7,#e2e2e2);border-radius:0;border-bottom:1px solid #d0d0d0;border-right:none;border-top:none;color:#3b5998;cursor:pointer;font-size:14px;margin:0;padding:2px 20px 0 18px}.graphiql-container .docExplorerShow{border-left:1px solid rgba(0,0,0,.2)}.graphiql-container .historyShow{border-right:1px solid rgba(0,0,0,.2);border-left:0}.graphiql-container .docExplorerShow:before{border-left:2px solid #3B5998;border-top:2px solid #3B5998;content:"";display:inline-block;height:9px;margin:0 3px -1px 0;position:relative;transform:rotate(-45deg);width:9px}.graphiql-container .editorBar{display:flex;flex-direction:row;flex:1}.graphiql-container .queryWrap{display:flex;flex-direction:column;flex:1}.graphiql-container .resultWrap{border-left:solid 1px #e0e0e0;display:flex;flex-direction:column;flex:1;flex-basis:1em;position:relative}.graphiql-container .docExplorerWrap,.graphiql-container .historyPaneWrap{background:white;box-shadow:0 0 8px #00000026;position:relative;z-index:3}.graphiql-container .historyPaneWrap{min-width:230px;z-index:5}.graphiql-container .docExplorerResizer{cursor:col-resize;height:100%;left:-5px;position:absolute;top:0;width:10px;z-index:10}.graphiql-container .docExplorerHide{cursor:pointer;font-size:18px;margin:-7px -8px -6px 0;padding:18px 16px 15px 12px;background:0;border:0;line-height:14px}.graphiql-container div .query-editor{flex:1;position:relative}.graphiql-container .secondary-editor{display:flex;flex-direction:column;height:30px;position:relative}.graphiql-container .secondary-editor-title{background:#eeeeee;border-bottom:1px solid #d6d6d6;border-top:1px solid #e0e0e0;color:#777;font-variant:small-caps;font-weight:700;letter-spacing:1px;line-height:14px;padding:6px 0 8px 43px;text-transform:lowercase;user-select:none}.graphiql-container .codemirrorWrap,.graphiql-container .result-window{flex:1;height:100%;position:relative}.graphiql-container .footer{background:#f6f7f8;border-left:1px solid #e0e0e0;border-top:1px solid #e0e0e0;margin-left:12px;position:relative}.graphiql-container .footer:before{background:#eeeeee;bottom:0;content:" ";left:-13px;position:absolute;top:-1px;width:12px}.result-window .CodeMirror{background:#f6f7f8}.graphiql-container .result-window .CodeMirror-gutters{background-color:#eee;border-color:#e0e0e0;cursor:col-resize}.graphiql-container .result-window .CodeMirror-foldgutter,.graphiql-container .result-window .CodeMirror-foldgutter-open:after,.graphiql-container .result-window .CodeMirror-foldgutter-folded:after{padding-left:3px}.graphiql-container .toolbar-button{background:#fdfdfd;background:linear-gradient(#f9f9f9,#ececec);border:0;border-radius:3px;box-shadow:inset 0 0 0 1px #0003,0 1px #ffffffb3,inset 0 1px #fff;color:#555;cursor:pointer;display:inline-block;margin:0 5px;padding:3px 11px 5px;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;max-width:150px}.graphiql-container .toolbar-button:active{background:linear-gradient(#ececec,#d5d5d5);box-shadow:0 1px #ffffffb3,inset 0 0 0 1px #0000001a,inset 0 1px 1px 1px #0000001f,inset 0 0 5px #0000001a}.graphiql-container .toolbar-button.error{background:linear-gradient(#fdf3f3,#e6d6d7);color:#b00}.graphiql-container .toolbar-button-group{margin:0 5px;white-space:nowrap}.graphiql-container .toolbar-button-group>*{margin:0}.graphiql-container .toolbar-button-group>*:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.graphiql-container .toolbar-button-group>*:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0;margin-left:-1px}.graphiql-container .execute-button-wrap{height:34px;margin:0 14px 0 28px;position:relative}.graphiql-container .execute-button{background:linear-gradient(#fdfdfd,#d2d3d6);border-radius:17px;border:1px solid rgba(0,0,0,.25);box-shadow:0 1px #fff;cursor:pointer;fill:#444;height:34px;margin:0;padding:0;width:34px}.graphiql-container .execute-button svg{pointer-events:none}.graphiql-container .execute-button:active{background:linear-gradient(#e6e6e6,#c3c3c3);box-shadow:0 1px #fff,inset 0 0 2px #0003,inset 0 0 6px #0000001a}.graphiql-container .toolbar-menu,.graphiql-container .toolbar-select{position:relative}.graphiql-container .execute-options,.graphiql-container .toolbar-menu-items,.graphiql-container .toolbar-select-options{background:#fff;box-shadow:0 0 0 1px #0000001a,0 2px 4px #00000040;margin:0;padding:6px 0;position:absolute;z-index:100}.graphiql-container .execute-options{min-width:100px;top:37px;left:-1px}.graphiql-container .toolbar-menu-items{left:1px;margin-top:-1px;min-width:110%;top:100%;visibility:hidden}.graphiql-container .toolbar-menu-items.open{visibility:visible}.graphiql-container .toolbar-select-options{left:0;min-width:100%;top:-5px;visibility:hidden}.graphiql-container .toolbar-select-options.open{visibility:visible}.graphiql-container .execute-options>li,.graphiql-container .toolbar-menu-items>li,.graphiql-container .toolbar-select-options>li{cursor:pointer;display:block;margin:none;max-width:300px;overflow:hidden;padding:2px 20px 4px 11px;white-space:nowrap}.graphiql-container .execute-options>li.selected,.graphiql-container .toolbar-menu-items>li.hover,.graphiql-container .toolbar-menu-items>li:active,.graphiql-container .toolbar-menu-items>li:hover,.graphiql-container .toolbar-select-options>li.hover,.graphiql-container .toolbar-select-options>li:active,.graphiql-container .toolbar-select-options>li:hover,.graphiql-container .history-contents>li:hover,.graphiql-container .history-contents>li:active{background:#e10098;color:#fff}.graphiql-container .toolbar-select-options>li>svg{display:inline;fill:#666;margin:0 -6px 0 6px;pointer-events:none;vertical-align:middle}.graphiql-container .toolbar-select-options>li.hover>svg,.graphiql-container .toolbar-select-options>li:active>svg,.graphiql-container .toolbar-select-options>li:hover>svg{fill:#fff}.graphiql-container .CodeMirror-scroll{overflow-scrolling:touch}.graphiql-container .CodeMirror{color:#141823;font-family:Consolas,Inconsolata,Droid Sans Mono,Monaco,monospace;font-size:13px;height:100%;left:0;position:absolute;top:0;width:100%}.graphiql-container .CodeMirror-lines{padding:20px 0}.CodeMirror-hint-information .content{box-orient:vertical;color:#141823;display:flex;font-family:system,-apple-system,San Francisco,".SFNSDisplay-Regular",Segoe UI,Segoe,Segoe WP,Helvetica Neue,helvetica,Lucida Grande,arial,sans-serif;font-size:13px;line-clamp:3;line-height:16px;max-height:48px;overflow:hidden;text-overflow:-o-ellipsis-lastline}.CodeMirror-hint-information .content p:first-child{margin-top:0}.CodeMirror-hint-information .content p:last-child{margin-bottom:0}.CodeMirror-hint-information .infoType{color:#ca9800;cursor:pointer;display:inline;margin-right:.5em}.autoInsertedLeaf.cm-property{animation-duration:6s;animation-name:insertionFade;border-bottom:2px solid rgba(255,255,255,0);border-radius:2px;margin:-2px -4px -1px;padding:2px 4px 1px}@keyframes insertionFade{0%,to{background:rgba(255,255,255,0);border-color:#fff0}15%,85%{background:#fbffc9;border-color:#f0f3c0}}div.CodeMirror-lint-tooltip{background-color:#fff;border-radius:2px;border:0;color:#141823;box-shadow:0 1px 3px #00000073;font-size:13px;line-height:16px;max-width:430px;opacity:0;padding:8px 10px;transition:opacity .15s;white-space:pre-wrap}div.CodeMirror-lint-tooltip>*{padding-left:23px}div.CodeMirror-lint-tooltip>*+*{margin-top:12px}.graphiql-container .CodeMirror-foldmarker{border-radius:4px;background:#08f;background:linear-gradient(#43A8FF,#0F83E8);box-shadow:0 1px 1px #0003,inset 0 0 0 1px #0000001a;color:#fff;font-family:arial;font-size:12px;line-height:0;margin:0 3px;padding:0 4px 1px;text-shadow:0 -1px rgba(0,0,0,.1)}.graphiql-container div.CodeMirror span.CodeMirror-matchingbracket{color:#555;text-decoration:underline}.graphiql-container div.CodeMirror span.CodeMirror-nonmatchingbracket{color:red}.cm-comment{color:#999}.cm-punctuation{color:#555}.cm-keyword{color:#b11a04}.cm-def{color:#d2054e}.cm-property{color:#1f61a0}.cm-qualifier{color:#1c92a9}.cm-attribute{color:#8b2bb9}.cm-number{color:#2882f9}.cm-string{color:#d64292}.cm-builtin{color:#d47509}.cm-string-2{color:#0b7fc7}.cm-variable{color:#397d13}.cm-meta{color:#b33086}.cm-atom{color:#ca9800}.CodeMirror{color:#000;font-family:monospace;height:300px}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{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{color:#999;min-width:20px;padding:0 3px 0 5px;text-align:right;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror .CodeMirror-cursor{border-left:1px solid black}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.CodeMirror.cm-fat-cursor div.CodeMirror-cursor{background:#7e7;border:0;width:auto}.CodeMirror.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{animation:blink 1.06s steps(1) infinite;border:0;width:auto}@keyframes blink{0%{background:#7e7}50%{background:none}to{background:#7e7}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.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{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-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.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-error,.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{background:white;overflow:hidden;position:relative}.CodeMirror-scroll{height:100%;margin-bottom:-30px;margin-right:-30px;outline:none;overflow:scroll!important;padding-bottom:30px;position:relative}.CodeMirror-sizer{border-right:30px solid transparent;position:relative}.CodeMirror-vscrollbar,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{display:none;position:absolute;z-index:6}.CodeMirror-vscrollbar{overflow-x:hidden;overflow-y:scroll;right:0;top:0}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-x:scroll;overflow-y:hidden}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{min-height:100%;position:absolute;left:0;top:0;z-index:3}.CodeMirror-gutter{display:inline-block;height:100%;margin-bottom:-30px;vertical-align:top;white-space:normal;*zoom:1;*display:inline}.CodeMirror-gutter-wrapper{background:none!important;border:none!important;position:absolute;z-index:4}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{cursor:default;position:absolute;z-index:4}.CodeMirror-gutter-wrapper{user-select:none}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-webkit-tap-highlight-color:transparent;background:transparent;border-radius:0;border-width:0;color:inherit;font-family:inherit;font-size:inherit;font-variant-ligatures:none;line-height:inherit;margin:0;overflow:visible;position:relative;white-space:pre;word-wrap:normal;z-index:2}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;inset:0;z-index:0}.CodeMirror-linewidget{overflow:auto;position:relative;z-index:2}.CodeMirror-code{outline:none}.CodeMirror-scroll,.CodeMirror-sizer,.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber{box-sizing:content-box}.CodeMirror-measure{height:0;overflow:hidden;position:absolute;visibility:hidden;width:100%}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{position:relative;visibility:hidden;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:#ffa;background:rgba(255,255,0,.4)}.CodeMirror span{*vertical-align: text-bottom}.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}.CodeMirror-dialog{background:inherit;color:inherit;left:0;right:0;overflow:hidden;padding:.1em .8em;position:absolute;z-index:15}.CodeMirror-dialog-top{border-bottom:1px solid #eee;top:0}.CodeMirror-dialog-bottom{border-top:1px solid #eee;bottom:0}.CodeMirror-dialog input{background:transparent;border:1px solid #d3d6db;color:inherit;font-family:monospace;outline:none;width:20em}.CodeMirror-dialog button{font-size:70%}.CodeMirror-foldmarker{color:#00f;cursor:pointer;font-family:arial;line-height:.3;text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px}.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-info{background:white;border-radius:2px;box-shadow:0 1px 3px #00000073;box-sizing:border-box;color:#555;font-family:system,-apple-system,San Francisco,".SFNSDisplay-Regular",Segoe UI,Segoe,Segoe WP,Helvetica Neue,helvetica,Lucida Grande,arial,sans-serif;font-size:13px;line-height:16px;margin:8px -8px;max-width:400px;opacity:0;overflow:hidden;padding:8px;position:fixed;transition:opacity .15s;z-index:50}.CodeMirror-info :first-child{margin-top:0}.CodeMirror-info :last-child{margin-bottom:0}.CodeMirror-info p{margin:1em 0}.CodeMirror-info .info-description{color:#777;line-height:16px;margin-top:1em;max-height:80px;overflow:hidden}.CodeMirror-info .info-deprecation{background:#fffae8;box-shadow:inset 0 1px 1px -1px #bfb063;color:#867f70;line-height:16px;margin:8px -8px -8px;max-height:80px;overflow:hidden;padding:8px}.CodeMirror-info .info-deprecation-label{color:#c79b2e;cursor:default;display:block;font-size:9px;font-weight:700;letter-spacing:1px;line-height:1;padding-bottom:5px;text-transform:uppercase;user-select:none}.CodeMirror-info .info-deprecation-label+*{margin-top:0}.CodeMirror-info a{text-decoration:none}.CodeMirror-info a:hover{text-decoration:underline}.CodeMirror-info .type-name{color:#ca9800}.CodeMirror-info .field-name{color:#1f61a0}.CodeMirror-info .enum-value{color:#0b7fc7}.CodeMirror-info .arg-name{color:#8b2bb9}.CodeMirror-info .directive-name{color:#b33086}.CodeMirror-jump-token{text-decoration:underline;cursor:pointer}.CodeMirror-lint-markers{width:16px}.CodeMirror-lint-tooltip{background-color:infobackground;border-radius:4px;border:1px solid black;color:infotext;font-family:monospace;font-size:10pt;max-width:600px;opacity:0;overflow:hidden;padding:2px 5px;position:fixed;transition:opacity .4s;white-space:pre-wrap;z-index:100}.CodeMirror-lint-mark-error,.CodeMirror-lint-mark-warning{background-position:left bottom;background-repeat:repeat-x}.CodeMirror-lint-mark-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==)}.CodeMirror-lint-mark-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=)}.CodeMirror-lint-marker-error,.CodeMirror-lint-marker-warning{background-position:center center;background-repeat:no-repeat;cursor:pointer;display:inline-block;height:16px;position:relative;vertical-align:middle;width:16px}.CodeMirror-lint-message-error,.CodeMirror-lint-message-warning{background-position:top left;background-repeat:no-repeat;padding-left:18px}.CodeMirror-lint-marker-error,.CodeMirror-lint-message-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=)}.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-multiple{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC);background-position:right bottom;background-repeat:no-repeat;width:100%;height:100%}.graphiql-container .spinner-container{height:36px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:36px;z-index:10}.graphiql-container .spinner{animation:rotation .6s infinite linear;border-bottom:6px solid rgba(150,150,150,.15);border-left:6px solid rgba(150,150,150,.15);border-radius:100%;border-right:6px solid rgba(150,150,150,.15);border-top:6px solid rgba(150,150,150,.8);display:inline-block;height:24px;position:absolute;vertical-align:middle;width:24px}@keyframes rotation{0%{transform:rotate(0)}to{transform:rotate(359deg)}}.CodeMirror-hints{background:white;box-shadow:0 1px 3px #00000073;font-family:Consolas,Inconsolata,Droid Sans Mono,Monaco,monospace;font-size:13px;list-style:none;margin:0;max-height:14.5em;overflow:hidden;overflow-y:auto;padding:0;position:absolute;z-index:10}.CodeMirror-hint{border-top:solid 1px #f7f7f7;color:#141823;cursor:pointer;margin:0;max-width:300px;overflow:hidden;padding:2px 6px;white-space:pre}li.CodeMirror-hint-active{background-color:#08f;border-top-color:#fff;color:#fff}.CodeMirror-hint-information{border-top:solid 1px #c0c0c0;max-width:300px;padding:4px 6px;position:relative;z-index:1}.CodeMirror-hint-information:first-child{border-bottom:solid 1px #c0c0c0;border-top:none;margin-bottom:-1px}.CodeMirror-hint-deprecation{background:#fffae8;box-shadow:inset 0 1px 1px -1px #bfb063;color:#867f70;font-family:system,-apple-system,San Francisco,".SFNSDisplay-Regular",Segoe UI,Segoe,Segoe WP,Helvetica Neue,helvetica,Lucida Grande,arial,sans-serif;font-size:13px;line-height:16px;margin-top:4px;max-height:80px;overflow:hidden;padding:6px}.CodeMirror-hint-deprecation .deprecation-label{color:#c79b2e;cursor:default;display:block;font-size:9px;font-weight:700;letter-spacing:1px;line-height:1;padding-bottom:5px;text-transform:uppercase;user-select:none}.CodeMirror-hint-deprecation .deprecation-label+*{margin-top:0}.CodeMirror-hint-deprecation :last-child{margin-bottom:0}.graphiql-container .doc-explorer{background:white}.graphiql-container .doc-explorer-title-bar,.graphiql-container .history-title-bar{cursor:default;display:flex;height:34px;line-height:14px;padding:8px 8px 5px;position:relative;user-select:none}.graphiql-container .doc-explorer-title,.graphiql-container .history-title{flex:1;font-weight:700;overflow-x:hidden;padding:10px 0 10px 10px;text-align:center;text-overflow:ellipsis;user-select:text;white-space:nowrap}.graphiql-container .doc-explorer-back{color:#3b5998;cursor:pointer;margin:-7px 0 -6px -8px;overflow-x:hidden;padding:17px 12px 16px 16px;text-overflow:ellipsis;white-space:nowrap;background:0;border:0;line-height:14px}.doc-explorer-narrow .doc-explorer-back{width:0}.graphiql-container .doc-explorer-back:before{border-left:2px solid #3B5998;border-top:2px solid #3B5998;content:"";display:inline-block;height:9px;margin:0 3px -1px 0;position:relative;transform:rotate(-45deg);width:9px}.graphiql-container .doc-explorer-rhs{position:relative}.graphiql-container .doc-explorer-contents,.graphiql-container .history-contents{background-color:#fff;border-top:1px solid #d6d6d6;inset:47px 0 0;overflow-y:auto;padding:20px 15px;position:absolute}.graphiql-container .doc-explorer-contents{min-width:300px}.graphiql-container .doc-type-description p:first-child,.graphiql-container .doc-type-description blockquote:first-child{margin-top:0}.graphiql-container .doc-explorer-contents a{cursor:pointer;text-decoration:none}.graphiql-container .doc-explorer-contents a:hover{text-decoration:underline}.graphiql-container .doc-value-description>:first-child{margin-top:4px}.graphiql-container .doc-value-description>:last-child{margin-bottom:4px}.graphiql-container .doc-type-description code,.graphiql-container .doc-type-description pre,.graphiql-container .doc-category code,.graphiql-container .doc-category pre{--saf-0: rgba(var(--sk_foreground_low,29,28,29),.13);font-size:12px;line-height:1.50001;font-variant-ligatures:none;white-space:pre;white-space:pre-wrap;word-wrap:break-word;word-break:normal;-webkit-tab-size:4;-moz-tab-size:4;tab-size:4}.graphiql-container .doc-type-description code,.graphiql-container .doc-category code{padding:2px 3px 1px;border:1px solid var(--saf-0);border-radius:3px;background-color:rgba(var(--sk_foreground_min,29,28,29),.04);color:#e01e5a;background-color:#fff}.graphiql-container .doc-category{margin:20px 0}.graphiql-container .doc-category-title{border-bottom:1px solid #e0e0e0;color:#777;cursor:default;font-size:14px;font-variant:small-caps;font-weight:700;letter-spacing:1px;margin:0 -15px 10px 0;padding:10px 0;user-select:none}.graphiql-container .doc-category-item{margin:12px 0;color:#555}.graphiql-container .keyword{color:#b11a04}.graphiql-container .type-name{color:#ca9800}.graphiql-container .field-name{color:#1f61a0}.graphiql-container .field-short-description{color:#999;margin-left:5px;overflow:hidden;text-overflow:ellipsis}.graphiql-container .enum-value{color:#0b7fc7}.graphiql-container .arg-name{color:#8b2bb9}.graphiql-container .arg{display:block;margin-left:1em}.graphiql-container .arg:first-child:last-child,.graphiql-container .arg:first-child:nth-last-child(2),.graphiql-container .arg:first-child:nth-last-child(2)~.arg{display:inherit;margin:inherit}.graphiql-container .arg:first-child:nth-last-child(2):after{content:", "}.graphiql-container .arg-default-value{color:#43a047}.graphiql-container .doc-deprecation{background:#fffae8;box-shadow:inset 0 0 1px #bfb063;color:#867f70;line-height:16px;margin:8px -8px;max-height:80px;overflow:hidden;padding:8px;border-radius:3px}.graphiql-container .doc-deprecation:before{content:"Deprecated:";color:#c79b2e;cursor:default;display:block;font-size:9px;font-weight:700;letter-spacing:1px;line-height:1;padding-bottom:5px;text-transform:uppercase;user-select:none}.graphiql-container .doc-deprecation>:first-child{margin-top:0}.graphiql-container .doc-deprecation>:last-child{margin-bottom:0}.graphiql-container .show-btn{-webkit-appearance:initial;display:block;border-radius:3px;border:solid 1px #ccc;text-align:center;padding:8px 12px 10px;width:100%;box-sizing:border-box;background:#fbfcfc;color:#555;cursor:pointer}.graphiql-container .search-box{border-bottom:1px solid #d3d6db;display:flex;align-items:center;font-size:14px;margin:-15px -15px 12px 0;position:relative}.graphiql-container .search-box-icon{cursor:pointer;display:block;font-size:24px;transform:rotate(-45deg);user-select:none}.graphiql-container .search-box .search-box-clear{background-color:#d0d0d0;border-radius:12px;color:#fff;cursor:pointer;font-size:11px;padding:1px 5px 2px;position:absolute;right:3px;user-select:none;border:0}.graphiql-container .search-box .search-box-clear:hover{background-color:#b9b9b9}.graphiql-container .search-box>input{border:none;box-sizing:border-box;font-size:14px;outline:none;padding:6px 24px 8px 20px;width:100%}.graphiql-container .error-container{font-weight:700;left:0;letter-spacing:1px;opacity:.5;position:absolute;right:0;text-align:center;text-transform:uppercase;top:50%;transform:translateY(-50%)}.graphiql-container .history-contents{font-family:Consolas,Inconsolata,Droid Sans Mono,Monaco,monospace}.graphiql-container .history-contents{margin:0;padding:0}.graphiql-container .history-contents li{align-items:center;display:flex;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin:0;padding:8px;border-bottom:1px solid #e0e0e0}.graphiql-container .history-contents li button:not(.history-label){display:none;margin-left:10px}.graphiql-container .history-contents li:hover button:not(.history-label),.graphiql-container .history-contents li:focus-within button:not(.history-label){display:inline-block}.graphiql-container .history-contents input,.graphiql-container .history-contents button{padding:0;background:0;border:0;font-size:inherit;font-family:inherit;line-height:14px;color:inherit}.graphiql-container .history-contents input{flex-grow:1}.graphiql-container .history-contents input::placeholder{color:inherit}.graphiql-container .history-contents button{cursor:pointer;text-align:left}.graphiql-container .history-contents .history-label{flex-grow:1;overflow:hidden;text-overflow:ellipsis}
+.graphiql-container,.graphiql-container button,.graphiql-container input{color:#141823;font-family:system,-apple-system,San Francisco,".SFNSDisplay-Regular",Segoe UI,Segoe,Segoe WP,Helvetica Neue,helvetica,Lucida Grande,arial,sans-serif;font-size:14px}.graphiql-container{display:flex;flex-direction:row;height:100%;margin:0;overflow:hidden;width:100%}.graphiql-container .editorWrap{display:flex;flex-direction:column;flex:1;overflow-x:hidden}.graphiql-container .title{font-size:18px}.graphiql-container .title em{font-family:georgia;font-size:19px}.graphiql-container .topBarWrap{display:flex;flex-direction:row}.graphiql-container .topBar{align-items:center;background:linear-gradient(#f7f7f7,#e2e2e2);border-bottom:1px solid #d0d0d0;cursor:default;display:flex;flex-direction:row;flex:1;height:34px;overflow-y:visible;padding:7px 14px 6px;user-select:none}.graphiql-container .toolbar{overflow-x:visible;display:flex}.graphiql-container .docExplorerShow,.graphiql-container .historyShow{background:linear-gradient(#f7f7f7,#e2e2e2);border-radius:0;border-bottom:1px solid #d0d0d0;border-right:none;border-top:none;color:#3b5998;cursor:pointer;font-size:14px;margin:0;padding:2px 20px 0 18px}.graphiql-container .docExplorerShow{border-left:1px solid rgba(0,0,0,.2)}.graphiql-container .historyShow{border-right:1px solid rgba(0,0,0,.2);border-left:0}.graphiql-container .docExplorerShow:before{border-left:2px solid #3b5998;border-top:2px solid #3b5998;content:"";display:inline-block;height:9px;margin:0 3px -1px 0;position:relative;transform:rotate(-45deg);width:9px}.graphiql-container .editorBar{display:flex;flex-direction:row;flex:1;max-height:100%}.graphiql-container .queryWrap{display:flex;flex-direction:column;flex:1}.graphiql-container .resultWrap{border-left:solid 1px #e0e0e0;display:flex;flex-direction:column;flex:1;flex-basis:1em;position:relative}.graphiql-container .docExplorerWrap,.graphiql-container .historyPaneWrap{background:white;box-shadow:0 0 8px #00000026;position:relative;z-index:3}.graphiql-container .historyPaneWrap{min-width:230px;z-index:5}.graphiql-container .docExplorerResizer{cursor:col-resize;height:100%;left:-5px;position:absolute;top:0;width:10px;z-index:10}.graphiql-container .docExplorerHide{cursor:pointer;font-size:18px;margin:-7px -8px -6px 0;padding:18px 16px 15px 12px;background:0;border:0;line-height:14px}.graphiql-container div .query-editor{flex:1;position:relative}.graphiql-container .secondary-editor{display:flex;flex-direction:column;height:30px;position:relative}.graphiql-container .secondary-editor-title{background:#eeeeee;border-bottom:1px solid #d6d6d6;border-top:1px solid #e0e0e0;color:#777;font-variant:small-caps;font-weight:700;letter-spacing:1px;line-height:14px;padding:6px 0 8px 43px;text-transform:lowercase;user-select:none}.graphiql-container .codemirrorWrap,.graphiql-container .result-window{flex:1;height:100%;position:relative}.graphiql-container .footer{background:#f6f7f8;border-left:1px solid #e0e0e0;border-top:1px solid #e0e0e0;margin-left:12px;position:relative}.graphiql-container .footer:before{background:#eeeeee;bottom:0;content:" ";left:-13px;position:absolute;top:-1px;width:12px}.result-window .CodeMirror.cm-s-graphiql{background:#f6f7f8}.graphiql-container .result-window .CodeMirror-gutters{background-color:#eee;border-color:#e0e0e0;cursor:col-resize}.graphiql-container .result-window .CodeMirror-foldgutter,.graphiql-container .result-window .CodeMirror-foldgutter-open:after,.graphiql-container .result-window .CodeMirror-foldgutter-folded:after{padding-left:3px}.graphiql-container .toolbar-button{background:#fdfdfd;background:linear-gradient(#f9f9f9,#ececec);border:0;border-radius:3px;box-shadow:inset 0 0 0 1px #0003,0 1px #ffffffb3,inset 0 1px #fff;color:#555;cursor:pointer;display:inline-block;margin:0 5px;padding:3px 11px 5px;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;max-width:150px}.graphiql-container .toolbar-button:active{background:linear-gradient(#ececec,#d5d5d5);box-shadow:0 1px #ffffffb3,inset 0 0 0 1px #0000001a,inset 0 1px 1px 1px #0000001f,inset 0 0 5px #0000001a}.graphiql-container .toolbar-button.error{background:linear-gradient(#fdf3f3,#e6d6d7);color:#b00}.graphiql-container .toolbar-button-group{margin:0 5px;white-space:nowrap}.graphiql-container .toolbar-button-group>*{margin:0}.graphiql-container .toolbar-button-group>*:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.graphiql-container .toolbar-button-group>*:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0;margin-left:-1px}.graphiql-container .execute-button-wrap{height:34px;margin:0 14px 0 28px;position:relative}.graphiql-container .execute-button{background:linear-gradient(#fdfdfd,#d2d3d6);border-radius:17px;border:1px solid rgba(0,0,0,.25);box-shadow:0 1px #fff;cursor:pointer;fill:#444;height:34px;margin:0;padding:0;width:34px}.graphiql-container .toolbar-button>svg,.graphiql-container .execute-button svg{pointer-events:none}.graphiql-container .execute-button:active{background:linear-gradient(#e6e6e6,#c3c3c3);box-shadow:0 1px #fff,inset 0 0 2px #0003,inset 0 0 6px #0000001a}.graphiql-container .toolbar-menu,.graphiql-container .toolbar-select{position:relative}.graphiql-container .execute-options,.graphiql-container .toolbar-menu-items,.graphiql-container .toolbar-select-options{background:#fff;box-shadow:0 0 0 1px #0000001a,0 2px 4px #00000040;margin:0;padding:6px 0;position:absolute;z-index:100}.graphiql-container .execute-options{min-width:100px;top:37px;left:-1px}.graphiql-container .toolbar-menu-items{left:1px;margin-top:-1px;min-width:110%;top:100%;visibility:hidden}.graphiql-container .toolbar-menu-items.open{visibility:visible}.graphiql-container .toolbar-select-options{left:0;min-width:100%;top:-5px;visibility:hidden}.graphiql-container .toolbar-select-options.open{visibility:visible}.graphiql-container .execute-options>li,.graphiql-container .toolbar-menu-items>li,.graphiql-container .toolbar-select-options>li{cursor:pointer;display:block;margin:none;max-width:300px;overflow:hidden;padding:2px 20px 4px 11px;white-space:nowrap}.graphiql-container .execute-options>li.selected,.graphiql-container .toolbar-menu-items>li.hover,.graphiql-container .toolbar-menu-items>li:active,.graphiql-container .toolbar-menu-items>li:hover,.graphiql-container .toolbar-select-options>li.hover,.graphiql-container .toolbar-select-options>li:active,.graphiql-container .toolbar-select-options>li:hover,.graphiql-container .history-contents>li:hover,.graphiql-container .history-contents>li:active{background:#e10098;color:#fff}.graphiql-container .toolbar-select-options>li>svg{display:inline;fill:#666;margin:0 -6px 0 6px;pointer-events:none;vertical-align:middle}.graphiql-container .toolbar-select-options>li.hover>svg,.graphiql-container .toolbar-select-options>li:active>svg,.graphiql-container .toolbar-select-options>li:hover>svg{fill:#fff}.graphiql-container .CodeMirror-scroll{overflow-scrolling:touch}.graphiql-container .CodeMirror{color:#141823;font-family:Consolas,Inconsolata,Droid Sans Mono,Monaco,monospace;font-size:13px;height:100%;left:0;position:absolute;top:0;width:100%}.graphiql-container .CodeMirror-lines{padding:20px 0}.CodeMirror-hint-information .content{box-orient:vertical;color:#141823;display:flex;font-family:system,-apple-system,San Francisco,".SFNSDisplay-Regular",Segoe UI,Segoe,Segoe WP,Helvetica Neue,helvetica,Lucida Grande,arial,sans-serif;font-size:13px;line-clamp:3;line-height:16px;max-height:48px;overflow:hidden;text-overflow:-o-ellipsis-lastline}.CodeMirror-hint-information .content p:first-child{margin-top:0}.CodeMirror-hint-information .content p:last-child{margin-bottom:0}.CodeMirror-hint-information .infoType{color:#ca9800;cursor:pointer;display:inline;margin-right:.5em}.autoInsertedLeaf.cm-property{animation-duration:6s;animation-name:insertionFade;border-bottom:2px solid rgba(255,255,255,0);border-radius:2px;margin:-2px -4px -1px;padding:2px 4px 1px}@keyframes insertionFade{0%,to{background:rgba(255,255,255,0);border-color:#fff0}15%,85%{background:#fbffc9;border-color:#f0f3c0}}div.CodeMirror-lint-tooltip{background-color:#fff;border-radius:2px;border:0;color:#141823;box-shadow:0 1px 3px #00000073;font-size:13px;line-height:16px;max-width:430px;opacity:0;padding:8px 10px;transition:opacity .15s;white-space:pre-wrap}div.CodeMirror-lint-tooltip>*{padding-left:23px}div.CodeMirror-lint-tooltip>*+*{margin-top:12px}.graphiql-container .variable-editor-title-text{cursor:pointer;display:inline-block;color:gray}.graphiql-container .variable-editor-title-text.active{color:#000}.graphiql-container .tabs{height:42px;background-image:linear-gradient(#f7f7f7,#e2e2e2);display:flex;align-items:center}.graphiql-container .tab{position:relative;cursor:pointer;display:flex;align-items:center;justify-content:center;padding-top:0;padding-right:6px;padding-left:14px;height:100%;color:#0009;border-left:1px solid lightgray;border-top-style:none;border-bottom-style:none;border-right-style:none}.graphiql-container .tab:first-child:nth-last-child(2){padding-right:14px}.graphiql-container .tab:hover{background-image:linear-gradient(rgba(245,245,245,.7),rgba(215,215,215,1));color:#000c}.graphiql-container .tab.active{background-image:linear-gradient(rgba(233,233,233,.7),rgba(205,205,205,1));color:#000}.graphiql-container .tab .close{display:inline-block;cursor:pointer;border:none;background:transparent;margin-left:6px;padding:3px 6px;border-radius:4px}.graphiql-container .tab:hover .close,.graphiql-container .tab.active .close{opacity:1}.graphiql-container .tab .close:before{content:"\2715";display:inline-block;font-weight:700;font-size:12px;color:#000000b3;height:14px}.graphiql-container .tab .close:hover{background:rgba(0,0,0,.08)}.graphiql-container .tab .close:active{background:rgba(0,0,0,.12)}.graphiql-container .tab-add{display:flex;align-items:center;justify-content:center;border:none;background:transparent;line-height:1;font-size:26px;padding:0 8px 3px;height:30px;border-radius:4px;color:#00000080;margin-left:6px;cursor:pointer}.graphiql-container .tab-add:hover{background:rgba(0,0,0,.06)}.graphiql-container .tab-add:active{background:rgba(0,0,0,.1)}.graphiql-container .CodeMirror-foldmarker{border-radius:4px;background:#08f;background:linear-gradient(#43a8ff,#0f83e8);box-shadow:0 1px 1px #0003,inset 0 0 0 1px #0000001a;color:#fff;font-family:arial;font-size:12px;line-height:0;margin:0 3px;padding:0 4px 1px;text-shadow:0 -1px rgba(0,0,0,.1)}.graphiql-container div.CodeMirror span.CodeMirror-matchingbracket{color:#555;text-decoration:underline}.graphiql-container div.CodeMirror span.CodeMirror-nonmatchingbracket{color:red}.cm-comment{color:#666}.cm-punctuation{color:#555}.cm-keyword{color:#b11a04}.cm-def{color:#d2054e}.cm-property{color:#1f61a0}.cm-qualifier{color:#1c92a9}.cm-attribute{color:#8b2bb9}.cm-number{color:#2882f9}.cm-string{color:#d64292}.cm-builtin{color:#d47509}.cm-string-2{color:#0b7fc7}.cm-variable{color:#397d13}.cm-meta{color:#b33086}.cm-atom{color:#ca9800}.CodeMirror{color:#000;font-family:monospace;height:300px}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{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{color:#666;min-width:20px;padding:0 3px 0 5px;text-align:right;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#666}.CodeMirror .CodeMirror-cursor{border-left:1px solid black}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.CodeMirror.cm-fat-cursor div.CodeMirror-cursor{background:#7e7;border:0;width:auto}.CodeMirror.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{animation:blink 1.06s steps(1) infinite;border:0;width:auto}@keyframes blink{0%{background:#7e7}50%{background:none}to{background:#7e7}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.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{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:#666}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-s-default .cm-hr{color:#666}.cm-s-default .cm-link{color:#00c}.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-error,.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{background:white;overflow:hidden;position:relative}.CodeMirror-scroll{height:100%;margin-bottom:-30px;margin-right:-30px;outline:none;overflow:scroll!important;padding-bottom:30px;position:relative}.CodeMirror-sizer{border-right:30px solid transparent;position:relative}.CodeMirror-vscrollbar,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{display:none;position:absolute;z-index:6}.CodeMirror-vscrollbar{overflow-x:hidden;overflow-y:scroll;right:0;top:0}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-x:scroll;overflow-y:hidden}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{min-height:100%;position:absolute;left:0;top:0;z-index:3}.CodeMirror-gutter{display:inline-block;height:100%;margin-bottom:-30px;vertical-align:top;white-space:normal}.CodeMirror-gutter-wrapper{background:none!important;border:none!important;position:absolute;z-index:4}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{cursor:default;position:absolute;z-index:4}.CodeMirror-gutter-wrapper{user-select:none}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-webkit-tap-highlight-color:transparent;background:transparent;border-radius:0;border-width:0;color:inherit;font-family:inherit;font-size:inherit;font-variant-ligatures:none;line-height:inherit;margin:0;overflow:visible;position:relative;white-space:pre;word-wrap:normal;z-index:2}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;inset:0;z-index:0}.CodeMirror-linewidget{overflow:auto;position:relative;z-index:2}.CodeMirror-code{outline:none}.CodeMirror-scroll,.CodeMirror-sizer,.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber{box-sizing:content-box}.CodeMirror-measure{height:0;overflow:hidden;position:absolute;visibility:hidden;width:100%}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{position:relative;visibility:hidden;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:#ffa;background:rgba(255,255,0,.4)}.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}.CodeMirror-dialog{background:inherit;color:inherit;left:0;right:0;overflow:hidden;padding:.1em .8em;position:absolute;z-index:15}.CodeMirror-dialog-top{border-bottom:1px solid #eee;top:0}.CodeMirror-dialog-bottom{border-top:1px solid #eee;bottom:0}.CodeMirror-dialog input{background:transparent;border:1px solid #d3d6db;color:inherit;font-family:monospace;outline:none;width:20em}.CodeMirror-dialog button{font-size:70%}.CodeMirror-foldmarker{color:#00f;cursor:pointer;font-family:arial;line-height:.3;text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px}.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-info{background:white;border-radius:2px;box-shadow:0 1px 3px #00000073;box-sizing:border-box;color:#555;font-family:system,-apple-system,San Francisco,".SFNSDisplay-Regular",Segoe UI,Segoe,Segoe WP,Helvetica Neue,helvetica,Lucida Grande,arial,sans-serif;font-size:13px;line-height:16px;margin:8px -8px;max-width:400px;opacity:0;overflow:hidden;padding:8px;position:fixed;transition:opacity .15s;z-index:50}.CodeMirror-info :first-child{margin-top:0}.CodeMirror-info :last-child{margin-bottom:0}.CodeMirror-info p{margin:1em 0}.CodeMirror-info .info-description{color:#777;line-height:16px;margin-top:1em;max-height:80px;overflow:hidden}.CodeMirror-info .info-deprecation{background:#fffae8;box-shadow:inset 0 1px 1px -1px #bfb063;color:#867f70;line-height:16px;margin:8px -8px -8px;max-height:80px;overflow:hidden;padding:8px}.CodeMirror-info .info-deprecation-label{color:#c79b2e;cursor:default;display:block;font-size:9px;font-weight:700;letter-spacing:1px;line-height:1;padding-bottom:5px;text-transform:uppercase;user-select:none}.CodeMirror-info .info-deprecation-label+*{margin-top:0}.CodeMirror-info a{text-decoration:none}.CodeMirror-info a:hover{text-decoration:underline}.CodeMirror-info .type-name{color:#ca9800}.CodeMirror-info .field-name{color:#1f61a0}.CodeMirror-info .enum-value{color:#0b7fc7}.CodeMirror-info .arg-name{color:#8b2bb9}.CodeMirror-info .directive-name{color:#b33086}.CodeMirror-jump-token{text-decoration:underline;cursor:pointer}.CodeMirror-lint-markers{width:16px}.CodeMirror-lint-tooltip{background-color:infobackground;border-radius:4px;border:1px solid black;color:infotext;font-family:monospace;font-size:10pt;max-width:600px;opacity:0;overflow:hidden;padding:2px 5px;position:fixed;transition:opacity .4s;white-space:pre-wrap;z-index:100}.CodeMirror-lint-mark-error,.CodeMirror-lint-mark-warning{background-position:left bottom;background-repeat:repeat-x}.CodeMirror-lint-mark-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==)}.CodeMirror-lint-mark-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=)}.CodeMirror-lint-marker-error,.CodeMirror-lint-marker-warning{background-position:center center;background-repeat:no-repeat;cursor:pointer;display:inline-block;height:16px;position:relative;vertical-align:middle;width:16px}.CodeMirror-lint-message-error,.CodeMirror-lint-message-warning{background-position:top left;background-repeat:no-repeat;padding-left:18px}.CodeMirror-lint-marker-error,.CodeMirror-lint-message-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=)}.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-multiple{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC);background-position:right bottom;background-repeat:no-repeat;width:100%;height:100%}.graphiql-container .spinner-container{height:36px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:36px;z-index:10}.graphiql-container .spinner{animation:rotation .6s infinite linear;border-bottom:6px solid rgba(150,150,150,.15);border-left:6px solid rgba(150,150,150,.15);border-radius:100%;border-right:6px solid rgba(150,150,150,.15);border-top:6px solid rgba(150,150,150,.8);display:inline-block;height:24px;position:absolute;vertical-align:middle;width:24px}@keyframes rotation{0%{transform:rotate(0)}to{transform:rotate(359deg)}}.CodeMirror-hints{background:white;box-shadow:0 1px 3px #00000073;font-family:Consolas,Inconsolata,Droid Sans Mono,Monaco,monospace;font-size:13px;list-style:none;margin:0;max-height:14.5em;overflow:hidden;overflow-y:auto;padding:0;position:absolute;z-index:10}.CodeMirror-hint{border-top:solid 1px #f7f7f7;color:#141823;cursor:pointer;margin:0;max-width:300px;overflow:hidden;padding:2px 6px;white-space:pre}li.CodeMirror-hint-active{background-color:#08f;border-top-color:#fff;color:#fff}.CodeMirror-hint-information{border-top:solid 1px #c0c0c0;max-width:300px;padding:4px 6px;position:relative;z-index:1}.CodeMirror-hint-information:first-child{border-bottom:solid 1px #c0c0c0;border-top:none;margin-bottom:-1px}.CodeMirror-hint-deprecation{background:#fffae8;box-shadow:inset 0 1px 1px -1px #bfb063;color:#867f70;font-family:system,-apple-system,San Francisco,".SFNSDisplay-Regular",Segoe UI,Segoe,Segoe WP,Helvetica Neue,helvetica,Lucida Grande,arial,sans-serif;font-size:13px;line-height:16px;margin-top:4px;max-height:80px;overflow:hidden;padding:6px}.CodeMirror-hint-deprecation .deprecation-label{color:#c79b2e;cursor:default;display:block;font-size:9px;font-weight:700;letter-spacing:1px;line-height:1;padding-bottom:5px;text-transform:uppercase;user-select:none}.CodeMirror-hint-deprecation .deprecation-label+*{margin-top:0}.CodeMirror-hint-deprecation :last-child{margin-bottom:0}.graphiql-container .doc-explorer{background:white}.graphiql-container .doc-explorer-title-bar,.graphiql-container .history-title-bar{cursor:default;display:flex;height:34px;line-height:14px;padding:8px 8px 5px;position:relative;user-select:none}.graphiql-container .doc-explorer-title,.graphiql-container .history-title{flex:1;font-weight:700;overflow-x:hidden;padding:10px 0 10px 10px;text-align:center;text-overflow:ellipsis;user-select:text;white-space:nowrap}.graphiql-container .doc-explorer-back{color:#3b5998;cursor:pointer;margin:-7px 0 -6px -8px;overflow-x:hidden;padding:17px 12px 16px 16px;text-overflow:ellipsis;white-space:nowrap;background:0;border:0;line-height:14px}.doc-explorer-narrow .doc-explorer-back{width:0}.graphiql-container .doc-explorer-back:before{border-left:2px solid #3b5998;border-top:2px solid #3b5998;content:"";display:inline-block;height:9px;margin:0 3px -1px 0;position:relative;transform:rotate(-45deg);width:9px}.graphiql-container .doc-explorer-rhs{position:relative}.graphiql-container .doc-explorer-contents,.graphiql-container .history-contents{background-color:#fff;border-top:1px solid #d6d6d6;inset:47px 0 0;overflow-y:auto;padding:20px 15px;position:absolute}.graphiql-container .doc-explorer-contents{min-width:300px}.graphiql-container .doc-type-description p:first-child,.graphiql-container .doc-type-description blockquote:first-child{margin-top:0}.graphiql-container .doc-explorer-contents a{cursor:pointer;text-decoration:none}.graphiql-container .doc-explorer-contents a:hover{text-decoration:underline}.graphiql-container .doc-value-description>:first-child{margin-top:4px}.graphiql-container .doc-value-description>:last-child{margin-bottom:4px}.graphiql-container .doc-type-description code,.graphiql-container .doc-type-description pre,.graphiql-container .doc-category code,.graphiql-container .doc-category pre{--saf-0: rgba(var(--sk_foreground_low, 29, 28, 29), .13);font-size:12px;line-height:1.50001;font-variant-ligatures:none;white-space:pre;white-space:pre-wrap;word-wrap:break-word;word-break:normal;-webkit-tab-size:4;-moz-tab-size:4;tab-size:4}.graphiql-container .doc-type-description code,.graphiql-container .doc-category code{padding:2px 3px 1px;border:1px solid var(--saf-0);border-radius:3px;background-color:rgba(var(--sk_foreground_min, 29, 28, 29),.04);color:#e01e5a;background-color:#fff}.graphiql-container .doc-category{margin:20px 0}.graphiql-container .doc-category-title{border-bottom:1px solid #e0e0e0;color:#777;cursor:default;font-size:14px;font-variant:small-caps;font-weight:700;letter-spacing:1px;margin:0 -15px 10px 0;padding:10px 0;user-select:none}.graphiql-container .doc-category-item{margin:12px 0;color:#555}.graphiql-container .keyword{color:#b11a04}.graphiql-container .type-name{color:#ca9800}.graphiql-container .field-name{color:#1f61a0}.graphiql-container .field-short-description{color:#666;margin-left:5px;overflow:hidden;text-overflow:ellipsis}.graphiql-container .enum-value{color:#0b7fc7}.graphiql-container .arg-name{color:#8b2bb9}.graphiql-container .arg{display:block;margin-left:1em}.graphiql-container .arg:first-child:last-child,.graphiql-container .arg:first-child:nth-last-child(2),.graphiql-container .arg:first-child:nth-last-child(2)~.arg{display:inherit;margin:inherit}.graphiql-container .arg:first-child:nth-last-child(2):after{content:", "}.graphiql-container .arg-default-value{color:#43a047}.graphiql-container .doc-deprecation{background:#fffae8;box-shadow:inset 0 0 1px #bfb063;color:#867f70;line-height:16px;margin:8px -8px;max-height:80px;overflow:hidden;padding:8px;border-radius:3px}.graphiql-container .doc-deprecation:before{content:"Deprecated:";color:#c79b2e;cursor:default;display:block;font-size:9px;font-weight:700;letter-spacing:1px;line-height:1;padding-bottom:5px;text-transform:uppercase;user-select:none}.graphiql-container .doc-deprecation>:first-child{margin-top:0}.graphiql-container .doc-deprecation>:last-child{margin-bottom:0}.graphiql-container .show-btn{-webkit-appearance:initial;display:block;border-radius:3px;border:solid 1px #ccc;text-align:center;padding:8px 12px 10px;width:100%;box-sizing:border-box;background:#fbfcfc;color:#555;cursor:pointer}.graphiql-container .search-box{border-bottom:1px solid #d3d6db;display:flex;align-items:center;font-size:14px;margin:-15px -15px 12px 0;position:relative}.graphiql-container .search-box-icon{cursor:pointer;display:block;font-size:24px;transform:rotate(-45deg);user-select:none}.graphiql-container .search-box .search-box-clear{background-color:#d0d0d0;border-radius:12px;color:#fff;cursor:pointer;font-size:11px;padding:1px 5px 2px;position:absolute;right:3px;user-select:none;border:0}.graphiql-container .search-box .search-box-clear:hover{background-color:#b9b9b9}.graphiql-container .search-box>input{border:none;box-sizing:border-box;font-size:14px;outline:none;padding:6px 24px 8px 20px;width:100%}.graphiql-container .error-container{font-weight:700;left:0;letter-spacing:1px;opacity:.5;position:absolute;right:0;text-align:center;text-transform:uppercase;top:50%;transform:translateY(-50%)}.graphiql-container .history-contents{font-family:Consolas,Inconsolata,Droid Sans Mono,Monaco,monospace}.graphiql-container .history-contents{margin:0;padding:0}.graphiql-container .history-contents li{align-items:center;display:flex;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin:0;padding:8px;border-bottom:1px solid #e0e0e0}.graphiql-container .history-contents li button:not(.history-label){display:none;margin-left:10px}.graphiql-container .history-contents li:hover button:not(.history-label),.graphiql-container .history-contents li:focus-within button:not(.history-label){display:inline-block}.graphiql-container .history-contents input,.graphiql-container .history-contents button{padding:0;background:0;border:0;font-size:inherit;font-family:inherit;line-height:14px;color:inherit}.graphiql-container .history-contents input{flex-grow:1}.graphiql-container .history-contents input::placeholder{color:inherit}.graphiql-container .history-contents button{cursor:pointer;text-align:left}.graphiql-container .history-contents .history-label{flex-grow:1;overflow:hidden;text-overflow:ellipsis}
diff --git a/netbox/project-static/dist/graphiql.js b/netbox/project-static/dist/graphiql.js
index 0d4b3288b..1b6949d02 100644
--- a/netbox/project-static/dist/graphiql.js
+++ b/netbox/project-static/dist/graphiql.js
@@ -1,50 +1,50 @@
-(()=>{var _V=Object.create;var a0=Object.defineProperty;var SV=Object.getOwnPropertyDescriptor;var DV=Object.getOwnPropertyNames;var kV=Object.getPrototypeOf,OV=Object.prototype.hasOwnProperty;var CV=e=>a0(e,"__esModule",{value:!0});var eC=(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 U=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var wV=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of DV(t))!OV.call(e,n)&&n!=="default"&&a0(e,n,{get:()=>t[n],enumerable:!(r=SV(t,n))||r.enumerable});return e},Ye=e=>wV(CV(a0(e!=null?_V(kV(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var o0=U((ste,rC)=>{"use strict";var tC=Object.getOwnPropertySymbols,AV=Object.prototype.hasOwnProperty,NV=Object.prototype.propertyIsEnumerable;function LV(e){if(e==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}function xV(){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 a={};return"abcdefghijklmnopqrst".split("").forEach(function(o){a[o]=o}),Object.keys(Object.assign({},a)).join("")==="abcdefghijklmnopqrst"}catch(o){return!1}}rC.exports=xV()?Object.assign:function(e,t){for(var r,n=LV(e),a,o=1;o{"use strict";var u0=o0(),$s=60103,nC=60106;vt.Fragment=60107;vt.StrictMode=60108;vt.Profiler=60114;var iC=60109,aC=60110,oC=60112;vt.Suspense=60113;var uC=60115,sC=60116;typeof Symbol=="function"&&Symbol.for&&(Ti=Symbol.for,$s=Ti("react.element"),nC=Ti("react.portal"),vt.Fragment=Ti("react.fragment"),vt.StrictMode=Ti("react.strict_mode"),vt.Profiler=Ti("react.profiler"),iC=Ti("react.provider"),aC=Ti("react.context"),oC=Ti("react.forward_ref"),vt.Suspense=Ti("react.suspense"),uC=Ti("react.memo"),sC=Ti("react.lazy"));var Ti,lC=typeof Symbol=="function"&&Symbol.iterator;function IV(e){return e===null||typeof e!="object"?null:(e=lC&&e[lC]||e["@@iterator"],typeof e=="function"?e:null)}function pf(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,r=1;r{"use strict";bC.exports=yC()});var kC=U(Lt=>{"use strict";var tl,hf,kh,p0;typeof performance=="object"&&typeof performance.now=="function"?(TC=performance,Lt.unstable_now=function(){return TC.now()}):(h0=Date,EC=h0.now(),Lt.unstable_now=function(){return h0.now()-EC});var TC,h0,EC;typeof window=="undefined"||typeof MessageChannel!="function"?(rl=null,v0=null,g0=function(){if(rl!==null)try{var e=Lt.unstable_now();rl(!0,e),rl=null}catch(t){throw setTimeout(g0,0),t}},tl=function(e){rl!==null?setTimeout(tl,0,e):(rl=e,setTimeout(g0,0))},hf=function(e,t){v0=setTimeout(e,t)},kh=function(){clearTimeout(v0)},Lt.unstable_shouldYield=function(){return!1},p0=Lt.unstable_forceFrameRate=function(){}):(_C=window.setTimeout,SC=window.clearTimeout,typeof console!="undefined"&&(DC=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 DC!="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")),vf=!1,gf=null,Oh=-1,m0=5,y0=0,Lt.unstable_shouldYield=function(){return Lt.unstable_now()>=y0},p0=function(){},Lt.unstable_forceFrameRate=function(e){0>e||125>>1,a=e[n];if(a!==void 0&&0Ah(s,r))d!==void 0&&0>Ah(d,s)?(e[n]=d,e[l]=r,n=l):(e[n]=s,e[o]=r,n=o);else if(d!==void 0&&0>Ah(d,r))e[n]=d,e[l]=r,n=l;else break e}}return t}return null}function Ah(e,t){var r=e.sortIndex-t.sortIndex;return r!==0?r:e.id-t.id}var ga=[],xo=[],MV=1,Ei=null,hn=3,Nh=!1,ju=!1,mf=!1;function E0(e){for(var t=Xi(xo);t!==null;){if(t.callback===null)wh(xo);else if(t.startTime<=e)wh(xo),t.sortIndex=t.expirationTime,T0(ga,t);else break;t=Xi(xo)}}function _0(e){if(mf=!1,E0(e),!ju)if(Xi(ga)!==null)ju=!0,tl(S0);else{var t=Xi(xo);t!==null&&hf(_0,t.startTime-e)}}function S0(e,t){ju=!1,mf&&(mf=!1,kh()),Nh=!0;var r=hn;try{for(E0(t),Ei=Xi(ga);Ei!==null&&(!(Ei.expirationTime>t)||e&&!Lt.unstable_shouldYield());){var n=Ei.callback;if(typeof n=="function"){Ei.callback=null,hn=Ei.priorityLevel;var a=n(Ei.expirationTime<=t);t=Lt.unstable_now(),typeof a=="function"?Ei.callback=a:Ei===Xi(ga)&&wh(ga),E0(t)}else wh(ga);Ei=Xi(ga)}if(Ei!==null)var o=!0;else{var s=Xi(xo);s!==null&&hf(_0,s.startTime-t),o=!1}return o}finally{Ei=null,hn=r,Nh=!1}}var qV=p0;Lt.unstable_IdlePriority=5;Lt.unstable_ImmediatePriority=1;Lt.unstable_LowPriority=4;Lt.unstable_NormalPriority=3;Lt.unstable_Profiling=null;Lt.unstable_UserBlockingPriority=2;Lt.unstable_cancelCallback=function(e){e.callback=null};Lt.unstable_continueExecution=function(){ju||Nh||(ju=!0,tl(S0))};Lt.unstable_getCurrentPriorityLevel=function(){return hn};Lt.unstable_getFirstCallbackNode=function(){return Xi(ga)};Lt.unstable_next=function(e){switch(hn){case 1:case 2:case 3:var t=3;break;default:t=hn}var r=hn;hn=t;try{return e()}finally{hn=r}};Lt.unstable_pauseExecution=function(){};Lt.unstable_requestPaint=qV;Lt.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var r=hn;hn=e;try{return t()}finally{hn=r}};Lt.unstable_scheduleCallback=function(e,t,r){var n=Lt.unstable_now();switch(typeof r=="object"&&r!==null?(r=r.delay,r=typeof r=="number"&&0n?(e.sortIndex=r,T0(xo,e),Xi(ga)===null&&e===Xi(xo)&&(mf?kh():mf=!0,hf(_0,r-n))):(e.sortIndex=a,T0(ga,e),ju||Nh||(ju=!0,tl(S0))),e};Lt.unstable_wrapCallback=function(e){var t=hn;return function(){var r=hn;hn=t;try{return e.apply(this,arguments)}finally{hn=r}}}});var CC=U((dte,OC)=>{"use strict";OC.exports=kC()});var pA=U(Ci=>{"use strict";var Lh=Bt(),er=o0(),Yr=CC();function pe(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,r=1;rt}return!1}function xn(e,t,r,n,a,o,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=n,this.attributeNamespace=a,this.mustUseProperty=r,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=s}var nn={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){nn[e]=new xn(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];nn[t]=new xn(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){nn[e]=new xn(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){nn[e]=new xn(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){nn[e]=new xn(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){nn[e]=new xn(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){nn[e]=new xn(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){nn[e]=new xn(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){nn[e]=new xn(e,5,!1,e.toLowerCase(),null,!1,!1)});var D0=/[\-:]([a-z])/g;function k0(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(D0,k0);nn[t]=new xn(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(D0,k0);nn[t]=new xn(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(D0,k0);nn[t]=new xn(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){nn[e]=new xn(e,1,!1,e.toLowerCase(),null,!1,!1)});nn.xlinkHref=new xn("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){nn[e]=new xn(e,1,!1,e.toLowerCase(),null,!0,!0)});function O0(e,t,r,n){var a=nn.hasOwnProperty(t)?nn[t]:null,o=a!==null?a.type===0:n?!1:!(!(2{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||a[s]!==o[l])return`
-`+a[s].replace(" at new "," at ");while(1<=s&&0<=l);break}}}finally{j0=!1,Error.prepareStackTrace=r}return(e=e?e.displayName||e.name:"")?Sf(e):""}function QV(e){switch(e.tag){case 5:return Sf(e.type);case 16:return Sf("Lazy");case 13:return Sf("Suspense");case 19:return Sf("SuspenseList");case 0:case 2:case 15:return e=Fh(e.type,!1),e;case 11:return e=Fh(e.type.render,!1),e;case 22:return e=Fh(e.type._render,!1),e;case 1:return e=Fh(e.type,!0),e;default:return""}}function il(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 Io:return"Fragment";case qu:return"Portal";case Tf:return"Profiler";case C0:return"StrictMode";case Ef:return"Suspense";case Ih:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case A0:return(e.displayName||"Context")+".Consumer";case w0: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 Rh:return il(e.type);case L0:return il(e._render);case N0:t=e._payload,e=e._init;try{return il(e(t))}catch(r){}}return null}function Ro(e){switch(typeof e){case"boolean":case"number":case"object":case"string":case"undefined":return e;default:return""}}function RC(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function KV(e){var t=RC(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 a=r.get,o=r.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return a.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 jh(e){e._valueTracker||(e._valueTracker=KV(e))}function FC(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var r=t.getValue(),n="";return e&&(n=RC(e)?e.checked?"true":"false":e.value),e=n,e!==r?(t.setValue(e),!0):!1}function Ph(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 P0(e,t){var r=t.checked;return er({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:r!=null?r:e._wrapperState.initialChecked})}function jC(e,t){var r=t.defaultValue==null?"":t.defaultValue,n=t.checked!=null?t.checked:t.defaultChecked;r=Ro(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 PC(e,t){t=t.checked,t!=null&&O0(e,"checked",t,!1)}function M0(e,t){PC(e,t);var r=Ro(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")?q0(e,t.type,r):t.hasOwnProperty("defaultValue")&&q0(e,t.type,Ro(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function MC(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 q0(e,t,r){(t!=="number"||Ph(e.ownerDocument)!==e)&&(r==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+r&&(e.defaultValue=""+r))}function HV(e){var t="";return Lh.Children.forEach(e,function(r){r!=null&&(t+=r)}),t}function B0(e,t){return e=er({children:void 0},t),(t=HV(t.children))&&(e.children=t),e}function al(e,t,r,n){if(e=e.options,t){t={};for(var a=0;a=r.length))throw Error(pe(93));r=r[0]}t=r}t==null&&(t=""),r=t}e._wrapperState={initialValue:Ro(r)}}function BC(e,t){var r=Ro(t.value),n=Ro(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 VC(e){var t=e.textContent;t===e._wrapperState.initialValue&&t!==""&&t!==null&&(e.value=t)}var U0={html:"http://www.w3.org/1999/xhtml",mathml:"http://www.w3.org/1998/Math/MathML",svg:"http://www.w3.org/2000/svg"};function UC(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 G0(e,t){return e==null||e==="http://www.w3.org/1999/xhtml"?UC(t):e==="http://www.w3.org/2000/svg"&&t==="foreignObject"?"http://www.w3.org/1999/xhtml":e}var Mh,GC=function(e){return typeof MSApp!="undefined"&&MSApp.execUnsafeLocalFunction?function(t,r,n,a){MSApp.execUnsafeLocalFunction(function(){return e(t,r,n,a)})}:e}(function(e,t){if(e.namespaceURI!==U0.svg||"innerHTML"in e)e.innerHTML=t;else{for(Mh=Mh||document.createElement("div"),Mh.innerHTML=""+t.valueOf().toString()+" ",t=Mh.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Df(e,t){if(t){var r=e.firstChild;if(r&&r===e.lastChild&&r.nodeType===3){r.nodeValue=t;return}}e.textContent=t}var kf={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},zV=["Webkit","ms","Moz","O"];Object.keys(kf).forEach(function(e){zV.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),kf[t]=kf[e]})});function QC(e,t,r){return t==null||typeof t=="boolean"||t===""?"":r||typeof t!="number"||t===0||kf.hasOwnProperty(e)&&kf[e]?(""+t).trim():t+"px"}function KC(e,t){e=e.style;for(var r in t)if(t.hasOwnProperty(r)){var n=r.indexOf("--")===0,a=QC(r,t[r],n);r==="float"&&(r="cssFloat"),n?e.setProperty(r,a):e[r]=a}}var WV=er({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 Q0(e,t){if(t){if(WV[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(pe(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(pe(60));if(!(typeof t.dangerouslySetInnerHTML=="object"&&"__html"in t.dangerouslySetInnerHTML))throw Error(pe(61))}if(t.style!=null&&typeof t.style!="object")throw Error(pe(62))}}function K0(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 H0(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var z0=null,ol=null,ul=null;function HC(e){if(e=Gf(e)){if(typeof z0!="function")throw Error(pe(280));var t=e.stateNode;t&&(t=av(t),z0(e.stateNode,e.type,t))}}function zC(e){ol?ul?ul.push(e):ul=[e]:ol=e}function WC(){if(ol){var e=ol,t=ul;if(ul=ol=null,HC(e),t)for(e=0;en?0:1<r;r++)t.push(e);return t}function Kh(e,t,r){e.pendingLanes|=t;var n=t-1;e.suspendedLanes&=n,e.pingedLanes&=n,e=e.eventTimes,t=31-Mo(t),e[t]=r}var Mo=Math.clz32?Math.clz32:cU,sU=Math.log,lU=Math.LN2;function cU(e){return e===0?32:31-(sU(e)/lU|0)|0}var fU=Yr.unstable_UserBlockingPriority,dU=Yr.unstable_runWithPriority,Hh=!0;function pU(e,t,r,n){Bu||Y0();var a=ub,o=Bu;Bu=!0;try{YC(a,e,t,r,n)}finally{(Bu=o)||X0()}}function hU(e,t,r,n){dU(fU,ub.bind(null,e,t,r,n))}function ub(e,t,r,n){if(Hh){var a;if((a=(t&4)==0)&&0=jf),E2=String.fromCharCode(32),_2=!1;function S2(e,t){switch(e){case"keyup":return PU.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function D2(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var pl=!1;function qU(e,t){switch(e){case"compositionend":return D2(t);case"keypress":return t.which!==32?null:(_2=!0,E2);case"textInput":return e=t.data,e===E2&&_2?null:e;default:return null}}function BU(e,t){if(pl)return e==="compositionend"||!vb&&S2(e,t)?(e=v2(),zh=lb=qo=null,pl=!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=N2(r)}}function x2(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?x2(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function I2(){for(var e=window,t=Ph();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=Ph(e.document)}return t}function mb(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 JU=ro&&"documentMode"in document&&11>=document.documentMode,hl=null,yb=null,Bf=null,bb=!1;function R2(e,t,r){var n=r.window===r?r.document:r.nodeType===9?r:r.ownerDocument;bb||hl==null||hl!==Ph(n)||(n=hl,"selectionStart"in n&&mb(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}),Bf&&qf(Bf,n)||(Bf=n,n=tv(yb,"onSelect"),0bl||(e.current=Ob[bl],Ob[bl]=null,bl--)}function lr(e,t){bl++,Ob[bl]=e.current,e.current=t}var Uo={},vn=Vo(Uo),Qn=Vo(!1),Gu=Uo;function Tl(e,t){var r=e.type.contextTypes;if(!r)return Uo;var n=e.stateNode;if(n&&n.__reactInternalMemoizedUnmaskedChildContext===t)return n.__reactInternalMemoizedMaskedChildContext;var a={},o;for(o in r)a[o]=t[o];return n&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=a),a}function Kn(e){return e=e.childContextTypes,e!=null}function ov(){Jt(Qn),Jt(vn)}function Y2(e,t,r){if(vn.current!==Uo)throw Error(pe(168));lr(vn,t),lr(Qn,r)}function J2(e,t,r){var n=e.stateNode;if(e=t.childContextTypes,typeof n.getChildContext!="function")return r;n=n.getChildContext();for(var a in n)if(!(a in e))throw Error(pe(108,il(t)||"Unknown",a));return er({},r,n)}function uv(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Uo,Gu=vn.current,lr(vn,e),lr(Qn,Qn.current),!0}function X2(e,t,r){var n=e.stateNode;if(!n)throw Error(pe(169));r?(e=J2(e,t,Gu),n.__reactInternalMemoizedMergedChildContext=e,Jt(Qn),Jt(vn),lr(vn,e)):Jt(Qn),lr(Qn,r)}var Cb=null,Qu=null,$U=Yr.unstable_runWithPriority,wb=Yr.unstable_scheduleCallback,Ab=Yr.unstable_cancelCallback,eG=Yr.unstable_shouldYield,Z2=Yr.unstable_requestPaint,Nb=Yr.unstable_now,tG=Yr.unstable_getCurrentPriorityLevel,sv=Yr.unstable_ImmediatePriority,$2=Yr.unstable_UserBlockingPriority,ew=Yr.unstable_NormalPriority,tw=Yr.unstable_LowPriority,rw=Yr.unstable_IdlePriority,Lb={},rG=Z2!==void 0?Z2:function(){},no=null,lv=null,xb=!1,nw=Nb(),gn=1e4>nw?Nb:function(){return Nb()-nw};function El(){switch(tG()){case sv:return 99;case $2:return 98;case ew:return 97;case tw:return 96;case rw:return 95;default:throw Error(pe(332))}}function iw(e){switch(e){case 99:return sv;case 98:return $2;case 97:return ew;case 96:return tw;case 95:return rw;default:throw Error(pe(332))}}function Ku(e,t){return e=iw(e),$U(e,t)}function Qf(e,t,r){return e=iw(e),wb(e,t,r)}function ya(){if(lv!==null){var e=lv;lv=null,Ab(e)}aw()}function aw(){if(!xb&&no!==null){xb=!0;var e=0;try{var t=no;Ku(99,function(){for(;eR?(M=D,D=null):M=D.sibling;var q=T(y,D,m[R],k);if(q===null){D===null&&(D=M);break}e&&D&&q.alternate===null&&t(y,D),_=o(q,_,R),C===null?w=q:C.sibling=q,C=q,D=M}if(R===m.length)return r(y,D),w;if(D===null){for(;RR?(M=D,D=null):M=D.sibling;var z=T(y,D,q.value,k);if(z===null){D===null&&(D=M);break}e&&D&&z.alternate===null&&t(y,D),_=o(z,_,R),C===null?w=z:C.sibling=z,C=z,D=M}if(q.done)return r(y,D),w;if(D===null){for(;!q.done;R++,q=m.next())q=b(y,q.value,k),q!==null&&(_=o(q,_,R),C===null?w=q:C.sibling=q,C=q);return w}for(D=n(y,D);!q.done;R++,q=m.next())q=A(D,y,R,q.value,k),q!==null&&(e&&q.alternate!==null&&D.delete(q.key===null?R:q.key),_=o(q,_,R),C===null?w=q:C.sibling=q,C=q);return e&&D.forEach(function(Q){return t(y,Q)}),w}return function(y,_,m,k){var w=typeof m=="object"&&m!==null&&m.type===Io&&m.key===null;w&&(m=m.props.children);var C=typeof m=="object"&&m!==null;if(C)switch(m.$$typeof){case bf:e:{for(C=m.key,w=_;w!==null;){if(w.key===C){switch(w.tag){case 7:if(m.type===Io){r(y,w.sibling),_=a(w,m.props.children),_.return=y,y=_;break e}break;default:if(w.elementType===m.type){r(y,w.sibling),_=a(w,m.props),_.ref=Hf(y,w,m),_.return=y,y=_;break e}}r(y,w);break}else t(y,w);w=w.sibling}m.type===Io?(_=Ll(m.props.children,y.mode,k,m.key),_.return=y,y=_):(k=Rv(m.type,m.key,m.props,null,y.mode,k),k.ref=Hf(y,_,m),k.return=y,y=k)}return s(y);case qu:e:{for(w=m.key;_!==null;){if(_.key===w)if(_.tag===4&&_.stateNode.containerInfo===m.containerInfo&&_.stateNode.implementation===m.implementation){r(y,_.sibling),_=a(_,m.children||[]),_.return=y,y=_;break e}else{r(y,_);break}else t(y,_);_=_.sibling}_=bT(m,y.mode,k),_.return=y,y=_}return s(y)}if(typeof m=="string"||typeof m=="number")return m=""+m,_!==null&&_.tag===6?(r(y,_.sibling),_=a(_,m),_.return=y,y=_):(r(y,_),_=yT(m,y.mode,k),_.return=y,y=_),s(y);if(vv(m))return L(y,_,m,k);if(_f(m))return S(y,_,m,k);if(C&&gv(y,m),typeof m=="undefined"&&!w)switch(y.tag){case 1:case 22:case 0:case 11:case 15:throw Error(pe(152,il(y.type)||"Component"))}return r(y,_)}}var mv=hw(!0),vw=hw(!1),zf={},ba=Vo(zf),Wf=Vo(zf),Yf=Vo(zf);function Hu(e){if(e===zf)throw Error(pe(174));return e}function Pb(e,t){switch(lr(Yf,t),lr(Wf,e),lr(ba,zf),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:G0(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=G0(t,e)}Jt(ba),lr(ba,t)}function Dl(){Jt(ba),Jt(Wf),Jt(Yf)}function gw(e){Hu(Yf.current);var t=Hu(ba.current),r=G0(t,e.type);t!==r&&(lr(Wf,e),lr(ba,r))}function Mb(e){Wf.current===e&&(Jt(ba),Jt(Wf))}var cr=Vo(0);function yv(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 io=null,Ho=null,Ta=!1;function mw(e,t){var r=Oi(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 yw(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 qb(e){if(Ta){var t=Ho;if(t){var r=t;if(!yw(e,t)){if(t=gl(r.nextSibling),!t||!yw(e,t)){e.flags=e.flags&-1025|2,Ta=!1,io=e;return}mw(io,r)}io=e,Ho=gl(t.firstChild)}else e.flags=e.flags&-1025|2,Ta=!1,io=e}}function bw(e){for(e=e.return;e!==null&&e.tag!==5&&e.tag!==3&&e.tag!==13;)e=e.return;io=e}function bv(e){if(e!==io)return!1;if(!Ta)return bw(e),Ta=!0,!1;var t=e.type;if(e.tag!==5||t!=="head"&&t!=="body"&&!Sb(t,e.memoizedProps))for(t=Ho;t;)mw(e,t),t=gl(t.nextSibling);if(bw(e),e.tag===13){if(e=e.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(pe(317));e:{for(e=e.nextSibling,t=0;e;){if(e.nodeType===8){var r=e.data;if(r==="/$"){if(t===0){Ho=gl(e.nextSibling);break e}t--}else r!=="$"&&r!=="$!"&&r!=="$?"||t++}e=e.nextSibling}Ho=null}}else Ho=io?gl(e.stateNode.nextSibling):null;return!0}function Bb(){Ho=io=null,Ta=!1}var kl=[];function Vb(){for(var e=0;eo))throw Error(pe(301));o+=1,an=mn=null,t.updateQueue=null,Jf.current=uG,e=r(n,a)}while(Zf)}if(Jf.current=Dv,t=mn!==null&&mn.next!==null,Xf=0,an=mn=gr=null,Tv=!1,t)throw Error(pe(300));return e}function zu(){var e={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return an===null?gr.memoizedState=an=e:an=an.next=e,an}function Wu(){if(mn===null){var e=gr.alternate;e=e!==null?e.memoizedState:null}else e=mn.next;var t=an===null?gr.memoizedState:an.next;if(t!==null)an=t,mn=e;else{if(e===null)throw Error(pe(310));mn=e,e={memoizedState:mn.memoizedState,baseState:mn.baseState,baseQueue:mn.baseQueue,queue:mn.queue,next:null},an===null?gr.memoizedState=an=e:an=an.next=e}return an}function Ea(e,t){return typeof t=="function"?t(e):t}function $f(e){var t=Wu(),r=t.queue;if(r===null)throw Error(pe(311));r.lastRenderedReducer=e;var n=mn,a=n.baseQueue,o=r.pending;if(o!==null){if(a!==null){var s=a.next;a.next=o.next,o.next=s}n.baseQueue=a=o,r.pending=null}if(a!==null){a=a.next,n=n.baseState;var l=s=o=null,d=a;do{var h=d.lane;if((Xf&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,gr.lanes|=h,nd|=h}d=d.next}while(d!==null&&d!==a);l===null?o=n:l.next=s,_i(n,t.memoizedState)||($i=!0),t.memoizedState=n,t.baseState=o,t.baseQueue=l,r.lastRenderedState=n}return[t.memoizedState,r.dispatch]}function ed(e){var t=Wu(),r=t.queue;if(r===null)throw Error(pe(311));r.lastRenderedReducer=e;var n=r.dispatch,a=r.pending,o=t.memoizedState;if(a!==null){r.pending=null;var s=a=a.next;do o=e(o,s.action),s=s.next;while(s!==a);_i(o,t.memoizedState)||($i=!0),t.memoizedState=o,t.baseQueue===null&&(t.baseState=o),r.lastRenderedState=o}return[o,n]}function Tw(e,t,r){var n=t._getVersion;n=n(t._source);var a=t._workInProgressVersionPrimary;if(a!==null?e=a===n:(e=e.mutableReadLanes,(e=(Xf&e)===e)&&(t._workInProgressVersionPrimary=n,kl.push(t))),e)return r(t._source);throw kl.push(t),Error(pe(350))}function Ew(e,t,r,n){var a=In;if(a===null)throw Error(pe(349));var o=t._getVersion,s=o(t._source),l=Jf.current,d=l.useState(function(){return Tw(a,t,r)}),h=d[1],v=d[0];d=an;var b=e.memoizedState,T=b.refs,A=T.getSnapshot,L=b.source;b=b.subscribe;var S=gr;return e.memoizedState={refs:T,source:t,subscribe:n},l.useEffect(function(){T.getSnapshot=r,T.setSnapshot=h;var y=o(t._source);if(!_i(s,y)){y=r(t._source),_i(v,y)||(h(y),y=Wo(S),a.mutableReadLanes|=y&a.pendingLanes),y=a.mutableReadLanes,a.entangledLanes|=y;for(var _=a.entanglements,m=y;0r?98:r,function(){e(!0)}),Ku(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[Bo]=t,e[iv]=n,Uw(e,t,!1,!1),t.stateNode=e,s=K0(r,n),r){case"dialog":Yt("cancel",e),Yt("close",e),a=n;break;case"iframe":case"object":case"embed":Yt("load",e),a=n;break;case"video":case"audio":for(a=0;alT&&(t.flags|=64,o=!0,rd(n,!1),t.lanes=33554432)}else{if(!o)if(e=yv(s),e!==null){if(t.flags|=64,o=!0,r=e.updateQueue,r!==null&&(t.updateQueue=r,t.flags|=4),rd(n,!0),n.tail===null&&n.tailMode==="hidden"&&!s.alternate&&!Ta)return t=t.lastEffect=n.lastEffect,t!==null&&(t.nextEffect=null),null}else 2*gn()-n.renderingStartTime>lT&&r!==1073741824&&(t.flags|=64,o=!0,rd(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=gn(),r.sibling=null,t=cr.current,lr(cr,o?t&1|2:t&1),r):null;case 23:case 24:return vT(),e!==null&&e.memoizedState!==null!=(t.memoizedState!==null)&&n.mode!=="unstable-defer-without-hiding"&&(t.flags|=4),null}throw Error(pe(156,t.tag))}function cG(e){switch(e.tag){case 1:Kn(e.type)&&ov();var t=e.flags;return t&4096?(e.flags=t&-4097|64,e):null;case 3:if(Dl(),Jt(Qn),Jt(vn),Vb(),t=e.flags,(t&64)!=0)throw Error(pe(285));return e.flags=t&-4097|64,e;case 5:return Mb(e),null;case 13:return Jt(cr),t=e.flags,t&4096?(e.flags=t&-4097|64,e):null;case 19:return Jt(cr),null;case 4:return Dl(),null;case 10:return Rb(e),null;case 23:case 24:return vT(),null;default:return null}}function $b(e,t){try{var r="",n=t;do r+=QV(n),n=n.return;while(n);var a=r}catch(o){a=`
+`),s=i.length-1,l=o.length-1;1<=s&&0<=l&&i[s]!==o[l];)l--;for(;1<=s&&0<=l;s--,l--)if(i[s]!==o[l]){if(s!==1||l!==1)do if(s--,l--,0>l||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:a}}function eT(e,t){try{console.error(t.value)}catch(r){setTimeout(function(){throw r})}}var fG=typeof WeakMap=="function"?WeakMap:Map;function Kw(e,t,r){r=Qo(-1,r),r.tag=3,r.payload={element:null};var n=t.value;return r.callback=function(){wv||(wv=!0,cT=n),eT(e,t)},r}function Hw(e,t,r){r=Qo(-1,r),r.tag=3;var n=e.type.getDerivedStateFromError;if(typeof n=="function"){var a=t.value;r.payload=function(){return eT(e,t),n(a)}}var o=e.stateNode;return o!==null&&typeof o.componentDidCatch=="function"&&(r.callback=function(){typeof n!="function"&&(_a===null?_a=new Set([this]):_a.add(this),eT(e,t));var s=t.stack;this.componentDidCatch(t.value,{componentStack:s!==null?s:""})}),r}var dG=typeof WeakSet=="function"?WeakSet:Set;function zw(e){var t=e.ref;if(t!==null)if(typeof t=="function")try{t(null)}catch(r){Xo(e,r)}else t.current=null}function pG(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:Zi(t.type,r),n),e.__reactInternalSnapshotBeforeUpdate=t}return;case 3:t.flags&256&&Db(t.stateNode.containerInfo);return;case 5:case 6:case 4:case 17:return}throw Error(pe(163))}function hG(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 a=e;n=a.next,a=a.tag,(a&4)!=0&&(a&1)!=0&&(sA(r,e),_G(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:Zi(r.type,t.memoizedProps),e.componentDidUpdate(n,t.memoizedState,e.__reactInternalSnapshotBeforeUpdate))),t=r.updateQueue,t!==null&&lw(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}lw(r,t,e)}return;case 5:e=r.stateNode,t===null&&r.flags&4&&Q2(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&&u2(r))));return;case 19:case 17:case 20:case 21:case 23:case 24:return}throw Error(pe(163))}function Ww(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 a=r.memoizedProps.style;a=a!=null&&a.hasOwnProperty("display")?a.display:null,n.style.display=QC("display",a)}}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 Yw(e,t){if(Qu&&typeof Qu.onCommitFiberUnmount=="function")try{Qu.onCommitFiberUnmount(Cb,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,a=n.destroy;if(n=n.tag,a!==void 0)if((n&4)!=0)sA(t,r);else{n=t;try{a()}catch(o){Xo(n,o)}}r=r.next}while(r!==e)}break;case 1:if(zw(t),e=t.stateNode,typeof e.componentWillUnmount=="function")try{e.props=t.memoizedProps,e.state=t.memoizedState,e.componentWillUnmount()}catch(o){Xo(t,o)}break;case 5:zw(t);break;case 4:$w(e,t)}}function Jw(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 Xw(e){return e.tag===5||e.tag===3||e.tag===4}function Zw(e){e:{for(var t=e.return;t!==null;){if(Xw(t))break e;t=t.return}throw Error(pe(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(pe(161))}r.flags&16&&(Df(t,""),r.flags&=-17);e:t:for(r=e;;){for(;r.sibling===null;){if(r.return===null||Xw(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?tT(e,r,t):rT(e,r,t)}function tT(e,t,r){var n=e.tag,a=n===5||n===6;if(a)e=a?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=rv));else if(n!==4&&(e=e.child,e!==null))for(tT(e,t,r),e=e.sibling;e!==null;)tT(e,t,r),e=e.sibling}function rT(e,t,r){var n=e.tag,a=n===5||n===6;if(a)e=a?e.stateNode:e.stateNode.instance,t?r.insertBefore(e,t):r.appendChild(e);else if(n!==4&&(e=e.child,e!==null))for(rT(e,t,r),e=e.sibling;e!==null;)rT(e,t,r),e=e.sibling}function $w(e,t){for(var r=t,n=!1,a,o;;){if(!n){n=r.return;e:for(;;){if(n===null)throw Error(pe(160));switch(a=n.stateNode,n.tag){case 5:o=!1;break e;case 3:a=a.containerInfo,o=!0;break e;case 4:a=a.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(Yw(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=a,l=r.stateNode,s.nodeType===8?s.parentNode.removeChild(l):s.removeChild(l)):a.removeChild(r.stateNode)}else if(r.tag===4){if(r.child!==null){a=r.stateNode.containerInfo,o=!0,r.child.return=r,r=r.child;continue}}else if(Yw(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 nT(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 a=e!==null?e.memoizedProps:n;e=t.type;var o=t.updateQueue;if(t.updateQueue=null,o!==null){for(r[iv]=n,e==="input"&&n.type==="radio"&&n.name!=null&&PC(r,n),K0(e,a),t=K0(e,n),a=0;aa&&(a=s),r&=~o}if(r=a,r=gn()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*gG(r/1960))-r,10i&&(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.`)}on!==5&&(on=2),d=$b(d,l),T=s;do{switch(T.tag){case 3:o=d,T.flags|=4096,t&=-t,T.lanes|=t;var C=Kw(T,o,t);sw(T,C);break e;case 1:o=d;var D=T.type,R=T.stateNode;if((T.flags&64)==0&&(typeof D.getDerivedStateFromError=="function"||R!==null&&typeof R.componentDidCatch=="function"&&(_a===null||!_a.has(R)))){T.flags|=4096,t&=-t,T.lanes|=t;var M=Hw(T,o,t);sw(T,M);break e}}T=T.return}while(T!==null)}uA(r)}catch(q){t=q,Pr===r&&r!==null&&(Pr=r=r.return);continue}break}while(1)}function aA(){var e=Ov.current;return Ov.current=Dv,e===null?Dv:e}function sd(e,t){var r=ze;ze|=16;var n=aA();In===e&&yn===t||Nl(e,t);do try{yG();break}catch(a){iA(e,a)}while(1);if(Ib(),ze=r,Ov.current=n,Pr!==null)throw Error(pe(261));return In=null,yn=0,on}function yG(){for(;Pr!==null;)oA(Pr)}function bG(){for(;Pr!==null&&!eG();)oA(Pr)}function oA(e){var t=cA(e.alternate,e,Yu);e.memoizedProps=e.pendingProps,t===null?uA(e):Pr=t,iT.current=null}function uA(e){var t=e;do{var r=t.alternate;if(e=t.return,(t.flags&2048)==0){if(r=lG(r,t,Yu),r!==null){Pr=r;return}if(r=t,r.tag!==24&&r.tag!==23||r.memoizedState===null||(Yu&1073741824)!=0||(r.mode&4)==0){for(var n=0,a=r.child;a!==null;)n|=a.lanes|a.childLanes,a=a.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=C,C=l),l=L2(m,C),o=L2(m,s),l&&o&&(w.rangeCount!==1||w.anchorNode!==l.node||w.anchorOffset!==l.offset||w.focusNode!==o.node||w.focusOffset!==o.offset)&&(k=k.createRange(),k.setStart(l.node,l.offset),w.removeAllRanges(),C>s?(w.addRange(k),w.extend(o.node,o.offset)):(k.setEnd(o.node,o.offset),w.addRange(k)))))),k=[],w=m;w=w.parentNode;)w.nodeType===1&&k.push({element:w,left:w.scrollLeft,top:w.scrollTop});for(typeof m.focus=="function"&&m.focus(),m=0;mgn()-sT?Nl(e,0):oT|=r),ki(e,t)}function kG(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=El()===99?1:2:(uo===0&&(uo=Ol),t=fl(62914560&~uo),t===0&&(t=4194304))),r=ai(),e=xv(e,t),e!==null&&(Kh(e,t,r),ki(e,r))}var cA;cA=function(e,t,r){var n=t.lanes;if(e!==null)if(e.memoizedProps!==t.pendingProps||Qn.current)$i=!0;else if((r&n)!=0)$i=(e.flags&16384)!=0;else{switch($i=!1,t.tag){case 3:Fw(t),Bb();break;case 5:gw(t);break;case 1:Kn(t.type)&&uv(t);break;case 4:Pb(t,t.stateNode.containerInfo);break;case 10:n=t.memoizedProps.value;var a=t.type._context;lr(cv,a._currentValue),a._currentValue=n;break;case 13:if(t.memoizedState!==null)return(r&t.child.childLanes)!=0?jw(e,t,r):(lr(cr,cr.current&1),t=ao(e,t,r),t!==null?t.sibling:null);lr(cr,cr.current&1);break;case 19:if(n=(r&t.childLanes)!=0,(e.flags&64)!=0){if(n)return Vw(e,t,r);t.flags|=64}if(a=t.memoizedState,a!==null&&(a.rendering=null,a.tail=null,a.lastEffect=null),lr(cr,cr.current),n)break;return null;case 23:case 24:return t.lanes=0,Wb(e,t,r)}return ao(e,t,r)}else $i=!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,a=Tl(t,vn.current),Sl(t,r),a=Gb(null,t,n,e,a,r),t.flags|=1,typeof a=="object"&&a!==null&&typeof a.render=="function"&&a.$$typeof===void 0){if(t.tag=1,t.memoizedState=null,t.updateQueue=null,Kn(n)){var o=!0;uv(t)}else o=!1;t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,Fb(t);var s=n.getDerivedStateFromProps;typeof s=="function"&&pv(t,n,s,e),a.updater=hv,t.stateNode=a,a._reactInternals=t,jb(t,n,e,r),t=Jb(null,t,n,!0,o,r)}else t.tag=0,zn(null,t,a,r),t=t.child;return t;case 16:a=t.elementType;e:{switch(e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2),e=t.pendingProps,o=a._init,a=o(a._payload),t.type=a,o=t.tag=CG(a),e=Zi(a,e),o){case 0:t=Yb(null,t,a,e,r);break e;case 1:t=Rw(null,t,a,e,r);break e;case 11:t=Nw(null,t,a,e,r);break e;case 14:t=Lw(null,t,a,Zi(a.type,e),n,r);break e}throw Error(pe(306,a,""))}return t;case 0:return n=t.type,a=t.pendingProps,a=t.elementType===n?a:Zi(n,a),Yb(e,t,n,a,r);case 1:return n=t.type,a=t.pendingProps,a=t.elementType===n?a:Zi(n,a),Rw(e,t,n,a,r);case 3:if(Fw(t),n=t.updateQueue,e===null||n===null)throw Error(pe(282));if(n=t.pendingProps,a=t.memoizedState,a=a!==null?a.element:null,uw(e,t),Kf(t,n,null,r),n=t.memoizedState.element,n===a)Bb(),t=ao(e,t,r);else{if(a=t.stateNode,(o=a.hydrate)&&(Ho=gl(t.stateNode.containerInfo.firstChild),io=t,o=Ta=!0),o){if(e=a.mutableSourceEagerHydrationData,e!=null)for(a=0;a{"use strict";function hA(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__=="undefined"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(hA)}catch(e){console.error(e)}}hA(),vA.exports=pA()});var gA=U(xl=>{"use strict";Object.defineProperty(xl,"__esModule",{value:!0});xl.versionInfo=xl.version=void 0;var RG="15.5.0";xl.version=RG;var FG=Object.freeze({major:15,minor:5,patch:0,preReleaseTag:null});xl.versionInfo=FG});var Pv=U(DT=>{"use strict";Object.defineProperty(DT,"__esModule",{value:!0});DT.default=jG;function jG(e){return typeof(e==null?void 0:e.then)=="function"}});var Sa=U(kT=>{"use strict";Object.defineProperty(kT,"__esModule",{value:!0});kT.default=PG;function Mv(e){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Mv=function(r){return typeof r}:Mv=function(r){return r&&typeof Symbol=="function"&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},Mv(e)}function PG(e){return Mv(e)=="object"&&e!==null}});var Da=U($o=>{"use strict";Object.defineProperty($o,"__esModule",{value:!0});$o.SYMBOL_TO_STRING_TAG=$o.SYMBOL_ASYNC_ITERATOR=$o.SYMBOL_ITERATOR=void 0;var MG=typeof Symbol=="function"&&Symbol.iterator!=null?Symbol.iterator:"@@iterator";$o.SYMBOL_ITERATOR=MG;var qG=typeof Symbol=="function"&&Symbol.asyncIterator!=null?Symbol.asyncIterator:"@@asyncIterator";$o.SYMBOL_ASYNC_ITERATOR=qG;var BG=typeof Symbol=="function"&&Symbol.toStringTag!=null?Symbol.toStringTag:"@@toStringTag";$o.SYMBOL_TO_STRING_TAG=BG});var qv=U(OT=>{"use strict";Object.defineProperty(OT,"__esModule",{value:!0});OT.getLocation=VG;function VG(e,t){for(var r=/\r\n|[\n\r]/g,n=1,a=t+1,o;(o=r.exec(e.body))&&o.index{"use strict";Object.defineProperty(Vv,"__esModule",{value:!0});Vv.printLocation=GG;Vv.printSourceLocation=mA;var UG=qv();function GG(e){return mA(e.source,(0,UG.getLocation)(e.source,e.start))}function mA(e,t){var r=e.locationOffset.column-1,n=Bv(r)+e.body,a=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),b=v[a];if(b.length>120){for(var T=Math.floor(d/80),A=d%80,L=[],S=0;S{"use strict";function Uv(e){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Uv=function(r){return typeof r}:Uv=function(r){return r&&typeof Symbol=="function"&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},Uv(e)}Object.defineProperty(vd,"__esModule",{value:!0});vd.printError=DA;vd.GraphQLError=void 0;var KG=zG(Sa()),HG=Da(),bA=qv(),TA=CT();function zG(e){return e&&e.__esModule?e:{default:e}}function WG(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function EA(e,t){for(var r=0;r 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(AT,"__esModule",{value:!0});AT.syntaxError=tQ;var eQ=Be();function tQ(e,t,r){return new eQ.GraphQLError("Syntax Error: ".concat(r),void 0,e,[t])}});var Vt=U(Kv=>{"use strict";Object.defineProperty(Kv,"__esModule",{value:!0});Kv.Kind=void 0;var rQ=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"});Kv.Kind=rQ});var un=U(NT=>{"use strict";Object.defineProperty(NT,"__esModule",{value:!0});NT.default=nQ;function nQ(e,t){var r=Boolean(e);if(!r)throw new Error(t!=null?t:"Unexpected invariant triggered.")}});var LT=U(Hv=>{"use strict";Object.defineProperty(Hv,"__esModule",{value:!0});Hv.default=void 0;var iQ=typeof Symbol=="function"&&typeof Symbol.for=="function"?Symbol.for("nodejs.util.inspect.custom"):void 0,aQ=iQ;Hv.default=aQ});var zv=U(xT=>{"use strict";Object.defineProperty(xT,"__esModule",{value:!0});xT.default=uQ;var oQ=OA(un()),kA=OA(LT());function OA(e){return e&&e.__esModule?e:{default:e}}function uQ(e){var t=e.prototype.toJSON;typeof t=="function"||(0,oQ.default)(0),e.prototype.inspect=t,kA.default&&(e.prototype[kA.default]=t)}});var Il=U(Xu=>{"use strict";Object.defineProperty(Xu,"__esModule",{value:!0});Xu.isNode=lQ;Xu.Token=Xu.Location=void 0;var CA=sQ(zv());function sQ(e){return e&&e.__esModule?e:{default:e}}var wA=function(){function e(r,n,a){this.start=r.start,this.end=n.end,this.startToken=r,this.endToken=n,this.source=a}var t=e.prototype;return t.toJSON=function(){return{start:this.start,end:this.end}},e}();Xu.Location=wA;(0,CA.default)(wA);var AA=function(){function e(r,n,a,o,s,l,d){this.kind=r,this.start=n,this.end=a,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}();Xu.Token=AA;(0,CA.default)(AA);function lQ(e){return e!=null&&typeof e.kind=="string"}});var Rl=U(Wv=>{"use strict";Object.defineProperty(Wv,"__esModule",{value:!0});Wv.TokenKind=void 0;var cQ=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"});Wv.TokenKind=cQ});var Ot=U(IT=>{"use strict";Object.defineProperty(IT,"__esModule",{value:!0});IT.default=hQ;var fQ=dQ(LT());function dQ(e){return e&&e.__esModule?e:{default:e}}function Yv(e){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Yv=function(r){return typeof r}:Yv=function(r){return r&&typeof Symbol=="function"&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},Yv(e)}var pQ=10,NA=2;function hQ(e){return Jv(e,[])}function Jv(e,t){switch(Yv(e)){case"string":return JSON.stringify(e);case"function":return e.name?"[function ".concat(e.name,"]"):"[function]";case"object":return e===null?"null":vQ(e,t);default:return String(e)}}function vQ(e,t){if(t.indexOf(e)!==-1)return"[Circular]";var r=[].concat(t,[e]),n=yQ(e);if(n!==void 0){var a=n.call(e);if(a!==e)return typeof a=="string"?a:Jv(a,r)}else if(Array.isArray(e))return mQ(e,r);return gQ(e,r)}function gQ(e,t){var r=Object.keys(e);if(r.length===0)return"{}";if(t.length>NA)return"["+bQ(e)+"]";var n=r.map(function(a){var o=Jv(e[a],t);return a+": "+o});return"{ "+n.join(", ")+" }"}function mQ(e,t){if(e.length===0)return"[]";if(t.length>NA)return"[Array]";for(var r=Math.min(pQ,e.length),n=e.length-r,a=[],o=0;o1&&a.push("... ".concat(n," more items")),"["+a.join(", ")+"]"}function yQ(e){var t=e[String(fQ.default)];if(typeof t=="function")return t;if(typeof e.inspect=="function")return e.inspect}function bQ(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 wi=U(RT=>{"use strict";Object.defineProperty(RT,"__esModule",{value:!0});RT.default=TQ;function TQ(e,t){var r=Boolean(e);if(!r)throw new Error(t)}});var gd=U(Xv=>{"use strict";Object.defineProperty(Xv,"__esModule",{value:!0});Xv.default=void 0;var EQ=function(t,r){return t instanceof r};Xv.default=EQ});var Zv=U(md=>{"use strict";Object.defineProperty(md,"__esModule",{value:!0});md.isSource=OQ;md.Source=void 0;var _Q=Da(),SQ=jT(Ot()),FT=jT(wi()),DQ=jT(gd());function jT(e){return e&&e.__esModule?e:{default:e}}function LA(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,FT.default)(0,"Body must be a string. Received: ".concat((0,SQ.default)(t),".")),this.body=t,this.name=r,this.locationOffset=n,this.locationOffset.line>0||(0,FT.default)(0,"line in locationOffset is 1-indexed and must be positive."),this.locationOffset.column>0||(0,FT.default)(0,"column in locationOffset is 1-indexed and must be positive.")}return kQ(e,[{key:_Q.SYMBOL_TO_STRING_TAG,get:function(){return"Source"}}]),e}();md.Source=xA;function OQ(e){return(0,DQ.default)(e,xA)}});var Fl=U($v=>{"use strict";Object.defineProperty($v,"__esModule",{value:!0});$v.DirectiveLocation=void 0;var CQ=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"});$v.DirectiveLocation=CQ});var jl=U(yd=>{"use strict";Object.defineProperty(yd,"__esModule",{value:!0});yd.dedentBlockStringValue=wQ;yd.getBlockStringIndentation=RA;yd.printBlockString=AQ;function wQ(e){var t=e.split(/\r\n|[\n\r]/g),r=RA(e);if(r!==0)for(var n=1;na&&IA(t[o-1]);)--o;return t.slice(a,o).join(`
-`)}function IA(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,a=e[0]===" "||e[0]===" ",o=e[e.length-1]==='"',s=e[e.length-1]==="\\",l=!n||o||s||r,d="";return l&&!(n&&a)&&(d+=`
+`+(0,_1.printSourceLocation)(e.source,l)}return t}});var lg=G(f_=>{"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=U(bd=>{"use strict";Object.defineProperty(bd,"__esModule",{value:!0});bd.isPunctuatorTokenKind=xQ;bd.Lexer=void 0;var ka=Qv(),mr=Il(),tt=Rl(),NQ=jl(),LQ=function(){function e(r){var n=new mr.Token(tt.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!==tt.TokenKind.EOF)do{var a;n=(a=n.next)!==null&&a!==void 0?a:n.next=IQ(this,n)}while(n.kind===tt.TokenKind.COMMENT);return n},e}();bd.Lexer=LQ;function xQ(e){return e===tt.TokenKind.BANG||e===tt.TokenKind.DOLLAR||e===tt.TokenKind.AMP||e===tt.TokenKind.PAREN_L||e===tt.TokenKind.PAREN_R||e===tt.TokenKind.SPREAD||e===tt.TokenKind.COLON||e===tt.TokenKind.EQUALS||e===tt.TokenKind.AT||e===tt.TokenKind.BRACKET_L||e===tt.TokenKind.BRACKET_R||e===tt.TokenKind.BRACE_L||e===tt.TokenKind.PIPE||e===tt.TokenKind.BRACE_R}function Zu(e){return isNaN(e)?tt.TokenKind.EOF:e<127?JSON.stringify(String.fromCharCode(e)):'"\\u'.concat(("00"+e.toString(16).toUpperCase()).slice(-4),'"')}function IQ(e,t){for(var r=e.source,n=r.body,a=n.length,o=t.end;o31||s===9));return new mr.Token(tt.TokenKind.COMMENT,t,l,r,n,a,o.slice(t+1,l))}function jQ(e,t,r,n,a,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,ka.syntaxError)(e,d,"Invalid number, unexpected digit after 0: ".concat(Zu(l),"."))}else d=PT(e,d,l),l=s.charCodeAt(d);if(l===46&&(h=!0,l=s.charCodeAt(++d),d=PT(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=PT(e,d,l),l=s.charCodeAt(d)),l===46||VQ(l))throw(0,ka.syntaxError)(e,d,"Invalid number, expected digit but got: ".concat(Zu(l),"."));return new mr.Token(h?tt.TokenKind.FLOAT:tt.TokenKind.INT,t,d,n,a,o,s.slice(t,d))}function PT(e,t,r){var n=e.body,a=t,o=r;if(o>=48&&o<=57){do o=n.charCodeAt(++a);while(o>=48&&o<=57);return a}throw(0,ka.syntaxError)(e,a,"Invalid number, expected digit but got: ".concat(Zu(o),"."))}function PQ(e,t,r,n,a){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 BQ(e,t,r,n,a){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 mr.Token(tt.TokenKind.NAME,t,l,r,n,a,o.slice(t,l))}function VQ(e){return e===95||e>=65&&e<=90||e>=97&&e<=122}});var Pl=U($u=>{"use strict";Object.defineProperty($u,"__esModule",{value:!0});$u.parse=QQ;$u.parseValue=KQ;$u.parseType=HQ;$u.Parser=void 0;var MT=Qv(),Ke=Vt(),UQ=Il(),_e=Rl(),FA=Zv(),GQ=Fl(),jA=tg();function QQ(e,t){var r=new rg(e,t);return r.parseDocument()}function KQ(e,t){var r=new rg(e,t);r.expectToken(_e.TokenKind.SOF);var n=r.parseValueLiteral(!1);return r.expectToken(_e.TokenKind.EOF),n}function HQ(e,t){var r=new rg(e,t);r.expectToken(_e.TokenKind.SOF);var n=r.parseTypeReference();return r.expectToken(_e.TokenKind.EOF),n}var rg=function(){function e(r,n){var a=(0,FA.isSource)(r)?r:new FA.Source(r);this._lexer=new jA.Lexer(a),this._options=n}var t=e.prototype;return t.parseName=function(){var n=this.expectToken(_e.TokenKind.NAME);return{kind:Ke.Kind.NAME,value:n.value,loc:this.loc(n)}},t.parseDocument=function(){var n=this._lexer.token;return{kind:Ke.Kind.DOCUMENT,definitions:this.many(_e.TokenKind.SOF,this.parseDefinition,_e.TokenKind.EOF),loc:this.loc(n)}},t.parseDefinition=function(){if(this.peek(_e.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(_e.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(_e.TokenKind.BRACE_L))return{kind:Ke.Kind.OPERATION_DEFINITION,operation:"query",name:void 0,variableDefinitions:[],directives:[],selectionSet:this.parseSelectionSet(),loc:this.loc(n)};var a=this.parseOperationType(),o;return this.peek(_e.TokenKind.NAME)&&(o=this.parseName()),{kind:Ke.Kind.OPERATION_DEFINITION,operation:a,name:o,variableDefinitions:this.parseVariableDefinitions(),directives:this.parseDirectives(!1),selectionSet:this.parseSelectionSet(),loc:this.loc(n)}},t.parseOperationType=function(){var n=this.expectToken(_e.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(_e.TokenKind.PAREN_L,this.parseVariableDefinition,_e.TokenKind.PAREN_R)},t.parseVariableDefinition=function(){var n=this._lexer.token;return{kind:Ke.Kind.VARIABLE_DEFINITION,variable:this.parseVariable(),type:(this.expectToken(_e.TokenKind.COLON),this.parseTypeReference()),defaultValue:this.expectOptionalToken(_e.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(_e.TokenKind.DOLLAR),{kind:Ke.Kind.VARIABLE,name:this.parseName(),loc:this.loc(n)}},t.parseSelectionSet=function(){var n=this._lexer.token;return{kind:Ke.Kind.SELECTION_SET,selections:this.many(_e.TokenKind.BRACE_L,this.parseSelection,_e.TokenKind.BRACE_R),loc:this.loc(n)}},t.parseSelection=function(){return this.peek(_e.TokenKind.SPREAD)?this.parseFragment():this.parseField()},t.parseField=function(){var n=this._lexer.token,a=this.parseName(),o,s;return this.expectOptionalToken(_e.TokenKind.COLON)?(o=a,s=this.parseName()):s=a,{kind:Ke.Kind.FIELD,alias:o,name:s,arguments:this.parseArguments(!1),directives:this.parseDirectives(!1),selectionSet:this.peek(_e.TokenKind.BRACE_L)?this.parseSelectionSet():void 0,loc:this.loc(n)}},t.parseArguments=function(n){var a=n?this.parseConstArgument:this.parseArgument;return this.optionalMany(_e.TokenKind.PAREN_L,a,_e.TokenKind.PAREN_R)},t.parseArgument=function(){var n=this._lexer.token,a=this.parseName();return this.expectToken(_e.TokenKind.COLON),{kind:Ke.Kind.ARGUMENT,name:a,value:this.parseValueLiteral(!1),loc:this.loc(n)}},t.parseConstArgument=function(){var n=this._lexer.token;return{kind:Ke.Kind.ARGUMENT,name:this.parseName(),value:(this.expectToken(_e.TokenKind.COLON),this.parseValueLiteral(!0)),loc:this.loc(n)}},t.parseFragment=function(){var n=this._lexer.token;this.expectToken(_e.TokenKind.SPREAD);var a=this.expectOptionalKeyword("on");return!a&&this.peek(_e.TokenKind.NAME)?{kind:Ke.Kind.FRAGMENT_SPREAD,name:this.parseFragmentName(),directives:this.parseDirectives(!1),loc:this.loc(n)}:{kind:Ke.Kind.INLINE_FRAGMENT,typeCondition:a?this.parseNamedType():void 0,directives:this.parseDirectives(!1),selectionSet:this.parseSelectionSet(),loc:this.loc(n)}},t.parseFragmentDefinition=function(){var n,a=this._lexer.token;return this.expectKeyword("fragment"),((n=this._options)===null||n===void 0?void 0:n.experimentalFragmentVariables)===!0?{kind:Ke.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(a)}:{kind:Ke.Kind.FRAGMENT_DEFINITION,name:this.parseFragmentName(),typeCondition:(this.expectKeyword("on"),this.parseNamedType()),directives:this.parseDirectives(!1),selectionSet:this.parseSelectionSet(),loc:this.loc(a)}},t.parseFragmentName=function(){if(this._lexer.token.value==="on")throw this.unexpected();return this.parseName()},t.parseValueLiteral=function(n){var a=this._lexer.token;switch(a.kind){case _e.TokenKind.BRACKET_L:return this.parseList(n);case _e.TokenKind.BRACE_L:return this.parseObject(n);case _e.TokenKind.INT:return this._lexer.advance(),{kind:Ke.Kind.INT,value:a.value,loc:this.loc(a)};case _e.TokenKind.FLOAT:return this._lexer.advance(),{kind:Ke.Kind.FLOAT,value:a.value,loc:this.loc(a)};case _e.TokenKind.STRING:case _e.TokenKind.BLOCK_STRING:return this.parseStringLiteral();case _e.TokenKind.NAME:switch(this._lexer.advance(),a.value){case"true":return{kind:Ke.Kind.BOOLEAN,value:!0,loc:this.loc(a)};case"false":return{kind:Ke.Kind.BOOLEAN,value:!1,loc:this.loc(a)};case"null":return{kind:Ke.Kind.NULL,loc:this.loc(a)};default:return{kind:Ke.Kind.ENUM,value:a.value,loc:this.loc(a)}}case _e.TokenKind.DOLLAR:if(!n)return this.parseVariable();break}throw this.unexpected()},t.parseStringLiteral=function(){var n=this._lexer.token;return this._lexer.advance(),{kind:Ke.Kind.STRING,value:n.value,block:n.kind===_e.TokenKind.BLOCK_STRING,loc:this.loc(n)}},t.parseList=function(n){var a=this,o=this._lexer.token,s=function(){return a.parseValueLiteral(n)};return{kind:Ke.Kind.LIST,values:this.any(_e.TokenKind.BRACKET_L,s,_e.TokenKind.BRACKET_R),loc:this.loc(o)}},t.parseObject=function(n){var a=this,o=this._lexer.token,s=function(){return a.parseObjectField(n)};return{kind:Ke.Kind.OBJECT,fields:this.any(_e.TokenKind.BRACE_L,s,_e.TokenKind.BRACE_R),loc:this.loc(o)}},t.parseObjectField=function(n){var a=this._lexer.token,o=this.parseName();return this.expectToken(_e.TokenKind.COLON),{kind:Ke.Kind.OBJECT_FIELD,name:o,value:this.parseValueLiteral(n),loc:this.loc(a)}},t.parseDirectives=function(n){for(var a=[];this.peek(_e.TokenKind.AT);)a.push(this.parseDirective(n));return a},t.parseDirective=function(n){var a=this._lexer.token;return this.expectToken(_e.TokenKind.AT),{kind:Ke.Kind.DIRECTIVE,name:this.parseName(),arguments:this.parseArguments(n),loc:this.loc(a)}},t.parseTypeReference=function(){var n=this._lexer.token,a;return this.expectOptionalToken(_e.TokenKind.BRACKET_L)?(a=this.parseTypeReference(),this.expectToken(_e.TokenKind.BRACKET_R),a={kind:Ke.Kind.LIST_TYPE,type:a,loc:this.loc(n)}):a=this.parseNamedType(),this.expectOptionalToken(_e.TokenKind.BANG)?{kind:Ke.Kind.NON_NULL_TYPE,type:a,loc:this.loc(n)}:a},t.parseNamedType=function(){var n=this._lexer.token;return{kind:Ke.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===_e.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(_e.TokenKind.STRING)||this.peek(_e.TokenKind.BLOCK_STRING)},t.parseDescription=function(){if(this.peekDescription())return this.parseStringLiteral()},t.parseSchemaDefinition=function(){var n=this._lexer.token,a=this.parseDescription();this.expectKeyword("schema");var o=this.parseDirectives(!0),s=this.many(_e.TokenKind.BRACE_L,this.parseOperationTypeDefinition,_e.TokenKind.BRACE_R);return{kind:Ke.Kind.SCHEMA_DEFINITION,description:a,directives:o,operationTypes:s,loc:this.loc(n)}},t.parseOperationTypeDefinition=function(){var n=this._lexer.token,a=this.parseOperationType();this.expectToken(_e.TokenKind.COLON);var o=this.parseNamedType();return{kind:Ke.Kind.OPERATION_TYPE_DEFINITION,operation:a,type:o,loc:this.loc(n)}},t.parseScalarTypeDefinition=function(){var n=this._lexer.token,a=this.parseDescription();this.expectKeyword("scalar");var o=this.parseName(),s=this.parseDirectives(!0);return{kind:Ke.Kind.SCALAR_TYPE_DEFINITION,description:a,name:o,directives:s,loc:this.loc(n)}},t.parseObjectTypeDefinition=function(){var n=this._lexer.token,a=this.parseDescription();this.expectKeyword("type");var o=this.parseName(),s=this.parseImplementsInterfaces(),l=this.parseDirectives(!0),d=this.parseFieldsDefinition();return{kind:Ke.Kind.OBJECT_TYPE_DEFINITION,description:a,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 a=[];this.expectOptionalToken(_e.TokenKind.AMP);do a.push(this.parseNamedType());while(this.expectOptionalToken(_e.TokenKind.AMP)||this.peek(_e.TokenKind.NAME));return a}return this.delimitedMany(_e.TokenKind.AMP,this.parseNamedType)},t.parseFieldsDefinition=function(){var n;return((n=this._options)===null||n===void 0?void 0:n.allowLegacySDLEmptyFields)===!0&&this.peek(_e.TokenKind.BRACE_L)&&this._lexer.lookahead().kind===_e.TokenKind.BRACE_R?(this._lexer.advance(),this._lexer.advance(),[]):this.optionalMany(_e.TokenKind.BRACE_L,this.parseFieldDefinition,_e.TokenKind.BRACE_R)},t.parseFieldDefinition=function(){var n=this._lexer.token,a=this.parseDescription(),o=this.parseName(),s=this.parseArgumentDefs();this.expectToken(_e.TokenKind.COLON);var l=this.parseTypeReference(),d=this.parseDirectives(!0);return{kind:Ke.Kind.FIELD_DEFINITION,description:a,name:o,arguments:s,type:l,directives:d,loc:this.loc(n)}},t.parseArgumentDefs=function(){return this.optionalMany(_e.TokenKind.PAREN_L,this.parseInputValueDef,_e.TokenKind.PAREN_R)},t.parseInputValueDef=function(){var n=this._lexer.token,a=this.parseDescription(),o=this.parseName();this.expectToken(_e.TokenKind.COLON);var s=this.parseTypeReference(),l;this.expectOptionalToken(_e.TokenKind.EQUALS)&&(l=this.parseValueLiteral(!0));var d=this.parseDirectives(!0);return{kind:Ke.Kind.INPUT_VALUE_DEFINITION,description:a,name:o,type:s,defaultValue:l,directives:d,loc:this.loc(n)}},t.parseInterfaceTypeDefinition=function(){var n=this._lexer.token,a=this.parseDescription();this.expectKeyword("interface");var o=this.parseName(),s=this.parseImplementsInterfaces(),l=this.parseDirectives(!0),d=this.parseFieldsDefinition();return{kind:Ke.Kind.INTERFACE_TYPE_DEFINITION,description:a,name:o,interfaces:s,directives:l,fields:d,loc:this.loc(n)}},t.parseUnionTypeDefinition=function(){var n=this._lexer.token,a=this.parseDescription();this.expectKeyword("union");var o=this.parseName(),s=this.parseDirectives(!0),l=this.parseUnionMemberTypes();return{kind:Ke.Kind.UNION_TYPE_DEFINITION,description:a,name:o,directives:s,types:l,loc:this.loc(n)}},t.parseUnionMemberTypes=function(){return this.expectOptionalToken(_e.TokenKind.EQUALS)?this.delimitedMany(_e.TokenKind.PIPE,this.parseNamedType):[]},t.parseEnumTypeDefinition=function(){var n=this._lexer.token,a=this.parseDescription();this.expectKeyword("enum");var o=this.parseName(),s=this.parseDirectives(!0),l=this.parseEnumValuesDefinition();return{kind:Ke.Kind.ENUM_TYPE_DEFINITION,description:a,name:o,directives:s,values:l,loc:this.loc(n)}},t.parseEnumValuesDefinition=function(){return this.optionalMany(_e.TokenKind.BRACE_L,this.parseEnumValueDefinition,_e.TokenKind.BRACE_R)},t.parseEnumValueDefinition=function(){var n=this._lexer.token,a=this.parseDescription(),o=this.parseName(),s=this.parseDirectives(!0);return{kind:Ke.Kind.ENUM_VALUE_DEFINITION,description:a,name:o,directives:s,loc:this.loc(n)}},t.parseInputObjectTypeDefinition=function(){var n=this._lexer.token,a=this.parseDescription();this.expectKeyword("input");var o=this.parseName(),s=this.parseDirectives(!0),l=this.parseInputFieldsDefinition();return{kind:Ke.Kind.INPUT_OBJECT_TYPE_DEFINITION,description:a,name:o,directives:s,fields:l,loc:this.loc(n)}},t.parseInputFieldsDefinition=function(){return this.optionalMany(_e.TokenKind.BRACE_L,this.parseInputValueDef,_e.TokenKind.BRACE_R)},t.parseTypeSystemExtension=function(){var n=this._lexer.lookahead();if(n.kind===_e.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 a=this.parseDirectives(!0),o=this.optionalMany(_e.TokenKind.BRACE_L,this.parseOperationTypeDefinition,_e.TokenKind.BRACE_R);if(a.length===0&&o.length===0)throw this.unexpected();return{kind:Ke.Kind.SCHEMA_EXTENSION,directives:a,operationTypes:o,loc:this.loc(n)}},t.parseScalarTypeExtension=function(){var n=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("scalar");var a=this.parseName(),o=this.parseDirectives(!0);if(o.length===0)throw this.unexpected();return{kind:Ke.Kind.SCALAR_TYPE_EXTENSION,name:a,directives:o,loc:this.loc(n)}},t.parseObjectTypeExtension=function(){var n=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("type");var a=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:Ke.Kind.OBJECT_TYPE_EXTENSION,name:a,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 a=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:Ke.Kind.INTERFACE_TYPE_EXTENSION,name:a,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 a=this.parseName(),o=this.parseDirectives(!0),s=this.parseUnionMemberTypes();if(o.length===0&&s.length===0)throw this.unexpected();return{kind:Ke.Kind.UNION_TYPE_EXTENSION,name:a,directives:o,types:s,loc:this.loc(n)}},t.parseEnumTypeExtension=function(){var n=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("enum");var a=this.parseName(),o=this.parseDirectives(!0),s=this.parseEnumValuesDefinition();if(o.length===0&&s.length===0)throw this.unexpected();return{kind:Ke.Kind.ENUM_TYPE_EXTENSION,name:a,directives:o,values:s,loc:this.loc(n)}},t.parseInputObjectTypeExtension=function(){var n=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("input");var a=this.parseName(),o=this.parseDirectives(!0),s=this.parseInputFieldsDefinition();if(o.length===0&&s.length===0)throw this.unexpected();return{kind:Ke.Kind.INPUT_OBJECT_TYPE_EXTENSION,name:a,directives:o,fields:s,loc:this.loc(n)}},t.parseDirectiveDefinition=function(){var n=this._lexer.token,a=this.parseDescription();this.expectKeyword("directive"),this.expectToken(_e.TokenKind.AT);var o=this.parseName(),s=this.parseArgumentDefs(),l=this.expectOptionalKeyword("repeatable");this.expectKeyword("on");var d=this.parseDirectiveLocations();return{kind:Ke.Kind.DIRECTIVE_DEFINITION,description:a,name:o,arguments:s,repeatable:l,locations:d,loc:this.loc(n)}},t.parseDirectiveLocations=function(){return this.delimitedMany(_e.TokenKind.PIPE,this.parseDirectiveLocation)},t.parseDirectiveLocation=function(){var n=this._lexer.token,a=this.parseName();if(GQ.DirectiveLocation[a.value]!==void 0)return a;throw this.unexpected(n)},t.loc=function(n){var a;if(((a=this._options)===null||a===void 0?void 0:a.noLocation)!==!0)return new UQ.Location(n,this._lexer.lastToken,this._lexer.source)},t.peek=function(n){return this._lexer.token.kind===n},t.expectToken=function(n){var a=this._lexer.token;if(a.kind===n)return this._lexer.advance(),a;throw(0,MT.syntaxError)(this._lexer.source,a.start,"Expected ".concat(PA(n),", found ").concat(qT(a),"."))},t.expectOptionalToken=function(n){var a=this._lexer.token;if(a.kind===n)return this._lexer.advance(),a},t.expectKeyword=function(n){var a=this._lexer.token;if(a.kind===_e.TokenKind.NAME&&a.value===n)this._lexer.advance();else throw(0,MT.syntaxError)(this._lexer.source,a.start,'Expected "'.concat(n,'", found ').concat(qT(a),"."))},t.expectOptionalKeyword=function(n){var a=this._lexer.token;return a.kind===_e.TokenKind.NAME&&a.value===n?(this._lexer.advance(),!0):!1},t.unexpected=function(n){var a=n!=null?n:this._lexer.token;return(0,MT.syntaxError)(this._lexer.source,a.start,"Unexpected ".concat(qT(a),"."))},t.any=function(n,a,o){this.expectToken(n);for(var s=[];!this.expectOptionalToken(o);)s.push(a.call(this));return s},t.optionalMany=function(n,a,o){if(this.expectOptionalToken(n)){var s=[];do s.push(a.call(this));while(!this.expectOptionalToken(o));return s}return[]},t.many=function(n,a,o){this.expectToken(n);var s=[];do s.push(a.call(this));while(!this.expectOptionalToken(o));return s},t.delimitedMany=function(n,a){this.expectOptionalToken(n);var o=[];do o.push(a.call(this));while(this.expectOptionalToken(n));return o},e}();$u.Parser=rg;function qT(e){var t=e.value;return PA(e.kind)+(t!=null?' "'.concat(t,'"'):"")}function PA(e){return(0,jA.isPunctuatorTokenKind)(e)?'"'.concat(e,'"'):e}});var eu=U(so=>{"use strict";Object.defineProperty(so,"__esModule",{value:!0});so.visit=YQ;so.visitInParallel=JQ;so.getVisitFn=ng;so.BREAK=so.QueryDocumentKeys=void 0;var zQ=WQ(Ot()),MA=Il();function WQ(e){return e&&e.__esModule?e:{default:e}}var qA={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"]};so.QueryDocumentKeys=qA;var Ml=Object.freeze({});so.BREAK=Ml;function YQ(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:qA,n=void 0,a=Array.isArray(e),o=[e],s=-1,l=[],d=void 0,h=void 0,v=void 0,b=[],T=[],A=e;do{s++;var L=s===o.length,S=L&&l.length!==0;if(L){if(h=T.length===0?void 0:b[b.length-1],d=v,v=T.pop(),S){if(a)d=d.slice();else{for(var y={},_=0,m=Object.keys(d);_{"use strict";Object.defineProperty(ig,"__esModule",{value:!0});ig.default=void 0;var XQ=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(ag,"__esModule",{value:!0});ag.default=void 0;var $Q=Object.values||function(e){return Object.keys(e).map(function(t){return e[t]})},e5=$Q;ag.default=e5});var Td=U(BT=>{"use strict";Object.defineProperty(BT,"__esModule",{value:!0});BT.locatedError=i5;var t5=n5(Ot()),r5=Be();function n5(e){return e&&e.__esModule?e:{default:e}}function i5(e,t,r){var n,a=e instanceof Error?e:new Error("Unexpected error value: "+(0,t5.default)(e));return Array.isArray(a.path)?a:new r5.GraphQLError(a.message,(n=a.nodes)!==null&&n!==void 0?n:t,a.source,a.positions,r,a)}});var VT=U(og=>{"use strict";Object.defineProperty(og,"__esModule",{value:!0});og.assertValidName=s5;og.isValidNameError=VA;var a5=o5(wi()),BA=Be();function o5(e){return e&&e.__esModule?e:{default:e}}var u5=/^[_a-zA-Z][_a-zA-Z0-9]*$/;function s5(e){var t=VA(e);if(t)throw t;return e}function VA(e){if(typeof e=="string"||(0,a5.default)(0,"Expected name to be a string."),e.length>1&&e[0]==="_"&&e[1]==="_")return new BA.GraphQLError('Name "'.concat(e,'" must not begin with "__", which is reserved by GraphQL introspection.'));if(!u5.test(e))return new BA.GraphQLError('Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "'.concat(e,'" does not.'))}});var Bl=U(ug=>{"use strict";Object.defineProperty(ug,"__esModule",{value:!0});ug.default=void 0;var l5=Object.entries||function(e){return Object.keys(e).map(function(t){return[t,e[t]]})},c5=l5;ug.default=c5});var tu=U(UT=>{"use strict";Object.defineProperty(UT,"__esModule",{value:!0});UT.default=f5;function f5(e,t){return e.reduce(function(r,n){return r[t(n)]=n,r},Object.create(null))}});var QT=U(GT=>{"use strict";Object.defineProperty(GT,"__esModule",{value:!0});GT.default=h5;var d5=p5(Bl());function p5(e){return e&&e.__esModule?e:{default:e}}function h5(e,t){for(var r=Object.create(null),n=0,a=(0,d5.default)(e);n{"use strict";Object.defineProperty(KT,"__esModule",{value:!0});KT.default=m5;var v5=g5(Bl());function g5(e){return e&&e.__esModule?e:{default:e}}function m5(e){if(Object.getPrototypeOf(e)===null)return e;for(var t=Object.create(null),r=0,n=(0,v5.default)(e);r{"use strict";Object.defineProperty(HT,"__esModule",{value:!0});HT.default=y5;function y5(e,t,r){return e.reduce(function(n,a){return n[t(a)]=r(a),n},Object.create(null))}});var ru=U(zT=>{"use strict";Object.defineProperty(zT,"__esModule",{value:!0});zT.default=T5;var b5=5;function T5(e,t){var r=typeof e=="string"?[e,t]:[void 0,e],n=r[0],a=r[1],o=" Did you mean ";n&&(o+=n+" ");var s=a.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,b5),d=l.pop();return o+l.join(", ")+", or "+d+"?"}});var UA=U(WT=>{"use strict";Object.defineProperty(WT,"__esModule",{value:!0});WT.default=E5;function E5(e){return e}});var _d=U(JT=>{"use strict";Object.defineProperty(JT,"__esModule",{value:!0});JT.default=_5;function _5(e,t){for(var r=0,n=0;r0);var l=0;do++n,l=l*10+o-YT,o=t.charCodeAt(n);while(lg(o)&&l>0);if(sl)return 1}else{if(ao)return 1;++r,++n}}return e.length-t.length}var YT=48,S5=57;function lg(e){return!isNaN(e)&&YT<=e&&e<=S5}});var nu=U(XT=>{"use strict";Object.defineProperty(XT,"__esModule",{value:!0});XT.default=O5;var D5=k5(_d());function k5(e){return e&&e.__esModule?e:{default:e}}function O5(e,t){for(var r=Object.create(null),n=new C5(e),a=Math.floor(e.length*.4)+1,o=0;oa)){for(var b=this._rows,T=0;T<=v;T++)b[0][T]=T;for(var A=1;A<=h;A++){for(var L=b[(A-1)%3],S=b[A%3],y=S[0]=A,_=1;_<=v;_++){var m=s[A-1]===l[_-1]?0:1,k=Math.min(L[_]+1,S[_-1]+1,L[_-1]+m);if(A>1&&_>1&&s[A-1]===l[_-2]&&s[A-2]===l[_-1]){var w=b[(A-2)%3][_-2];k=Math.min(k,w+1)}ka)return}var C=b[h%3][v];return C<=a?C:void 0}},e}();function GA(e){for(var t=e.length,r=new Array(t),n=0;n{"use strict";Object.defineProperty(ZT,"__esModule",{value:!0});ZT.print=N5;var w5=eu(),A5=jl();function N5(e){return(0,w5.visit)(e,{leave:x5})}var L5=80,x5={Name:function(t){return t.value},Variable:function(t){return"$"+t.name},Document:function(t){return Le(t.definitions,`
+`),'"""'+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,a=or("(",Le(t.variableDefinitions,", "),")"),o=Le(t.directives," "),s=t.selectionSet;return!n&&!o&&!a&&r==="query"?s:Le([r,Le([n,a]),o,s]," ")},VariableDefinition:function(t){var r=t.variable,n=t.type,a=t.defaultValue,o=t.directives;return r+": "+n+or(" = ",a)+or(" ",Le(o," "))},SelectionSet:function(t){var r=t.selections;return ta(r)},Field:function(t){var r=t.alias,n=t.name,a=t.arguments,o=t.directives,s=t.selectionSet,l=or("",r,": ")+n,d=l+or("(",Le(a,", "),")");return d.length>L5&&(d=l+or(`(
-`,cg(Le(a,`
+`},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,`
`)),`
-)`)),Le([d,Le(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+or(" ",Le(n," "))},InlineFragment:function(t){var r=t.typeCondition,n=t.directives,a=t.selectionSet;return Le(["...",or("on ",r),Le(n," "),a]," ")},FragmentDefinition:function(t){var r=t.name,n=t.typeCondition,a=t.variableDefinitions,o=t.directives,s=t.selectionSet;return"fragment ".concat(r).concat(or("(",Le(a,", "),")")," ")+"on ".concat(n," ").concat(or("",Le(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,a=t.block;return a?(0,A5.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"["+Le(r,", ")+"]"},ObjectValue:function(t){var r=t.fields;return"{"+Le(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+or("(",Le(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:ea(function(e){var t=e.directives,r=e.operationTypes;return Le(["schema",Le(t," "),ta(r)]," ")}),OperationTypeDefinition:function(t){var r=t.operation,n=t.type;return r+": "+n},ScalarTypeDefinition:ea(function(e){var t=e.name,r=e.directives;return Le(["scalar",t,Le(r," ")]," ")}),ObjectTypeDefinition:ea(function(e){var t=e.name,r=e.interfaces,n=e.directives,a=e.fields;return Le(["type",t,or("implements ",Le(r," & ")),Le(n," "),ta(a)]," ")}),FieldDefinition:ea(function(e){var t=e.name,r=e.arguments,n=e.type,a=e.directives;return t+(QA(r)?or(`(
-`,cg(Le(r,`
+)`)),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,`
`)),`
-)`):or("(",Le(r,", "),")"))+": "+n+or(" ",Le(a," "))}),InputValueDefinition:ea(function(e){var t=e.name,r=e.type,n=e.defaultValue,a=e.directives;return Le([t+": "+r,or("= ",n),Le(a," ")]," ")}),InterfaceTypeDefinition:ea(function(e){var t=e.name,r=e.interfaces,n=e.directives,a=e.fields;return Le(["interface",t,or("implements ",Le(r," & ")),Le(n," "),ta(a)]," ")}),UnionTypeDefinition:ea(function(e){var t=e.name,r=e.directives,n=e.types;return Le(["union",t,Le(r," "),n&&n.length!==0?"= "+Le(n," | "):""]," ")}),EnumTypeDefinition:ea(function(e){var t=e.name,r=e.directives,n=e.values;return Le(["enum",t,Le(r," "),ta(n)]," ")}),EnumValueDefinition:ea(function(e){var t=e.name,r=e.directives;return Le([t,Le(r," ")]," ")}),InputObjectTypeDefinition:ea(function(e){var t=e.name,r=e.directives,n=e.fields;return Le(["input",t,Le(r," "),ta(n)]," ")}),DirectiveDefinition:ea(function(e){var t=e.name,r=e.arguments,n=e.repeatable,a=e.locations;return"directive @"+t+(QA(r)?or(`(
-`,cg(Le(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,`
`)),`
-)`):or("(",Le(r,", "),")"))+(n?" repeatable":"")+" on "+Le(a," | ")}),SchemaExtension:function(t){var r=t.directives,n=t.operationTypes;return Le(["extend schema",Le(r," "),ta(n)]," ")},ScalarTypeExtension:function(t){var r=t.name,n=t.directives;return Le(["extend scalar",r,Le(n," ")]," ")},ObjectTypeExtension:function(t){var r=t.name,n=t.interfaces,a=t.directives,o=t.fields;return Le(["extend type",r,or("implements ",Le(n," & ")),Le(a," "),ta(o)]," ")},InterfaceTypeExtension:function(t){var r=t.name,n=t.interfaces,a=t.directives,o=t.fields;return Le(["extend interface",r,or("implements ",Le(n," & ")),Le(a," "),ta(o)]," ")},UnionTypeExtension:function(t){var r=t.name,n=t.directives,a=t.types;return Le(["extend union",r,Le(n," "),a&&a.length!==0?"= "+Le(a," | "):""]," ")},EnumTypeExtension:function(t){var r=t.name,n=t.directives,a=t.values;return Le(["extend enum",r,Le(n," "),ta(a)]," ")},InputObjectTypeExtension:function(t){var r=t.name,n=t.directives,a=t.fields;return Le(["extend input",r,Le(n," "),ta(a)]," ")}};function ea(e){return function(t){return Le([t.description,e(t)],`
-`)}}function Le(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 ta(e){return or(`{
-`,cg(Le(e,`
+)`):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 or(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:"";return t!=null&&t!==""?e+t+r:""}function cg(e){return or(" ",e.replace(/\n/g,`
- `))}function I5(e){return e.indexOf(`
-`)!==-1}function QA(e){return e!=null&&e.some(I5)}});var rE=U(tE=>{"use strict";Object.defineProperty(tE,"__esModule",{value:!0});tE.valueFromASTUntyped=eE;var R5=$T(Ot()),F5=$T(un()),j5=$T(Ed()),lo=Vt();function $T(e){return e&&e.__esModule?e:{default:e}}function eE(e,t){switch(e.kind){case lo.Kind.NULL:return null;case lo.Kind.INT:return parseInt(e.value,10);case lo.Kind.FLOAT:return parseFloat(e.value);case lo.Kind.STRING:case lo.Kind.ENUM:case lo.Kind.BOOLEAN:return e.value;case lo.Kind.LIST:return e.values.map(function(r){return eE(r,t)});case lo.Kind.OBJECT:return(0,j5.default)(e.fields,function(r){return r.name.value},function(r){return eE(r.value,t)});case lo.Kind.VARIABLE:return t==null?void 0:t[e.name.value]}(0,F5.default)(0,"Unexpected value node: "+(0,R5.default)(e))}});var lt=U(je=>{"use strict";Object.defineProperty(je,"__esModule",{value:!0});je.isType=nE;je.assertType=JA;je.isScalarType=es;je.assertScalarType=G5;je.isObjectType=Ul;je.assertObjectType=Q5;je.isInterfaceType=ts;je.assertInterfaceType=K5;je.isUnionType=rs;je.assertUnionType=H5;je.isEnumType=ns;je.assertEnumType=z5;je.isInputObjectType=Dd;je.assertInputObjectType=W5;je.isListType=dg;je.assertListType=Y5;je.isNonNullType=uu;je.assertNonNullType=J5;je.isInputType=iE;je.assertInputType=X5;je.isOutputType=aE;je.assertOutputType=Z5;je.isLeafType=XA;je.assertLeafType=$5;je.isCompositeType=ZA;je.assertCompositeType=e9;je.isAbstractType=$A;je.assertAbstractType=t9;je.GraphQLList=su;je.GraphQLNonNull=lu;je.isWrappingType=kd;je.assertWrappingType=r9;je.isNullableType=eN;je.assertNullableType=tN;je.getNullableType=n9;je.isNamedType=rN;je.assertNamedType=i9;je.getNamedType=a9;je.argsToArgsConfig=oN;je.isRequiredArgument=o9;je.isRequiredInputField=c9;je.GraphQLInputObjectType=je.GraphQLEnumType=je.GraphQLUnionType=je.GraphQLInterfaceType=je.GraphQLObjectType=je.GraphQLScalarType=void 0;var KA=ui(Bl()),iu=Da(),Xt=ui(Ot()),P5=ui(tu()),fg=ui(QT()),Oa=ui(sg()),tr=ui(wi()),HA=ui(Ed()),au=ui(gd()),M5=ui(ru()),q5=ui(Sa()),zA=ui(UA()),ou=ui(zv()),B5=ui(nu()),Sd=Be(),V5=Vt(),WA=Wn(),U5=rE();function ui(e){return e&&e.__esModule?e:{default:e}}function YA(e,t){for(var r=0;r0?e:void 0}var oE=function(){function e(r){var n,a,o,s=(n=r.parseValue)!==null&&n!==void 0?n:zA.default;this.name=r.name,this.description=r.description,this.specifiedByUrl=r.specifiedByUrl,this.serialize=(a=r.serialize)!==null&&a!==void 0?a:zA.default,this.parseValue=s,this.parseLiteral=(o=r.parseLiteral)!==null&&o!==void 0?o:function(l,d){return s((0,U5.valueFromASTUntyped)(l,d))},this.extensions=r.extensions&&(0,Oa.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=Gl(r.extensionASTNodes),typeof r.name=="string"||(0,tr.default)(0,"Must provide name."),r.specifiedByUrl==null||typeof r.specifiedByUrl=="string"||(0,tr.default)(0,"".concat(this.name,' must provide "specifiedByUrl" as a string, ')+"but got: ".concat((0,Xt.default)(r.specifiedByUrl),".")),r.serialize==null||typeof r.serialize=="function"||(0,tr.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,tr.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()},Vl(e,[{key:iu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLScalarType"}}]),e}();je.GraphQLScalarType=oE;(0,ou.default)(oE);var uE=function(){function e(r){this.name=r.name,this.description=r.description,this.isTypeOf=r.isTypeOf,this.extensions=r.extensions&&(0,Oa.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=Gl(r.extensionASTNodes),this._fields=iN.bind(void 0,r),this._interfaces=nN.bind(void 0,r),typeof r.name=="string"||(0,tr.default)(0,"Must provide name."),r.isTypeOf==null||typeof r.isTypeOf=="function"||(0,tr.default)(0,"".concat(this.name,' must provide "isTypeOf" as a function, ')+"but got: ".concat((0,Xt.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:aN(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()},Vl(e,[{key:iu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLObjectType"}}]),e}();je.GraphQLObjectType=uE;(0,ou.default)(uE);function nN(e){var t,r=(t=pg(e.interfaces))!==null&&t!==void 0?t:[];return Array.isArray(r)||(0,tr.default)(0,"".concat(e.name," interfaces must be an Array or a function which returns an Array.")),r}function iN(e){var t=pg(e.fields);return Ql(t)||(0,tr.default)(0,"".concat(e.name," fields must be an object with field names as keys or a function which returns such an object.")),(0,fg.default)(t,function(r,n){var a;Ql(r)||(0,tr.default)(0,"".concat(e.name,".").concat(n," field config must be an object.")),!("isDeprecated"in r)||(0,tr.default)(0,"".concat(e.name,".").concat(n,' should provide "deprecationReason" instead of "isDeprecated".')),r.resolve==null||typeof r.resolve=="function"||(0,tr.default)(0,"".concat(e.name,".").concat(n," field resolver must be a function if ")+"provided, but got: ".concat((0,Xt.default)(r.resolve),"."));var o=(a=r.args)!==null&&a!==void 0?a:{};Ql(o)||(0,tr.default)(0,"".concat(e.name,".").concat(n," args must be an object with argument names as keys."));var s=(0,KA.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,Oa.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,Oa.default)(r.extensions),astNode:r.astNode}})}function Ql(e){return(0,q5.default)(e)&&!Array.isArray(e)}function aN(e){return(0,fg.default)(e,function(t){return{description:t.description,type:t.type,args:oN(t.args),resolve:t.resolve,subscribe:t.subscribe,deprecationReason:t.deprecationReason,extensions:t.extensions,astNode:t.astNode}})}function oN(e){return(0,HA.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 o9(e){return uu(e.type)&&e.defaultValue===void 0}var sE=function(){function e(r){this.name=r.name,this.description=r.description,this.resolveType=r.resolveType,this.extensions=r.extensions&&(0,Oa.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=Gl(r.extensionASTNodes),this._fields=iN.bind(void 0,r),this._interfaces=nN.bind(void 0,r),typeof r.name=="string"||(0,tr.default)(0,"Must provide name."),r.resolveType==null||typeof r.resolveType=="function"||(0,tr.default)(0,"".concat(this.name,' must provide "resolveType" as a function, ')+"but got: ".concat((0,Xt.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:aN(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()},Vl(e,[{key:iu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLInterfaceType"}}]),e}();je.GraphQLInterfaceType=sE;(0,ou.default)(sE);var lE=function(){function e(r){this.name=r.name,this.description=r.description,this.resolveType=r.resolveType,this.extensions=r.extensions&&(0,Oa.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=Gl(r.extensionASTNodes),this._types=u9.bind(void 0,r),typeof r.name=="string"||(0,tr.default)(0,"Must provide name."),r.resolveType==null||typeof r.resolveType=="function"||(0,tr.default)(0,"".concat(this.name,' must provide "resolveType" as a function, ')+"but got: ".concat((0,Xt.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()},Vl(e,[{key:iu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLUnionType"}}]),e}();je.GraphQLUnionType=lE;(0,ou.default)(lE);function u9(e){var t=pg(e.types);return Array.isArray(t)||(0,tr.default)(0,"Must provide Array of types or a function which returns such an array for Union ".concat(e.name,".")),t}var cE=function(){function e(r){this.name=r.name,this.description=r.description,this.extensions=r.extensions&&(0,Oa.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=Gl(r.extensionASTNodes),this._values=s9(this.name,r.values),this._valueLookup=new Map(this._values.map(function(n){return[n.value,n]})),this._nameLookup=(0,P5.default)(this._values,function(n){return n.name}),typeof r.name=="string"||(0,tr.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 a=this._valueLookup.get(n);if(a===void 0)throw new Sd.GraphQLError('Enum "'.concat(this.name,'" cannot represent value: ').concat((0,Xt.default)(n)));return a.name},t.parseValue=function(n){if(typeof n!="string"){var a=(0,Xt.default)(n);throw new Sd.GraphQLError('Enum "'.concat(this.name,'" cannot represent non-string value: ').concat(a,".")+hg(this,a))}var o=this.getValue(n);if(o==null)throw new Sd.GraphQLError('Value "'.concat(n,'" does not exist in "').concat(this.name,'" enum.')+hg(this,n));return o.value},t.parseLiteral=function(n,a){if(n.kind!==V5.Kind.ENUM){var o=(0,WA.print)(n);throw new Sd.GraphQLError('Enum "'.concat(this.name,'" cannot represent non-enum value: ').concat(o,".")+hg(this,o),n)}var s=this.getValue(n.value);if(s==null){var l=(0,WA.print)(n);throw new Sd.GraphQLError('Value "'.concat(l,'" does not exist in "').concat(this.name,'" enum.')+hg(this,l),n)}return s.value},t.toConfig=function(){var n,a=(0,HA.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:a,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()},Vl(e,[{key:iu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLEnumType"}}]),e}();je.GraphQLEnumType=cE;(0,ou.default)(cE);function hg(e,t){var r=e.getValues().map(function(a){return a.name}),n=(0,B5.default)(t,r);return(0,M5.default)("the enum value",n)}function s9(e,t){return Ql(t)||(0,tr.default)(0,"".concat(e," values must be an object with value names as keys.")),(0,KA.default)(t).map(function(r){var n=r[0],a=r[1];return Ql(a)||(0,tr.default)(0,"".concat(e,".").concat(n,' must refer to an object with a "value" key ')+"representing an internal value but got: ".concat((0,Xt.default)(a),".")),!("isDeprecated"in a)||(0,tr.default)(0,"".concat(e,".").concat(n,' should provide "deprecationReason" instead of "isDeprecated".')),{name:n,description:a.description,value:a.value!==void 0?a.value:n,isDeprecated:a.deprecationReason!=null,deprecationReason:a.deprecationReason,extensions:a.extensions&&(0,Oa.default)(a.extensions),astNode:a.astNode}})}var fE=function(){function e(r){this.name=r.name,this.description=r.description,this.extensions=r.extensions&&(0,Oa.default)(r.extensions),this.astNode=r.astNode,this.extensionASTNodes=Gl(r.extensionASTNodes),this._fields=l9.bind(void 0,r),typeof r.name=="string"||(0,tr.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,a=(0,fg.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:a,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()},Vl(e,[{key:iu.SYMBOL_TO_STRING_TAG,get:function(){return"GraphQLInputObjectType"}}]),e}();je.GraphQLInputObjectType=fE;(0,ou.default)(fE);function l9(e){var t=pg(e.fields);return Ql(t)||(0,tr.default)(0,"".concat(e.name," fields must be an object with field names as keys or a function which returns such an object.")),(0,fg.default)(t,function(r,n){return!("resolve"in r)||(0,tr.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,Oa.default)(r.extensions),astNode:r.astNode}})}function c9(e){return uu(e.type)&&e.defaultValue===void 0}});var Cd=U(Od=>{"use strict";Object.defineProperty(Od,"__esModule",{value:!0});Od.isEqualType=dE;Od.isTypeSubTypeOf=vg;Od.doTypesOverlap=f9;var bn=lt();function dE(e,t){return e===t?!0:(0,bn.isNonNullType)(e)&&(0,bn.isNonNullType)(t)||(0,bn.isListType)(e)&&(0,bn.isListType)(t)?dE(e.ofType,t.ofType):!1}function vg(e,t,r){return t===r?!0:(0,bn.isNonNullType)(r)?(0,bn.isNonNullType)(t)?vg(e,t.ofType,r.ofType):!1:(0,bn.isNonNullType)(t)?vg(e,t.ofType,r):(0,bn.isListType)(r)?(0,bn.isListType)(t)?vg(e,t.ofType,r.ofType):!1:(0,bn.isListType)(t)?!1:(0,bn.isAbstractType)(r)&&((0,bn.isInterfaceType)(t)||(0,bn.isObjectType)(t))&&e.isSubType(r,t)}function f9(e,t,r){return t===r?!0:(0,bn.isAbstractType)(t)?(0,bn.isAbstractType)(r)?e.getPossibleTypes(t).some(function(n){return e.isSubType(r,n)}):e.isSubType(t,r):(0,bn.isAbstractType)(r)?e.isSubType(r,t):!1}});var pE=U(gg=>{"use strict";Object.defineProperty(gg,"__esModule",{value:!0});gg.default=void 0;var d9=Da(),p9=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[d9.SYMBOL_ITERATOR];if(typeof n=="function"){for(var a=n.call(e),o=[],s,l=0;!(s=a.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(mg,"__esModule",{value:!0});mg.default=void 0;var v9=Number.isFinite||function(e){return typeof e=="number"&&isFinite(e)},g9=v9;mg.default=g9});var bg=U(vE=>{"use strict";Object.defineProperty(vE,"__esModule",{value:!0});vE.default=y9;var m9=Da();function yg(e){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?yg=function(r){return typeof r}:yg=function(r){return r&&typeof Symbol=="function"&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},yg(e)}function y9(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:function(v){return v};if(e==null||yg(e)!=="object")return null;if(Array.isArray(e))return e.map(t);var r=e[m9.SYMBOL_ITERATOR];if(typeof r=="function"){for(var n=r.call(e),a=[],o,s=0;!(o=n.next()).done;++s)a.push(t(o.value,s));return a}var l=e.length;if(typeof l=="number"&&l>=0&&l%1==0){for(var d=[],h=0;h{"use strict";Object.defineProperty(Tg,"__esModule",{value:!0});Tg.default=void 0;var b9=Number.isInteger||function(e){return typeof e=="number"&&isFinite(e)&&Math.floor(e)===e},T9=b9;Tg.default=T9});var Ca=U(Rn=>{"use strict";Object.defineProperty(Rn,"__esModule",{value:!0});Rn.isSpecifiedScalarType=L9;Rn.specifiedScalarTypes=Rn.GraphQLID=Rn.GraphQLBoolean=Rn.GraphQLString=Rn.GraphQLFloat=Rn.GraphQLInt=void 0;var Eg=Sg(hE()),_g=Sg(uN()),ra=Sg(Ot()),sN=Sg(Sa()),is=Vt(),wd=Wn(),Jr=Be(),Ad=lt();function Sg(e){return e&&e.__esModule?e:{default:e}}var gE=2147483647,mE=-2147483648;function E9(e){var t=Nd(e);if(typeof t=="boolean")return t?1:0;var r=t;if(typeof t=="string"&&t!==""&&(r=Number(t)),!(0,_g.default)(r))throw new Jr.GraphQLError("Int cannot represent non-integer value: ".concat((0,ra.default)(t)));if(r>gE||rgE||egE||r{"use strict";Object.defineProperty(yE,"__esModule",{value:!0});yE.astFromValue=xd;var x9=Kl(hE()),I9=Kl(oi()),vN=Kl(Ot()),R9=Kl(un()),F9=Kl(Sa()),j9=Kl(bg()),Ai=Vt(),P9=Ca(),Ld=lt();function Kl(e){return e&&e.__esModule?e:{default:e}}function xd(e,t){if((0,Ld.isNonNullType)(t)){var r=xd(e,t.ofType);return(r==null?void 0:r.kind)===Ai.Kind.NULL?null:r}if(e===null)return{kind:Ai.Kind.NULL};if(e===void 0)return null;if((0,Ld.isListType)(t)){var n=t.ofType,a=(0,j9.default)(e);if(a!=null){for(var o=[],s=0;s{"use strict";Object.defineProperty(Rt,"__esModule",{value:!0});Rt.isIntrospectionType=K9;Rt.introspectionTypes=Rt.TypeNameMetaFieldDef=Rt.TypeMetaFieldDef=Rt.SchemaMetaFieldDef=Rt.__TypeKind=Rt.TypeKind=Rt.__EnumValue=Rt.__InputValue=Rt.__Field=Rt.__Type=Rt.__DirectiveLocation=Rt.__Directive=Rt.__Schema=void 0;var bE=TE(oi()),M9=TE(Ot()),q9=TE(un()),B9=Wn(),Mr=Fl(),V9=Id(),Qt=Ca(),xe=lt();function TE(e){return e&&e.__esModule?e:{default:e}}var EE=new xe.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:Qt.GraphQLString,resolve:function(r){return r.description}},types:{description:"A list of all types supported by this server.",type:new xe.GraphQLNonNull(new xe.GraphQLList(new xe.GraphQLNonNull(Ni))),resolve:function(r){return(0,bE.default)(r.getTypeMap())}},queryType:{description:"The type that query operations will be rooted at.",type:new xe.GraphQLNonNull(Ni),resolve:function(r){return r.getQueryType()}},mutationType:{description:"If this server supports mutation, the type that mutation operations will be rooted at.",type:Ni,resolve:function(r){return r.getMutationType()}},subscriptionType:{description:"If this server support subscription, the type that subscription operations will be rooted at.",type:Ni,resolve:function(r){return r.getSubscriptionType()}},directives:{description:"A list of all directives supported by this server.",type:new xe.GraphQLNonNull(new xe.GraphQLList(new xe.GraphQLNonNull(_E))),resolve:function(r){return r.getDirectives()}}}}});Rt.__Schema=EE;var _E=new xe.GraphQLObjectType({name:"__Directive",description:`A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
+}`)}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 xe.GraphQLNonNull(Qt.GraphQLString),resolve:function(r){return r.name}},description:{type:Qt.GraphQLString,resolve:function(r){return r.description}},isRepeatable:{type:new xe.GraphQLNonNull(Qt.GraphQLBoolean),resolve:function(r){return r.isRepeatable}},locations:{type:new xe.GraphQLNonNull(new xe.GraphQLList(new xe.GraphQLNonNull(SE))),resolve:function(r){return r.locations}},args:{type:new xe.GraphQLNonNull(new xe.GraphQLList(new xe.GraphQLNonNull(Rd))),resolve:function(r){return r.args}}}}});Rt.__Directive=_E;var SE=new xe.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:Mr.DirectiveLocation.QUERY,description:"Location adjacent to a query operation."},MUTATION:{value:Mr.DirectiveLocation.MUTATION,description:"Location adjacent to a mutation operation."},SUBSCRIPTION:{value:Mr.DirectiveLocation.SUBSCRIPTION,description:"Location adjacent to a subscription operation."},FIELD:{value:Mr.DirectiveLocation.FIELD,description:"Location adjacent to a field."},FRAGMENT_DEFINITION:{value:Mr.DirectiveLocation.FRAGMENT_DEFINITION,description:"Location adjacent to a fragment definition."},FRAGMENT_SPREAD:{value:Mr.DirectiveLocation.FRAGMENT_SPREAD,description:"Location adjacent to a fragment spread."},INLINE_FRAGMENT:{value:Mr.DirectiveLocation.INLINE_FRAGMENT,description:"Location adjacent to an inline fragment."},VARIABLE_DEFINITION:{value:Mr.DirectiveLocation.VARIABLE_DEFINITION,description:"Location adjacent to a variable definition."},SCHEMA:{value:Mr.DirectiveLocation.SCHEMA,description:"Location adjacent to a schema definition."},SCALAR:{value:Mr.DirectiveLocation.SCALAR,description:"Location adjacent to a scalar definition."},OBJECT:{value:Mr.DirectiveLocation.OBJECT,description:"Location adjacent to an object type definition."},FIELD_DEFINITION:{value:Mr.DirectiveLocation.FIELD_DEFINITION,description:"Location adjacent to a field definition."},ARGUMENT_DEFINITION:{value:Mr.DirectiveLocation.ARGUMENT_DEFINITION,description:"Location adjacent to an argument definition."},INTERFACE:{value:Mr.DirectiveLocation.INTERFACE,description:"Location adjacent to an interface definition."},UNION:{value:Mr.DirectiveLocation.UNION,description:"Location adjacent to a union definition."},ENUM:{value:Mr.DirectiveLocation.ENUM,description:"Location adjacent to an enum definition."},ENUM_VALUE:{value:Mr.DirectiveLocation.ENUM_VALUE,description:"Location adjacent to an enum value definition."},INPUT_OBJECT:{value:Mr.DirectiveLocation.INPUT_OBJECT,description:"Location adjacent to an input object type definition."},INPUT_FIELD_DEFINITION:{value:Mr.DirectiveLocation.INPUT_FIELD_DEFINITION,description:"Location adjacent to an input object field definition."}}});Rt.__DirectiveLocation=SE;var Ni=new xe.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 xe.GraphQLNonNull(OE),resolve:function(r){if((0,xe.isScalarType)(r))return sn.SCALAR;if((0,xe.isObjectType)(r))return sn.OBJECT;if((0,xe.isInterfaceType)(r))return sn.INTERFACE;if((0,xe.isUnionType)(r))return sn.UNION;if((0,xe.isEnumType)(r))return sn.ENUM;if((0,xe.isInputObjectType)(r))return sn.INPUT_OBJECT;if((0,xe.isListType)(r))return sn.LIST;if((0,xe.isNonNullType)(r))return sn.NON_NULL;(0,q9.default)(0,'Unexpected type: "'.concat((0,M9.default)(r),'".'))}},name:{type:Qt.GraphQLString,resolve:function(r){return r.name!==void 0?r.name:void 0}},description:{type:Qt.GraphQLString,resolve:function(r){return r.description!==void 0?r.description:void 0}},specifiedByUrl:{type:Qt.GraphQLString,resolve:function(r){return r.specifiedByUrl!==void 0?r.specifiedByUrl:void 0}},fields:{type:new xe.GraphQLList(new xe.GraphQLNonNull(DE)),args:{includeDeprecated:{type:Qt.GraphQLBoolean,defaultValue:!1}},resolve:function(r,n){var a=n.includeDeprecated;if((0,xe.isObjectType)(r)||(0,xe.isInterfaceType)(r)){var o=(0,bE.default)(r.getFields());return a?o:o.filter(function(s){return s.deprecationReason==null})}}},interfaces:{type:new xe.GraphQLList(new xe.GraphQLNonNull(Ni)),resolve:function(r){if((0,xe.isObjectType)(r)||(0,xe.isInterfaceType)(r))return r.getInterfaces()}},possibleTypes:{type:new xe.GraphQLList(new xe.GraphQLNonNull(Ni)),resolve:function(r,n,a,o){var s=o.schema;if((0,xe.isAbstractType)(r))return s.getPossibleTypes(r)}},enumValues:{type:new xe.GraphQLList(new xe.GraphQLNonNull(kE)),args:{includeDeprecated:{type:Qt.GraphQLBoolean,defaultValue:!1}},resolve:function(r,n){var a=n.includeDeprecated;if((0,xe.isEnumType)(r)){var o=r.getValues();return a?o:o.filter(function(s){return s.deprecationReason==null})}}},inputFields:{type:new xe.GraphQLList(new xe.GraphQLNonNull(Rd)),args:{includeDeprecated:{type:Qt.GraphQLBoolean,defaultValue:!1}},resolve:function(r,n){var a=n.includeDeprecated;if((0,xe.isInputObjectType)(r)){var o=(0,bE.default)(r.getFields());return a?o:o.filter(function(s){return s.deprecationReason==null})}}},ofType:{type:Ni,resolve:function(r){return r.ofType!==void 0?r.ofType:void 0}}}}});Rt.__Type=Ni;var DE=new xe.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 xe.GraphQLNonNull(Qt.GraphQLString),resolve:function(r){return r.name}},description:{type:Qt.GraphQLString,resolve:function(r){return r.description}},args:{type:new xe.GraphQLNonNull(new xe.GraphQLList(new xe.GraphQLNonNull(Rd))),args:{includeDeprecated:{type:Qt.GraphQLBoolean,defaultValue:!1}},resolve:function(r,n){var a=n.includeDeprecated;return a?r.args:r.args.filter(function(o){return o.deprecationReason==null})}},type:{type:new xe.GraphQLNonNull(Ni),resolve:function(r){return r.type}},isDeprecated:{type:new xe.GraphQLNonNull(Qt.GraphQLBoolean),resolve:function(r){return r.deprecationReason!=null}},deprecationReason:{type:Qt.GraphQLString,resolve:function(r){return r.deprecationReason}}}}});Rt.__Field=DE;var Rd=new xe.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 xe.GraphQLNonNull(Qt.GraphQLString),resolve:function(r){return r.name}},description:{type:Qt.GraphQLString,resolve:function(r){return r.description}},type:{type:new xe.GraphQLNonNull(Ni),resolve:function(r){return r.type}},defaultValue:{type:Qt.GraphQLString,description:"A GraphQL-formatted string representing the default value for this input value.",resolve:function(r){var n=r.type,a=r.defaultValue,o=(0,V9.astFromValue)(a,n);return o?(0,B9.print)(o):null}},isDeprecated:{type:new xe.GraphQLNonNull(Qt.GraphQLBoolean),resolve:function(r){return r.deprecationReason!=null}},deprecationReason:{type:Qt.GraphQLString,resolve:function(r){return r.deprecationReason}}}}});Rt.__InputValue=Rd;var kE=new xe.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 xe.GraphQLNonNull(Qt.GraphQLString),resolve:function(r){return r.name}},description:{type:Qt.GraphQLString,resolve:function(r){return r.description}},isDeprecated:{type:new xe.GraphQLNonNull(Qt.GraphQLBoolean),resolve:function(r){return r.deprecationReason!=null}},deprecationReason:{type:Qt.GraphQLString,resolve:function(r){return r.deprecationReason}}}}});Rt.__EnumValue=kE;var sn=Object.freeze({SCALAR:"SCALAR",OBJECT:"OBJECT",INTERFACE:"INTERFACE",UNION:"UNION",ENUM:"ENUM",INPUT_OBJECT:"INPUT_OBJECT",LIST:"LIST",NON_NULL:"NON_NULL"});Rt.TypeKind=sn;var OE=new xe.GraphQLEnumType({name:"__TypeKind",description:"An enum describing what kind of type a given `__Type` is.",values:{SCALAR:{value:sn.SCALAR,description:"Indicates this type is a scalar."},OBJECT:{value:sn.OBJECT,description:"Indicates this type is an object. `fields` and `interfaces` are valid fields."},INTERFACE:{value:sn.INTERFACE,description:"Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields."},UNION:{value:sn.UNION,description:"Indicates this type is a union. `possibleTypes` is a valid field."},ENUM:{value:sn.ENUM,description:"Indicates this type is an enum. `enumValues` is a valid field."},INPUT_OBJECT:{value:sn.INPUT_OBJECT,description:"Indicates this type is an input object. `inputFields` is a valid field."},LIST:{value:sn.LIST,description:"Indicates this type is a list. `ofType` is a valid field."},NON_NULL:{value:sn.NON_NULL,description:"Indicates this type is a non-null. `ofType` is a valid field."}}});Rt.__TypeKind=OE;var U9={name:"__schema",type:new xe.GraphQLNonNull(EE),description:"Access the current type schema of this server.",args:[],resolve:function(t,r,n,a){var o=a.schema;return o},isDeprecated:!1,deprecationReason:void 0,extensions:void 0,astNode:void 0};Rt.SchemaMetaFieldDef=U9;var G9={name:"__type",type:Ni,description:"Request the type information of a single type.",args:[{name:"name",description:void 0,type:new xe.GraphQLNonNull(Qt.GraphQLString),defaultValue:void 0,deprecationReason:void 0,extensions:void 0,astNode:void 0}],resolve:function(t,r,n,a){var o=r.name,s=a.schema;return s.getType(o)},isDeprecated:!1,deprecationReason:void 0,extensions:void 0,astNode:void 0};Rt.TypeMetaFieldDef=G9;var Q9={name:"__typename",type:new xe.GraphQLNonNull(Qt.GraphQLString),description:"The name of the current Object type at runtime.",args:[],resolve:function(t,r,n,a){var o=a.parentType;return o.name},isDeprecated:!1,deprecationReason:void 0,extensions:void 0,astNode:void 0};Rt.TypeNameMetaFieldDef=Q9;var mN=Object.freeze([EE,_E,SE,Ni,DE,Rd,kE,OE]);Rt.introspectionTypes=mN;function K9(e){return mN.some(function(t){var r=t.name;return e.name===r})}});var Jn=U(qr=>{"use strict";Object.defineProperty(qr,"__esModule",{value:!0});qr.isDirective=TN;qr.assertDirective=$9;qr.isSpecifiedDirective=e4;qr.specifiedDirectives=qr.GraphQLSpecifiedByDirective=qr.GraphQLDeprecatedDirective=qr.DEFAULT_DEPRECATION_REASON=qr.GraphQLSkipDirective=qr.GraphQLIncludeDirective=qr.GraphQLDirective=void 0;var H9=as(Bl()),z9=Da(),W9=as(Ot()),yN=as(sg()),CE=as(wi()),Y9=as(gd()),J9=as(Sa()),X9=as(zv()),na=Fl(),Dg=Ca(),kg=lt();function as(e){return e&&e.__esModule?e:{default:e}}function bN(e,t){for(var r=0;r{"use strict";Object.defineProperty(Hl,"__esModule",{value:!0});Hl.isSchema=AN;Hl.assertSchema=l4;Hl.GraphQLSchema=void 0;var t4=cu(ql()),r4=cu(pE()),wE=cu(oi()),n4=Da(),AE=cu(Ot()),i4=cu(sg()),Og=cu(wi()),a4=cu(gd()),o4=cu(Sa()),u4=Yn(),CN=Jn(),ia=lt();function cu(e){return e&&e.__esModule?e:{default:e}}function wN(e,t){for(var r=0;r{"use strict";Object.defineProperty(Cg,"__esModule",{value:!0});Cg.validateSchema=RN;Cg.assertValidSchema=v4;var LN=NE(ql()),Fd=NE(oi()),Tn=NE(Ot()),c4=Be(),f4=Td(),d4=VT(),xN=Cd(),p4=us(),h4=Yn(),IN=Jn(),yr=lt();function NE(e){return e&&e.__esModule?e:{default:e}}function RN(e){if((0,p4.assertSchema)(e),e.__validationErrors)return e.__validationErrors;var t=new g4(e);m4(t),y4(t),b4(t);var r=t.getErrors();return e.__validationErrors=r,r}function v4(e){var t=RN(e);if(t.length!==0)throw new Error(t.map(function(r){return r.message}).join(`
+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 g4=function(){function e(r){this._errors=[],this.schema=r}var t=e.prototype;return t.reportError=function(n,a){var o=Array.isArray(a)?a.filter(Boolean):a;this.addError(new c4.GraphQLError(n,o))},t.addError=function(n){this._errors.push(n)},t.getErrors=function(){return this._errors},e}();function m4(e){var t=e.schema,r=t.getQueryType();if(!r)e.reportError("Query root type must be provided.",t.astNode);else if(!(0,yr.isObjectType)(r)){var n;e.reportError("Query root type must be Object type, it cannot be ".concat((0,Tn.default)(r),"."),(n=LE(t,"query"))!==null&&n!==void 0?n:r.astNode)}var a=t.getMutationType();if(a&&!(0,yr.isObjectType)(a)){var o;e.reportError("Mutation root type must be Object type if provided, it cannot be "+"".concat((0,Tn.default)(a),"."),(o=LE(t,"mutation"))!==null&&o!==void 0?o:a.astNode)}var s=t.getSubscriptionType();if(s&&!(0,yr.isObjectType)(s)){var l;e.reportError("Subscription root type must be Object type if provided, it cannot be "+"".concat((0,Tn.default)(s),"."),(l=LE(t,"subscription"))!==null&&l!==void 0?l:s.astNode)}}function LE(e,t){for(var r=xE(e,function(o){return o.operationTypes}),n=0;n{"use strict";Object.defineProperty(jE,"__esModule",{value:!0});jE.typeFromAST=FE;var O4=qN(Ot()),C4=qN(un()),RE=Vt(),MN=lt();function qN(e){return e&&e.__esModule?e:{default:e}}function FE(e,t){var r;if(t.kind===RE.Kind.LIST_TYPE)return r=FE(e,t.type),r&&new MN.GraphQLList(r);if(t.kind===RE.Kind.NON_NULL_TYPE)return r=FE(e,t.type),r&&new MN.GraphQLNonNull(r);if(t.kind===RE.Kind.NAMED_TYPE)return e.getType(t.name.value);(0,C4.default)(0,"Unexpected type node: "+(0,O4.default)(t))}});var wg=U(Md=>{"use strict";Object.defineProperty(Md,"__esModule",{value:!0});Md.visitWithTypeInfo=I4;Md.TypeInfo=void 0;var w4=N4(ql()),fr=Vt(),A4=Il(),BN=eu(),dr=lt(),Wl=Yn(),VN=wa();function N4(e){return e&&e.__esModule?e:{default:e}}var L4=function(){function e(r,n,a){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:x4,a&&((0,dr.isInputType)(a)&&this._inputTypeStack.push(a),(0,dr.isCompositeType)(a)&&this._parentTypeStack.push(a),(0,dr.isOutputType)(a)&&this._typeStack.push(a))}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 a=this._schema;switch(n.kind){case fr.Kind.SELECTION_SET:{var o=(0,dr.getNamedType)(this.getType());this._parentTypeStack.push((0,dr.isCompositeType)(o)?o:void 0);break}case fr.Kind.FIELD:{var s=this.getParentType(),l,d;s&&(l=this._getFieldDef(a,s,n),l&&(d=l.type)),this._fieldDefStack.push(l),this._typeStack.push((0,dr.isOutputType)(d)?d:void 0);break}case fr.Kind.DIRECTIVE:this._directive=a.getDirective(n.name.value);break;case fr.Kind.OPERATION_DEFINITION:{var h;switch(n.operation){case"query":h=a.getQueryType();break;case"mutation":h=a.getMutationType();break;case"subscription":h=a.getSubscriptionType();break}this._typeStack.push((0,dr.isObjectType)(h)?h:void 0);break}case fr.Kind.INLINE_FRAGMENT:case fr.Kind.FRAGMENT_DEFINITION:{var v=n.typeCondition,b=v?(0,VN.typeFromAST)(a,v):(0,dr.getNamedType)(this.getType());this._typeStack.push((0,dr.isOutputType)(b)?b:void 0);break}case fr.Kind.VARIABLE_DEFINITION:{var T=(0,VN.typeFromAST)(a,n.type);this._inputTypeStack.push((0,dr.isInputType)(T)?T:void 0);break}case fr.Kind.ARGUMENT:{var A,L,S,y=(A=this.getDirective())!==null&&A!==void 0?A:this.getFieldDef();y&&(L=(0,w4.default)(y.args,function(M){return M.name===n.name.value}),L&&(S=L.type)),this._argument=L,this._defaultValueStack.push(L?L.defaultValue:void 0),this._inputTypeStack.push((0,dr.isInputType)(S)?S:void 0);break}case fr.Kind.LIST:{var _=(0,dr.getNullableType)(this.getInputType()),m=(0,dr.isListType)(_)?_.ofType:_;this._defaultValueStack.push(void 0),this._inputTypeStack.push((0,dr.isInputType)(m)?m:void 0);break}case fr.Kind.OBJECT_FIELD:{var k=(0,dr.getNamedType)(this.getInputType()),w,C;(0,dr.isInputObjectType)(k)&&(C=k.getFields()[n.name.value],C&&(w=C.type)),this._defaultValueStack.push(C?C.defaultValue:void 0),this._inputTypeStack.push((0,dr.isInputType)(w)?w:void 0);break}case fr.Kind.ENUM:{var D=(0,dr.getNamedType)(this.getInputType()),R;(0,dr.isEnumType)(D)&&(R=D.getValue(n.value)),this._enumValue=R;break}}},t.leave=function(n){switch(n.kind){case fr.Kind.SELECTION_SET:this._parentTypeStack.pop();break;case fr.Kind.FIELD:this._fieldDefStack.pop(),this._typeStack.pop();break;case fr.Kind.DIRECTIVE:this._directive=null;break;case fr.Kind.OPERATION_DEFINITION:case fr.Kind.INLINE_FRAGMENT:case fr.Kind.FRAGMENT_DEFINITION:this._typeStack.pop();break;case fr.Kind.VARIABLE_DEFINITION:this._inputTypeStack.pop();break;case fr.Kind.ARGUMENT:this._argument=null,this._defaultValueStack.pop(),this._inputTypeStack.pop();break;case fr.Kind.LIST:case fr.Kind.OBJECT_FIELD:this._defaultValueStack.pop(),this._inputTypeStack.pop();break;case fr.Kind.ENUM:this._enumValue=null;break}},e}();Md.TypeInfo=L4;function x4(e,t,r){var n=r.name.value;if(n===Wl.SchemaMetaFieldDef.name&&e.getQueryType()===t)return Wl.SchemaMetaFieldDef;if(n===Wl.TypeMetaFieldDef.name&&e.getQueryType()===t)return Wl.TypeMetaFieldDef;if(n===Wl.TypeNameMetaFieldDef.name&&(0,dr.isCompositeType)(t))return Wl.TypeNameMetaFieldDef;if((0,dr.isObjectType)(t)||(0,dr.isInterfaceType)(t))return t.getFields()[n]}function I4(e,t){return{enter:function(n){e.enter(n);var a=(0,BN.getVisitFn)(t,n.kind,!1);if(a){var o=a.apply(t,arguments);return o!==void 0&&(e.leave(n),(0,A4.isNode)(o)&&e.enter(o)),o}},leave:function(n){var a=(0,BN.getVisitFn)(t,n.kind,!0),o;return a&&(o=a.apply(t,arguments)),e.leave(n),o}}}});var ls=U(oa=>{"use strict";Object.defineProperty(oa,"__esModule",{value:!0});oa.isDefinitionNode=R4;oa.isExecutableDefinitionNode=UN;oa.isSelectionNode=F4;oa.isValueNode=j4;oa.isTypeNode=P4;oa.isTypeSystemDefinitionNode=GN;oa.isTypeDefinitionNode=QN;oa.isTypeSystemExtensionNode=KN;oa.isTypeExtensionNode=HN;var Tt=Vt();function R4(e){return UN(e)||GN(e)||KN(e)}function UN(e){return e.kind===Tt.Kind.OPERATION_DEFINITION||e.kind===Tt.Kind.FRAGMENT_DEFINITION}function F4(e){return e.kind===Tt.Kind.FIELD||e.kind===Tt.Kind.FRAGMENT_SPREAD||e.kind===Tt.Kind.INLINE_FRAGMENT}function j4(e){return e.kind===Tt.Kind.VARIABLE||e.kind===Tt.Kind.INT||e.kind===Tt.Kind.FLOAT||e.kind===Tt.Kind.STRING||e.kind===Tt.Kind.BOOLEAN||e.kind===Tt.Kind.NULL||e.kind===Tt.Kind.ENUM||e.kind===Tt.Kind.LIST||e.kind===Tt.Kind.OBJECT}function P4(e){return e.kind===Tt.Kind.NAMED_TYPE||e.kind===Tt.Kind.LIST_TYPE||e.kind===Tt.Kind.NON_NULL_TYPE}function GN(e){return e.kind===Tt.Kind.SCHEMA_DEFINITION||QN(e)||e.kind===Tt.Kind.DIRECTIVE_DEFINITION}function QN(e){return e.kind===Tt.Kind.SCALAR_TYPE_DEFINITION||e.kind===Tt.Kind.OBJECT_TYPE_DEFINITION||e.kind===Tt.Kind.INTERFACE_TYPE_DEFINITION||e.kind===Tt.Kind.UNION_TYPE_DEFINITION||e.kind===Tt.Kind.ENUM_TYPE_DEFINITION||e.kind===Tt.Kind.INPUT_OBJECT_TYPE_DEFINITION}function KN(e){return e.kind===Tt.Kind.SCHEMA_EXTENSION||HN(e)}function HN(e){return e.kind===Tt.Kind.SCALAR_TYPE_EXTENSION||e.kind===Tt.Kind.OBJECT_TYPE_EXTENSION||e.kind===Tt.Kind.INTERFACE_TYPE_EXTENSION||e.kind===Tt.Kind.UNION_TYPE_EXTENSION||e.kind===Tt.Kind.ENUM_TYPE_EXTENSION||e.kind===Tt.Kind.INPUT_OBJECT_TYPE_EXTENSION}});var ME=U(PE=>{"use strict";Object.defineProperty(PE,"__esModule",{value:!0});PE.ExecutableDefinitionsRule=B4;var M4=Be(),zN=Vt(),q4=ls();function B4(e){return{Document:function(r){for(var n=0,a=r.definitions;n{"use strict";Object.defineProperty(qE,"__esModule",{value:!0});qE.UniqueOperationNamesRule=U4;var V4=Be();function U4(e){var t=Object.create(null);return{OperationDefinition:function(n){var a=n.name;return a&&(t[a.value]?e.reportError(new V4.GraphQLError('There can be only one operation named "'.concat(a.value,'".'),[t[a.value],a])):t[a.value]=a),!1},FragmentDefinition:function(){return!1}}}});var UE=U(VE=>{"use strict";Object.defineProperty(VE,"__esModule",{value:!0});VE.LoneAnonymousOperationRule=K4;var G4=Be(),Q4=Vt();function K4(e){var t=0;return{Document:function(n){t=n.definitions.filter(function(a){return a.kind===Q4.Kind.OPERATION_DEFINITION}).length},OperationDefinition:function(n){!n.name&&t>1&&e.reportError(new G4.GraphQLError("This anonymous operation must be the only defined operation.",n))}}}});var QE=U(GE=>{"use strict";Object.defineProperty(GE,"__esModule",{value:!0});GE.SingleFieldSubscriptionsRule=z4;var H4=Be();function z4(e){return{OperationDefinition:function(r){r.operation==="subscription"&&r.selectionSet.selections.length!==1&&e.reportError(new H4.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 zE=U(HE=>{"use strict";Object.defineProperty(HE,"__esModule",{value:!0});HE.KnownTypeNamesRule=$4;var W4=WN(ru()),Y4=WN(nu()),J4=Be(),KE=ls(),X4=Ca(),Z4=Yn();function WN(e){return e&&e.__esModule?e:{default:e}}function $4(e){for(var t=e.getSchema(),r=t?t.getTypeMap():Object.create(null),n=Object.create(null),a=0,o=e.getDocument().definitions;a{"use strict";Object.defineProperty(WE,"__esModule",{value:!0});WE.FragmentsOnCompositeTypesRule=rK;var JN=Be(),XN=Wn(),ZN=lt(),$N=wa();function rK(e){return{InlineFragment:function(r){var n=r.typeCondition;if(n){var a=(0,$N.typeFromAST)(e.getSchema(),n);if(a&&!(0,ZN.isCompositeType)(a)){var o=(0,XN.print)(n);e.reportError(new JN.GraphQLError('Fragment cannot condition on non composite type "'.concat(o,'".'),n))}}},FragmentDefinition:function(r){var n=(0,$N.typeFromAST)(e.getSchema(),r.typeCondition);if(n&&!(0,ZN.isCompositeType)(n)){var a=(0,XN.print)(r.typeCondition);e.reportError(new JN.GraphQLError('Fragment "'.concat(r.name.value,'" cannot condition on non composite type "').concat(a,'".'),r.typeCondition))}}}}});var XE=U(JE=>{"use strict";Object.defineProperty(JE,"__esModule",{value:!0});JE.VariablesAreInputTypesRule=uK;var nK=Be(),iK=Wn(),aK=lt(),oK=wa();function uK(e){return{VariableDefinition:function(r){var n=(0,oK.typeFromAST)(e.getSchema(),r.type);if(n&&!(0,aK.isInputType)(n)){var a=r.variable.name.value,o=(0,iK.print)(r.type);e.reportError(new nK.GraphQLError('Variable "$'.concat(a,'" cannot be non-input type "').concat(o,'".'),r.type))}}}}});var $E=U(ZE=>{"use strict";Object.defineProperty(ZE,"__esModule",{value:!0});ZE.ScalarLeafsRule=lK;var eL=sK(Ot()),tL=Be(),rL=lt();function sK(e){return e&&e.__esModule?e:{default:e}}function lK(e){return{Field:function(r){var n=e.getType(),a=r.selectionSet;if(n){if((0,rL.isLeafType)((0,rL.getNamedType)(n))){if(a){var o=r.name.value,s=(0,eL.default)(n);e.reportError(new tL.GraphQLError('Field "'.concat(o,'" must not have a selection since type "').concat(s,'" has no subfields.'),a))}}else if(!a){var l=r.name.value,d=(0,eL.default)(n);e.reportError(new tL.GraphQLError('Field "'.concat(l,'" of type "').concat(d,'" must have a selection of subfields. Did you mean "').concat(l,' { ... }"?'),r))}}}}}});var t_=U(e_=>{"use strict";Object.defineProperty(e_,"__esModule",{value:!0});e_.FieldsOnCorrectTypeRule=hK;var cK=Ag(pE()),nL=Ag(ru()),fK=Ag(nu()),dK=Ag(_d()),pK=Be(),qd=lt();function Ag(e){return e&&e.__esModule?e:{default:e}}function hK(e){return{Field:function(r){var n=e.getParentType();if(n){var a=e.getFieldDef();if(!a){var o=e.getSchema(),s=r.name.value,l=(0,nL.default)("to use an inline fragment on",vK(o,n,s));l===""&&(l=(0,nL.default)(gK(n,s))),e.reportError(new pK.GraphQLError('Cannot query field "'.concat(s,'" on type "').concat(n.name,'".')+l,r))}}}}}function vK(e,t,r){if(!(0,qd.isAbstractType)(t))return[];for(var n=new Set,a=Object.create(null),o=0,s=e.getPossibleTypes(t);o{"use strict";Object.defineProperty(r_,"__esModule",{value:!0});r_.UniqueFragmentNamesRule=yK;var mK=Be();function yK(e){var t=Object.create(null);return{OperationDefinition:function(){return!1},FragmentDefinition:function(n){var a=n.name.value;return t[a]?e.reportError(new mK.GraphQLError('There can be only one fragment named "'.concat(a,'".'),[t[a],n.name])):t[a]=n.name,!1}}}});var a_=U(i_=>{"use strict";Object.defineProperty(i_,"__esModule",{value:!0});i_.KnownFragmentNamesRule=TK;var bK=Be();function TK(e){return{FragmentSpread:function(r){var n=r.name.value,a=e.getFragment(n);a||e.reportError(new bK.GraphQLError('Unknown fragment "'.concat(n,'".'),r.name))}}}});var u_=U(o_=>{"use strict";Object.defineProperty(o_,"__esModule",{value:!0});o_.NoUnusedFragmentsRule=_K;var EK=Be();function _K(e){var t=[],r=[];return{OperationDefinition:function(a){return t.push(a),!1},FragmentDefinition:function(a){return r.push(a),!1},Document:{leave:function(){for(var a=Object.create(null),o=0;o{"use strict";Object.defineProperty(l_,"__esModule",{value:!0});l_.PossibleFragmentSpreadsRule=kK;var Ng=DK(Ot()),iL=Be(),s_=lt(),SK=wa(),aL=Cd();function DK(e){return e&&e.__esModule?e:{default:e}}function kK(e){return{InlineFragment:function(r){var n=e.getType(),a=e.getParentType();if((0,s_.isCompositeType)(n)&&(0,s_.isCompositeType)(a)&&!(0,aL.doTypesOverlap)(e.getSchema(),n,a)){var o=(0,Ng.default)(a),s=(0,Ng.default)(n);e.reportError(new iL.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,a=OK(e,n),o=e.getParentType();if(a&&o&&!(0,aL.doTypesOverlap)(e.getSchema(),a,o)){var s=(0,Ng.default)(o),l=(0,Ng.default)(a);e.reportError(new iL.GraphQLError('Fragment "'.concat(n,'" cannot be spread here as objects of type "').concat(s,'" can never be of type "').concat(l,'".'),r))}}}}function OK(e,t){var r=e.getFragment(t);if(r){var n=(0,SK.typeFromAST)(e.getSchema(),r.typeCondition);if((0,s_.isCompositeType)(n))return n}}});var d_=U(f_=>{"use strict";Object.defineProperty(f_,"__esModule",{value:!0});f_.NoFragmentCyclesRule=wK;var CK=Be();function wK(e){var t=Object.create(null),r=[],n=Object.create(null);return{OperationDefinition:function(){return!1},FragmentDefinition:function(s){return a(s),!1}};function a(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(p_,"__esModule",{value:!0});p_.UniqueVariableNamesRule=NK;var AK=Be();function NK(e){var t=Object.create(null);return{OperationDefinition:function(){t=Object.create(null)},VariableDefinition:function(n){var a=n.variable.name.value;t[a]?e.reportError(new AK.GraphQLError('There can be only one variable named "$'.concat(a,'".'),[t[a],n.variable.name])):t[a]=n.variable.name}}}});var g_=U(v_=>{"use strict";Object.defineProperty(v_,"__esModule",{value:!0});v_.NoUndefinedVariablesRule=xK;var LK=Be();function xK(e){var t=Object.create(null);return{OperationDefinition:{enter:function(){t=Object.create(null)},leave:function(n){for(var a=e.getRecursiveVariableUsages(n),o=0;o{"use strict";Object.defineProperty(m_,"__esModule",{value:!0});m_.NoUnusedVariablesRule=RK;var IK=Be();function RK(e){var t=[];return{OperationDefinition:{enter:function(){t=[]},leave:function(n){for(var a=Object.create(null),o=e.getRecursiveVariableUsages(n),s=0;s{"use strict";Object.defineProperty(b_,"__esModule",{value:!0});b_.KnownDirectivesRule=PK;var FK=sL(Ot()),oL=sL(un()),uL=Be(),Zt=Vt(),Br=Fl(),jK=Jn();function sL(e){return e&&e.__esModule?e:{default:e}}function PK(e){for(var t=Object.create(null),r=e.getSchema(),n=r?r.getDirectives():jK.specifiedDirectives,a=0;a{"use strict";Object.defineProperty(__,"__esModule",{value:!0});__.UniqueDirectivesPerLocationRule=UK;var BK=Be(),E_=Vt(),lL=ls(),VK=Jn();function UK(e){for(var t=Object.create(null),r=e.getSchema(),n=r?r.getDirectives():VK.specifiedDirectives,a=0;a{"use strict";Object.defineProperty(Lg,"__esModule",{value:!0});Lg.KnownArgumentNamesRule=HK;Lg.KnownArgumentNamesOnDirectivesRule=gL;var cL=pL(ru()),fL=pL(nu()),dL=Be(),GK=Vt(),QK=Jn();function pL(e){return e&&e.__esModule?e:{default:e}}function hL(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function vL(e){for(var t=1;t{"use strict";Object.defineProperty(k_,"__esModule",{value:!0});k_.UniqueArgumentNamesRule=WK;var zK=Be();function WK(e){var t=Object.create(null);return{Field:function(){t=Object.create(null)},Directive:function(){t=Object.create(null)},Argument:function(n){var a=n.name.value;return t[a]?e.reportError(new zK.GraphQLError('There can be only one argument named "'.concat(a,'".'),[t[a],n.name])):t[a]=n.name,!1}}}});var w_=U(C_=>{"use strict";Object.defineProperty(C_,"__esModule",{value:!0});C_.ValuesOfCorrectTypeRule=$K;var YK=Vd(oi()),JK=Vd(tu()),Bd=Vd(Ot()),XK=Vd(ru()),ZK=Vd(nu()),cs=Be(),xg=Wn(),Aa=lt();function Vd(e){return e&&e.__esModule?e:{default:e}}function $K(e){return{ListValue:function(r){var n=(0,Aa.getNullableType)(e.getParentInputType());if(!(0,Aa.isListType)(n))return fs(e,r),!1},ObjectValue:function(r){var n=(0,Aa.getNamedType)(e.getInputType());if(!(0,Aa.isInputObjectType)(n))return fs(e,r),!1;for(var a=(0,JK.default)(r.fields,function(v){return v.name.value}),o=0,s=(0,YK.default)(n.getFields());o{"use strict";Object.defineProperty(Rg,"__esModule",{value:!0});Rg.ProvidedRequiredArgumentsRule=n8;Rg.ProvidedRequiredArgumentsOnDirectivesRule=SL;var mL=TL(Ot()),Ig=TL(tu()),yL=Be(),bL=Vt(),e8=Wn(),t8=Jn(),A_=lt();function TL(e){return e&&e.__esModule?e:{default:e}}function EL(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function _L(e){for(var t=1;t{"use strict";Object.defineProperty(L_,"__esModule",{value:!0});L_.VariablesInAllowedPositionRule=l8;var DL=s8(Ot()),a8=Be(),o8=Vt(),kL=lt(),u8=wa(),OL=Cd();function s8(e){return e&&e.__esModule?e:{default:e}}function l8(e){var t=Object.create(null);return{OperationDefinition:{enter:function(){t=Object.create(null)},leave:function(n){for(var a=e.getRecursiveVariableUsages(n),o=0;o{"use strict";Object.defineProperty(M_,"__esModule",{value:!0});M_.OverlappingFieldsCanBeMergedRule=h8;var f8=R_(ql()),d8=R_(Bl()),CL=R_(Ot()),p8=Be(),I_=Vt(),wL=Wn(),Xn=lt(),AL=wa();function R_(e){return e&&e.__esModule?e:{default:e}}function NL(e){return Array.isArray(e)?e.map(function(t){var r=t[0],n=t[1];return'subfields "'.concat(r,'" conflict because ')+NL(n)}).join(" and "):e}function h8(e){var t=new E8,r=new Map;return{SelectionSet:function(a){for(var o=v8(e,r,t,e.getParentType(),a),s=0;s1)for(var v=0;v0)return[[t,e.map(function(a){var o=a[0];return o})],e.reduce(function(a,o){var s=o[1];return a.concat(s)},[r]),e.reduce(function(a,o){var s=o[2];return a.concat(s)},[n])]}var E8=function(){function e(){this._data=Object.create(null)}var t=e.prototype;return t.has=function(n,a,o){var s=this._data[n],l=s&&s[a];return l===void 0?!1:o===!1?l===!1:!0},t.add=function(n,a,o){this._pairSetAdd(n,a,o),this._pairSetAdd(a,n,o)},t._pairSetAdd=function(n,a,o){var s=this._data[n];s||(s=Object.create(null),this._data[n]=s),s[a]=o},e}()});var V_=U(B_=>{"use strict";Object.defineProperty(B_,"__esModule",{value:!0});B_.UniqueInputFieldNamesRule=S8;var _8=Be();function S8(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(a){var o=a.name.value;r[o]?e.reportError(new _8.GraphQLError('There can be only one input field named "'.concat(o,'".'),[r[o],a.name])):r[o]=a.name}}}});var G_=U(U_=>{"use strict";Object.defineProperty(U_,"__esModule",{value:!0});U_.LoneSchemaDefinitionRule=D8;var IL=Be();function D8(e){var t,r,n,a=e.getSchema(),o=(t=(r=(n=a==null?void 0:a.astNode)!==null&&n!==void 0?n:a==null?void 0:a.getQueryType())!==null&&r!==void 0?r:a==null?void 0:a.getMutationType())!==null&&t!==void 0?t:a==null?void 0:a.getSubscriptionType(),s=0;return{SchemaDefinition:function(d){if(o){e.reportError(new IL.GraphQLError("Cannot define a new schema within a schema extension.",d));return}s>0&&e.reportError(new IL.GraphQLError("Must provide only one schema definition.",d)),++s}}}});var K_=U(Q_=>{"use strict";Object.defineProperty(Q_,"__esModule",{value:!0});Q_.UniqueOperationTypesRule=k8;var RL=Be();function k8(e){var t=e.getSchema(),r=Object.create(null),n=t?{query:t.getQueryType(),mutation:t.getMutationType(),subscription:t.getSubscriptionType()}:{};return{SchemaDefinition:a,SchemaExtension:a};function a(o){for(var s,l=(s=o.operationTypes)!==null&&s!==void 0?s:[],d=0;d{"use strict";Object.defineProperty(H_,"__esModule",{value:!0});H_.UniqueTypeNamesRule=O8;var FL=Be();function O8(e){var t=Object.create(null),r=e.getSchema();return{ScalarTypeDefinition:n,ObjectTypeDefinition:n,InterfaceTypeDefinition:n,UnionTypeDefinition:n,EnumTypeDefinition:n,InputObjectTypeDefinition:n};function n(a){var o=a.name.value;if(r!=null&&r.getType(o)){e.reportError(new FL.GraphQLError('Type "'.concat(o,'" already exists in the schema. It cannot also be defined in this type definition.'),a.name));return}return t[o]?e.reportError(new FL.GraphQLError('There can be only one type named "'.concat(o,'".'),[t[o],a.name])):t[o]=a.name,!1}}});var Y_=U(W_=>{"use strict";Object.defineProperty(W_,"__esModule",{value:!0});W_.UniqueEnumValueNamesRule=w8;var jL=Be(),C8=lt();function w8(e){var t=e.getSchema(),r=t?t.getTypeMap():Object.create(null),n=Object.create(null);return{EnumTypeDefinition:a,EnumTypeExtension:a};function a(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(X_,"__esModule",{value:!0});X_.UniqueFieldDefinitionNamesRule=A8;var PL=Be(),J_=lt();function A8(e){var t=e.getSchema(),r=t?t.getTypeMap():Object.create(null),n=Object.create(null);return{InputObjectTypeDefinition:a,InputObjectTypeExtension:a,InterfaceTypeDefinition:a,InterfaceTypeExtension:a,ObjectTypeDefinition:a,ObjectTypeExtension:a};function a(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($_,"__esModule",{value:!0});$_.UniqueDirectiveNamesRule=L8;var ML=Be();function L8(e){var t=Object.create(null),r=e.getSchema();return{DirectiveDefinition:function(a){var o=a.name.value;if(r!=null&&r.getDirective(o)){e.reportError(new ML.GraphQLError('Directive "@'.concat(o,'" already exists in the schema. It cannot be redefined.'),a.name));return}return t[o]?e.reportError(new ML.GraphQLError('There can be only one directive named "@'.concat(o,'".'),[t[o],a.name])):t[o]=a.name,!1}}}});var rS=U(tS=>{"use strict";Object.defineProperty(tS,"__esModule",{value:!0});tS.PossibleTypeExtensionsRule=F8;var qL=Mg(Ot()),BL=Mg(un()),x8=Mg(ru()),I8=Mg(nu()),VL=Be(),rr=Vt(),R8=ls(),Yl=lt(),fu;function Mg(e){return e&&e.__esModule?e:{default:e}}function Jl(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function F8(e){for(var t=e.getSchema(),r=Object.create(null),n=0,a=e.getDocument().definitions;n{"use strict";Object.defineProperty(Xl,"__esModule",{value:!0});Xl.specifiedSDLRules=Xl.specifiedRules=void 0;var q8=ME(),B8=BE(),V8=UE(),U8=QE(),UL=zE(),G8=YE(),Q8=XE(),K8=$E(),H8=t_(),z8=n_(),W8=a_(),Y8=u_(),J8=c_(),X8=d_(),Z8=h_(),$8=g_(),e6=y_(),GL=T_(),QL=S_(),KL=D_(),HL=O_(),t6=w_(),zL=N_(),r6=x_(),n6=q_(),WL=V_(),i6=G_(),a6=K_(),o6=z_(),u6=Y_(),s6=Z_(),l6=eS(),c6=rS(),f6=Object.freeze([q8.ExecutableDefinitionsRule,B8.UniqueOperationNamesRule,V8.LoneAnonymousOperationRule,U8.SingleFieldSubscriptionsRule,UL.KnownTypeNamesRule,G8.FragmentsOnCompositeTypesRule,Q8.VariablesAreInputTypesRule,K8.ScalarLeafsRule,H8.FieldsOnCorrectTypeRule,z8.UniqueFragmentNamesRule,W8.KnownFragmentNamesRule,Y8.NoUnusedFragmentsRule,J8.PossibleFragmentSpreadsRule,X8.NoFragmentCyclesRule,Z8.UniqueVariableNamesRule,$8.NoUndefinedVariablesRule,e6.NoUnusedVariablesRule,GL.KnownDirectivesRule,QL.UniqueDirectivesPerLocationRule,KL.KnownArgumentNamesRule,HL.UniqueArgumentNamesRule,t6.ValuesOfCorrectTypeRule,zL.ProvidedRequiredArgumentsRule,r6.VariablesInAllowedPositionRule,n6.OverlappingFieldsCanBeMergedRule,WL.UniqueInputFieldNamesRule]);Xl.specifiedRules=f6;var d6=Object.freeze([i6.LoneSchemaDefinitionRule,a6.UniqueOperationTypesRule,o6.UniqueTypeNamesRule,u6.UniqueEnumValueNamesRule,s6.UniqueFieldDefinitionNamesRule,l6.UniqueDirectiveNamesRule,UL.KnownTypeNamesRule,GL.KnownDirectivesRule,QL.UniqueDirectivesPerLocationRule,c6.PossibleTypeExtensionsRule,KL.KnownArgumentNamesOnDirectivesRule,HL.UniqueArgumentNamesRule,WL.UniqueInputFieldNamesRule,zL.ProvidedRequiredArgumentsOnDirectivesRule]);Xl.specifiedSDLRules=d6});var aS=U(du=>{"use strict";Object.defineProperty(du,"__esModule",{value:!0});du.ValidationContext=du.SDLValidationContext=du.ASTValidationContext=void 0;var YL=Vt(),p6=eu(),JL=wg();function XL(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,e.__proto__=t}var iS=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 a=this._fragments;return a||(this._fragments=a=this.getDocument().definitions.reduce(function(o,s){return s.kind===YL.Kind.FRAGMENT_DEFINITION&&(o[s.name.value]=s),o},Object.create(null))),a[n]},t.getFragmentSpreads=function(n){var a=this._fragmentSpreads.get(n);if(!a){a=[];for(var o=[n];o.length!==0;)for(var s=o.pop(),l=0,d=s.selections;l{"use strict";Object.defineProperty(Zl,"__esModule",{value:!0});Zl.validate=T6;Zl.validateSDL=oS;Zl.assertValidSDL=E6;Zl.assertValidSDLExtension=_6;var g6=b6(wi()),m6=Be(),qg=eu(),y6=Pd(),ZL=wg(),$L=nS(),ex=aS();function b6(e){return e&&e.__esModule?e:{default:e}}function T6(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:$L.specifiedRules,n=arguments.length>3&&arguments[3]!==void 0?arguments[3]:new ZL.TypeInfo(e),a=arguments.length>4&&arguments[4]!==void 0?arguments[4]:{maxErrors:void 0};t||(0,g6.default)(0,"Must provide document."),(0,y6.assertValidSchema)(e);var o=Object.freeze({}),s=[],l=new ex.ValidationContext(e,t,n,function(h){if(a.maxErrors!=null&&s.length>=a.maxErrors)throw s.push(new m6.GraphQLError("Too many validation errors, error limit reached. Validation aborted.")),o;s.push(h)}),d=(0,qg.visitInParallel)(r.map(function(h){return h(l)}));try{(0,qg.visit)(t,(0,ZL.visitWithTypeInfo)(n,d))}catch(h){if(h!==o)throw h}return s}function oS(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:$L.specifiedSDLRules,n=[],a=new ex.SDLValidationContext(e,t,function(s){n.push(s)}),o=r.map(function(s){return s(a)});return(0,qg.visit)(e,(0,qg.visitInParallel)(o)),n}function E6(e){var t=oS(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 _6(e,t){var r=oS(e,t);if(r.length!==0)throw new Error(r.map(function(n){return n.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 tx=U(uS=>{"use strict";Object.defineProperty(uS,"__esModule",{value:!0});uS.default=S6;function S6(e){var t;return function(n,a,o){t||(t=new WeakMap);var s=t.get(n),l;if(s){if(l=s.get(a),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(a,l));var h=e(n,a,o);return l.set(o,h),h}}});var rx=U(sS=>{"use strict";Object.defineProperty(sS,"__esModule",{value:!0});sS.default=O6;var D6=k6(Pv());function k6(e){return e&&e.__esModule?e:{default:e}}function O6(e,t,r){return e.reduce(function(n,a){return(0,D6.default)(n)?n.then(function(o){return t(o,a)}):t(n,a)},r)}});var nx=U(lS=>{"use strict";Object.defineProperty(lS,"__esModule",{value:!0});lS.default=C6;function C6(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(a,o,s){return a[t[s]]=o,a},Object.create(null))})}});var Ud=U(Bg=>{"use strict";Object.defineProperty(Bg,"__esModule",{value:!0});Bg.addPath=w6;Bg.pathToArray=A6;function w6(e,t,r){return{prev:e,key:t,typename:r}}function A6(e){for(var t=[],r=e;r;)t.push(r.key),r=r.prev;return t.reverse()}});var Ug=U(cS=>{"use strict";Object.defineProperty(cS,"__esModule",{value:!0});cS.getOperationRootType=N6;var Vg=Be();function N6(e,t){if(t.operation==="query"){var r=e.getQueryType();if(!r)throw new Vg.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 Vg.GraphQLError("Schema is not configured for mutations.",t);return n}if(t.operation==="subscription"){var a=e.getSubscriptionType();if(!a)throw new Vg.GraphQLError("Schema is not configured for subscriptions.",t);return a}throw new Vg.GraphQLError("Can only have query, mutation and subscription operations.",t)}});var dS=U(fS=>{"use strict";Object.defineProperty(fS,"__esModule",{value:!0});fS.default=L6;function L6(e){return e.map(function(t){return typeof t=="number"?"["+t.toString()+"]":"."+t}).join("")}});var Qd=U(pS=>{"use strict";Object.defineProperty(pS,"__esModule",{value:!0});pS.valueFromAST=Gd;var x6=Gg(oi()),I6=Gg(tu()),R6=Gg(Ot()),F6=Gg(un()),ec=Vt(),ds=lt();function Gg(e){return e&&e.__esModule?e:{default:e}}function Gd(e,t,r){if(!!e){if(e.kind===ec.Kind.VARIABLE){var n=e.name.value;if(r==null||r[n]===void 0)return;var a=r[n];return a===null&&(0,ds.isNonNullType)(t)?void 0:a}if((0,ds.isNonNullType)(t))return e.kind===ec.Kind.NULL?void 0:Gd(e,t.ofType,r);if(e.kind===ec.Kind.NULL)return null;if((0,ds.isListType)(t)){var o=t.ofType;if(e.kind===ec.Kind.LIST){for(var s=[],l=0,d=e.values;l{"use strict";Object.defineProperty(hS,"__esModule",{value:!0});hS.coerceInputValue=G6;var j6=pu(oi()),Qg=pu(Ot()),P6=pu(un()),M6=pu(ru()),q6=pu(Sa()),B6=pu(bg()),V6=pu(nu()),U6=pu(dS()),co=Ud(),ps=Be(),Kd=lt();function pu(e){return e&&e.__esModule?e:{default:e}}function G6(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:Q6;return Hd(e,t,r)}function Q6(e,t,r){var n="Invalid value "+(0,Qg.default)(t);throw e.length>0&&(n+=' at "value'.concat((0,U6.default)(e),'"')),r.message=n+": "+r.message,r}function Hd(e,t,r,n){if((0,Kd.isNonNullType)(t)){if(e!=null)return Hd(e,t.ofType,r,n);r((0,co.pathToArray)(n),e,new ps.GraphQLError('Expected non-nullable type "'.concat((0,Qg.default)(t),'" not to be null.')));return}if(e==null)return null;if((0,Kd.isListType)(t)){var a=t.ofType,o=(0,B6.default)(e,function(m,k){var w=(0,co.addPath)(n,k,void 0);return Hd(m,a,r,w)});return o!=null?o:[Hd(e,a,r,n)]}if((0,Kd.isInputObjectType)(t)){if(!(0,q6.default)(e)){r((0,co.pathToArray)(n),e,new ps.GraphQLError('Expected type "'.concat(t.name,'" to be an object.')));return}for(var s={},l=t.getFields(),d=0,h=(0,j6.default)(l);d{"use strict";Object.defineProperty(zd,"__esModule",{value:!0});zd.getVariableValues=J6;zd.getArgumentValues=sx;zd.getDirectiveValues=Z6;var K6=Kg(ql()),H6=Kg(tu()),tc=Kg(Ot()),z6=Kg(dS()),fo=Be(),ax=Vt(),ox=Wn(),rc=lt(),W6=wa(),ux=Qd(),Y6=vS();function Kg(e){return e&&e.__esModule?e:{default:e}}function J6(e,t,r,n){var a=[],o=n==null?void 0:n.maxErrors;try{var s=X6(e,t,r,function(l){if(o!=null&&a.length>=o)throw new fo.GraphQLError("Too many errors processing variables, error limit reached. Execution aborted.");a.push(l)});if(a.length===0)return{coerced:s}}catch(l){a.push(l)}return{errors:a}}function X6(e,t,r,n){for(var a={},o=function(h){var v=t[h],b=v.variable.name.value,T=(0,W6.typeFromAST)(e,v.type);if(!(0,rc.isInputType)(T)){var A=(0,ox.print)(v.type);return n(new fo.GraphQLError('Variable "$'.concat(b,'" expected value of type "').concat(A,'" which cannot be used as an input type.'),v.type)),"continue"}if(!lx(r,b)){if(v.defaultValue)a[b]=(0,ux.valueFromAST)(v.defaultValue,T);else if((0,rc.isNonNullType)(T)){var L=(0,tc.default)(T);n(new fo.GraphQLError('Variable "$'.concat(b,'" of required type "').concat(L,'" was not provided.'),v))}return"continue"}var S=r[b];if(S===null&&(0,rc.isNonNullType)(T)){var y=(0,tc.default)(T);return n(new fo.GraphQLError('Variable "$'.concat(b,'" of non-null type "').concat(y,'" must not be null.'),v)),"continue"}a[b]=(0,Y6.coerceInputValue)(S,T,function(_,m,k){var w='Variable "$'.concat(b,'" got invalid value ')+(0,tc.default)(m);_.length>0&&(w+=' at "'.concat(b).concat((0,z6.default)(_),'"')),n(new fo.GraphQLError(w+"; "+k.message,v,void 0,void 0,void 0,k.originalError))})},s=0;s{"use strict";Object.defineProperty(si,"__esModule",{value:!0});si.execute=uH;si.executeSync=sH;si.assertValidExecutionArguments=px;si.buildExecutionContext=hx;si.collectFields=Jd;si.buildResolveInfo=yx;si.getFieldDef=Dx;si.defaultFieldResolver=si.defaultTypeResolver=void 0;var nc=ho(Ot()),$6=ho(tx()),eH=ho(un()),cx=ho(wi()),Li=ho(Pv()),gS=ho(Sa()),tH=ho(bg()),rH=ho(rx()),nH=ho(nx()),hs=Ud(),Na=Be(),Hg=Td(),Yd=Vt(),iH=Pd(),ic=Yn(),fx=Jn(),po=lt(),aH=wa(),oH=Ug(),zg=Wd();function ho(e){return e&&e.__esModule?e:{default:e}}function uH(e,t,r,n,a,o,s,l){return arguments.length===1?mS(e):mS({schema:e,document:t,rootValue:r,contextValue:n,variableValues:a,operationName:o,fieldResolver:s,typeResolver:l})}function sH(e){var t=mS(e);if((0,Li.default)(t))throw new Error("GraphQL execution failed to complete synchronously.");return t}function mS(e){var t=e.schema,r=e.document,n=e.rootValue,a=e.contextValue,o=e.variableValues,s=e.operationName,l=e.fieldResolver,d=e.typeResolver;px(t,r,o);var h=hx(t,r,n,a,o,s,l,d);if(Array.isArray(h))return{errors:h};var v=lH(h,h.operation,n);return dx(h,v)}function dx(e,t){return(0,Li.default)(t)?t.then(function(r){return dx(e,r)}):e.errors.length===0?{data:t}:{errors:e.errors,data:t}}function px(e,t,r){t||(0,cx.default)(0,"Must provide document."),(0,iH.assertValidSchema)(e),r==null||(0,gS.default)(r)||(0,cx.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 hx(e,t,r,n,a,o,s,l){for(var d,h,v,b=Object.create(null),T=0,A=t.definitions;T{"use strict";Object.defineProperty(Jg,"__esModule",{value:!0});Jg.graphql=SH;Jg.graphqlSync=DH;var mH=_H(Pv()),yH=Pl(),bH=$l(),TH=Pd(),EH=Zd();function _H(e){return e&&e.__esModule?e:{default:e}}function SH(e,t,r,n,a,o,s,l){var d=arguments;return new Promise(function(h){return h(d.length===1?Yg(e):Yg({schema:e,source:t,rootValue:r,contextValue:n,variableValues:a,operationName:o,fieldResolver:s,typeResolver:l}))})}function DH(e,t,r,n,a,o,s,l){var d=arguments.length===1?Yg(e):Yg({schema:e,source:t,rootValue:r,contextValue:n,variableValues:a,operationName:o,fieldResolver:s,typeResolver:l});if((0,mH.default)(d))throw new Error("GraphQL execution failed to complete synchronously.");return d}function Yg(e){var t=e.schema,r=e.source,n=e.rootValue,a=e.contextValue,o=e.variableValues,s=e.operationName,l=e.fieldResolver,d=e.typeResolver,h=(0,TH.validateSchema)(t);if(h.length>0)return{errors:h};var v;try{v=(0,yH.parse)(r)}catch(T){return{errors:[T]}}var b=(0,bH.validate)(t,v);return b.length>0?{errors:b}:(0,EH.execute)({schema:t,document:v,rootValue:n,contextValue:a,variableValues:o,operationName:s,fieldResolver:l,typeResolver:d})}});var Cx=U(me=>{"use strict";Object.defineProperty(me,"__esModule",{value:!0});Object.defineProperty(me,"isSchema",{enumerable:!0,get:function(){return TS.isSchema}});Object.defineProperty(me,"assertSchema",{enumerable:!0,get:function(){return TS.assertSchema}});Object.defineProperty(me,"GraphQLSchema",{enumerable:!0,get:function(){return TS.GraphQLSchema}});Object.defineProperty(me,"isType",{enumerable:!0,get:function(){return We.isType}});Object.defineProperty(me,"isScalarType",{enumerable:!0,get:function(){return We.isScalarType}});Object.defineProperty(me,"isObjectType",{enumerable:!0,get:function(){return We.isObjectType}});Object.defineProperty(me,"isInterfaceType",{enumerable:!0,get:function(){return We.isInterfaceType}});Object.defineProperty(me,"isUnionType",{enumerable:!0,get:function(){return We.isUnionType}});Object.defineProperty(me,"isEnumType",{enumerable:!0,get:function(){return We.isEnumType}});Object.defineProperty(me,"isInputObjectType",{enumerable:!0,get:function(){return We.isInputObjectType}});Object.defineProperty(me,"isListType",{enumerable:!0,get:function(){return We.isListType}});Object.defineProperty(me,"isNonNullType",{enumerable:!0,get:function(){return We.isNonNullType}});Object.defineProperty(me,"isInputType",{enumerable:!0,get:function(){return We.isInputType}});Object.defineProperty(me,"isOutputType",{enumerable:!0,get:function(){return We.isOutputType}});Object.defineProperty(me,"isLeafType",{enumerable:!0,get:function(){return We.isLeafType}});Object.defineProperty(me,"isCompositeType",{enumerable:!0,get:function(){return We.isCompositeType}});Object.defineProperty(me,"isAbstractType",{enumerable:!0,get:function(){return We.isAbstractType}});Object.defineProperty(me,"isWrappingType",{enumerable:!0,get:function(){return We.isWrappingType}});Object.defineProperty(me,"isNullableType",{enumerable:!0,get:function(){return We.isNullableType}});Object.defineProperty(me,"isNamedType",{enumerable:!0,get:function(){return We.isNamedType}});Object.defineProperty(me,"isRequiredArgument",{enumerable:!0,get:function(){return We.isRequiredArgument}});Object.defineProperty(me,"isRequiredInputField",{enumerable:!0,get:function(){return We.isRequiredInputField}});Object.defineProperty(me,"assertType",{enumerable:!0,get:function(){return We.assertType}});Object.defineProperty(me,"assertScalarType",{enumerable:!0,get:function(){return We.assertScalarType}});Object.defineProperty(me,"assertObjectType",{enumerable:!0,get:function(){return We.assertObjectType}});Object.defineProperty(me,"assertInterfaceType",{enumerable:!0,get:function(){return We.assertInterfaceType}});Object.defineProperty(me,"assertUnionType",{enumerable:!0,get:function(){return We.assertUnionType}});Object.defineProperty(me,"assertEnumType",{enumerable:!0,get:function(){return We.assertEnumType}});Object.defineProperty(me,"assertInputObjectType",{enumerable:!0,get:function(){return We.assertInputObjectType}});Object.defineProperty(me,"assertListType",{enumerable:!0,get:function(){return We.assertListType}});Object.defineProperty(me,"assertNonNullType",{enumerable:!0,get:function(){return We.assertNonNullType}});Object.defineProperty(me,"assertInputType",{enumerable:!0,get:function(){return We.assertInputType}});Object.defineProperty(me,"assertOutputType",{enumerable:!0,get:function(){return We.assertOutputType}});Object.defineProperty(me,"assertLeafType",{enumerable:!0,get:function(){return We.assertLeafType}});Object.defineProperty(me,"assertCompositeType",{enumerable:!0,get:function(){return We.assertCompositeType}});Object.defineProperty(me,"assertAbstractType",{enumerable:!0,get:function(){return We.assertAbstractType}});Object.defineProperty(me,"assertWrappingType",{enumerable:!0,get:function(){return We.assertWrappingType}});Object.defineProperty(me,"assertNullableType",{enumerable:!0,get:function(){return We.assertNullableType}});Object.defineProperty(me,"assertNamedType",{enumerable:!0,get:function(){return We.assertNamedType}});Object.defineProperty(me,"getNullableType",{enumerable:!0,get:function(){return We.getNullableType}});Object.defineProperty(me,"getNamedType",{enumerable:!0,get:function(){return We.getNamedType}});Object.defineProperty(me,"GraphQLScalarType",{enumerable:!0,get:function(){return We.GraphQLScalarType}});Object.defineProperty(me,"GraphQLObjectType",{enumerable:!0,get:function(){return We.GraphQLObjectType}});Object.defineProperty(me,"GraphQLInterfaceType",{enumerable:!0,get:function(){return We.GraphQLInterfaceType}});Object.defineProperty(me,"GraphQLUnionType",{enumerable:!0,get:function(){return We.GraphQLUnionType}});Object.defineProperty(me,"GraphQLEnumType",{enumerable:!0,get:function(){return We.GraphQLEnumType}});Object.defineProperty(me,"GraphQLInputObjectType",{enumerable:!0,get:function(){return We.GraphQLInputObjectType}});Object.defineProperty(me,"GraphQLList",{enumerable:!0,get:function(){return We.GraphQLList}});Object.defineProperty(me,"GraphQLNonNull",{enumerable:!0,get:function(){return We.GraphQLNonNull}});Object.defineProperty(me,"isDirective",{enumerable:!0,get:function(){return La.isDirective}});Object.defineProperty(me,"assertDirective",{enumerable:!0,get:function(){return La.assertDirective}});Object.defineProperty(me,"GraphQLDirective",{enumerable:!0,get:function(){return La.GraphQLDirective}});Object.defineProperty(me,"isSpecifiedDirective",{enumerable:!0,get:function(){return La.isSpecifiedDirective}});Object.defineProperty(me,"specifiedDirectives",{enumerable:!0,get:function(){return La.specifiedDirectives}});Object.defineProperty(me,"GraphQLIncludeDirective",{enumerable:!0,get:function(){return La.GraphQLIncludeDirective}});Object.defineProperty(me,"GraphQLSkipDirective",{enumerable:!0,get:function(){return La.GraphQLSkipDirective}});Object.defineProperty(me,"GraphQLDeprecatedDirective",{enumerable:!0,get:function(){return La.GraphQLDeprecatedDirective}});Object.defineProperty(me,"GraphQLSpecifiedByDirective",{enumerable:!0,get:function(){return La.GraphQLSpecifiedByDirective}});Object.defineProperty(me,"DEFAULT_DEPRECATION_REASON",{enumerable:!0,get:function(){return La.DEFAULT_DEPRECATION_REASON}});Object.defineProperty(me,"isSpecifiedScalarType",{enumerable:!0,get:function(){return vs.isSpecifiedScalarType}});Object.defineProperty(me,"specifiedScalarTypes",{enumerable:!0,get:function(){return vs.specifiedScalarTypes}});Object.defineProperty(me,"GraphQLInt",{enumerable:!0,get:function(){return vs.GraphQLInt}});Object.defineProperty(me,"GraphQLFloat",{enumerable:!0,get:function(){return vs.GraphQLFloat}});Object.defineProperty(me,"GraphQLString",{enumerable:!0,get:function(){return vs.GraphQLString}});Object.defineProperty(me,"GraphQLBoolean",{enumerable:!0,get:function(){return vs.GraphQLBoolean}});Object.defineProperty(me,"GraphQLID",{enumerable:!0,get:function(){return vs.GraphQLID}});Object.defineProperty(me,"isIntrospectionType",{enumerable:!0,get:function(){return Zn.isIntrospectionType}});Object.defineProperty(me,"introspectionTypes",{enumerable:!0,get:function(){return Zn.introspectionTypes}});Object.defineProperty(me,"__Schema",{enumerable:!0,get:function(){return Zn.__Schema}});Object.defineProperty(me,"__Directive",{enumerable:!0,get:function(){return Zn.__Directive}});Object.defineProperty(me,"__DirectiveLocation",{enumerable:!0,get:function(){return Zn.__DirectiveLocation}});Object.defineProperty(me,"__Type",{enumerable:!0,get:function(){return Zn.__Type}});Object.defineProperty(me,"__Field",{enumerable:!0,get:function(){return Zn.__Field}});Object.defineProperty(me,"__InputValue",{enumerable:!0,get:function(){return Zn.__InputValue}});Object.defineProperty(me,"__EnumValue",{enumerable:!0,get:function(){return Zn.__EnumValue}});Object.defineProperty(me,"__TypeKind",{enumerable:!0,get:function(){return Zn.__TypeKind}});Object.defineProperty(me,"TypeKind",{enumerable:!0,get:function(){return Zn.TypeKind}});Object.defineProperty(me,"SchemaMetaFieldDef",{enumerable:!0,get:function(){return Zn.SchemaMetaFieldDef}});Object.defineProperty(me,"TypeMetaFieldDef",{enumerable:!0,get:function(){return Zn.TypeMetaFieldDef}});Object.defineProperty(me,"TypeNameMetaFieldDef",{enumerable:!0,get:function(){return Zn.TypeNameMetaFieldDef}});Object.defineProperty(me,"validateSchema",{enumerable:!0,get:function(){return Ox.validateSchema}});Object.defineProperty(me,"assertValidSchema",{enumerable:!0,get:function(){return Ox.assertValidSchema}});var TS=us(),We=lt(),La=Jn(),vs=Ca(),Zn=Yn(),Ox=Pd()});var Nx=U(Ft=>{"use strict";Object.defineProperty(Ft,"__esModule",{value:!0});Object.defineProperty(Ft,"Source",{enumerable:!0,get:function(){return kH.Source}});Object.defineProperty(Ft,"getLocation",{enumerable:!0,get:function(){return OH.getLocation}});Object.defineProperty(Ft,"printLocation",{enumerable:!0,get:function(){return wx.printLocation}});Object.defineProperty(Ft,"printSourceLocation",{enumerable:!0,get:function(){return wx.printSourceLocation}});Object.defineProperty(Ft,"Kind",{enumerable:!0,get:function(){return CH.Kind}});Object.defineProperty(Ft,"TokenKind",{enumerable:!0,get:function(){return wH.TokenKind}});Object.defineProperty(Ft,"Lexer",{enumerable:!0,get:function(){return AH.Lexer}});Object.defineProperty(Ft,"parse",{enumerable:!0,get:function(){return ES.parse}});Object.defineProperty(Ft,"parseValue",{enumerable:!0,get:function(){return ES.parseValue}});Object.defineProperty(Ft,"parseType",{enumerable:!0,get:function(){return ES.parseType}});Object.defineProperty(Ft,"print",{enumerable:!0,get:function(){return NH.print}});Object.defineProperty(Ft,"visit",{enumerable:!0,get:function(){return Xg.visit}});Object.defineProperty(Ft,"visitInParallel",{enumerable:!0,get:function(){return Xg.visitInParallel}});Object.defineProperty(Ft,"getVisitFn",{enumerable:!0,get:function(){return Xg.getVisitFn}});Object.defineProperty(Ft,"BREAK",{enumerable:!0,get:function(){return Xg.BREAK}});Object.defineProperty(Ft,"Location",{enumerable:!0,get:function(){return Ax.Location}});Object.defineProperty(Ft,"Token",{enumerable:!0,get:function(){return Ax.Token}});Object.defineProperty(Ft,"isDefinitionNode",{enumerable:!0,get:function(){return vo.isDefinitionNode}});Object.defineProperty(Ft,"isExecutableDefinitionNode",{enumerable:!0,get:function(){return vo.isExecutableDefinitionNode}});Object.defineProperty(Ft,"isSelectionNode",{enumerable:!0,get:function(){return vo.isSelectionNode}});Object.defineProperty(Ft,"isValueNode",{enumerable:!0,get:function(){return vo.isValueNode}});Object.defineProperty(Ft,"isTypeNode",{enumerable:!0,get:function(){return vo.isTypeNode}});Object.defineProperty(Ft,"isTypeSystemDefinitionNode",{enumerable:!0,get:function(){return vo.isTypeSystemDefinitionNode}});Object.defineProperty(Ft,"isTypeDefinitionNode",{enumerable:!0,get:function(){return vo.isTypeDefinitionNode}});Object.defineProperty(Ft,"isTypeSystemExtensionNode",{enumerable:!0,get:function(){return vo.isTypeSystemExtensionNode}});Object.defineProperty(Ft,"isTypeExtensionNode",{enumerable:!0,get:function(){return vo.isTypeExtensionNode}});Object.defineProperty(Ft,"DirectiveLocation",{enumerable:!0,get:function(){return LH.DirectiveLocation}});var kH=Zv(),OH=qv(),wx=CT(),CH=Vt(),wH=Rl(),AH=tg(),ES=Pl(),NH=Wn(),Xg=eu(),Ax=Il(),vo=ls(),LH=Fl()});var Lx=U(hu=>{"use strict";Object.defineProperty(hu,"__esModule",{value:!0});Object.defineProperty(hu,"responsePathAsArray",{enumerable:!0,get:function(){return xH.pathToArray}});Object.defineProperty(hu,"execute",{enumerable:!0,get:function(){return Zg.execute}});Object.defineProperty(hu,"executeSync",{enumerable:!0,get:function(){return Zg.executeSync}});Object.defineProperty(hu,"defaultFieldResolver",{enumerable:!0,get:function(){return Zg.defaultFieldResolver}});Object.defineProperty(hu,"defaultTypeResolver",{enumerable:!0,get:function(){return Zg.defaultTypeResolver}});Object.defineProperty(hu,"getDirectiveValues",{enumerable:!0,get:function(){return IH.getDirectiveValues}});var xH=Ud(),Zg=Zd(),IH=Wd()});var xx=U(_S=>{"use strict";Object.defineProperty(_S,"__esModule",{value:!0});_S.default=FH;var RH=Da();function FH(e){return typeof(e==null?void 0:e[RH.SYMBOL_ASYNC_ITERATOR])=="function"}});var jx=U(SS=>{"use strict";Object.defineProperty(SS,"__esModule",{value:!0});SS.default=PH;var Ix=Da();function jH(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function PH(e,t,r){var n=e[Ix.SYMBOL_ASYNC_ITERATOR],a=n.call(e),o,s;typeof a.return=="function"&&(o=a.return,s=function(b){var T=function(){return Promise.reject(b)};return o.call(a).then(T,T)});function l(v){return v.done?v:Rx(v.value,t).then(Fx,s)}var d;if(r){var h=r;d=function(b){return Rx(b,h).then(Fx,s)}}return jH({next:function(){return a.next().then(l,d)},return:function(){return o?o.call(a).then(l,d):Promise.resolve({value:void 0,done:!0})},throw:function(b){return typeof a.throw=="function"?a.throw(b).then(l,d):Promise.reject(b).catch(s)}},Ix.SYMBOL_ASYNC_ITERATOR,function(){return this})}function Rx(e,t){return new Promise(function(r){return r(t(e))})}function Fx(e){return{value:e,done:!1}}});var Gx=U($g=>{"use strict";Object.defineProperty($g,"__esModule",{value:!0});$g.subscribe=UH;$g.createSourceEventStream=Ux;var MH=kS(Ot()),Px=kS(xx()),DS=Ud(),Mx=Be(),qx=Td(),qH=Wd(),ac=Zd(),BH=Ug(),VH=kS(jx());function kS(e){return e&&e.__esModule?e:{default:e}}function UH(e,t,r,n,a,o,s,l){return arguments.length===1?Vx(e):Vx({schema:e,document:t,rootValue:r,contextValue:n,variableValues:a,operationName:o,fieldResolver:s,subscribeFieldResolver:l})}function Bx(e){if(e instanceof Mx.GraphQLError)return{errors:[e]};throw e}function Vx(e){var t=e.schema,r=e.document,n=e.rootValue,a=e.contextValue,o=e.variableValues,s=e.operationName,l=e.fieldResolver,d=e.subscribeFieldResolver,h=Ux(t,r,n,a,o,s,d),v=function(T){return(0,ac.execute)({schema:t,document:r,rootValue:T,contextValue:a,variableValues:o,operationName:s,fieldResolver:l})};return h.then(function(b){return(0,Px.default)(b)?(0,VH.default)(b,v,Bx):b})}function Ux(e,t,r,n,a,o,s){return(0,ac.assertValidExecutionArguments)(e,t,a),new Promise(function(l){var d=(0,ac.buildExecutionContext)(e,t,r,n,a,o,s);l(Array.isArray(d)?{errors:d}:GH(d))}).catch(Bx)}function GH(e){var t=e.schema,r=e.operation,n=e.variableValues,a=e.rootValue,o=(0,BH.getOperationRootType)(t,r),s=(0,ac.collectFields)(e,o,r.selectionSet,Object.create(null),Object.create(null)),l=Object.keys(s),d=l[0],h=s[d],v=h[0],b=v.name.value,T=(0,ac.getFieldDef)(t,o,b);if(!T)throw new Mx.GraphQLError('The subscription field "'.concat(b,'" is not defined.'),h);var A=(0,DS.addPath)(void 0,d,o.name),L=(0,ac.buildResolveInfo)(e,T,h,o,A);return new Promise(function(S){var y,_=(0,qH.getArgumentValues)(T,h[0],n),m=e.contextValue,k=(y=T.subscribe)!==null&&y!==void 0?y:e.fieldResolver;S(k(a,_,m,L))}).then(function(S){if(S instanceof Error)throw(0,qx.locatedError)(S,h,(0,DS.pathToArray)(A));if(!(0,Px.default)(S))throw new Error("Subscription field must return Async Iterable. "+"Received: ".concat((0,MH.default)(S),"."));return S},function(S){throw(0,qx.locatedError)(S,h,(0,DS.pathToArray)(A))})}});var Kx=U(em=>{"use strict";Object.defineProperty(em,"__esModule",{value:!0});Object.defineProperty(em,"subscribe",{enumerable:!0,get:function(){return Qx.subscribe}});Object.defineProperty(em,"createSourceEventStream",{enumerable:!0,get:function(){return Qx.createSourceEventStream}});var Qx=Gx()});var AS=U(wS=>{"use strict";Object.defineProperty(wS,"__esModule",{value:!0});wS.NoDeprecatedCustomRule=KH;var OS=QH(un()),$d=Be(),CS=lt();function QH(e){return e&&e.__esModule?e:{default:e}}function KH(e){return{Field:function(r){var n=e.getFieldDef(),a=n==null?void 0:n.deprecationReason;if(n&&a!=null){var o=e.getParentType();o!=null||(0,OS.default)(0),e.reportError(new $d.GraphQLError("The field ".concat(o.name,".").concat(n.name," is deprecated. ").concat(a),r))}},Argument:function(r){var n=e.getArgument(),a=n==null?void 0:n.deprecationReason;if(n&&a!=null){var o=e.getDirective();if(o!=null)e.reportError(new $d.GraphQLError('Directive "@'.concat(o.name,'" argument "').concat(n.name,'" is deprecated. ').concat(a),r));else{var s=e.getParentType(),l=e.getFieldDef();s!=null&&l!=null||(0,OS.default)(0),e.reportError(new $d.GraphQLError('Field "'.concat(s.name,".").concat(l.name,'" argument "').concat(n.name,'" is deprecated. ').concat(a),r))}}},ObjectField:function(r){var n=(0,CS.getNamedType)(e.getParentInputType());if((0,CS.isInputObjectType)(n)){var a=n.getFields()[r.name.value],o=a==null?void 0:a.deprecationReason;o!=null&&e.reportError(new $d.GraphQLError("The input field ".concat(n.name,".").concat(a.name," is deprecated. ").concat(o),r))}},EnumValue:function(r){var n=e.getEnumValue(),a=n==null?void 0:n.deprecationReason;if(n&&a!=null){var o=(0,CS.getNamedType)(e.getInputType());o!=null||(0,OS.default)(0),e.reportError(new $d.GraphQLError('The enum value "'.concat(o.name,".").concat(n.name,'" is deprecated. ').concat(a),r))}}}}});var Hx=U(NS=>{"use strict";Object.defineProperty(NS,"__esModule",{value:!0});NS.NoSchemaIntrospectionCustomRule=YH;var HH=Be(),zH=lt(),WH=Yn();function YH(e){return{Field:function(r){var n=(0,zH.getNamedType)(e.getType());n&&(0,WH.isIntrospectionType)(n)&&e.reportError(new HH.GraphQLError('GraphQL introspection has been disabled, but the requested query contained the field "'.concat(r.name.value,'".'),r))}}}});var zx=U(et=>{"use strict";Object.defineProperty(et,"__esModule",{value:!0});Object.defineProperty(et,"validate",{enumerable:!0,get:function(){return JH.validate}});Object.defineProperty(et,"ValidationContext",{enumerable:!0,get:function(){return XH.ValidationContext}});Object.defineProperty(et,"specifiedRules",{enumerable:!0,get:function(){return ZH.specifiedRules}});Object.defineProperty(et,"ExecutableDefinitionsRule",{enumerable:!0,get:function(){return $H.ExecutableDefinitionsRule}});Object.defineProperty(et,"FieldsOnCorrectTypeRule",{enumerable:!0,get:function(){return ez.FieldsOnCorrectTypeRule}});Object.defineProperty(et,"FragmentsOnCompositeTypesRule",{enumerable:!0,get:function(){return tz.FragmentsOnCompositeTypesRule}});Object.defineProperty(et,"KnownArgumentNamesRule",{enumerable:!0,get:function(){return rz.KnownArgumentNamesRule}});Object.defineProperty(et,"KnownDirectivesRule",{enumerable:!0,get:function(){return nz.KnownDirectivesRule}});Object.defineProperty(et,"KnownFragmentNamesRule",{enumerable:!0,get:function(){return iz.KnownFragmentNamesRule}});Object.defineProperty(et,"KnownTypeNamesRule",{enumerable:!0,get:function(){return az.KnownTypeNamesRule}});Object.defineProperty(et,"LoneAnonymousOperationRule",{enumerable:!0,get:function(){return oz.LoneAnonymousOperationRule}});Object.defineProperty(et,"NoFragmentCyclesRule",{enumerable:!0,get:function(){return uz.NoFragmentCyclesRule}});Object.defineProperty(et,"NoUndefinedVariablesRule",{enumerable:!0,get:function(){return sz.NoUndefinedVariablesRule}});Object.defineProperty(et,"NoUnusedFragmentsRule",{enumerable:!0,get:function(){return lz.NoUnusedFragmentsRule}});Object.defineProperty(et,"NoUnusedVariablesRule",{enumerable:!0,get:function(){return cz.NoUnusedVariablesRule}});Object.defineProperty(et,"OverlappingFieldsCanBeMergedRule",{enumerable:!0,get:function(){return fz.OverlappingFieldsCanBeMergedRule}});Object.defineProperty(et,"PossibleFragmentSpreadsRule",{enumerable:!0,get:function(){return dz.PossibleFragmentSpreadsRule}});Object.defineProperty(et,"ProvidedRequiredArgumentsRule",{enumerable:!0,get:function(){return pz.ProvidedRequiredArgumentsRule}});Object.defineProperty(et,"ScalarLeafsRule",{enumerable:!0,get:function(){return hz.ScalarLeafsRule}});Object.defineProperty(et,"SingleFieldSubscriptionsRule",{enumerable:!0,get:function(){return vz.SingleFieldSubscriptionsRule}});Object.defineProperty(et,"UniqueArgumentNamesRule",{enumerable:!0,get:function(){return gz.UniqueArgumentNamesRule}});Object.defineProperty(et,"UniqueDirectivesPerLocationRule",{enumerable:!0,get:function(){return mz.UniqueDirectivesPerLocationRule}});Object.defineProperty(et,"UniqueFragmentNamesRule",{enumerable:!0,get:function(){return yz.UniqueFragmentNamesRule}});Object.defineProperty(et,"UniqueInputFieldNamesRule",{enumerable:!0,get:function(){return bz.UniqueInputFieldNamesRule}});Object.defineProperty(et,"UniqueOperationNamesRule",{enumerable:!0,get:function(){return Tz.UniqueOperationNamesRule}});Object.defineProperty(et,"UniqueVariableNamesRule",{enumerable:!0,get:function(){return Ez.UniqueVariableNamesRule}});Object.defineProperty(et,"ValuesOfCorrectTypeRule",{enumerable:!0,get:function(){return _z.ValuesOfCorrectTypeRule}});Object.defineProperty(et,"VariablesAreInputTypesRule",{enumerable:!0,get:function(){return Sz.VariablesAreInputTypesRule}});Object.defineProperty(et,"VariablesInAllowedPositionRule",{enumerable:!0,get:function(){return Dz.VariablesInAllowedPositionRule}});Object.defineProperty(et,"LoneSchemaDefinitionRule",{enumerable:!0,get:function(){return kz.LoneSchemaDefinitionRule}});Object.defineProperty(et,"UniqueOperationTypesRule",{enumerable:!0,get:function(){return Oz.UniqueOperationTypesRule}});Object.defineProperty(et,"UniqueTypeNamesRule",{enumerable:!0,get:function(){return Cz.UniqueTypeNamesRule}});Object.defineProperty(et,"UniqueEnumValueNamesRule",{enumerable:!0,get:function(){return wz.UniqueEnumValueNamesRule}});Object.defineProperty(et,"UniqueFieldDefinitionNamesRule",{enumerable:!0,get:function(){return Az.UniqueFieldDefinitionNamesRule}});Object.defineProperty(et,"UniqueDirectiveNamesRule",{enumerable:!0,get:function(){return Nz.UniqueDirectiveNamesRule}});Object.defineProperty(et,"PossibleTypeExtensionsRule",{enumerable:!0,get:function(){return Lz.PossibleTypeExtensionsRule}});Object.defineProperty(et,"NoDeprecatedCustomRule",{enumerable:!0,get:function(){return xz.NoDeprecatedCustomRule}});Object.defineProperty(et,"NoSchemaIntrospectionCustomRule",{enumerable:!0,get:function(){return Iz.NoSchemaIntrospectionCustomRule}});var JH=$l(),XH=aS(),ZH=nS(),$H=ME(),ez=t_(),tz=YE(),rz=D_(),nz=T_(),iz=a_(),az=zE(),oz=UE(),uz=d_(),sz=g_(),lz=u_(),cz=y_(),fz=q_(),dz=c_(),pz=N_(),hz=$E(),vz=QE(),gz=O_(),mz=S_(),yz=n_(),bz=V_(),Tz=BE(),Ez=h_(),_z=w_(),Sz=XE(),Dz=x_(),kz=G_(),Oz=K_(),Cz=z_(),wz=Y_(),Az=Z_(),Nz=eS(),Lz=rS(),xz=AS(),Iz=Hx()});var Wx=U(LS=>{"use strict";Object.defineProperty(LS,"__esModule",{value:!0});LS.formatError=jz;var Rz=Fz(wi());function Fz(e){return e&&e.__esModule?e:{default:e}}function jz(e){var t;e||(0,Rz.default)(0,"Received null or undefined error.");var r=(t=e.message)!==null&&t!==void 0?t:"An unknown error occurred.",n=e.locations,a=e.path,o=e.extensions;return o?{message:r,locations:n,path:a,extensions:o}:{message:r,locations:n,path:a}}});var Jx=U(gs=>{"use strict";Object.defineProperty(gs,"__esModule",{value:!0});Object.defineProperty(gs,"GraphQLError",{enumerable:!0,get:function(){return Yx.GraphQLError}});Object.defineProperty(gs,"printError",{enumerable:!0,get:function(){return Yx.printError}});Object.defineProperty(gs,"syntaxError",{enumerable:!0,get:function(){return Pz.syntaxError}});Object.defineProperty(gs,"locatedError",{enumerable:!0,get:function(){return Mz.locatedError}});Object.defineProperty(gs,"formatError",{enumerable:!0,get:function(){return qz.formatError}});var Yx=Be(),Pz=Qv(),Mz=Td(),qz=Wx()});var IS=U(xS=>{"use strict";Object.defineProperty(xS,"__esModule",{value:!0});xS.getIntrospectionQuery=Uz;function Xx(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function Bz(e){for(var t=1;t{"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(RS,"__esModule",{value:!0});RS.getOperationAST=Qz;var Gz=Vt();function Qz(e,t){for(var r=null,n=0,a=e.definitions;n{"use strict";Object.defineProperty(jS,"__esModule",{value:!0});jS.introspectionFromSchema=Zz;var Kz=Yz(un()),Hz=Pl(),zz=Zd(),Wz=IS();function Yz(e){return e&&e.__esModule?e:{default:e}}function Zx(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function Jz(e){for(var t=1;t{"use strict";Object.defineProperty(PS,"__esModule",{value:!0});PS.buildClientSchema=o7;var $z=ep(oi()),li=ep(Ot()),e7=ep(wi()),tm=ep(Ed()),eI=ep(Sa()),t7=Pl(),r7=us(),n7=Jn(),i7=Ca(),xa=Yn(),ci=lt(),a7=Qd();function ep(e){return e&&e.__esModule?e:{default:e}}function o7(e,t){(0,eI.default)(e)&&(0,eI.default)(e.__schema)||(0,e7.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,li.default)(e),"."));for(var r=e.__schema,n=(0,tm.default)(r.types,function(G){return G.name},function(G){return S(G)}),a=0,o=[].concat(i7.specifiedScalarTypes,xa.introspectionTypes);a{"use strict";Object.defineProperty(rp,"__esModule",{value:!0});rp.extendSchema=h7;rp.extendSchemaImpl=fI;rp.getDescription=ms;var u7=oc(oi()),s7=oc(tu()),rI=oc(Ot()),tp=oc(QT()),nI=oc(un()),l7=oc(wi()),xi=Vt(),c7=Rl(),f7=jl(),iI=ls(),d7=$l(),aI=Wd(),oI=us(),uI=Ca(),sI=Yn(),rm=Jn(),nr=lt(),lI=Qd();function oc(e){return e&&e.__esModule?e:{default:e}}function cI(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function Et(e){for(var t=1;t0?r.reverse().join(`
-`):void 0}}});var gI=U(im=>{"use strict";Object.defineProperty(im,"__esModule",{value:!0});im.buildASTSchema=vI;im.buildSchema=S7;var g7=_7(wi()),m7=Vt(),y7=Pl(),b7=$l(),T7=us(),hI=Jn(),E7=MS();function _7(e){return e&&e.__esModule?e:{default:e}}function vI(e,t){e!=null&&e.kind===m7.Kind.DOCUMENT||(0,g7.default)(0,"Must provide valid Document AST."),(t==null?void 0:t.assumeValid)!==!0&&(t==null?void 0:t.assumeValidSDL)!==!0&&(0,b7.assertValidSDL)(e);var r={description:void 0,types:[],directives:[],extensions:void 0,extensionASTNodes:[],assumeValid:!1},n=(0,E7.extendSchemaImpl)(r,e,t);if(n.astNode==null)for(var a=0,o=n.types;a{"use strict";Object.defineProperty(VS,"__esModule",{value:!0});VS.lexicographicSortSchema=I7;var D7=np(oi()),k7=np(Ot()),O7=np(un()),C7=np(Ed()),w7=np(_d()),A7=us(),N7=Jn(),L7=Yn(),Fn=lt();function np(e){return e&&e.__esModule?e:{default:e}}function mI(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function kr(e){for(var t=1;t{"use strict";Object.defineProperty(ip,"__esModule",{value:!0});ip.printSchema=j7;ip.printIntrospectionSchema=P7;ip.printType=_I;var US=zS(oi()),R7=zS(Ot()),bI=zS(un()),GS=Wn(),F7=jl(),TI=Yn(),QS=Ca(),KS=Jn(),uc=lt(),HS=Id();function zS(e){return e&&e.__esModule?e:{default:e}}function j7(e,t){return EI(e,function(r){return!(0,KS.isSpecifiedDirective)(r)},M7,t)}function P7(e,t){return EI(e,KS.isSpecifiedDirective,TI.isIntrospectionType,t)}function M7(e){return!(0,QS.isSpecifiedScalarType)(e)&&!(0,TI.isIntrospectionType)(e)}function EI(e,t,r,n){var a=e.getDirectives().filter(t),o=(0,US.default)(e.getTypeMap()).filter(r);return[q7(e)].concat(a.map(function(s){return z7(s,n)}),o.map(function(s){return _I(s,n)})).filter(Boolean).join(`
+ `)}});var mk=G(gk=>{"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 q7(e){if(!(e.description==null&&B7(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 a=e.getSubscriptionType();return a&&t.push(" subscription: ".concat(a.name)),Ii({},e)+`schema {
+`}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 B7(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 _I(e,t){if((0,uc.isScalarType)(e))return V7(e,t);if((0,uc.isObjectType)(e))return U7(e,t);if((0,uc.isInterfaceType)(e))return G7(e,t);if((0,uc.isUnionType)(e))return Q7(e,t);if((0,uc.isEnumType)(e))return K7(e,t);if((0,uc.isInputObjectType)(e))return H7(e,t);(0,bI.default)(0,"Unexpected type: "+(0,R7.default)(e))}function V7(e,t){return Ii(t,e)+"scalar ".concat(e.name)+W7(e)}function SI(e){var t=e.getInterfaces();return t.length?" implements "+t.map(function(r){return r.name}).join(" & "):""}function U7(e,t){return Ii(t,e)+"type ".concat(e.name)+SI(e)+DI(t,e)}function G7(e,t){return Ii(t,e)+"interface ".concat(e.name)+SI(e)+DI(t,e)}function Q7(e,t){var r=e.getTypes(),n=r.length?" = "+r.join(" | "):"";return Ii(t,e)+"union "+e.name+n}function K7(e,t){var r=e.getValues().map(function(n,a){return Ii(t,n," ",!a)+" "+n.name+JS(n.deprecationReason)});return Ii(t,e)+"enum ".concat(e.name)+WS(r)}function H7(e,t){var r=(0,US.default)(e.getFields()).map(function(n,a){return Ii(t,n," ",!a)+" "+YS(n)});return Ii(t,e)+"input ".concat(e.name)+WS(r)}function DI(e,t){var r=(0,US.default)(t.getFields()).map(function(n,a){return Ii(e,n," ",!a)+" "+n.name+kI(e,n.args," ")+": "+String(n.type)+JS(n.deprecationReason)});return WS(r)}function WS(e){return e.length!==0?` {
+}`)}}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 kI(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(YS).join(", ")+")":`(
-`+t.map(function(n,a){return Ii(e,n," "+r,!a)+" "+r+YS(n)}).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 YS(e){var t=(0,HS.astFromValue)(e.defaultValue,e.type),r=e.name+": "+String(e.type);return t&&(r+=" = ".concat((0,GS.print)(t))),r+JS(e.deprecationReason)}function z7(e,t){return Ii(t,e)+"directive @"+e.name+kI(t,e.args)+(e.isRepeatable?" repeatable":"")+" on "+e.locations.join(" | ")}function JS(e){if(e==null)return"";var t=(0,HS.astFromValue)(e,QS.GraphQLString);return t&&e!==KS.DEFAULT_DEPRECATION_REASON?" @deprecated(reason: "+(0,GS.print)(t)+")":" @deprecated"}function W7(e){if(e.specifiedByUrl==null)return"";var t=e.specifiedByUrl,r=(0,HS.astFromValue)(t,QS.GraphQLString);return r||(0,bI.default)(0,"Unexpected null value returned from `astFromValue` for specifiedByUrl")," @specifiedBy(url: "+(0,GS.print)(r)+")"}function Ii(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:"",n=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!0,a=t.description;if(a==null)return"";if((e==null?void 0:e.commentDescriptions)===!0)return Y7(a,r,n);var o=a.length>70,s=(0,F7.printBlockString)(a,"",o),l=r&&!n?`
+`+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 Y7(e,t,r){var n=t&&!r?`
-`:"",a=e.split(`
+`}function bJ(e,t,r){var n=t&&!r?`
+`:"",i=e.split(`
`).map(function(o){return t+(o!==""?"# "+o:"#")}).join(`
-`);return n+a+`
-`}});var CI=U(XS=>{"use strict";Object.defineProperty(XS,"__esModule",{value:!0});XS.concatAST=J7;function J7(e){for(var t=[],r=0;r{"use strict";Object.defineProperty(ZS,"__esModule",{value:!0});ZS.separateOperations=Z7;var om=Vt(),X7=eu();function Z7(e){for(var t=[],r=Object.create(null),n=0,a=e.definitions;n{"use strict";Object.defineProperty(eD,"__esModule",{value:!0});eD.stripIgnoredCharacters=$7;var LI=Zv(),$S=Rl(),xI=tg(),II=jl();function $7(e){for(var t=(0,LI.isSource)(e)?e:new LI.Source(e),r=t.body,n=new xI.Lexer(t),a="",o=!1;n.advance().kind!==$S.TokenKind.EOF;){var s=n.token,l=s.kind,d=!(0,xI.isPunctuatorTokenKind)(s.kind);o&&(d||s.kind===$S.TokenKind.SPREAD)&&(a+=" ");var h=r.slice(s.start,s.end);l===$S.TokenKind.BLOCK_STRING?a+=eW(h):a+=h,o=d}return a}function eW(e){var t=e.slice(3,-3),r=(0,II.dedentBlockStringValue)(t);(0,II.getBlockStringIndentation)(r)>0&&(r=`
-`+r);var n=r[r.length-1],a=n==='"'&&r.slice(-4)!=='\\"""';return(a||n==="\\")&&(r+=`
-`),'"""'+r+'"""'}});var QI=U(vu=>{"use strict";Object.defineProperty(vu,"__esModule",{value:!0});vu.findBreakingChanges=sW;vu.findDangerousChanges=lW;vu.DangerousChangeType=vu.BreakingChangeType=void 0;var sc=ap(oi()),FI=ap(tu()),tW=ap(Ot()),jI=ap(un()),rW=ap(_d()),nW=Wn(),iW=eu(),aW=Ca(),_t=lt(),oW=Id();function ap(e){return e&&e.__esModule?e:{default:e}}function PI(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable})),r.push.apply(r,n)}return r}function MI(e){for(var t=1;t{"use strict";Object.defineProperty(tD,"__esModule",{value:!0});tD.findDeprecatedUsages=yW;var gW=$l(),mW=AS();function yW(e,t){return(0,gW.validate)(e,t,[mW.NoDeprecatedCustomRule])}});var JI=U(st=>{"use strict";Object.defineProperty(st,"__esModule",{value:!0});Object.defineProperty(st,"getIntrospectionQuery",{enumerable:!0,get:function(){return bW.getIntrospectionQuery}});Object.defineProperty(st,"getOperationAST",{enumerable:!0,get:function(){return TW.getOperationAST}});Object.defineProperty(st,"getOperationRootType",{enumerable:!0,get:function(){return EW.getOperationRootType}});Object.defineProperty(st,"introspectionFromSchema",{enumerable:!0,get:function(){return _W.introspectionFromSchema}});Object.defineProperty(st,"buildClientSchema",{enumerable:!0,get:function(){return SW.buildClientSchema}});Object.defineProperty(st,"buildASTSchema",{enumerable:!0,get:function(){return HI.buildASTSchema}});Object.defineProperty(st,"buildSchema",{enumerable:!0,get:function(){return HI.buildSchema}});Object.defineProperty(st,"extendSchema",{enumerable:!0,get:function(){return zI.extendSchema}});Object.defineProperty(st,"getDescription",{enumerable:!0,get:function(){return zI.getDescription}});Object.defineProperty(st,"lexicographicSortSchema",{enumerable:!0,get:function(){return DW.lexicographicSortSchema}});Object.defineProperty(st,"printSchema",{enumerable:!0,get:function(){return rD.printSchema}});Object.defineProperty(st,"printType",{enumerable:!0,get:function(){return rD.printType}});Object.defineProperty(st,"printIntrospectionSchema",{enumerable:!0,get:function(){return rD.printIntrospectionSchema}});Object.defineProperty(st,"typeFromAST",{enumerable:!0,get:function(){return kW.typeFromAST}});Object.defineProperty(st,"valueFromAST",{enumerable:!0,get:function(){return OW.valueFromAST}});Object.defineProperty(st,"valueFromASTUntyped",{enumerable:!0,get:function(){return CW.valueFromASTUntyped}});Object.defineProperty(st,"astFromValue",{enumerable:!0,get:function(){return wW.astFromValue}});Object.defineProperty(st,"TypeInfo",{enumerable:!0,get:function(){return WI.TypeInfo}});Object.defineProperty(st,"visitWithTypeInfo",{enumerable:!0,get:function(){return WI.visitWithTypeInfo}});Object.defineProperty(st,"coerceInputValue",{enumerable:!0,get:function(){return AW.coerceInputValue}});Object.defineProperty(st,"concatAST",{enumerable:!0,get:function(){return NW.concatAST}});Object.defineProperty(st,"separateOperations",{enumerable:!0,get:function(){return LW.separateOperations}});Object.defineProperty(st,"stripIgnoredCharacters",{enumerable:!0,get:function(){return xW.stripIgnoredCharacters}});Object.defineProperty(st,"isEqualType",{enumerable:!0,get:function(){return nD.isEqualType}});Object.defineProperty(st,"isTypeSubTypeOf",{enumerable:!0,get:function(){return nD.isTypeSubTypeOf}});Object.defineProperty(st,"doTypesOverlap",{enumerable:!0,get:function(){return nD.doTypesOverlap}});Object.defineProperty(st,"assertValidName",{enumerable:!0,get:function(){return YI.assertValidName}});Object.defineProperty(st,"isValidNameError",{enumerable:!0,get:function(){return YI.isValidNameError}});Object.defineProperty(st,"BreakingChangeType",{enumerable:!0,get:function(){return um.BreakingChangeType}});Object.defineProperty(st,"DangerousChangeType",{enumerable:!0,get:function(){return um.DangerousChangeType}});Object.defineProperty(st,"findBreakingChanges",{enumerable:!0,get:function(){return um.findBreakingChanges}});Object.defineProperty(st,"findDangerousChanges",{enumerable:!0,get:function(){return um.findDangerousChanges}});Object.defineProperty(st,"findDeprecatedUsages",{enumerable:!0,get:function(){return IW.findDeprecatedUsages}});var bW=IS(),TW=FS(),EW=Ug(),_W=$x(),SW=tI(),HI=gI(),zI=MS(),DW=yI(),rD=OI(),kW=wa(),OW=Qd(),CW=rE(),wW=Id(),WI=wg(),AW=vS(),NW=CI(),LW=NI(),xW=RI(),nD=Cd(),YI=VT(),um=QI(),IW=KI()});var ct=U(Z=>{"use strict";Object.defineProperty(Z,"__esModule",{value:!0});Object.defineProperty(Z,"version",{enumerable:!0,get:function(){return XI.version}});Object.defineProperty(Z,"versionInfo",{enumerable:!0,get:function(){return XI.versionInfo}});Object.defineProperty(Z,"graphql",{enumerable:!0,get:function(){return ZI.graphql}});Object.defineProperty(Z,"graphqlSync",{enumerable:!0,get:function(){return ZI.graphqlSync}});Object.defineProperty(Z,"GraphQLSchema",{enumerable:!0,get:function(){return Te.GraphQLSchema}});Object.defineProperty(Z,"GraphQLDirective",{enumerable:!0,get:function(){return Te.GraphQLDirective}});Object.defineProperty(Z,"GraphQLScalarType",{enumerable:!0,get:function(){return Te.GraphQLScalarType}});Object.defineProperty(Z,"GraphQLObjectType",{enumerable:!0,get:function(){return Te.GraphQLObjectType}});Object.defineProperty(Z,"GraphQLInterfaceType",{enumerable:!0,get:function(){return Te.GraphQLInterfaceType}});Object.defineProperty(Z,"GraphQLUnionType",{enumerable:!0,get:function(){return Te.GraphQLUnionType}});Object.defineProperty(Z,"GraphQLEnumType",{enumerable:!0,get:function(){return Te.GraphQLEnumType}});Object.defineProperty(Z,"GraphQLInputObjectType",{enumerable:!0,get:function(){return Te.GraphQLInputObjectType}});Object.defineProperty(Z,"GraphQLList",{enumerable:!0,get:function(){return Te.GraphQLList}});Object.defineProperty(Z,"GraphQLNonNull",{enumerable:!0,get:function(){return Te.GraphQLNonNull}});Object.defineProperty(Z,"specifiedScalarTypes",{enumerable:!0,get:function(){return Te.specifiedScalarTypes}});Object.defineProperty(Z,"GraphQLInt",{enumerable:!0,get:function(){return Te.GraphQLInt}});Object.defineProperty(Z,"GraphQLFloat",{enumerable:!0,get:function(){return Te.GraphQLFloat}});Object.defineProperty(Z,"GraphQLString",{enumerable:!0,get:function(){return Te.GraphQLString}});Object.defineProperty(Z,"GraphQLBoolean",{enumerable:!0,get:function(){return Te.GraphQLBoolean}});Object.defineProperty(Z,"GraphQLID",{enumerable:!0,get:function(){return Te.GraphQLID}});Object.defineProperty(Z,"specifiedDirectives",{enumerable:!0,get:function(){return Te.specifiedDirectives}});Object.defineProperty(Z,"GraphQLIncludeDirective",{enumerable:!0,get:function(){return Te.GraphQLIncludeDirective}});Object.defineProperty(Z,"GraphQLSkipDirective",{enumerable:!0,get:function(){return Te.GraphQLSkipDirective}});Object.defineProperty(Z,"GraphQLDeprecatedDirective",{enumerable:!0,get:function(){return Te.GraphQLDeprecatedDirective}});Object.defineProperty(Z,"GraphQLSpecifiedByDirective",{enumerable:!0,get:function(){return Te.GraphQLSpecifiedByDirective}});Object.defineProperty(Z,"TypeKind",{enumerable:!0,get:function(){return Te.TypeKind}});Object.defineProperty(Z,"DEFAULT_DEPRECATION_REASON",{enumerable:!0,get:function(){return Te.DEFAULT_DEPRECATION_REASON}});Object.defineProperty(Z,"introspectionTypes",{enumerable:!0,get:function(){return Te.introspectionTypes}});Object.defineProperty(Z,"__Schema",{enumerable:!0,get:function(){return Te.__Schema}});Object.defineProperty(Z,"__Directive",{enumerable:!0,get:function(){return Te.__Directive}});Object.defineProperty(Z,"__DirectiveLocation",{enumerable:!0,get:function(){return Te.__DirectiveLocation}});Object.defineProperty(Z,"__Type",{enumerable:!0,get:function(){return Te.__Type}});Object.defineProperty(Z,"__Field",{enumerable:!0,get:function(){return Te.__Field}});Object.defineProperty(Z,"__InputValue",{enumerable:!0,get:function(){return Te.__InputValue}});Object.defineProperty(Z,"__EnumValue",{enumerable:!0,get:function(){return Te.__EnumValue}});Object.defineProperty(Z,"__TypeKind",{enumerable:!0,get:function(){return Te.__TypeKind}});Object.defineProperty(Z,"SchemaMetaFieldDef",{enumerable:!0,get:function(){return Te.SchemaMetaFieldDef}});Object.defineProperty(Z,"TypeMetaFieldDef",{enumerable:!0,get:function(){return Te.TypeMetaFieldDef}});Object.defineProperty(Z,"TypeNameMetaFieldDef",{enumerable:!0,get:function(){return Te.TypeNameMetaFieldDef}});Object.defineProperty(Z,"isSchema",{enumerable:!0,get:function(){return Te.isSchema}});Object.defineProperty(Z,"isDirective",{enumerable:!0,get:function(){return Te.isDirective}});Object.defineProperty(Z,"isType",{enumerable:!0,get:function(){return Te.isType}});Object.defineProperty(Z,"isScalarType",{enumerable:!0,get:function(){return Te.isScalarType}});Object.defineProperty(Z,"isObjectType",{enumerable:!0,get:function(){return Te.isObjectType}});Object.defineProperty(Z,"isInterfaceType",{enumerable:!0,get:function(){return Te.isInterfaceType}});Object.defineProperty(Z,"isUnionType",{enumerable:!0,get:function(){return Te.isUnionType}});Object.defineProperty(Z,"isEnumType",{enumerable:!0,get:function(){return Te.isEnumType}});Object.defineProperty(Z,"isInputObjectType",{enumerable:!0,get:function(){return Te.isInputObjectType}});Object.defineProperty(Z,"isListType",{enumerable:!0,get:function(){return Te.isListType}});Object.defineProperty(Z,"isNonNullType",{enumerable:!0,get:function(){return Te.isNonNullType}});Object.defineProperty(Z,"isInputType",{enumerable:!0,get:function(){return Te.isInputType}});Object.defineProperty(Z,"isOutputType",{enumerable:!0,get:function(){return Te.isOutputType}});Object.defineProperty(Z,"isLeafType",{enumerable:!0,get:function(){return Te.isLeafType}});Object.defineProperty(Z,"isCompositeType",{enumerable:!0,get:function(){return Te.isCompositeType}});Object.defineProperty(Z,"isAbstractType",{enumerable:!0,get:function(){return Te.isAbstractType}});Object.defineProperty(Z,"isWrappingType",{enumerable:!0,get:function(){return Te.isWrappingType}});Object.defineProperty(Z,"isNullableType",{enumerable:!0,get:function(){return Te.isNullableType}});Object.defineProperty(Z,"isNamedType",{enumerable:!0,get:function(){return Te.isNamedType}});Object.defineProperty(Z,"isRequiredArgument",{enumerable:!0,get:function(){return Te.isRequiredArgument}});Object.defineProperty(Z,"isRequiredInputField",{enumerable:!0,get:function(){return Te.isRequiredInputField}});Object.defineProperty(Z,"isSpecifiedScalarType",{enumerable:!0,get:function(){return Te.isSpecifiedScalarType}});Object.defineProperty(Z,"isIntrospectionType",{enumerable:!0,get:function(){return Te.isIntrospectionType}});Object.defineProperty(Z,"isSpecifiedDirective",{enumerable:!0,get:function(){return Te.isSpecifiedDirective}});Object.defineProperty(Z,"assertSchema",{enumerable:!0,get:function(){return Te.assertSchema}});Object.defineProperty(Z,"assertDirective",{enumerable:!0,get:function(){return Te.assertDirective}});Object.defineProperty(Z,"assertType",{enumerable:!0,get:function(){return Te.assertType}});Object.defineProperty(Z,"assertScalarType",{enumerable:!0,get:function(){return Te.assertScalarType}});Object.defineProperty(Z,"assertObjectType",{enumerable:!0,get:function(){return Te.assertObjectType}});Object.defineProperty(Z,"assertInterfaceType",{enumerable:!0,get:function(){return Te.assertInterfaceType}});Object.defineProperty(Z,"assertUnionType",{enumerable:!0,get:function(){return Te.assertUnionType}});Object.defineProperty(Z,"assertEnumType",{enumerable:!0,get:function(){return Te.assertEnumType}});Object.defineProperty(Z,"assertInputObjectType",{enumerable:!0,get:function(){return Te.assertInputObjectType}});Object.defineProperty(Z,"assertListType",{enumerable:!0,get:function(){return Te.assertListType}});Object.defineProperty(Z,"assertNonNullType",{enumerable:!0,get:function(){return Te.assertNonNullType}});Object.defineProperty(Z,"assertInputType",{enumerable:!0,get:function(){return Te.assertInputType}});Object.defineProperty(Z,"assertOutputType",{enumerable:!0,get:function(){return Te.assertOutputType}});Object.defineProperty(Z,"assertLeafType",{enumerable:!0,get:function(){return Te.assertLeafType}});Object.defineProperty(Z,"assertCompositeType",{enumerable:!0,get:function(){return Te.assertCompositeType}});Object.defineProperty(Z,"assertAbstractType",{enumerable:!0,get:function(){return Te.assertAbstractType}});Object.defineProperty(Z,"assertWrappingType",{enumerable:!0,get:function(){return Te.assertWrappingType}});Object.defineProperty(Z,"assertNullableType",{enumerable:!0,get:function(){return Te.assertNullableType}});Object.defineProperty(Z,"assertNamedType",{enumerable:!0,get:function(){return Te.assertNamedType}});Object.defineProperty(Z,"getNullableType",{enumerable:!0,get:function(){return Te.getNullableType}});Object.defineProperty(Z,"getNamedType",{enumerable:!0,get:function(){return Te.getNamedType}});Object.defineProperty(Z,"validateSchema",{enumerable:!0,get:function(){return Te.validateSchema}});Object.defineProperty(Z,"assertValidSchema",{enumerable:!0,get:function(){return Te.assertValidSchema}});Object.defineProperty(Z,"Token",{enumerable:!0,get:function(){return Ut.Token}});Object.defineProperty(Z,"Source",{enumerable:!0,get:function(){return Ut.Source}});Object.defineProperty(Z,"Location",{enumerable:!0,get:function(){return Ut.Location}});Object.defineProperty(Z,"getLocation",{enumerable:!0,get:function(){return Ut.getLocation}});Object.defineProperty(Z,"printLocation",{enumerable:!0,get:function(){return Ut.printLocation}});Object.defineProperty(Z,"printSourceLocation",{enumerable:!0,get:function(){return Ut.printSourceLocation}});Object.defineProperty(Z,"Lexer",{enumerable:!0,get:function(){return Ut.Lexer}});Object.defineProperty(Z,"TokenKind",{enumerable:!0,get:function(){return Ut.TokenKind}});Object.defineProperty(Z,"parse",{enumerable:!0,get:function(){return Ut.parse}});Object.defineProperty(Z,"parseValue",{enumerable:!0,get:function(){return Ut.parseValue}});Object.defineProperty(Z,"parseType",{enumerable:!0,get:function(){return Ut.parseType}});Object.defineProperty(Z,"print",{enumerable:!0,get:function(){return Ut.print}});Object.defineProperty(Z,"visit",{enumerable:!0,get:function(){return Ut.visit}});Object.defineProperty(Z,"visitInParallel",{enumerable:!0,get:function(){return Ut.visitInParallel}});Object.defineProperty(Z,"getVisitFn",{enumerable:!0,get:function(){return Ut.getVisitFn}});Object.defineProperty(Z,"BREAK",{enumerable:!0,get:function(){return Ut.BREAK}});Object.defineProperty(Z,"Kind",{enumerable:!0,get:function(){return Ut.Kind}});Object.defineProperty(Z,"DirectiveLocation",{enumerable:!0,get:function(){return Ut.DirectiveLocation}});Object.defineProperty(Z,"isDefinitionNode",{enumerable:!0,get:function(){return Ut.isDefinitionNode}});Object.defineProperty(Z,"isExecutableDefinitionNode",{enumerable:!0,get:function(){return Ut.isExecutableDefinitionNode}});Object.defineProperty(Z,"isSelectionNode",{enumerable:!0,get:function(){return Ut.isSelectionNode}});Object.defineProperty(Z,"isValueNode",{enumerable:!0,get:function(){return Ut.isValueNode}});Object.defineProperty(Z,"isTypeNode",{enumerable:!0,get:function(){return Ut.isTypeNode}});Object.defineProperty(Z,"isTypeSystemDefinitionNode",{enumerable:!0,get:function(){return Ut.isTypeSystemDefinitionNode}});Object.defineProperty(Z,"isTypeDefinitionNode",{enumerable:!0,get:function(){return Ut.isTypeDefinitionNode}});Object.defineProperty(Z,"isTypeSystemExtensionNode",{enumerable:!0,get:function(){return Ut.isTypeSystemExtensionNode}});Object.defineProperty(Z,"isTypeExtensionNode",{enumerable:!0,get:function(){return Ut.isTypeExtensionNode}});Object.defineProperty(Z,"execute",{enumerable:!0,get:function(){return lc.execute}});Object.defineProperty(Z,"executeSync",{enumerable:!0,get:function(){return lc.executeSync}});Object.defineProperty(Z,"defaultFieldResolver",{enumerable:!0,get:function(){return lc.defaultFieldResolver}});Object.defineProperty(Z,"defaultTypeResolver",{enumerable:!0,get:function(){return lc.defaultTypeResolver}});Object.defineProperty(Z,"responsePathAsArray",{enumerable:!0,get:function(){return lc.responsePathAsArray}});Object.defineProperty(Z,"getDirectiveValues",{enumerable:!0,get:function(){return lc.getDirectiveValues}});Object.defineProperty(Z,"subscribe",{enumerable:!0,get:function(){return $I.subscribe}});Object.defineProperty(Z,"createSourceEventStream",{enumerable:!0,get:function(){return $I.createSourceEventStream}});Object.defineProperty(Z,"validate",{enumerable:!0,get:function(){return it.validate}});Object.defineProperty(Z,"ValidationContext",{enumerable:!0,get:function(){return it.ValidationContext}});Object.defineProperty(Z,"specifiedRules",{enumerable:!0,get:function(){return it.specifiedRules}});Object.defineProperty(Z,"ExecutableDefinitionsRule",{enumerable:!0,get:function(){return it.ExecutableDefinitionsRule}});Object.defineProperty(Z,"FieldsOnCorrectTypeRule",{enumerable:!0,get:function(){return it.FieldsOnCorrectTypeRule}});Object.defineProperty(Z,"FragmentsOnCompositeTypesRule",{enumerable:!0,get:function(){return it.FragmentsOnCompositeTypesRule}});Object.defineProperty(Z,"KnownArgumentNamesRule",{enumerable:!0,get:function(){return it.KnownArgumentNamesRule}});Object.defineProperty(Z,"KnownDirectivesRule",{enumerable:!0,get:function(){return it.KnownDirectivesRule}});Object.defineProperty(Z,"KnownFragmentNamesRule",{enumerable:!0,get:function(){return it.KnownFragmentNamesRule}});Object.defineProperty(Z,"KnownTypeNamesRule",{enumerable:!0,get:function(){return it.KnownTypeNamesRule}});Object.defineProperty(Z,"LoneAnonymousOperationRule",{enumerable:!0,get:function(){return it.LoneAnonymousOperationRule}});Object.defineProperty(Z,"NoFragmentCyclesRule",{enumerable:!0,get:function(){return it.NoFragmentCyclesRule}});Object.defineProperty(Z,"NoUndefinedVariablesRule",{enumerable:!0,get:function(){return it.NoUndefinedVariablesRule}});Object.defineProperty(Z,"NoUnusedFragmentsRule",{enumerable:!0,get:function(){return it.NoUnusedFragmentsRule}});Object.defineProperty(Z,"NoUnusedVariablesRule",{enumerable:!0,get:function(){return it.NoUnusedVariablesRule}});Object.defineProperty(Z,"OverlappingFieldsCanBeMergedRule",{enumerable:!0,get:function(){return it.OverlappingFieldsCanBeMergedRule}});Object.defineProperty(Z,"PossibleFragmentSpreadsRule",{enumerable:!0,get:function(){return it.PossibleFragmentSpreadsRule}});Object.defineProperty(Z,"ProvidedRequiredArgumentsRule",{enumerable:!0,get:function(){return it.ProvidedRequiredArgumentsRule}});Object.defineProperty(Z,"ScalarLeafsRule",{enumerable:!0,get:function(){return it.ScalarLeafsRule}});Object.defineProperty(Z,"SingleFieldSubscriptionsRule",{enumerable:!0,get:function(){return it.SingleFieldSubscriptionsRule}});Object.defineProperty(Z,"UniqueArgumentNamesRule",{enumerable:!0,get:function(){return it.UniqueArgumentNamesRule}});Object.defineProperty(Z,"UniqueDirectivesPerLocationRule",{enumerable:!0,get:function(){return it.UniqueDirectivesPerLocationRule}});Object.defineProperty(Z,"UniqueFragmentNamesRule",{enumerable:!0,get:function(){return it.UniqueFragmentNamesRule}});Object.defineProperty(Z,"UniqueInputFieldNamesRule",{enumerable:!0,get:function(){return it.UniqueInputFieldNamesRule}});Object.defineProperty(Z,"UniqueOperationNamesRule",{enumerable:!0,get:function(){return it.UniqueOperationNamesRule}});Object.defineProperty(Z,"UniqueVariableNamesRule",{enumerable:!0,get:function(){return it.UniqueVariableNamesRule}});Object.defineProperty(Z,"ValuesOfCorrectTypeRule",{enumerable:!0,get:function(){return it.ValuesOfCorrectTypeRule}});Object.defineProperty(Z,"VariablesAreInputTypesRule",{enumerable:!0,get:function(){return it.VariablesAreInputTypesRule}});Object.defineProperty(Z,"VariablesInAllowedPositionRule",{enumerable:!0,get:function(){return it.VariablesInAllowedPositionRule}});Object.defineProperty(Z,"LoneSchemaDefinitionRule",{enumerable:!0,get:function(){return it.LoneSchemaDefinitionRule}});Object.defineProperty(Z,"UniqueOperationTypesRule",{enumerable:!0,get:function(){return it.UniqueOperationTypesRule}});Object.defineProperty(Z,"UniqueTypeNamesRule",{enumerable:!0,get:function(){return it.UniqueTypeNamesRule}});Object.defineProperty(Z,"UniqueEnumValueNamesRule",{enumerable:!0,get:function(){return it.UniqueEnumValueNamesRule}});Object.defineProperty(Z,"UniqueFieldDefinitionNamesRule",{enumerable:!0,get:function(){return it.UniqueFieldDefinitionNamesRule}});Object.defineProperty(Z,"UniqueDirectiveNamesRule",{enumerable:!0,get:function(){return it.UniqueDirectiveNamesRule}});Object.defineProperty(Z,"PossibleTypeExtensionsRule",{enumerable:!0,get:function(){return it.PossibleTypeExtensionsRule}});Object.defineProperty(Z,"NoDeprecatedCustomRule",{enumerable:!0,get:function(){return it.NoDeprecatedCustomRule}});Object.defineProperty(Z,"NoSchemaIntrospectionCustomRule",{enumerable:!0,get:function(){return it.NoSchemaIntrospectionCustomRule}});Object.defineProperty(Z,"GraphQLError",{enumerable:!0,get:function(){return sp.GraphQLError}});Object.defineProperty(Z,"syntaxError",{enumerable:!0,get:function(){return sp.syntaxError}});Object.defineProperty(Z,"locatedError",{enumerable:!0,get:function(){return sp.locatedError}});Object.defineProperty(Z,"printError",{enumerable:!0,get:function(){return sp.printError}});Object.defineProperty(Z,"formatError",{enumerable:!0,get:function(){return sp.formatError}});Object.defineProperty(Z,"getIntrospectionQuery",{enumerable:!0,get:function(){return gt.getIntrospectionQuery}});Object.defineProperty(Z,"getOperationAST",{enumerable:!0,get:function(){return gt.getOperationAST}});Object.defineProperty(Z,"getOperationRootType",{enumerable:!0,get:function(){return gt.getOperationRootType}});Object.defineProperty(Z,"introspectionFromSchema",{enumerable:!0,get:function(){return gt.introspectionFromSchema}});Object.defineProperty(Z,"buildClientSchema",{enumerable:!0,get:function(){return gt.buildClientSchema}});Object.defineProperty(Z,"buildASTSchema",{enumerable:!0,get:function(){return gt.buildASTSchema}});Object.defineProperty(Z,"buildSchema",{enumerable:!0,get:function(){return gt.buildSchema}});Object.defineProperty(Z,"getDescription",{enumerable:!0,get:function(){return gt.getDescription}});Object.defineProperty(Z,"extendSchema",{enumerable:!0,get:function(){return gt.extendSchema}});Object.defineProperty(Z,"lexicographicSortSchema",{enumerable:!0,get:function(){return gt.lexicographicSortSchema}});Object.defineProperty(Z,"printSchema",{enumerable:!0,get:function(){return gt.printSchema}});Object.defineProperty(Z,"printType",{enumerable:!0,get:function(){return gt.printType}});Object.defineProperty(Z,"printIntrospectionSchema",{enumerable:!0,get:function(){return gt.printIntrospectionSchema}});Object.defineProperty(Z,"typeFromAST",{enumerable:!0,get:function(){return gt.typeFromAST}});Object.defineProperty(Z,"valueFromAST",{enumerable:!0,get:function(){return gt.valueFromAST}});Object.defineProperty(Z,"valueFromASTUntyped",{enumerable:!0,get:function(){return gt.valueFromASTUntyped}});Object.defineProperty(Z,"astFromValue",{enumerable:!0,get:function(){return gt.astFromValue}});Object.defineProperty(Z,"TypeInfo",{enumerable:!0,get:function(){return gt.TypeInfo}});Object.defineProperty(Z,"visitWithTypeInfo",{enumerable:!0,get:function(){return gt.visitWithTypeInfo}});Object.defineProperty(Z,"coerceInputValue",{enumerable:!0,get:function(){return gt.coerceInputValue}});Object.defineProperty(Z,"concatAST",{enumerable:!0,get:function(){return gt.concatAST}});Object.defineProperty(Z,"separateOperations",{enumerable:!0,get:function(){return gt.separateOperations}});Object.defineProperty(Z,"stripIgnoredCharacters",{enumerable:!0,get:function(){return gt.stripIgnoredCharacters}});Object.defineProperty(Z,"isEqualType",{enumerable:!0,get:function(){return gt.isEqualType}});Object.defineProperty(Z,"isTypeSubTypeOf",{enumerable:!0,get:function(){return gt.isTypeSubTypeOf}});Object.defineProperty(Z,"doTypesOverlap",{enumerable:!0,get:function(){return gt.doTypesOverlap}});Object.defineProperty(Z,"assertValidName",{enumerable:!0,get:function(){return gt.assertValidName}});Object.defineProperty(Z,"isValidNameError",{enumerable:!0,get:function(){return gt.isValidNameError}});Object.defineProperty(Z,"BreakingChangeType",{enumerable:!0,get:function(){return gt.BreakingChangeType}});Object.defineProperty(Z,"DangerousChangeType",{enumerable:!0,get:function(){return gt.DangerousChangeType}});Object.defineProperty(Z,"findBreakingChanges",{enumerable:!0,get:function(){return gt.findBreakingChanges}});Object.defineProperty(Z,"findDangerousChanges",{enumerable:!0,get:function(){return gt.findDangerousChanges}});Object.defineProperty(Z,"findDeprecatedUsages",{enumerable:!0,get:function(){return gt.findDeprecatedUsages}});var XI=gA(),ZI=kx(),Te=Cx(),Ut=Nx(),lc=Lx(),$I=Kx(),it=zx(),sp=Jx(),gt=JI()});var tR=U((Rne,eR)=>{eR.exports=function(){var e=document.getSelection();if(!e.rangeCount)return function(){};for(var t=document.activeElement,r=[],n=0;n{"use strict";var RW=tR(),rR={"text/plain":"Text","text/html":"Url",default:"Text"},FW="Copy to clipboard: #{key}, Enter";function jW(e){var t=(/mac os x/i.test(navigator.userAgent)?"\u2318":"Ctrl")+"+C";return e.replace(/#{\s*key\s*}/g,t)}function PW(e,t){var r,n,a,o,s,l,d=!1;t||(t={}),r=t.debug||!1;try{a=RW(),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 b=rR[t.format]||rR.default;window.clipboardData.setData(b,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(b){r&&console.error("unable to copy using clipboardData: ",b),r&&console.error("falling back to prompt"),n=jW("message"in t?t.message:FW),window.prompt(n,e)}}finally{s&&(typeof s.removeRange=="function"?s.removeRange(o):s.removeAllRanges()),l&&document.body.removeChild(l),a()}return d}nR.exports=PW});var iD=U((jne,sm)=>{"use strict";function aR(e,t){if(e!=null)return e;var r=new Error(t!==void 0?t:"Got unexpected "+e);throw r.framesToPop=1,r}sm.exports=aR;sm.exports.default=aR;Object.defineProperty(sm.exports,"__esModule",{value:!0})});var fR=U((gie,QW)=>{QW.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 fD=U((mie,dR)=>{"use strict";dR.exports=fR()});var cm=U((yie,pR)=>{pR.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 gR=U((bie,vR)=>{"use strict";var hR={};function KW(e){var t,r,n=hR[e];if(n)return n;for(n=hR[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}fm.defaultChars=";/?:@&=+$,-_.!~*'()#";fm.componentChars="-_.!~*'()";vR.exports=fm});var bR=U((Tie,yR)=>{"use strict";var mR={};function HW(e){var t,r,n=mR[e];if(n)return n;for(n=mR[e]=[],t=0;t<128;t++)r=String.fromCharCode(t),n.push(r);for(t=0;t=55296&&v<=57343?b+="\uFFFD\uFFFD\uFFFD":b+=String.fromCharCode(v),a+=6;continue}if((s&248)==240&&a+91114111?b+="\uFFFD\uFFFD\uFFFD\uFFFD":(v-=65536,b+=String.fromCharCode(55296+(v>>10),56320+(v&1023))),a+=9;continue}b+="\uFFFD"}return b})}dm.defaultChars=";/?:@&=+$,#";dm.componentChars="";yR.exports=dm});var ER=U((Eie,TR)=>{"use strict";TR.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 wR=U((_ie,CR)=>{"use strict";function pm(){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 zW=/^([a-z0-9.+-]+:)/i,WW=/:[0-9]*$/,YW=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,JW=["<",">",'"',"`"," ","\r",`
-`," "],XW=["{","}","|","\\","^","`"].concat(JW),ZW=["'"].concat(XW),_R=["%","/","?",";","#"].concat(ZW),SR=["/","?","#"],$W=255,DR=/^[+a-z0-9A-Z_-]{0,63}$/,eY=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,kR={javascript:!0,"javascript:":!0},OR={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0};function tY(e,t){if(e&&e instanceof pm)return e;var r=new pm;return r.parse(e,t),r}pm.prototype.parse=function(e,t){var r,n,a,o,s,l=e;if(l=l.trim(),!t&&e.split("#").length===1){var d=YW.exec(l);if(d)return this.pathname=d[1],d[2]&&(this.search=d[2]),this}var h=zW.exec(l);if(h&&(h=h[0],a=h.toLowerCase(),this.protocol=h,l=l.substr(h.length)),(t||h||l.match(/^\/\/[^@\/]+@[^@\/]+/))&&(s=l.substr(0,2)==="//",s&&!(h&&kR[h])&&(l=l.substr(2),this.slashes=!0)),!kR[h]&&(s||h&&!OR[h])){var v=-1;for(r=0;r127?_+="x":_+=y[m];if(!_.match(DR)){var w=S.slice(0,r),C=S.slice(r+1),D=y.match(eY);D&&(w.push(D[1]),C.unshift(D[2])),C.length&&(l=C.join(".")+l),this.hostname=w.join(".");break}}}}this.hostname.length>$W&&(this.hostname=""),L&&(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),OR[a]&&this.hostname&&!this.pathname&&(this.pathname=""),this};pm.prototype.parseHost=function(e){var t=WW.exec(e);t&&(t=t[0],t!==":"&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)};CR.exports=tY});var dD=U((Sie,cp)=>{"use strict";cp.exports.encode=gR();cp.exports.decode=bR();cp.exports.format=ER();cp.exports.parse=wR()});var pD=U((Die,AR)=>{AR.exports=/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/});var hD=U((kie,NR)=>{NR.exports=/[\0-\x1F\x7F-\x9F]/});var xR=U((Oie,LR)=>{LR.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 vD=U((Cie,IR)=>{IR.exports=/[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/});var RR=U(cc=>{"use strict";cc.Any=pD();cc.Cc=hD();cc.Cf=xR();cc.P=cm();cc.Z=vD()});var Ct=U(Vr=>{"use strict";function rY(e){return Object.prototype.toString.call(e)}function nY(e){return rY(e)==="[object String]"}var iY=Object.prototype.hasOwnProperty;function FR(e,t){return iY.call(e,t)}function aY(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 oY(e,t,r){return[].concat(e.slice(0,t),r,e.slice(t+1))}function jR(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 PR(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 MR=/\\([!"#$%&'()*+,\-.\/:;<=>?@[\\\]^_`{|}~])/g,uY=/&([a-z#][a-z0-9]{1,31});/gi,sY=new RegExp(MR.source+"|"+uY.source,"gi"),lY=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i,qR=fD();function cY(e,t){var r=0;return FR(qR,t)?qR[t]:t.charCodeAt(0)===35&&lY.test(t)&&(r=t[1].toLowerCase()==="x"?parseInt(t.slice(2),16):parseInt(t.slice(1),10),jR(r))?PR(r):e}function fY(e){return e.indexOf("\\")<0?e:e.replace(MR,"$1")}function dY(e){return e.indexOf("\\")<0&&e.indexOf("&")<0?e:e.replace(sY,function(t,r,n){return r||cY(t,n)})}var pY=/[&<>"]/,hY=/[&<>"]/g,vY={"&":"&","<":"<",">":">",'"':"""};function gY(e){return vY[e]}function mY(e){return pY.test(e)?e.replace(hY,gY):e}var yY=/[.?*+^$[\]\\(){}|-]/g;function bY(e){return e.replace(yY,"\\$&")}function TY(e){switch(e){case 9:case 32:return!0}return!1}function EY(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 _Y=cm();function SY(e){return _Y.test(e)}function DY(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 kY(e){return e=e.trim().replace(/\s+/g," "),"\u1E9E".toLowerCase()==="\u1E7E"&&(e=e.replace(/ẞ/g,"\xDF")),e.toLowerCase().toUpperCase()}Vr.lib={};Vr.lib.mdurl=dD();Vr.lib.ucmicro=RR();Vr.assign=aY;Vr.isString=nY;Vr.has=FR;Vr.unescapeMd=fY;Vr.unescapeAll=dY;Vr.isValidEntityCode=jR;Vr.fromCodePoint=PR;Vr.escapeHtml=mY;Vr.arrayReplaceAt=oY;Vr.isSpace=TY;Vr.isWhiteSpace=EY;Vr.isMdAsciiPunct=DY;Vr.isPunctChar=SY;Vr.escapeRE=bY;Vr.normalizeReference=kY});var VR=U((Nie,BR)=>{"use strict";BR.exports=function(t,r,n){var a,o,s,l,d=-1,h=t.posMax,v=t.pos;for(t.pos=r+1,a=1;t.pos{"use strict";var UR=Ct().unescapeAll;GR.exports=function(t,r,n){var a,o,s=0,l=r,d={ok:!1,pos:0,lines:0,str:""};if(t.charCodeAt(r)===60){for(r++;r{"use strict";var OY=Ct().unescapeAll;KR.exports=function(t,r,n){var a,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=VR();hm.parseLinkDestination=QR();hm.parseLinkTitle=HR()});var YR=U((Rie,WR)=>{"use strict";var CY=Ct().assign,wY=Ct().unescapeAll,bs=Ct().escapeHtml,Ia={};Ia.code_inline=function(e,t,r,n,a){var o=e[t];return""+bs(e[t].content)+"
"};Ia.code_block=function(e,t,r,n,a){var o=e[t];return""+bs(e[t].content)+`
-`};Ia.fence=function(e,t,r,n,a){var o=e[t],s=o.info?wY(o.info).trim():"",l="",d,h,v,b;return s&&(l=s.split(/\s+/g)[0]),r.highlight?d=r.highlight(o.content,l)||bs(o.content):d=bs(o.content),d.indexOf(""+d+`
-`):""+d+`
-`};Ia.image=function(e,t,r,n,a){var o=e[t];return o.attrs[o.attrIndex("alt")][1]=a.renderInlineAsText(o.children,r,n),a.renderToken(e,t,r)};Ia.hardbreak=function(e,t,r){return r.xhtmlOut?`
+`);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?`
`:`
-`};Ia.softbreak=function(e,t,r){return r.breaks?r.xhtmlOut?`
+`};Wa.softbreak=function(e,t,r){return r.breaks?r.xhtmlOut?`
`:`
`:`
-`};Ia.text=function(e,t){return bs(e[t].content)};Ia.html_block=function(e,t){return e[t].content};Ia.html_inline=function(e,t){return e[t].content};function fc(){this.rules=CY({},Ia)}fc.prototype.renderAttrs=function(t){var r,n,a;if(!t.attrs)return"";for(a="",r=0,n=t.attrs.length;r
-`:">",o)};fc.prototype.renderInline=function(e,t,r){for(var n,a="",o=this.rules,s=0,l=e.length;s{"use strict";function ua(){this.__rules__=[],this.__cache__=null}ua.prototype.__find__=function(e){for(var t=0;t{"use strict";var AY=/\r\n?|\n/g,NY=/\0/g;XR.exports=function(t){var r;r=t.src.replace(AY,`
-`),r=r.replace(NY,"\uFFFD"),t.src=r}});var eF=U((Pie,$R)=>{"use strict";$R.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 rF=U((Mie,tF)=>{"use strict";tF.exports=function(t){var r=t.tokens,n,a,o;for(a=0,o=r.length;a{"use strict";var LY=Ct().arrayReplaceAt;function xY(e){return/^\s]/i.test(e)}function IY(e){return/^<\/a\s*>/i.test(e)}nF.exports=function(t){var r,n,a,o,s,l,d,h,v,b,T,A,L,S,y,_,m=t.tokens,k;if(!!t.md.options.linkify){for(n=0,a=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"&&(xY(l.content)&&L>0&&L--,IY(l.content)&&L++),!(L>0)&&l.type==="text"&&t.md.linkify.test(l.content)){for(v=l.content,k=t.md.linkify.match(v),d=[],A=l.level,T=0,h=0;hT&&(s=new t.Token("text","",0),s.content=v.slice(T,b),s.level=A,d.push(s)),s=new t.Token("link_open","a",1),s.attrs=[["href",y]],s.level=A++,s.markup="linkify",s.info="auto",d.push(s),s=new t.Token("text","",0),s.content=_,s.level=A,d.push(s),s=new t.Token("link_close","a",-1),s.level=--A,s.markup="linkify",s.info="auto",d.push(s),T=k[h].lastIndex);T{"use strict";var aF=/\+-|\.\.|\?\?\?\?|!!!!|,,|--/,RY=/\((c|tm|r|p)\)/i,FY=/\((c|tm|r|p)\)/ig,jY={c:"\xA9",r:"\xAE",p:"\xA7",tm:"\u2122"};function PY(e,t){return jY[t.toLowerCase()]}function MY(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(FY,PY)),r.type==="link_open"&&r.info==="auto"&&n--,r.type==="link_close"&&r.info==="auto"&&n++}function qY(e){var t,r,n=0;for(t=e.length-1;t>=0;t--)r=e[t],r.type==="text"&&!n&&aF.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$2").replace(/(^|\s)--(\s|$)/mg,"$1\u2013$2").replace(/(^|[^-\s])--([^-\s]|$)/mg,"$1\u2013$2")),r.type==="link_open"&&r.info==="auto"&&n--,r.type==="link_close"&&r.info==="auto"&&n++}oF.exports=function(t){var r;if(!!t.md.options.typographer)for(r=t.tokens.length-1;r>=0;r--)t.tokens[r].type==="inline"&&(RY.test(t.tokens[r].content)&&MY(t.tokens[r].children),aF.test(t.tokens[r].content)&&qY(t.tokens[r].children))}});var hF=U((Vie,pF)=>{"use strict";var sF=Ct().isWhiteSpace,lF=Ct().isPunctChar,cF=Ct().isMdAsciiPunct,BY=/['"]/,fF=/['"]/g,dF="\u2019";function gm(e,t,r){return e.substr(0,t)+r+e.substr(t+1)}function VY(e,t){var r,n,a,o,s,l,d,h,v,b,T,A,L,S,y,_,m,k,w,C,D;for(w=[],r=0;r=0&&!(w[m].level<=d);m--);if(w.length=m+1,n.type!=="text")continue;a=n.content,s=0,l=a.length;e:for(;s=0)v=a.charCodeAt(o.index-1);else for(m=r-1;m>=0&&!(e[m].type==="softbreak"||e[m].type==="hardbreak");m--)if(e[m].type==="text"){v=e[m].content.charCodeAt(e[m].content.length-1);break}if(b=32,s=48&&v<=57&&(_=y=!1),y&&_&&(y=!1,_=A),!y&&!_){k&&(n.content=gm(n.content,o.index,dF));continue}if(_){for(m=w.length-1;m>=0&&(h=w[m],!(w[m].level=0;r--)t.tokens[r].type!=="inline"||!BY.test(t.tokens[r].content)||VY(t.tokens[r].children,t)}});var mm=U((Uie,vF)=>{"use strict";function dc(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}dc.prototype.attrIndex=function(t){var r,n,a;if(!this.attrs)return-1;for(r=this.attrs,n=0,a=r.length;n=0&&(n=this.attrs[r][1]),n};dc.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};vF.exports=dc});var yF=U((Gie,mF)=>{"use strict";var UY=mm();function gF(e,t,r){this.src=e,this.env=r,this.tokens=[],this.inlineMode=!1,this.md=t}gF.prototype.Token=UY;mF.exports=gF});var TF=U((Qie,bF)=>{"use strict";var GY=vm(),gD=[["normalize",ZR()],["block",eF()],["inline",rF()],["linkify",iF()],["replacements",uF()],["smartquotes",hF()]];function mD(){this.ruler=new GY;for(var e=0;e{"use strict";var QY=Ct().isSpace;function yD(e,t){var r=e.bMarks[t]+e.blkIndent,n=e.eMarks[t];return e.src.substr(r,n-r)}function EF(e){var t=[],r=0,n=e.length,a,o=0,s=0,l=!1,d=0;for(a=e.charCodeAt(r);rn||(h=r+1,t.sCount[h]=4||(l=t.bMarks[h]+t.tShift[h],l>=t.eMarks[h])||(o=t.src.charCodeAt(l++),o!==124&&o!==45&&o!==58))return!1;for(;l=4||(v=EF(s.replace(/^\||\|$/g,"")),b=v.length,b>A.length))return!1;if(a)return!0;for(T=t.push("table_open","table",1),T.map=S=[r,0],T=t.push("thead_open","thead",1),T.map=[r,r+1],T=t.push("tr_open","tr",1),T.map=[r,r+1],d=0;d=4);h++){for(v=EF(s.replace(/^\||\|$/g,"")),T=t.push("tr_open","tr",1),d=0;d{"use strict";DF.exports=function(t,r,n){var a,o,s;if(t.sCount[r]-t.blkIndent<4)return!1;for(o=a=r+1;a=4){a++,o=a;continue}break}return t.line=o,s=t.push("code_block","code",0),s.content=t.getLines(r,o,4+t.blkIndent,!0),s.map=[r,t.line],!0}});var CF=U((zie,OF)=>{"use strict";OF.exports=function(t,r,n,a){var o,s,l,d,h,v,b,T=!1,A=t.bMarks[r]+t.tShift[r],L=t.eMarks[r];if(t.sCount[r]-t.blkIndent>=4||A+3>L||(o=t.src.charCodeAt(A),o!==126&&o!==96)||(h=A,A=t.skipChars(A,o),s=A-h,s<3)||(b=t.src.slice(h,A),l=t.src.slice(A,L),o===96&&l.indexOf(String.fromCharCode(o))>=0))return!1;if(a)return!0;for(d=r;d++,!(d>=n||(A=h=t.bMarks[d]+t.tShift[d],L=t.eMarks[d],A=4)&&(A=t.skipChars(A,o),!(A-h{"use strict";var wF=Ct().isSpace;AF.exports=function(t,r,n,a){var o,s,l,d,h,v,b,T,A,L,S,y,_,m,k,w,C,D,R,M,q=t.lineMax,z=t.bMarks[r]+t.tShift[r],Q=t.eMarks[r];if(t.sCount[r]-t.blkIndent>=4||t.src.charCodeAt(z++)!==62)return!1;if(a)return!0;for(d=A=t.sCount[r]+z-(t.bMarks[r]+t.tShift[r]),t.src.charCodeAt(z)===32?(z++,d++,A++,o=!1,w=!0):t.src.charCodeAt(z)===9?(w=!0,(t.bsCount[r]+A)%4==3?(z++,d++,A++,o=!1):o=!0):w=!1,L=[t.bMarks[r]],t.bMarks[r]=z;z=Q,m=[t.sCount[r]],t.sCount[r]=A-d,k=[t.tShift[r]],t.tShift[r]=z-t.bMarks[r],D=t.md.block.ruler.getRules("blockquote"),_=t.parentType,t.parentType="blockquote",M=!1,T=r+1;T=Q));T++){if(t.src.charCodeAt(z++)===62&&!M){for(d=A=t.sCount[T]+z-(t.bMarks[T]+t.tShift[T]),t.src.charCodeAt(z)===32?(z++,d++,A++,o=!1,w=!0):t.src.charCodeAt(z)===9?(w=!0,(t.bsCount[T]+A)%4==3?(z++,d++,A++,o=!1):o=!0):w=!1,L.push(t.bMarks[T]),t.bMarks[T]=z;z=Q,S.push(t.bsCount[T]),t.bsCount[T]=t.sCount[T]+1+(w?1:0),m.push(t.sCount[T]),t.sCount[T]=A-d,k.push(t.tShift[T]),t.tShift[T]=z-t.bMarks[T];continue}if(v)break;for(C=!1,l=0,h=D.length;l",R.map=b=[r,0],t.md.block.tokenize(t,r,T),R=t.push("blockquote_close","blockquote",-1),R.markup=">",t.lineMax=q,t.parentType=_,b[1]=t.line,l=0;l{"use strict";var KY=Ct().isSpace;LF.exports=function(t,r,n,a){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 IF=Ct().isSpace;function RF(e,t){var r,n,a,o;return n=e.bMarks[t]+e.tShift[t],a=e.eMarks[t],r=e.src.charCodeAt(n++),r!==42&&r!==45&&r!==43||n=o||(r=e.src.charCodeAt(a++),r<48||r>57))return-1;for(;;){if(a>=o)return-1;if(r=e.src.charCodeAt(a++),r>=48&&r<=57){if(a-n>=10)return-1;continue}if(r===41||r===46)break;return-1}return a=4||t.listIndent>=0&&t.sCount[r]-t.listIndent>=4&&t.sCount[r]=t.blkIndent&&(Ce=!0),(Q=FF(t,r))>=0){if(b=!0,j=t.bMarks[r]+t.tShift[r],_=Number(t.src.substr(j,Q-j-1)),Ce&&_!==1)return!1}else if((Q=RF(t,r))>=0)b=!1;else return!1;if(Ce&&t.skipSpaces(Q)>=t.eMarks[r])return!1;if(y=t.src.charCodeAt(Q-1),a)return!0;for(S=t.tokens.length,b?(be=t.push("ordered_list_open","ol",1),_!==1&&(be.attrs=[["start",_]])):be=t.push("bullet_list_open","ul",1),be.map=L=[r,0],be.markup=String.fromCharCode(y),k=r,G=!1,ke=t.md.block.ruler.getRules("list"),D=t.parentType,t.parentType="list";k=m?h=1:h=w-v,h>4&&(h=1),d=v+h,be=t.push("list_item_open","li",1),be.markup=String.fromCharCode(y),be.map=T=[r,0],q=t.tight,M=t.tShift[r],R=t.sCount[r],C=t.listIndent,t.listIndent=t.blkIndent,t.blkIndent=d,t.tight=!0,t.tShift[r]=s-t.bMarks[r],t.sCount[r]=w,s>=m&&t.isEmpty(r+1)?t.line=Math.min(t.line+2,n):t.md.block.tokenize(t,r,n,!0),(!t.tight||G)&&(we=!1),G=t.line-r>1&&t.isEmpty(t.line-1),t.blkIndent=t.listIndent,t.listIndent=C,t.tShift[r]=M,t.sCount[r]=R,t.tight=q,be=t.push("list_item_close","li",-1),be.markup=String.fromCharCode(y),k=r=t.line,T[1]=k,s=t.bMarks[r],k>=n||t.sCount[k]=4)break;for(ce=!1,l=0,A=ke.length;l{"use strict";var zY=Ct().normalizeReference,ym=Ct().isSpace;MF.exports=function(t,r,n,a){var o,s,l,d,h,v,b,T,A,L,S,y,_,m,k,w,C=0,D=t.bMarks[r]+t.tShift[r],R=t.eMarks[r],M=r+1;if(t.sCount[r]-t.blkIndent>=4||t.src.charCodeAt(D)!==91)return!1;for(;++D3)&&!(t.sCount[M]<0)){for(m=!1,v=0,b=k.length;v{"use strict";var BF=Ct().isSpace;VF.exports=function(t,r,n,a){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&&h