mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-20 10:16:42 -06:00
Merge branch 'main' into 19644-atomic-branching
This commit is contained in:
commit
ed9625d2dd
@ -15,7 +15,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.3.1
|
placeholder: v4.3.2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@ -27,7 +27,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox Version
|
label: NetBox Version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.3.1
|
placeholder: v4.3.2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<h3>
|
<h3>
|
||||||
:jigsaw: <a href="#jigsaw-creating-plugins">Create a plugin</a> ·
|
:jigsaw: <a href="#jigsaw-creating-plugins">Create a plugin</a> ·
|
||||||
:rescue_worker_helmet: <a href="#rescue_worker_helmet-become-a-maintainer">Become a maintainer</a> ·
|
:briefcase: <a href="#briefcase-looking-for-a-job">Work with us!</a> ·
|
||||||
:heart: <a href="#heart-other-ways-to-contribute">Other ideas</a>
|
:heart: <a href="#heart-other-ways-to-contribute">Other ideas</a>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -109,21 +109,9 @@ Do you have an idea for something you'd like to build in NetBox, but might not b
|
|||||||
|
|
||||||
Check out our [plugin development tutorial](https://github.com/netbox-community/netbox-plugin-tutorial) to get started!
|
Check out our [plugin development tutorial](https://github.com/netbox-community/netbox-plugin-tutorial) to get started!
|
||||||
|
|
||||||
## :rescue_worker_helmet: Become a Maintainer
|
## :briefcase: Looking for a Job?
|
||||||
|
|
||||||
We're always looking for motivated individuals to join the maintainers team and help drive NetBox's long-term development. Some of our most sought-after skills include:
|
At [NetBox Labs](https://netboxlabs.com/), we're always looking for highly skilled and motivated people to join our team. While NetBox is a core part of our product lineup, we have an ever-expanding suite of solutions serving the network automation space. Check out our [current openings](https://netboxlabs.com/careers/) to see if you might be a fit!
|
||||||
|
|
||||||
* Python development with a strong focus on the [Django](https://www.djangoproject.com/) framework
|
|
||||||
* Expertise working with PostgreSQL databases
|
|
||||||
* Javascript & TypeScript proficiency
|
|
||||||
* A knack for web application design (HTML & CSS)
|
|
||||||
* Familiarity with git and software development best practices
|
|
||||||
* Excellent attention to detail
|
|
||||||
* Working experience in the field of network operations & engineering
|
|
||||||
|
|
||||||
We generally ask that maintainers dedicate around four hours of work to the project each week on average, which includes both hands-on development and project management tasks such as issue triage. Maintainers are also encouraged (but not required) to attend our bi-weekly Zoom call to catch up on recent items.
|
|
||||||
|
|
||||||
Interested? You can contact our lead maintainer, Jeremy Stretch, at jeremy@netbox.dev or on the [NetDev Community Slack](https://netdev.chat/). We'd love to have you on the team!
|
|
||||||
|
|
||||||
## :heart: Other Ways to Contribute
|
## :heart: Other Ways to Contribute
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
|
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
|
||||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=main" alt="CI status" /></a>
|
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=main" alt="CI status" /></a>
|
||||||
<p>
|
<p>
|
||||||
<strong><a href="https://github.com/netbox-community/netbox/">NetBox Community</a></strong> |
|
<strong><a href="https://netboxlabs.com/community/">NetBox Community</a></strong> |
|
||||||
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |
|
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |
|
||||||
<strong><a href="https://netboxlabs.com/netbox-enterprise/">NetBox Enterprise</a></strong>
|
<strong><a href="https://netboxlabs.com/netbox-enterprise/">NetBox Enterprise</a></strong>
|
||||||
</p>
|
</p>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Default: `None`
|
Default: `None`
|
||||||
|
|
||||||
Defines a Sentry data source name (DSN) for automated error reporting. `SENTRY_ENABLED` must be True for this parameter to take effect. For example:
|
Defines a Sentry data source name (DSN) for automated error reporting. `SENTRY_ENABLED` must be `True` for this parameter to take effect. For example:
|
||||||
|
|
||||||
```
|
```
|
||||||
SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
|
SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
|
||||||
@ -16,7 +16,7 @@ SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
|
|||||||
|
|
||||||
Default: `False`
|
Default: `False`
|
||||||
|
|
||||||
Set to True to enable automatic error reporting via [Sentry](https://sentry.io/).
|
Set to `True` to enable automatic error reporting via [Sentry](https://sentry.io/).
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
The `sentry-sdk` Python package is required to enable Sentry integration.
|
The `sentry-sdk` Python package is required to enable Sentry integration.
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
Default: `True`
|
Default: `True`
|
||||||
|
|
||||||
Setting this to False will disable the GraphQL API.
|
Setting this to `False` will disable the GraphQL API.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ Sets content for the top banner in the user interface.
|
|||||||
|
|
||||||
Default: `True`
|
Default: `True`
|
||||||
|
|
||||||
Enables anonymous census reporting. To opt out of census reporting, set this to False.
|
Enables anonymous census reporting. To opt out of census reporting, set this to `False`.
|
||||||
|
|
||||||
This data enables the project maintainers to estimate how many NetBox deployments exist and track the adoption of new versions over time. Census reporting effects a single HTTP request each time a worker starts. The only data reported by this function are the NetBox version, Python version, and a pseudorandom unique identifier.
|
This data enables the project maintainers to estimate how many NetBox deployments exist and track the adoption of new versions over time. Census reporting effects a single HTTP request each time a worker starts. The only data reported by this function are the NetBox version, Python version, and a pseudorandom unique identifier.
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
|
|||||||
|
|
||||||
Default: `True`
|
Default: `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.
|
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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ The number of days to retain job results (scripts and reports). Set this to `0`
|
|||||||
|
|
||||||
Default: `False`
|
Default: `False`
|
||||||
|
|
||||||
Setting this to True will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled.
|
Setting this to `True` will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -181,7 +181,7 @@ Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Pr
|
|||||||
|
|
||||||
Default: `False`
|
Default: `False`
|
||||||
|
|
||||||
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead.
|
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to `True` to prefer IPv4 instead.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Remote Authentication Settings
|
# Remote Authentication Settings
|
||||||
|
|
||||||
The configuration parameters listed here control remote authentication for NetBox. Note that `REMOTE_AUTH_ENABLED` must be true in order for these settings to take effect.
|
The configuration parameters listed here control remote authentication for NetBox. Note that `REMOTE_AUTH_ENABLED` must be `True` in order for these settings to take effect.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ The configuration parameters listed here control remote authentication for NetBo
|
|||||||
|
|
||||||
Default: `False`
|
Default: `False`
|
||||||
|
|
||||||
If true, NetBox will automatically create groups specified in the `REMOTE_AUTH_GROUP_HEADER` header if they don't already exist. (Requires `REMOTE_AUTH_ENABLED`.)
|
If `True`, NetBox will automatically create groups specified in the `REMOTE_AUTH_GROUP_HEADER` header if they don't already exist. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ If true, NetBox will automatically create groups specified in the `REMOTE_AUTH_G
|
|||||||
|
|
||||||
Default: `False`
|
Default: `False`
|
||||||
|
|
||||||
If true, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
|
If `True`, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ The list of groups to assign a new user account when created using remote authen
|
|||||||
|
|
||||||
Default: `{}` (Empty dictionary)
|
Default: `{}` (Empty dictionary)
|
||||||
|
|
||||||
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED` as True and `REMOTE_AUTH_GROUP_SYNC_ENABLED` as False.)
|
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED` as `True` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` as `False`.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
## ALLOWED_HOSTS
|
## ALLOWED_HOSTS
|
||||||
|
|
||||||
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attacks](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
|
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attacks](https://docs.djangoproject.com/en/stable/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
This parameter must always be defined as a list or tuple, even if only a single value is provided.
|
This parameter must always be defined as a list or tuple, even if only a single value is provided.
|
||||||
|
|
||||||
The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts POST requests to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, sets `USE_X_FORWARDED_HOST` to true, which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts)).
|
The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts POST requests to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, sets `USE_X_FORWARDED_HOST` to `True`, which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts)).
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
Default: `False`
|
Default: `False`
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
The default value of this parameter changed from true to false in NetBox v4.3.0.
|
The default value of this parameter changed from `True` to `False` in NetBox v4.3.0.
|
||||||
|
|
||||||
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token prior to its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
|
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token prior to its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ If `True`, cross-origin resource sharing (CORS) requests will be accepted from a
|
|||||||
|
|
||||||
These settings specify a list of origins that are authorized to make cross-site API requests. Use
|
These settings specify a list of origins that are authorized to make cross-site API requests. Use
|
||||||
`CORS_ORIGIN_WHITELIST` to define a list of exact hostnames, or `CORS_ORIGIN_REGEX_WHITELIST` to define a set of regular
|
`CORS_ORIGIN_WHITELIST` to define a list of exact hostnames, or `CORS_ORIGIN_REGEX_WHITELIST` to define a set of regular
|
||||||
expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is True.) For example:
|
expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is `True`.) For example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
CORS_ORIGIN_WHITELIST = [
|
CORS_ORIGIN_WHITELIST = [
|
||||||
@ -92,7 +92,7 @@ If `True`, the cookie employed for cross-site request forgery (CSRF) protection
|
|||||||
|
|
||||||
Default: `[]`
|
Default: `[]`
|
||||||
|
|
||||||
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://).
|
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-trusted-origins) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
CSRF_TRUSTED_ORIGINS = (
|
CSRF_TRUSTED_ORIGINS = (
|
||||||
|
@ -54,6 +54,7 @@ If a new Django release is adopted or other major dependencies (Python, PostgreS
|
|||||||
* Update the installation guide (`docs/installation/index.md`) with the new minimum versions.
|
* Update the installation guide (`docs/installation/index.md`) with the new minimum versions.
|
||||||
* Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly.
|
* Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly.
|
||||||
* Update the minimum PostgreSQL version in the programming error template (`netbox/templates/exceptions/programming_error.html`).
|
* Update the minimum PostgreSQL version in the programming error template (`netbox/templates/exceptions/programming_error.html`).
|
||||||
|
* Update the minimum and supported Python versions in the project metadata file (`pyproject.toml`)
|
||||||
|
|
||||||
### Manually Perform a New Install
|
### Manually Perform a New Install
|
||||||
|
|
||||||
@ -165,7 +166,7 @@ Then, compile these portable (`.po`) files for use in the application:
|
|||||||
|
|
||||||
### Update Version and Changelog
|
### Update Version and Changelog
|
||||||
|
|
||||||
* Update the version number and date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable.
|
* Update the version number and date in `netbox/release.yaml` and `pyproject.toml`. Add or remove the designation (e.g. `beta1`) if applicable.
|
||||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||||
* Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release.
|
* Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release.
|
||||||
|
|
||||||
@ -191,15 +192,3 @@ Create a [new release](https://github.com/netbox-community/netbox/releases/new)
|
|||||||
* **Description:** Copy from the pull request body, then promote the `###` headers to `##` ones
|
* **Description:** Copy from the pull request body, then promote the `###` headers to `##` ones
|
||||||
|
|
||||||
Once created, the release will become available for users to install.
|
Once created, the release will become available for users to install.
|
||||||
|
|
||||||
### Update the Public Documentation
|
|
||||||
|
|
||||||
After a release has been published, the public NetBox documentation needs to be updated. This is accomplished by running two actions on the [netboxlabs-docs](https://github.com/netboxlabs/netboxlabs-docs) repository.
|
|
||||||
|
|
||||||
First, run the `build-site` action, by navigating to Actions > build-site > Run workflow. This process compiles the documentation along with an overlay for integration with the documentation portal at <https://netboxlabs.com/docs>. The job should take about two minutes.
|
|
||||||
|
|
||||||
Once the documentation files have been compiled, they must be published by running the `deploy-kinsta` action. Select the desired deployment environment (staging or production) and specify `latest` as the deploy tag.
|
|
||||||
|
|
||||||
Clear the CDN cache from the [Kinsta](https://my.kinsta.com/) portal. Navigate to _Sites_ / _NetBox Labs_ / _Live_, select _Cache_ in the left-nav, click the _Clear Cache_ button, and confirm the clear operation.
|
|
||||||
|
|
||||||
Finally, verify that the documentation at <https://netboxlabs.com/docs/netbox/en/stable/> has been updated.
|
|
||||||
|
@ -108,7 +108,7 @@ Open `configuration.py` with your preferred editor to begin configuring NetBox.
|
|||||||
|
|
||||||
### ALLOWED_HOSTS
|
### ALLOWED_HOSTS
|
||||||
|
|
||||||
This is a list of the valid hostnames and IP addresses by which this server can be reached. You must specify at least one name or IP address. (Note that this does not restrict the locations from which NetBox may be accessed: It is merely for [HTTP host header validation](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting).)
|
This is a list of the valid hostnames and IP addresses by which this server can be reached. You must specify at least one name or IP address. (Note that this does not restrict the locations from which NetBox may be accessed: It is merely for [HTTP host header validation](https://docs.djangoproject.com/en/stable/topics/security/#host-headers-virtual-hosting).)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
ALLOWED_HOSTS = ['netbox.example.com', '192.0.2.123']
|
ALLOWED_HOSTS = ['netbox.example.com', '192.0.2.123']
|
||||||
|
@ -28,7 +28,7 @@ NetBox ships with a default configuration file for uWSGI. To use it, copy `/opt/
|
|||||||
sudo cp /opt/netbox/contrib/uwsgi.ini /opt/netbox/uwsgi.ini
|
sudo cp /opt/netbox/contrib/uwsgi.ini /opt/netbox/uwsgi.ini
|
||||||
```
|
```
|
||||||
|
|
||||||
While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app.
|
While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/stable/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app.
|
||||||
|
|
||||||
## systemd Setup
|
## systemd Setup
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ from netbox.plugins import PluginConfig
|
|||||||
|
|
||||||
### ContentType renamed to ObjectType
|
### ContentType renamed to ObjectType
|
||||||
|
|
||||||
NetBox's proxy model for Django's [ContentType model](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#the-contenttype-model) has been renamed to ObjectType for clarity. In general, plugins should use the ObjectType proxy when referencing content types, as it includes several custom manager methods. The one exception to this is when defining [generic foreign keys](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#generic-relations): The ForeignKey field used for a GFK should point to Django's native ContentType.
|
NetBox's proxy model for Django's [ContentType model](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#the-contenttype-model) has been renamed to ObjectType for clarity. In general, plugins should use the ObjectType proxy when referencing content types, as it includes several custom manager methods. The one exception to this is when defining [generic foreign keys](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#generic-relations): The ForeignKey field used for a GFK should point to Django's native ContentType.
|
||||||
|
|
||||||
Additionally, plugin maintainers are strongly encouraged to adopt the "object type" terminology for field and filter names wherever feasible to be consistent with NetBox core (however this is not required for compatibility).
|
Additionally, plugin maintainers are strongly encouraged to adopt the "object type" terminology for field and filter names wherever feasible to be consistent with NetBox core (however this is not required for compatibility).
|
||||||
|
|
||||||
|
@ -86,3 +86,69 @@ netbox=> DELETE FROM django_migrations WHERE app='pluginname';
|
|||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Exercise extreme caution when altering Django system tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.
|
Exercise extreme caution when altering Django system tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.
|
||||||
|
|
||||||
|
## Clean Up Content Types and Permissions
|
||||||
|
|
||||||
|
After removing a plugin and its database tables, you may find that object type references (`ContentTypes`) created by the plugin still appear in the permissions management section (e.g., when editing permissions in the NetBox UI).
|
||||||
|
This happens because the `django_content_type` table retains entries for the models that the plugin registered with Django.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Please use caution when removing `ContentTypes`. It is strongly recommended to **back up your database** before making these changes.
|
||||||
|
|
||||||
|
**Identify Stale Content Types:**
|
||||||
|
|
||||||
|
Open the Django shell to inspect lingering `ContentType` entries related to the removed plugin.
|
||||||
|
Typically, the Content Type's `app_label` matches the plugin’s name.
|
||||||
|
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
$ cd /opt/netbox/
|
||||||
|
$ source /opt/netbox/venv/bin/activate
|
||||||
|
(venv) $ python3 netbox/manage.py nbshell
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in the shell:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
# Replace 'pluginname' with your plugin's actual name
|
||||||
|
stale_types = ContentType.objects.filter(app_label="pluginname")
|
||||||
|
for ct in stale_types:
|
||||||
|
print(ct)
|
||||||
|
### ^^^ These will be removed, make sure its ok
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Review the output carefully and confirm that each listed Content Type is related to the plugin you removed.
|
||||||
|
|
||||||
|
**Remove Stale Content Types and Related Permissions:**
|
||||||
|
|
||||||
|
Next, check for any permissions associated with these Content Types:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
for ct in stale_types:
|
||||||
|
perms = Permission.objects.filter(content_type=ct)
|
||||||
|
print(list(perms))
|
||||||
|
```
|
||||||
|
|
||||||
|
If there are related Permissions, you can remove them safely:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
for ct in stale_types:
|
||||||
|
Permission.objects.filter(content_type=ct).delete()
|
||||||
|
```
|
||||||
|
|
||||||
|
After removing any related permissions, delete the Content Type entries:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
stale_types.delete()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restart NetBox:**
|
||||||
|
|
||||||
|
After making these changes, restart the NetBox service to ensure all changes are reflected.
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
sudo systemctl restart netbox
|
||||||
|
```
|
||||||
|
@ -1,5 +1,31 @@
|
|||||||
# NetBox v4.3
|
# NetBox v4.3
|
||||||
|
|
||||||
|
## v4.3.2 (2025-06-05)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#19200](https://github.com/netbox-community/netbox/issues/19200) - Display assigned virtual chassis (if any) on device view
|
||||||
|
* [#19461](https://github.com/netbox-community/netbox/issues/19461) - Add color backgrounds for virtual circuit types
|
||||||
|
* [#19605](https://github.com/netbox-community/netbox/issues/19605) - Enable filtering IP addresses by family in GraphQL API
|
||||||
|
* [#19627](https://github.com/netbox-community/netbox/issues/19627) - Introduce object change migrators
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#19415](https://github.com/netbox-community/netbox/issues/19415) - Increase maximum supported distance for circuits and wireless links
|
||||||
|
* [#19475](https://github.com/netbox-community/netbox/issues/19475) - VLANs belonging to the same location as a VM's cluster should be eligible for assignment to interfaces on that VM
|
||||||
|
* [#19486](https://github.com/netbox-community/netbox/issues/19486) - Fix connection card rendering for console server ports
|
||||||
|
* [#19487](https://github.com/netbox-community/netbox/issues/19487) - Fix `FieldError` exception when ordering circuit or tunnel terminations by the terminating object
|
||||||
|
* [#19490](https://github.com/netbox-community/netbox/issues/19490) - Fix inclusion support for config templates populated via a data source
|
||||||
|
* [#19496](https://github.com/netbox-community/netbox/issues/19496) - Fix `AttributeError` exception when rendering a config template with no output
|
||||||
|
* [#19510](https://github.com/netbox-community/netbox/issues/19510) - Restore GraphQL API filtering for assigned IP addresses
|
||||||
|
* [#19520](https://github.com/netbox-community/netbox/issues/19520) - Restore ability to alter prefix scope via the REST API
|
||||||
|
* [#19587](https://github.com/netbox-community/netbox/issues/19587) - The `occupied` filter should include interfaces terminating a wireless link
|
||||||
|
* [#19599](https://github.com/netbox-community/netbox/issues/19599) - Fix `AttributeError` exception when sorting change history under user view
|
||||||
|
* [#19610](https://github.com/netbox-community/netbox/issues/19610) - Fix `FieldError` exception when sorting tunnel terminations by tenant
|
||||||
|
* [#19623](https://github.com/netbox-community/netbox/issues/19623) - Display description under provider account view
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v4.3.1 (2025-05-13)
|
## v4.3.1 (2025-05-13)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -191,12 +191,9 @@ class ProfileView(LoginRequiredMixin, View):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
||||||
# Compile changelog table
|
# Compile changelog table
|
||||||
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
|
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=request.user)[:20]
|
||||||
user=request.user
|
|
||||||
).prefetch_related(
|
|
||||||
'changed_object_type'
|
|
||||||
)[:20]
|
|
||||||
changelog_table = ObjectChangeTable(changelog)
|
changelog_table = ObjectChangeTable(changelog)
|
||||||
|
changelog_table.orderable = False
|
||||||
changelog_table.configure(request)
|
changelog_table.configure(request)
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@ -49,3 +50,26 @@ class Migration(migrations.Migration):
|
|||||||
# Copy over existing site assignments
|
# Copy over existing site assignments
|
||||||
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
|
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_circuittermination_termination(objectchange, reverting):
|
||||||
|
site_ct = ContentType.objects.get_by_natural_key('dcim', 'site').pk
|
||||||
|
provider_network_ct = ContentType.objects.get_by_natural_key('circuits', 'providernetwork').pk
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is None:
|
||||||
|
continue
|
||||||
|
if site_id := data.get('site'):
|
||||||
|
data.update({
|
||||||
|
'termination_type': site_ct,
|
||||||
|
'termination_id': site_id,
|
||||||
|
})
|
||||||
|
elif provider_network_id := data.get('provider_network'):
|
||||||
|
data.update({
|
||||||
|
'termination_type': provider_network_ct,
|
||||||
|
'termination_id': provider_network_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'circuits.circuittermination': oc_circuittermination_termination,
|
||||||
|
}
|
||||||
|
@ -86,3 +86,15 @@ class Migration(migrations.Migration):
|
|||||||
new_name='_provider_network',
|
new_name='_provider_network',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_circuittermination_remove_fields(objectchange, reverting):
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is not None:
|
||||||
|
data.pop('site', None)
|
||||||
|
data.pop('provider_network', None)
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'circuits.circuittermination': oc_circuittermination_remove_fields,
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@ -82,3 +83,21 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_circuitgroupassignment_member(objectchange, reverting):
|
||||||
|
circuit_ct = ContentType.objects.get_by_natural_key('circuits', 'circuit').pk
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is None:
|
||||||
|
continue
|
||||||
|
if circuit_id := data.get('circuit'):
|
||||||
|
data.update({
|
||||||
|
'member_type': circuit_ct,
|
||||||
|
'member_id': circuit_id,
|
||||||
|
})
|
||||||
|
data.pop('circuit', None)
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'circuits.circuitgroupassignment': oc_circuitgroupassignment_member,
|
||||||
|
}
|
||||||
|
@ -120,7 +120,8 @@ class CircuitTerminationTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
termination = tables.Column(
|
termination = tables.Column(
|
||||||
verbose_name=_('Termination Point'),
|
verbose_name=_('Termination Point'),
|
||||||
linkify=True
|
linkify=True,
|
||||||
|
orderable=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Termination types
|
# Termination types
|
||||||
@ -132,7 +133,7 @@ class CircuitTerminationTable(NetBoxTable):
|
|||||||
site_group = tables.Column(
|
site_group = tables.Column(
|
||||||
verbose_name=_('Site Group'),
|
verbose_name=_('Site Group'),
|
||||||
linkify=True,
|
linkify=True,
|
||||||
accessor='_sitegroup'
|
accessor='_site_group'
|
||||||
)
|
)
|
||||||
region = tables.Column(
|
region = tables.Column(
|
||||||
verbose_name=_('Region'),
|
verbose_name=_('Region'),
|
||||||
|
23
netbox/circuits/tests/test_tables.py
Normal file
23
netbox/circuits/tests/test_tables.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django.test import RequestFactory, tag, TestCase
|
||||||
|
|
||||||
|
from circuits.models import CircuitTermination
|
||||||
|
from circuits.tables import CircuitTerminationTable
|
||||||
|
|
||||||
|
|
||||||
|
@tag('regression')
|
||||||
|
class CircuitTerminationTableTest(TestCase):
|
||||||
|
def test_every_orderable_field_does_not_throw_exception(self):
|
||||||
|
terminations = CircuitTermination.objects.all()
|
||||||
|
disallowed = {'actions', }
|
||||||
|
|
||||||
|
orderable_columns = [
|
||||||
|
column.name for column in CircuitTerminationTable(terminations).columns
|
||||||
|
if column.orderable and column.name not in disallowed
|
||||||
|
]
|
||||||
|
fake_request = RequestFactory().get("/")
|
||||||
|
|
||||||
|
for col in orderable_columns:
|
||||||
|
for dir in ('-', ''):
|
||||||
|
table = CircuitTerminationTable(terminations)
|
||||||
|
table.order_by = f'{dir}{col}'
|
||||||
|
table.as_html(fake_request)
|
@ -162,6 +162,12 @@ def handle_deleted_object(sender, instance, **kwargs):
|
|||||||
getattr(obj, related_field_name).remove(instance)
|
getattr(obj, related_field_name).remove(instance)
|
||||||
elif type(relation) is ManyToOneRel and relation.field.null is True:
|
elif type(relation) is ManyToOneRel and relation.field.null is True:
|
||||||
setattr(obj, related_field_name, None)
|
setattr(obj, related_field_name, None)
|
||||||
|
# make sure the object hasn't been deleted - in case of
|
||||||
|
# deletion chaining of related objects
|
||||||
|
try:
|
||||||
|
obj.refresh_from_db()
|
||||||
|
except DoesNotExist:
|
||||||
|
continue
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
# Enqueue the object for event processing
|
# Enqueue the object for event processing
|
||||||
|
@ -6,12 +6,13 @@ from rest_framework import status
|
|||||||
from core.choices import ObjectChangeActionChoices
|
from core.choices import ObjectChangeActionChoices
|
||||||
from core.models import ObjectChange, ObjectType
|
from core.models import ObjectChange, ObjectType
|
||||||
from dcim.choices import SiteStatusChoices
|
from dcim.choices import SiteStatusChoices
|
||||||
from dcim.models import Site
|
from dcim.models import Site, CableTermination, Device, DeviceType, DeviceRole, Interface, Cable
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import CustomField, CustomFieldChoiceSet, Tag
|
from extras.models import CustomField, CustomFieldChoiceSet, Tag
|
||||||
from utilities.testing import APITestCase
|
from utilities.testing import APITestCase
|
||||||
from utilities.testing.utils import create_tags, post_data
|
from utilities.testing.utils import create_tags, post_data
|
||||||
from utilities.testing.views import ModelViewTestCase
|
from utilities.testing.views import ModelViewTestCase
|
||||||
|
from dcim.models import Manufacturer
|
||||||
|
|
||||||
|
|
||||||
class ChangeLogViewTest(ModelViewTestCase):
|
class ChangeLogViewTest(ModelViewTestCase):
|
||||||
@ -270,6 +271,81 @@ class ChangeLogViewTest(ModelViewTestCase):
|
|||||||
# Check that no ObjectChange records have been created
|
# Check that no ObjectChange records have been created
|
||||||
self.assertEqual(ObjectChange.objects.count(), 0)
|
self.assertEqual(ObjectChange.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_ordering_genericrelation(self):
|
||||||
|
# Create required objects first
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
|
||||||
|
device_type = DeviceType.objects.create(
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
model='Model 1',
|
||||||
|
slug='model-1'
|
||||||
|
)
|
||||||
|
device_role = DeviceRole.objects.create(
|
||||||
|
name='Role 1',
|
||||||
|
slug='role-1'
|
||||||
|
)
|
||||||
|
site = Site.objects.create(
|
||||||
|
name='Site 1',
|
||||||
|
slug='site-1'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create two devices
|
||||||
|
device1 = Device.objects.create(
|
||||||
|
name='Device 1',
|
||||||
|
device_type=device_type,
|
||||||
|
role=device_role,
|
||||||
|
site=site
|
||||||
|
)
|
||||||
|
device2 = Device.objects.create(
|
||||||
|
name='Device 2',
|
||||||
|
device_type=device_type,
|
||||||
|
role=device_role,
|
||||||
|
site=site
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create interfaces on both devices
|
||||||
|
interface1 = Interface.objects.create(
|
||||||
|
device=device1,
|
||||||
|
name='eth0',
|
||||||
|
type='1000base-t'
|
||||||
|
)
|
||||||
|
interface2 = Interface.objects.create(
|
||||||
|
device=device2,
|
||||||
|
name='eth0',
|
||||||
|
type='1000base-t'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a cable between the interfaces
|
||||||
|
_ = Cable.objects.create(
|
||||||
|
a_terminations=[interface1],
|
||||||
|
b_terminations=[interface2],
|
||||||
|
status='connected'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete device1
|
||||||
|
request = {
|
||||||
|
'path': reverse('dcim:device_delete', kwargs={'pk': device1.pk}),
|
||||||
|
'data': post_data({'confirm': True}),
|
||||||
|
}
|
||||||
|
self.add_permissions(
|
||||||
|
'dcim.delete_device',
|
||||||
|
'dcim.delete_interface',
|
||||||
|
'dcim.delete_cable',
|
||||||
|
'dcim.delete_cabletermination'
|
||||||
|
)
|
||||||
|
response = self.client.post(**request)
|
||||||
|
self.assertHttpStatus(response, 302)
|
||||||
|
|
||||||
|
# Get the ObjectChange records for delete actions ordered by time
|
||||||
|
changes = ObjectChange.objects.filter(
|
||||||
|
action=ObjectChangeActionChoices.ACTION_DELETE
|
||||||
|
).order_by('time')[:3]
|
||||||
|
|
||||||
|
# Verify the order of deletion
|
||||||
|
self.assertEqual(len(changes), 3)
|
||||||
|
self.assertEqual(changes[0].changed_object_type, ContentType.objects.get_for_model(CableTermination))
|
||||||
|
self.assertEqual(changes[1].changed_object_type, ContentType.objects.get_for_model(Interface))
|
||||||
|
self.assertEqual(changes[2].changed_object_type, ContentType.objects.get_for_model(Device))
|
||||||
|
|
||||||
|
|
||||||
class ChangeLogAPITest(APITestCase):
|
class ChangeLogAPITest(APITestCase):
|
||||||
|
|
||||||
|
@ -53,6 +53,11 @@ WIRELESS_IFACE_TYPES = [
|
|||||||
InterfaceTypeChoices.TYPE_802151,
|
InterfaceTypeChoices.TYPE_802151,
|
||||||
InterfaceTypeChoices.TYPE_802154,
|
InterfaceTypeChoices.TYPE_802154,
|
||||||
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
|
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
|
||||||
|
InterfaceTypeChoices.TYPE_GSM,
|
||||||
|
InterfaceTypeChoices.TYPE_CDMA,
|
||||||
|
InterfaceTypeChoices.TYPE_LTE,
|
||||||
|
InterfaceTypeChoices.TYPE_4G,
|
||||||
|
InterfaceTypeChoices.TYPE_5G,
|
||||||
]
|
]
|
||||||
|
|
||||||
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||||
|
@ -2012,6 +2012,21 @@ class InterfaceFilterSet(
|
|||||||
'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
|
'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
|
||||||
}.get(value, queryset.none())
|
}.get(value, queryset.none())
|
||||||
|
|
||||||
|
# Override the method on CabledObjectFilterSet to also check for wireless links
|
||||||
|
def filter_occupied(self, queryset, name, value):
|
||||||
|
if value:
|
||||||
|
return queryset.filter(
|
||||||
|
Q(cable__isnull=False) |
|
||||||
|
Q(wireless_link__isnull=False) |
|
||||||
|
Q(mark_connected=True)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return queryset.filter(
|
||||||
|
cable__isnull=True,
|
||||||
|
wireless_link__isnull=True,
|
||||||
|
mark_connected=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FrontPortFilterSet(
|
class FrontPortFilterSet(
|
||||||
ModularDeviceComponentFilterSet,
|
ModularDeviceComponentFilterSet,
|
||||||
|
@ -100,3 +100,16 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_rename_type(objectchange, reverting):
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is None:
|
||||||
|
continue
|
||||||
|
if 'type' in data:
|
||||||
|
data['form_factor'] = data.pop('type')
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'dcim.rack': oc_rename_type,
|
||||||
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.apps import apps
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@ -15,7 +17,7 @@ def populate_mac_addresses(apps, schema_editor):
|
|||||||
assigned_object_type=interface_ct,
|
assigned_object_type=interface_ct,
|
||||||
assigned_object_id=interface.pk
|
assigned_object_id=interface.pk
|
||||||
)
|
)
|
||||||
for interface in Interface.objects.filter(mac_address__isnull=False)
|
for interface in Interface.objects.using(db_alias).filter(mac_address__isnull=False)
|
||||||
]
|
]
|
||||||
MACAddress.objects.using(db_alias).bulk_create(mac_addresses, batch_size=100)
|
MACAddress.objects.using(db_alias).bulk_create(mac_addresses, batch_size=100)
|
||||||
|
|
||||||
@ -51,3 +53,43 @@ class Migration(migrations.Migration):
|
|||||||
name='mac_address',
|
name='mac_address',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# See peer migrator in virtualization.0048_populate_mac_addresses before making changes
|
||||||
|
def oc_interface_primary_mac_address(objectchange, reverting):
|
||||||
|
MACAddress = apps.get_model('dcim', 'MACAddress')
|
||||||
|
interface_ct = ContentType.objects.get_by_natural_key('dcim', 'interface')
|
||||||
|
|
||||||
|
# Swap data order if the change is being reverted
|
||||||
|
if not reverting:
|
||||||
|
before, after = objectchange.prechange_data, objectchange.postchange_data
|
||||||
|
else:
|
||||||
|
before, after = objectchange.postchange_data, objectchange.prechange_data
|
||||||
|
|
||||||
|
if after.get('mac_address') != before.get('mac_address'):
|
||||||
|
# Create & assign the new MACAddress (if any)
|
||||||
|
if after.get('mac_address'):
|
||||||
|
mac = MACAddress.objects.create(
|
||||||
|
mac_address=after['mac_address'],
|
||||||
|
assigned_object_type=interface_ct,
|
||||||
|
assigned_object_id=objectchange.changed_object_id,
|
||||||
|
)
|
||||||
|
after['primary_mac_address'] = mac.pk
|
||||||
|
else:
|
||||||
|
after['primary_mac_address'] = None
|
||||||
|
# Delete the old MACAddress (if any)
|
||||||
|
if before.get('mac_address'):
|
||||||
|
MACAddress.objects.filter(
|
||||||
|
mac_address=before['mac_address'],
|
||||||
|
assigned_object_type=interface_ct,
|
||||||
|
assigned_object_id=objectchange.changed_object_id,
|
||||||
|
).delete()
|
||||||
|
before['primary_mac_address'] = None
|
||||||
|
|
||||||
|
before.pop('mac_address', None)
|
||||||
|
after.pop('mac_address', None)
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'dcim.interface': oc_interface_primary_mac_address,
|
||||||
|
}
|
||||||
|
@ -144,7 +144,7 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
|||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate any attributes against the assigned profile's schema
|
# Validate any attributes against the assigned profile's schema
|
||||||
if self.profile:
|
if self.profile and self.profile.schema:
|
||||||
try:
|
try:
|
||||||
jsonschema.validate(self.attribute_data, schema=self.profile.schema)
|
jsonschema.validate(self.attribute_data, schema=self.profile.schema)
|
||||||
except JSONValidationError as e:
|
except JSONValidationError as e:
|
||||||
|
@ -12,6 +12,7 @@ from users.models import User
|
|||||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
|
||||||
from virtualization.models import Cluster, ClusterType, ClusterGroup, VMInterface, VirtualMachine
|
from virtualization.models import Cluster, ClusterType, ClusterGroup, VMInterface, VirtualMachine
|
||||||
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||||
|
from wireless.models import WirelessLink
|
||||||
|
|
||||||
|
|
||||||
class DeviceComponentFilterSetTests:
|
class DeviceComponentFilterSetTests:
|
||||||
@ -4496,7 +4497,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
# Cables
|
# Cables
|
||||||
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save()
|
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save()
|
||||||
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save()
|
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save()
|
||||||
# Third pair is not connected
|
|
||||||
|
# Wireless links
|
||||||
|
WirelessLink(interface_a=interfaces[7], interface_b=interfaces[8]).save()
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
params = {'name': ['Interface 1', 'Interface 2']}
|
params = {'name': ['Interface 1', 'Interface 2']}
|
||||||
@ -4684,15 +4687,15 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
|||||||
|
|
||||||
def test_occupied(self):
|
def test_occupied(self):
|
||||||
params = {'occupied': True}
|
params = {'occupied': True}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
params = {'occupied': False}
|
params = {'occupied': False}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
def test_connected(self):
|
def test_connected(self):
|
||||||
params = {'connected': True}
|
params = {'connected': True}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
params = {'connected': False}
|
params = {'connected': False}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||||
|
|
||||||
def test_kind(self):
|
def test_kind(self):
|
||||||
params = {'kind': 'physical'}
|
params = {'kind': 'physical'}
|
||||||
|
@ -954,6 +954,19 @@ class CableTestCase(TestCase):
|
|||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
cable.clean()
|
cable.clean()
|
||||||
|
|
||||||
|
@tag('regression')
|
||||||
|
def test_cable_cannot_terminate_to_a_cellular_interface(self):
|
||||||
|
"""
|
||||||
|
A cable cannot terminate to a cellular interface
|
||||||
|
"""
|
||||||
|
device1 = Device.objects.get(name='TestDevice1')
|
||||||
|
interface2 = Interface.objects.get(device__name='TestDevice2', name='eth0')
|
||||||
|
|
||||||
|
cellular_interface = Interface(device=device1, name="W1", type=InterfaceTypeChoices.TYPE_LTE)
|
||||||
|
cable = Cable(a_terminations=[interface2], b_terminations=[cellular_interface])
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
cable.clean()
|
||||||
|
|
||||||
|
|
||||||
class VirtualDeviceContextTestCase(TestCase):
|
class VirtualDeviceContextTestCase(TestCase):
|
||||||
|
|
||||||
|
@ -66,11 +66,11 @@ class ScriptInputSerializer(serializers.Serializer):
|
|||||||
interval = serializers.IntegerField(required=False, allow_null=True)
|
interval = serializers.IntegerField(required=False, allow_null=True)
|
||||||
|
|
||||||
def validate_schedule_at(self, value):
|
def validate_schedule_at(self, value):
|
||||||
if value and not self.context['script'].scheduling_enabled:
|
if value and not self.context['script'].python_class.scheduling_enabled:
|
||||||
raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
|
raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_interval(self, value):
|
def validate_interval(self, value):
|
||||||
if value and not self.context['script'].scheduling_enabled:
|
if value and not self.context['script'].python_class.scheduling_enabled:
|
||||||
raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
|
raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
|
||||||
return value
|
return value
|
||||||
|
@ -270,6 +270,7 @@ class ScriptViewSet(ModelViewSet):
|
|||||||
module_name, script_name = pk.split('.', maxsplit=1)
|
module_name, script_name = pk.split('.', maxsplit=1)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
return get_object_or_404(self.queryset, module__file_path=f'{module_name}.py', name=script_name)
|
return get_object_or_404(self.queryset, module__file_path=f'{module_name}.py', name=script_name)
|
||||||
|
|
||||||
def retrieve(self, request, pk):
|
def retrieve(self, request, pk):
|
||||||
|
@ -238,10 +238,18 @@ class TagImportForm(CSVModelForm):
|
|||||||
label=_('Weight'),
|
label=_('Weight'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
object_types = CSVMultipleContentTypeField(
|
||||||
|
label=_('Object types'),
|
||||||
|
queryset=ObjectType.objects.with_feature('tags'),
|
||||||
|
help_text=_("One or more assigned object types"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = ('name', 'slug', 'color', 'weight', 'description')
|
fields = (
|
||||||
|
'name', 'slug', 'color', 'weight', 'description', 'object_types',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryImportForm(NetBoxModelImportForm):
|
class JournalEntryImportForm(NetBoxModelImportForm):
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.files.storage import storages
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from core.choices import JobIntervalChoices
|
from core.choices import JobIntervalChoices
|
||||||
from core.forms import ManagedFileForm
|
from core.forms import ManagedFileForm
|
||||||
from extras.storage import ScriptFileSystemStorage
|
from django import forms
|
||||||
|
from django.core.files.storage import storages
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from utilities.datetime import local_now
|
from utilities.datetime import local_now
|
||||||
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
|
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
|
||||||
|
|
||||||
@ -74,12 +69,7 @@ class ScriptFileForm(ManagedFileForm):
|
|||||||
storage = storages.create_storage(storages.backends["scripts"])
|
storage = storages.create_storage(storages.backends["scripts"])
|
||||||
|
|
||||||
filename = self.cleaned_data['upload_file'].name
|
filename = self.cleaned_data['upload_file'].name
|
||||||
if isinstance(storage, ScriptFileSystemStorage):
|
self.instance.file_path = filename
|
||||||
full_path = os.path.join(settings.SCRIPTS_ROOT, filename)
|
|
||||||
else:
|
|
||||||
full_path = filename
|
|
||||||
|
|
||||||
self.instance.file_path = full_path
|
|
||||||
data = self.cleaned_data['upload_file']
|
data = self.cleaned_data['upload_file']
|
||||||
storage.save(filename, data)
|
storage.save(filename, data)
|
||||||
|
|
||||||
|
56
netbox/extras/migrations/0129_fix_script_paths.py
Normal file
56
netbox/extras/migrations/0129_fix_script_paths.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import storages
|
||||||
|
from django.db import migrations
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from extras.storage import ScriptFileSystemStorage
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(url):
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
if not parsed_url.path.endswith('/'):
|
||||||
|
return url + '/'
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def fix_script_paths(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Fix script paths for scripts that had incorrect path from NB 4.3.
|
||||||
|
"""
|
||||||
|
storage = storages.create_storage(storages.backends["scripts"])
|
||||||
|
if not isinstance(storage, ScriptFileSystemStorage):
|
||||||
|
return
|
||||||
|
|
||||||
|
ScriptModule = apps.get_model('extras', 'ScriptModule')
|
||||||
|
script_root_path = normalize(settings.SCRIPTS_ROOT)
|
||||||
|
for script in ScriptModule.objects.filter(file_path__startswith=script_root_path):
|
||||||
|
script.file_path = script.file_path[len(script_root_path):]
|
||||||
|
script.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0128_tableconfig'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(code=fix_script_paths, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_fix_script_paths(objectchange, reverting):
|
||||||
|
script_root_path = normalize(settings.SCRIPTS_ROOT)
|
||||||
|
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if file_path := data.get('file_path'):
|
||||||
|
if file_path.startswith(script_root_path):
|
||||||
|
data['file_path'] = file_path[len(script_root_path):]
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'extras.scriptmodule': oc_fix_script_paths,
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -144,6 +144,12 @@ class NotificationGroup(ChangeLoggedModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
related_name='notification_groups'
|
related_name='notification_groups'
|
||||||
)
|
)
|
||||||
|
event_rules = GenericRelation(
|
||||||
|
to='extras.EventRule',
|
||||||
|
content_type_field='action_object_type',
|
||||||
|
object_id_field='action_object_id',
|
||||||
|
related_query_name='+'
|
||||||
|
)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
@ -444,6 +444,8 @@ class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
site_ct = ContentType.objects.get_for_model(Site)
|
||||||
|
|
||||||
tags = (
|
tags = (
|
||||||
Tag(name='Tag 1', slug='tag-1'),
|
Tag(name='Tag 1', slug='tag-1'),
|
||||||
Tag(name='Tag 2', slug='tag-2', weight=1),
|
Tag(name='Tag 2', slug='tag-2', weight=1),
|
||||||
@ -456,14 +458,15 @@ class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
'slug': 'tag-x',
|
'slug': 'tag-x',
|
||||||
'color': 'c0c0c0',
|
'color': 'c0c0c0',
|
||||||
'comments': 'Some comments',
|
'comments': 'Some comments',
|
||||||
|
'object_types': [site_ct.pk],
|
||||||
'weight': 11,
|
'weight': 11,
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"name,slug,color,description,weight",
|
"name,slug,color,description,object_types,weight",
|
||||||
"Tag 4,tag-4,ff0000,Fourth tag,0",
|
"Tag 4,tag-4,ff0000,Fourth tag,dcim.interface,0",
|
||||||
"Tag 5,tag-5,00ff00,Fifth tag,1111",
|
"Tag 5,tag-5,00ff00,Fifth tag,'dcim.device,dcim.site',1111",
|
||||||
"Tag 6,tag-6,0000ff,Sixth tag,0",
|
"Tag 6,tag-6,0000ff,Sixth tag,dcim.site,0",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.csv_update_data = (
|
cls.csv_update_data = (
|
||||||
|
@ -1476,7 +1476,16 @@ class ScriptResultView(TableMixin, generic.ObjectView):
|
|||||||
table = None
|
table = None
|
||||||
job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk'))
|
job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk'))
|
||||||
|
|
||||||
if job.completed:
|
# If a direct export output has been requested, return the job data content as a
|
||||||
|
# downloadable file.
|
||||||
|
if job.completed and request.GET.get('export') == 'output':
|
||||||
|
content = (job.data.get("output") or "").encode()
|
||||||
|
response = HttpResponse(content, content_type='text')
|
||||||
|
filename = f"{job.object.name or 'script-output'}_{job.completed.strftime('%Y-%m-%d_%H%M%S')}.txt"
|
||||||
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||||
|
return response
|
||||||
|
|
||||||
|
elif job.completed:
|
||||||
table = self.get_table(job, request, bulk_actions=False)
|
table = self.get_table(job, request, bulk_actions=False)
|
||||||
|
|
||||||
log_threshold = request.GET.get('log_threshold', LogLevelChoices.LOG_INFO)
|
log_threshold = request.GET.get('log_threshold', LogLevelChoices.LOG_INFO)
|
||||||
|
@ -449,7 +449,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet, C
|
|||||||
@extend_schema_field(OpenApiTypes.STR)
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def filter_present_in_vrf(self, queryset, name, vrf):
|
def filter_present_in_vrf(self, queryset, name, vrf):
|
||||||
if vrf is None:
|
if vrf is None:
|
||||||
return queryset.none
|
return queryset.none()
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(vrf=vrf) |
|
Q(vrf=vrf) |
|
||||||
Q(vrf__export_targets__in=vrf.import_targets.all())
|
Q(vrf__export_targets__in=vrf.import_targets.all())
|
||||||
@ -729,7 +729,7 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFil
|
|||||||
@extend_schema_field(OpenApiTypes.STR)
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def filter_present_in_vrf(self, queryset, name, vrf):
|
def filter_present_in_vrf(self, queryset, name, vrf):
|
||||||
if vrf is None:
|
if vrf is None:
|
||||||
return queryset.none
|
return queryset.none()
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(vrf=vrf) |
|
Q(vrf=vrf) |
|
||||||
Q(vrf__export_targets__in=vrf.import_targets.all())
|
Q(vrf__export_targets__in=vrf.import_targets.all())
|
||||||
|
@ -826,7 +826,7 @@ class ServiceForm(NetBoxModelForm):
|
|||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if self.instance and parent_object_type_id != self.instance.parent_object_type_id:
|
if self.instance and self.instance.pk and parent_object_type_id != self.instance.parent_object_type_id:
|
||||||
self.initial['parent'] = None
|
self.initial['parent'] = None
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -159,6 +159,14 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
|||||||
return Q()
|
return Q()
|
||||||
return q
|
return q
|
||||||
|
|
||||||
|
@strawberry_django.filter_field()
|
||||||
|
def family(
|
||||||
|
self,
|
||||||
|
value: Annotated['IPAddressFamilyEnum', strawberry.lazy('ipam.graphql.enums')],
|
||||||
|
prefix,
|
||||||
|
) -> Q:
|
||||||
|
return Q(**{f"{prefix}address__family": value.value})
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter_type(models.IPRange, lookups=True)
|
@strawberry_django.filter_type(models.IPRange, lookups=True)
|
||||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@ -44,3 +45,20 @@ class Migration(migrations.Migration):
|
|||||||
# Copy over existing site assignments
|
# Copy over existing site assignments
|
||||||
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
|
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_prefix_scope(objectchange, reverting):
|
||||||
|
site_ct = ContentType.objects.get_by_natural_key('dcim', 'site').pk
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is None:
|
||||||
|
continue
|
||||||
|
if site_id := data.get('site'):
|
||||||
|
data.update({
|
||||||
|
'scope_type': site_ct,
|
||||||
|
'scope_id': site_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'ipam.prefix': oc_prefix_scope,
|
||||||
|
}
|
||||||
|
@ -60,3 +60,14 @@ class Migration(migrations.Migration):
|
|||||||
name='site',
|
name='site',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_prefix_remove_fields(objectchange, reverting):
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is not None:
|
||||||
|
data.pop('site', None)
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'ipam.prefix': oc_prefix_remove_fields,
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
|
|
||||||
@ -54,3 +55,26 @@ class Migration(migrations.Migration):
|
|||||||
reverse_code=repopulate_device_and_virtualmachine_relations,
|
reverse_code=repopulate_device_and_virtualmachine_relations,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_service_parent(objectchange, reverting):
|
||||||
|
device_ct = ContentType.objects.get_by_natural_key('dcim', 'device').pk
|
||||||
|
virtual_machine_ct = ContentType.objects.get_by_natural_key('virtualization', 'virtualmachine').pk
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is None:
|
||||||
|
continue
|
||||||
|
if device_id := data.get('device'):
|
||||||
|
data.update({
|
||||||
|
'parent_object_type': device_ct,
|
||||||
|
'parent_object_id': device_id,
|
||||||
|
})
|
||||||
|
elif virtual_machine_id := data.get('virtual_machine'):
|
||||||
|
data.update({
|
||||||
|
'parent_object_type': virtual_machine_ct,
|
||||||
|
'parent_object_id': virtual_machine_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'ipam.service': oc_service_parent,
|
||||||
|
}
|
||||||
|
@ -37,3 +37,15 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_service_remove_fields(objectchange, reverting):
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is not None:
|
||||||
|
data.pop('device', None)
|
||||||
|
data.pop('virtual_machine', None)
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'ipam.service': oc_service_remove_fields,
|
||||||
|
}
|
||||||
|
@ -231,14 +231,19 @@ SESSION_FILE_PATH = None
|
|||||||
# DISK_BASE_UNIT = 1024
|
# DISK_BASE_UNIT = 1024
|
||||||
# RAM_BASE_UNIT = 1024
|
# RAM_BASE_UNIT = 1024
|
||||||
|
|
||||||
# By default, uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the
|
# Within the STORAGES dictionary, "default" is used for image uploads, "staticfiles" is for static files and "scripts"
|
||||||
# class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example:
|
# is used for custom scripts. See django-storages and django-storage-swift libraries for more details. By default the
|
||||||
# STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage'
|
# following configuration is used:
|
||||||
# STORAGE_CONFIG = {
|
# STORAGES = {
|
||||||
# 'AWS_ACCESS_KEY_ID': 'Key ID',
|
# "default": {
|
||||||
# 'AWS_SECRET_ACCESS_KEY': 'Secret',
|
# "BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
# 'AWS_STORAGE_BUCKET_NAME': 'netbox',
|
# },
|
||||||
# 'AWS_S3_REGION_NAME': 'eu-west-1',
|
# "staticfiles": {
|
||||||
|
# "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
# },
|
||||||
|
# "scripts": {
|
||||||
|
# "BACKEND": "extras.storage.ScriptFileSystemStorage",
|
||||||
|
# },
|
||||||
# }
|
# }
|
||||||
|
|
||||||
# Time zone (default: UTC)
|
# Time zone (default: UTC)
|
||||||
|
90
netbox/netbox/models/deletion.py
Normal file
90
netbox/netbox/models/deletion.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.db import router
|
||||||
|
from django.db.models.deletion import Collector
|
||||||
|
|
||||||
|
logger = logging.getLogger("netbox.models.deletion")
|
||||||
|
|
||||||
|
|
||||||
|
class CustomCollector(Collector):
|
||||||
|
"""
|
||||||
|
Custom collector that handles GenericRelations correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def collect(
|
||||||
|
self,
|
||||||
|
objs,
|
||||||
|
source=None,
|
||||||
|
nullable=False,
|
||||||
|
collect_related=True,
|
||||||
|
source_attr=None,
|
||||||
|
reverse_dependency=False,
|
||||||
|
keep_parents=False,
|
||||||
|
fail_on_restricted=True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Override collect to first collect standard dependencies,
|
||||||
|
then add GenericRelations to the dependency graph.
|
||||||
|
"""
|
||||||
|
# Call parent collect first to get all standard dependencies
|
||||||
|
super().collect(
|
||||||
|
objs,
|
||||||
|
source=source,
|
||||||
|
nullable=nullable,
|
||||||
|
collect_related=collect_related,
|
||||||
|
source_attr=source_attr,
|
||||||
|
reverse_dependency=reverse_dependency,
|
||||||
|
keep_parents=keep_parents,
|
||||||
|
fail_on_restricted=fail_on_restricted,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track which GenericRelations we've already processed to prevent infinite recursion
|
||||||
|
processed_relations = set()
|
||||||
|
|
||||||
|
# Now add GenericRelations to the dependency graph
|
||||||
|
for _, instances in list(self.data.items()):
|
||||||
|
for instance in instances:
|
||||||
|
# Get all GenericRelations for this model
|
||||||
|
for field in instance._meta.private_fields:
|
||||||
|
if isinstance(field, GenericRelation):
|
||||||
|
# Create a unique key for this relation
|
||||||
|
relation_key = f"{instance._meta.model_name}.{field.name}"
|
||||||
|
if relation_key in processed_relations:
|
||||||
|
continue
|
||||||
|
processed_relations.add(relation_key)
|
||||||
|
|
||||||
|
# Add the model that the generic relation points to as a dependency
|
||||||
|
self.add_dependency(field.related_model, instance, reverse_dependency=True)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteMixin:
|
||||||
|
"""
|
||||||
|
Mixin to override the model delete function to use our custom collector.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def delete(self, using=None, keep_parents=False):
|
||||||
|
"""
|
||||||
|
Override delete to use our custom collector.
|
||||||
|
"""
|
||||||
|
using = using or router.db_for_write(self.__class__, instance=self)
|
||||||
|
assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (
|
||||||
|
self._meta.object_name,
|
||||||
|
self._meta.pk.attname,
|
||||||
|
)
|
||||||
|
|
||||||
|
collector = CustomCollector(using=using)
|
||||||
|
collector.collect([self], keep_parents=keep_parents)
|
||||||
|
|
||||||
|
return collector.delete()
|
||||||
|
|
||||||
|
delete.alters_data = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def verify_mro(cls, instance):
|
||||||
|
"""
|
||||||
|
Verify that this mixin is first in the MRO.
|
||||||
|
"""
|
||||||
|
mro = instance.__class__.__mro__
|
||||||
|
if mro.index(cls) != 0:
|
||||||
|
raise RuntimeError(f"{cls.__name__} must be first in the MRO. Current MRO: {mro}")
|
@ -16,6 +16,7 @@ from extras.choices import *
|
|||||||
from extras.constants import CUSTOMFIELD_EMPTY_VALUES
|
from extras.constants import CUSTOMFIELD_EMPTY_VALUES
|
||||||
from extras.utils import is_taggable
|
from extras.utils import is_taggable
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
|
from netbox.models.deletion import DeleteMixin
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from netbox.signals import post_clean
|
from netbox.signals import post_clean
|
||||||
from utilities.json import CustomFieldJSONEncoder
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
@ -45,7 +46,7 @@ __all__ = (
|
|||||||
# Feature mixins
|
# Feature mixins
|
||||||
#
|
#
|
||||||
|
|
||||||
class ChangeLoggingMixin(models.Model):
|
class ChangeLoggingMixin(DeleteMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Provides change logging support for a model. Adds the `created` and `last_updated` fields.
|
Provides change logging support for a model. Adds the `created` and `last_updated` fields.
|
||||||
"""
|
"""
|
||||||
|
@ -66,6 +66,9 @@ class BaseTable(tables.Table):
|
|||||||
if column.visible:
|
if column.visible:
|
||||||
model = getattr(self.Meta, 'model')
|
model = getattr(self.Meta, 'model')
|
||||||
accessor = column.accessor
|
accessor = column.accessor
|
||||||
|
if accessor.startswith('custom_field_data__'):
|
||||||
|
# Ignore custom field references
|
||||||
|
continue
|
||||||
prefetch_path = []
|
prefetch_path = []
|
||||||
for field_name in accessor.split(accessor.SEPARATOR):
|
for field_name in accessor.split(accessor.SEPARATOR):
|
||||||
try:
|
try:
|
||||||
|
BIN
netbox/project-static/dist/netbox.css
vendored
BIN
netbox/project-static/dist/netbox.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -23,14 +23,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "7.4.47",
|
"@mdi/font": "7.4.47",
|
||||||
"@tabler/core": "1.2.0",
|
"@tabler/core": "1.3.2",
|
||||||
"bootstrap": "5.3.6",
|
"bootstrap": "5.3.6",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"flatpickr": "4.6.13",
|
"flatpickr": "4.6.13",
|
||||||
"gridstack": "12.1.2",
|
"gridstack": "12.2.1",
|
||||||
"htmx.org": "2.0.4",
|
"htmx.org": "2.0.4",
|
||||||
"query-string": "9.1.2",
|
"query-string": "9.2.0",
|
||||||
"sass": "1.88.0",
|
"sass": "1.89.1",
|
||||||
"tom-select": "2.4.3",
|
"tom-select": "2.4.3",
|
||||||
"typeface-inter": "3.18.1",
|
"typeface-inter": "3.18.1",
|
||||||
"typeface-roboto-mono": "1.1.13"
|
"typeface-roboto-mono": "1.1.13"
|
||||||
|
@ -757,13 +757,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
|
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
|
||||||
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
|
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
|
||||||
|
|
||||||
"@tabler/core@1.2.0":
|
"@tabler/core@1.3.2":
|
||||||
version "1.2.0"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@tabler/core/-/core-1.2.0.tgz#cc61cd60d0bc644709bd708f1dd917e760203b4e"
|
resolved "https://registry.yarnpkg.com/@tabler/core/-/core-1.3.2.tgz#85e4a47b661bca4cd50e26039fc25c4bdb4aff34"
|
||||||
integrity sha512-Zrisg/pMi3c/X8AFbmwY6GNlWS/XPlW/jzt6grMar8ICOT7jO0weU9f/KCVgA49I1jMg2ev0uGxcpI5DP3CNdQ==
|
integrity sha512-QDVJbv48YJrahBLdxYkLi6NutQv4jGbkUWyzxE2NcNJ3s3GGpRx98JmbAoN92NZKNmf26vZdW6k2Q5haVKlS4A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@popperjs/core" "^2.11.8"
|
"@popperjs/core" "^2.11.8"
|
||||||
bootstrap "5.3.5"
|
bootstrap "5.3.6"
|
||||||
|
|
||||||
"@tanstack/react-virtual@^3.0.0-beta.60":
|
"@tanstack/react-virtual@^3.0.0-beta.60":
|
||||||
version "3.5.0"
|
version "3.5.0"
|
||||||
@ -1053,11 +1053,6 @@ binary-extensions@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
||||||
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
|
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
|
||||||
|
|
||||||
bootstrap@5.3.5:
|
|
||||||
version "5.3.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.5.tgz#be42cfe0d580e97ee1abb7d38ce94f5c393c9bb6"
|
|
||||||
integrity sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==
|
|
||||||
|
|
||||||
bootstrap@5.3.6:
|
bootstrap@5.3.6:
|
||||||
version "5.3.6"
|
version "5.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.6.tgz#fbd91ebaff093f5b191a1c01a8c866d24f9fa6e1"
|
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.6.tgz#fbd91ebaff093f5b191a1c01a8c866d24f9fa6e1"
|
||||||
@ -1908,10 +1903,10 @@ graphql@16.10.0:
|
|||||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c"
|
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c"
|
||||||
integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==
|
integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==
|
||||||
|
|
||||||
gridstack@12.1.2:
|
gridstack@12.2.1:
|
||||||
version "12.1.2"
|
version "12.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.1.2.tgz#784f6d55873bb48fa9230c1284f769c9fbf785a8"
|
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.2.1.tgz#0e82e3d9d11e5229388d73bd57f8ef1a0e7059c4"
|
||||||
integrity sha512-IC1mkm5xonhAnftwIxsG+B3bawxC61ciKWEvX15ExpVQPbNVN7O9aZZhM7Y/eE4JaIR8PXrdkjd12gMnwNYRLQ==
|
integrity sha512-xU69tThmmVxgMHTuM/z3rIKiiGm0zW4tcB6yRcuwiOUUBiwb3tslzFOrUjWz+PwaxoAW+JChT4fqOLl+oKAxZA==
|
||||||
|
|
||||||
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
@ -2519,10 +2514,10 @@ punycode@^2.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||||
|
|
||||||
query-string@9.1.2:
|
query-string@9.2.0:
|
||||||
version "9.1.2"
|
version "9.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.1.2.tgz#1e4c6a17e2eaab7a282240cf716dec5e72c36cba"
|
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.2.0.tgz#bf9909412689117865aac4e05c10422c4839828f"
|
||||||
integrity sha512-s3UlTyjxRux4KjwWaJsjh1Mp8zoCkSGKirbD9H89pEM9UOZsfpRZpdfzvsy2/mGlLfC3NnYVpy2gk7jXITHEtA==
|
integrity sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
decode-uri-component "^0.4.1"
|
decode-uri-component "^0.4.1"
|
||||||
filter-obj "^5.1.0"
|
filter-obj "^5.1.0"
|
||||||
@ -2665,10 +2660,10 @@ safe-regex-test@^1.0.3:
|
|||||||
es-errors "^1.3.0"
|
es-errors "^1.3.0"
|
||||||
is-regex "^1.1.4"
|
is-regex "^1.1.4"
|
||||||
|
|
||||||
sass@1.88.0:
|
sass@1.89.1:
|
||||||
version "1.88.0"
|
version "1.89.1"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.88.0.tgz#cd1495749bebd9e4aca86e93ee60b3904a107789"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.1.tgz#9281c52c85b4be54264d310fef63a811dfcfb9d9"
|
||||||
integrity sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==
|
integrity sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar "^4.0.0"
|
chokidar "^4.0.0"
|
||||||
immutable "^5.0.2"
|
immutable "^5.0.2"
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
version: "4.3.1"
|
version: "4.3.2"
|
||||||
edition: "Community"
|
edition: "Community"
|
||||||
published: "2025-05-13"
|
published: "2025-06-05"
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
{% extends 'account/base.html' %}
|
{% extends 'account/base.html' %}
|
||||||
{% load helpers %}
|
|
||||||
{% load render_table from django_tables2 %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "User Profile" %}{% endblock %}
|
{% block title %}{% trans "User Profile" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mb-3">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="card-header">{% trans "Account Details" %}</h2>
|
<h2 class="card-header">{% trans "Account Details" %}</h2>
|
||||||
@ -64,12 +62,7 @@
|
|||||||
{% if perms.core.view_objectchange %}
|
{% if perms.core.view_objectchange %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="card">
|
{% include 'users/inc/user_activity.html' with user=user table=changelog_table %}
|
||||||
<h2 class="card-header text-center">{% trans "Recent Activity" %}</h2>
|
|
||||||
<div class="table-responsive">
|
|
||||||
{% render_table changelog_table 'inc/table.html' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% elif perms.dcim.add_cable %}
|
{% elif perms.dcim.add_cable %}
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button type="button" class="btn btn-success dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
{% trans "Not Connected" %}
|
{% trans "Not Connected" %}
|
||||||
{% if perms.dcim.add_cable %}
|
{% if perms.dcim.add_cable %}
|
||||||
<div class="dropdown float-end">
|
<div class="dropdown float-end">
|
||||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
{% trans "Not Connected" %}
|
{% trans "Not Connected" %}
|
||||||
{% if perms.dcim.add_cable %}
|
{% if perms.dcim.add_cable %}
|
||||||
<div class="dropdown float-end">
|
<div class="dropdown float-end">
|
||||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
@ -308,7 +308,7 @@
|
|||||||
{% trans "Services" %}
|
{% trans "Services" %}
|
||||||
{% if perms.ipam.add_service %}
|
{% if perms.ipam.add_service %}
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="{% url 'ipam:service_add' %}?device={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
<a href="{% url 'ipam:service_add' %}?parent_object_type={{ object|content_type_id }}&parent={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a service" %}
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a service" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,10 +118,6 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="card-body text-muted">
|
<div class="card-body text-muted">
|
||||||
{% trans "Not connected" %}
|
{% trans "Not connected" %}
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if not object.mark_connected and not object.cable %}
|
|
||||||
<div class="card-footer">
|
|
||||||
{% if perms.dcim.add_cable %}
|
{% if perms.dcim.add_cable %}
|
||||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&return_url={{ object.get_absolute_url }}" class="btn btn-primary float-end">
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&return_url={{ object.get_absolute_url }}" class="btn btn-primary float-end">
|
||||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Connect" %}
|
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Connect" %}
|
||||||
|
@ -53,7 +53,16 @@
|
|||||||
{# Script output. Legacy reports will not have this. #}
|
{# Script output. Legacy reports will not have this. #}
|
||||||
{% if 'output' in job.data %}
|
{% if 'output' in job.data %}
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<h2 class="card-header">{% trans "Output" %}</h2>
|
<h2 class="card-header d-flex justify-content-between">
|
||||||
|
{% trans "Output" %}
|
||||||
|
{% if job.completed %}
|
||||||
|
<div>
|
||||||
|
<a href="?export=output" class="btn btn-primary lh-1" role="button">
|
||||||
|
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
{% if job.data.output %}
|
{% if job.data.output %}
|
||||||
<pre class="card-body font-monospace">{{ job.data.output }}</pre>
|
<pre class="card-body font-monospace">{{ job.data.output }}</pre>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
16
netbox/templates/users/inc/user_activity.html
Normal file
16
netbox/templates/users/inc/user_activity.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header text-center">
|
||||||
|
{% trans "Recent Activity" %}
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="{% url 'core:objectchange_list' %}?user_id={{ user.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||||
|
<i class="mdi mdi-arrow-right-thick" aria-hidden="true"></i> {% trans "View All" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
<div class="table-responsive">
|
||||||
|
{% render_table table 'inc/table.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,14 +1,12 @@
|
|||||||
{% extends 'generic/object.html' %}
|
{% extends 'generic/object.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load helpers %}
|
|
||||||
{% load render_table from django_tables2 %}
|
|
||||||
|
|
||||||
{% block title %}{% trans "User" %} {{ object.username }}{% endblock %}
|
{% block title %}{% trans "User" %} {{ object.username }}{% endblock %}
|
||||||
|
|
||||||
{% block subtitle %}{% endblock %}
|
{% block subtitle %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mb-3">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="card-header">{% trans "User" %}</h2>
|
<h2 class="card-header">{% trans "User" %}</h2>
|
||||||
@ -74,12 +72,7 @@
|
|||||||
{% if perms.core.view_objectchange %}
|
{% if perms.core.view_objectchange %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="card">
|
{% include 'users/inc/user_activity.html' with user=object table=changelog_table %}
|
||||||
<h2 class="text-center">{% trans "Recent Activity" %}</h2>
|
|
||||||
<div class="card-body table-responsive">
|
|
||||||
{% render_table changelog_table 'inc/table.html' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -154,7 +154,7 @@
|
|||||||
{% trans "Services" %}
|
{% trans "Services" %}
|
||||||
{% if perms.ipam.add_service %}
|
{% if perms.ipam.add_service %}
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="{% url 'ipam:service_add' %}?virtual_machine={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
<a href="{% url 'ipam:service_add' %}?parent_object_type={{ object|content_type_id }}&parent={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a service" %}
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a service" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,3 +66,17 @@ class Migration(migrations.Migration):
|
|||||||
name='group',
|
name='group',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_contact_groups(objectchange, reverting):
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is None:
|
||||||
|
continue
|
||||||
|
# Set the M2M field `groups` to a list containing the group ID
|
||||||
|
data['groups'] = [data['group']] if data.get('group') else []
|
||||||
|
data.pop('group', None)
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'tenancy.contact': oc_contact_groups,
|
||||||
|
}
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-05-23 05:01+0000\n"
|
"POT-Creation-Date: 2025-06-20 05:02+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -70,15 +70,15 @@ msgstr ""
|
|||||||
msgid "You have logged out."
|
msgid "You have logged out."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/account/views.py:227
|
#: netbox/account/views.py:224
|
||||||
msgid "Your preferences have been updated."
|
msgid "Your preferences have been updated."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/account/views.py:255
|
#: netbox/account/views.py:252
|
||||||
msgid "LDAP-authenticated user credentials cannot be changed within NetBox."
|
msgid "LDAP-authenticated user credentials cannot be changed within NetBox."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/account/views.py:270
|
#: netbox/account/views.py:267
|
||||||
msgid "Your password has been changed successfully."
|
msgid "Your password has been changed successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ msgstr ""
|
|||||||
#: netbox/extras/tables/tables.py:539 netbox/ipam/choices.py:31
|
#: netbox/extras/tables/tables.py:539 netbox/ipam/choices.py:31
|
||||||
#: netbox/ipam/choices.py:49 netbox/ipam/choices.py:69
|
#: netbox/ipam/choices.py:49 netbox/ipam/choices.py:69
|
||||||
#: netbox/ipam/choices.py:154 netbox/templates/extras/configcontext.html:25
|
#: netbox/ipam/choices.py:154 netbox/templates/extras/configcontext.html:25
|
||||||
#: netbox/templates/users/user.html:37 netbox/users/forms/bulk_edit.py:38
|
#: netbox/templates/users/user.html:35 netbox/users/forms/bulk_edit.py:38
|
||||||
#: netbox/virtualization/choices.py:22 netbox/virtualization/choices.py:45
|
#: netbox/virtualization/choices.py:22 netbox/virtualization/choices.py:45
|
||||||
#: netbox/vpn/choices.py:19 netbox/vpn/choices.py:280
|
#: netbox/vpn/choices.py:19 netbox/vpn/choices.py:280
|
||||||
#: netbox/wireless/choices.py:25
|
#: netbox/wireless/choices.py:25
|
||||||
@ -165,8 +165,8 @@ msgstr ""
|
|||||||
#: netbox/dcim/filtersets.py:215 netbox/dcim/filtersets.py:336
|
#: netbox/dcim/filtersets.py:215 netbox/dcim/filtersets.py:336
|
||||||
#: netbox/dcim/filtersets.py:467 netbox/dcim/filtersets.py:1075
|
#: netbox/dcim/filtersets.py:467 netbox/dcim/filtersets.py:1075
|
||||||
#: netbox/dcim/filtersets.py:1397 netbox/dcim/filtersets.py:1495
|
#: netbox/dcim/filtersets.py:1397 netbox/dcim/filtersets.py:1495
|
||||||
#: netbox/dcim/filtersets.py:2160 netbox/dcim/filtersets.py:2403
|
#: netbox/dcim/filtersets.py:2175 netbox/dcim/filtersets.py:2418
|
||||||
#: netbox/dcim/filtersets.py:2461 netbox/ipam/filtersets.py:954
|
#: netbox/dcim/filtersets.py:2476 netbox/ipam/filtersets.py:954
|
||||||
#: netbox/virtualization/filtersets.py:139 netbox/vpn/filtersets.py:361
|
#: netbox/virtualization/filtersets.py:139 netbox/vpn/filtersets.py:361
|
||||||
msgid "Region (ID)"
|
msgid "Region (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -177,8 +177,8 @@ msgstr ""
|
|||||||
#: netbox/dcim/filtersets.py:222 netbox/dcim/filtersets.py:343
|
#: netbox/dcim/filtersets.py:222 netbox/dcim/filtersets.py:343
|
||||||
#: netbox/dcim/filtersets.py:474 netbox/dcim/filtersets.py:1082
|
#: netbox/dcim/filtersets.py:474 netbox/dcim/filtersets.py:1082
|
||||||
#: netbox/dcim/filtersets.py:1404 netbox/dcim/filtersets.py:1502
|
#: netbox/dcim/filtersets.py:1404 netbox/dcim/filtersets.py:1502
|
||||||
#: netbox/dcim/filtersets.py:2167 netbox/dcim/filtersets.py:2410
|
#: netbox/dcim/filtersets.py:2182 netbox/dcim/filtersets.py:2425
|
||||||
#: netbox/dcim/filtersets.py:2468 netbox/extras/filtersets.py:602
|
#: netbox/dcim/filtersets.py:2483 netbox/extras/filtersets.py:602
|
||||||
#: netbox/ipam/filtersets.py:961 netbox/virtualization/filtersets.py:146
|
#: netbox/ipam/filtersets.py:961 netbox/virtualization/filtersets.py:146
|
||||||
#: netbox/vpn/filtersets.py:356
|
#: netbox/vpn/filtersets.py:356
|
||||||
msgid "Region (slug)"
|
msgid "Region (slug)"
|
||||||
@ -189,8 +189,8 @@ msgstr ""
|
|||||||
#: netbox/dcim/filtersets.py:131 netbox/dcim/filtersets.py:228
|
#: netbox/dcim/filtersets.py:131 netbox/dcim/filtersets.py:228
|
||||||
#: netbox/dcim/filtersets.py:349 netbox/dcim/filtersets.py:480
|
#: netbox/dcim/filtersets.py:349 netbox/dcim/filtersets.py:480
|
||||||
#: netbox/dcim/filtersets.py:1088 netbox/dcim/filtersets.py:1410
|
#: netbox/dcim/filtersets.py:1088 netbox/dcim/filtersets.py:1410
|
||||||
#: netbox/dcim/filtersets.py:1508 netbox/dcim/filtersets.py:2173
|
#: netbox/dcim/filtersets.py:1508 netbox/dcim/filtersets.py:2188
|
||||||
#: netbox/dcim/filtersets.py:2416 netbox/dcim/filtersets.py:2474
|
#: netbox/dcim/filtersets.py:2431 netbox/dcim/filtersets.py:2489
|
||||||
#: netbox/ipam/filtersets.py:239 netbox/ipam/filtersets.py:967
|
#: netbox/ipam/filtersets.py:239 netbox/ipam/filtersets.py:967
|
||||||
#: netbox/virtualization/filtersets.py:152
|
#: netbox/virtualization/filtersets.py:152
|
||||||
msgid "Site group (ID)"
|
msgid "Site group (ID)"
|
||||||
@ -201,8 +201,8 @@ msgstr ""
|
|||||||
#: netbox/dcim/filtersets.py:138 netbox/dcim/filtersets.py:235
|
#: netbox/dcim/filtersets.py:138 netbox/dcim/filtersets.py:235
|
||||||
#: netbox/dcim/filtersets.py:356 netbox/dcim/filtersets.py:487
|
#: netbox/dcim/filtersets.py:356 netbox/dcim/filtersets.py:487
|
||||||
#: netbox/dcim/filtersets.py:1095 netbox/dcim/filtersets.py:1417
|
#: netbox/dcim/filtersets.py:1095 netbox/dcim/filtersets.py:1417
|
||||||
#: netbox/dcim/filtersets.py:1515 netbox/dcim/filtersets.py:2180
|
#: netbox/dcim/filtersets.py:1515 netbox/dcim/filtersets.py:2195
|
||||||
#: netbox/dcim/filtersets.py:2423 netbox/dcim/filtersets.py:2481
|
#: netbox/dcim/filtersets.py:2438 netbox/dcim/filtersets.py:2496
|
||||||
#: netbox/extras/filtersets.py:608 netbox/ipam/filtersets.py:246
|
#: netbox/extras/filtersets.py:608 netbox/ipam/filtersets.py:246
|
||||||
#: netbox/ipam/filtersets.py:974 netbox/virtualization/filtersets.py:159
|
#: netbox/ipam/filtersets.py:974 netbox/virtualization/filtersets.py:159
|
||||||
msgid "Site group (slug)"
|
msgid "Site group (slug)"
|
||||||
@ -211,7 +211,7 @@ msgstr ""
|
|||||||
#: netbox/circuits/filtersets.py:62 netbox/circuits/forms/filtersets.py:59
|
#: netbox/circuits/filtersets.py:62 netbox/circuits/forms/filtersets.py:59
|
||||||
#: netbox/circuits/forms/filtersets.py:183
|
#: netbox/circuits/forms/filtersets.py:183
|
||||||
#: netbox/circuits/forms/filtersets.py:241
|
#: netbox/circuits/forms/filtersets.py:241
|
||||||
#: netbox/circuits/tables/circuits.py:128 netbox/dcim/forms/bulk_edit.py:177
|
#: netbox/circuits/tables/circuits.py:129 netbox/dcim/forms/bulk_edit.py:177
|
||||||
#: netbox/dcim/forms/bulk_edit.py:344 netbox/dcim/forms/bulk_edit.py:730
|
#: netbox/dcim/forms/bulk_edit.py:344 netbox/dcim/forms/bulk_edit.py:730
|
||||||
#: netbox/dcim/forms/bulk_edit.py:935 netbox/dcim/forms/bulk_import.py:134
|
#: netbox/dcim/forms/bulk_edit.py:935 netbox/dcim/forms/bulk_import.py:134
|
||||||
#: netbox/dcim/forms/bulk_import.py:236 netbox/dcim/forms/bulk_import.py:337
|
#: netbox/dcim/forms/bulk_import.py:236 netbox/dcim/forms/bulk_import.py:337
|
||||||
@ -318,8 +318,8 @@ msgstr ""
|
|||||||
#: netbox/dcim/base_filtersets.py:47 netbox/dcim/filtersets.py:239
|
#: netbox/dcim/base_filtersets.py:47 netbox/dcim/filtersets.py:239
|
||||||
#: netbox/dcim/filtersets.py:360 netbox/dcim/filtersets.py:455
|
#: netbox/dcim/filtersets.py:360 netbox/dcim/filtersets.py:455
|
||||||
#: netbox/dcim/filtersets.py:1099 netbox/dcim/filtersets.py:1422
|
#: netbox/dcim/filtersets.py:1099 netbox/dcim/filtersets.py:1422
|
||||||
#: netbox/dcim/filtersets.py:1520 netbox/dcim/filtersets.py:2185
|
#: netbox/dcim/filtersets.py:1520 netbox/dcim/filtersets.py:2200
|
||||||
#: netbox/dcim/filtersets.py:2427 netbox/dcim/filtersets.py:2486
|
#: netbox/dcim/filtersets.py:2442 netbox/dcim/filtersets.py:2501
|
||||||
#: netbox/ipam/filtersets.py:251 netbox/ipam/filtersets.py:978
|
#: netbox/ipam/filtersets.py:251 netbox/ipam/filtersets.py:978
|
||||||
#: netbox/virtualization/filtersets.py:163 netbox/vpn/filtersets.py:371
|
#: netbox/virtualization/filtersets.py:163 netbox/vpn/filtersets.py:371
|
||||||
msgid "Site (ID)"
|
msgid "Site (ID)"
|
||||||
@ -329,7 +329,7 @@ msgstr ""
|
|||||||
#: netbox/dcim/base_filtersets.py:59 netbox/dcim/filtersets.py:261
|
#: netbox/dcim/base_filtersets.py:59 netbox/dcim/filtersets.py:261
|
||||||
#: netbox/dcim/filtersets.py:372 netbox/dcim/filtersets.py:493
|
#: netbox/dcim/filtersets.py:372 netbox/dcim/filtersets.py:493
|
||||||
#: netbox/dcim/filtersets.py:1111 netbox/dcim/filtersets.py:1433
|
#: netbox/dcim/filtersets.py:1111 netbox/dcim/filtersets.py:1433
|
||||||
#: netbox/dcim/filtersets.py:1531 netbox/dcim/filtersets.py:2439
|
#: netbox/dcim/filtersets.py:1531 netbox/dcim/filtersets.py:2454
|
||||||
msgid "Location (ID)"
|
msgid "Location (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -341,7 +341,7 @@ msgstr ""
|
|||||||
#: netbox/circuits/filtersets.py:537 netbox/core/filtersets.py:81
|
#: netbox/circuits/filtersets.py:537 netbox/core/filtersets.py:81
|
||||||
#: netbox/core/filtersets.py:140 netbox/core/filtersets.py:177
|
#: netbox/core/filtersets.py:140 netbox/core/filtersets.py:177
|
||||||
#: netbox/dcim/filtersets.py:780 netbox/dcim/filtersets.py:1489
|
#: netbox/dcim/filtersets.py:780 netbox/dcim/filtersets.py:1489
|
||||||
#: netbox/dcim/filtersets.py:2534 netbox/extras/filtersets.py:45
|
#: netbox/dcim/filtersets.py:2549 netbox/extras/filtersets.py:45
|
||||||
#: netbox/extras/filtersets.py:67 netbox/extras/filtersets.py:96
|
#: netbox/extras/filtersets.py:67 netbox/extras/filtersets.py:96
|
||||||
#: netbox/extras/filtersets.py:136 netbox/extras/filtersets.py:185
|
#: netbox/extras/filtersets.py:136 netbox/extras/filtersets.py:185
|
||||||
#: netbox/extras/filtersets.py:213 netbox/extras/filtersets.py:243
|
#: netbox/extras/filtersets.py:213 netbox/extras/filtersets.py:243
|
||||||
@ -373,7 +373,7 @@ msgstr ""
|
|||||||
#: netbox/circuits/forms/model_forms.py:163
|
#: netbox/circuits/forms/model_forms.py:163
|
||||||
#: netbox/circuits/forms/model_forms.py:263
|
#: netbox/circuits/forms/model_forms.py:263
|
||||||
#: netbox/circuits/tables/circuits.py:107
|
#: netbox/circuits/tables/circuits.py:107
|
||||||
#: netbox/circuits/tables/circuits.py:202 netbox/dcim/forms/connections.py:73
|
#: netbox/circuits/tables/circuits.py:203 netbox/dcim/forms/connections.py:73
|
||||||
#: netbox/templates/circuits/circuit.html:15
|
#: netbox/templates/circuits/circuit.html:15
|
||||||
#: netbox/templates/circuits/circuitgroupassignment.html:30
|
#: netbox/templates/circuits/circuitgroupassignment.html:30
|
||||||
#: netbox/templates/circuits/circuittermination.html:19
|
#: netbox/templates/circuits/circuittermination.html:19
|
||||||
@ -503,6 +503,7 @@ msgstr ""
|
|||||||
#: netbox/templates/circuits/circuittype.html:26
|
#: netbox/templates/circuits/circuittype.html:26
|
||||||
#: netbox/templates/circuits/inc/circuit_termination_fields.html:83
|
#: netbox/templates/circuits/inc/circuit_termination_fields.html:83
|
||||||
#: netbox/templates/circuits/provider.html:33
|
#: netbox/templates/circuits/provider.html:33
|
||||||
|
#: netbox/templates/circuits/provideraccount.html:32
|
||||||
#: netbox/templates/circuits/providernetwork.html:32
|
#: netbox/templates/circuits/providernetwork.html:32
|
||||||
#: netbox/templates/circuits/virtualcircuit.html:56
|
#: netbox/templates/circuits/virtualcircuit.html:56
|
||||||
#: netbox/templates/circuits/virtualcircuittermination.html:68
|
#: netbox/templates/circuits/virtualcircuittermination.html:68
|
||||||
@ -624,7 +625,7 @@ msgstr ""
|
|||||||
#: netbox/circuits/forms/model_forms.py:77
|
#: netbox/circuits/forms/model_forms.py:77
|
||||||
#: netbox/circuits/forms/model_forms.py:111
|
#: netbox/circuits/forms/model_forms.py:111
|
||||||
#: netbox/circuits/tables/circuits.py:57 netbox/circuits/tables/circuits.py:111
|
#: netbox/circuits/tables/circuits.py:57 netbox/circuits/tables/circuits.py:111
|
||||||
#: netbox/circuits/tables/circuits.py:195
|
#: netbox/circuits/tables/circuits.py:196
|
||||||
#: netbox/circuits/tables/providers.py:70
|
#: netbox/circuits/tables/providers.py:70
|
||||||
#: netbox/circuits/tables/providers.py:101
|
#: netbox/circuits/tables/providers.py:101
|
||||||
#: netbox/circuits/tables/virtual_circuits.py:46
|
#: netbox/circuits/tables/virtual_circuits.py:46
|
||||||
@ -678,7 +679,7 @@ msgstr ""
|
|||||||
#: netbox/circuits/forms/bulk_import.py:221
|
#: netbox/circuits/forms/bulk_import.py:221
|
||||||
#: netbox/circuits/forms/filtersets.py:138
|
#: netbox/circuits/forms/filtersets.py:138
|
||||||
#: netbox/circuits/forms/filtersets.py:359
|
#: netbox/circuits/forms/filtersets.py:359
|
||||||
#: netbox/circuits/tables/circuits.py:65 netbox/circuits/tables/circuits.py:199
|
#: netbox/circuits/tables/circuits.py:65 netbox/circuits/tables/circuits.py:200
|
||||||
#: netbox/circuits/tables/virtual_circuits.py:58
|
#: netbox/circuits/tables/virtual_circuits.py:58
|
||||||
#: netbox/core/forms/bulk_edit.py:19 netbox/core/forms/filtersets.py:33
|
#: netbox/core/forms/bulk_edit.py:19 netbox/core/forms/filtersets.py:33
|
||||||
#: netbox/core/tables/change_logging.py:32 netbox/core/tables/data.py:20
|
#: netbox/core/tables/change_logging.py:32 netbox/core/tables/data.py:20
|
||||||
@ -704,7 +705,7 @@ msgstr ""
|
|||||||
#: netbox/dcim/tables/devices.py:852 netbox/dcim/tables/power.py:77
|
#: netbox/dcim/tables/devices.py:852 netbox/dcim/tables/power.py:77
|
||||||
#: netbox/dcim/tables/racks.py:141 netbox/extras/forms/bulk_import.py:42
|
#: netbox/dcim/tables/racks.py:141 netbox/extras/forms/bulk_import.py:42
|
||||||
#: netbox/extras/tables/tables.py:449 netbox/extras/tables/tables.py:509
|
#: netbox/extras/tables/tables.py:449 netbox/extras/tables/tables.py:509
|
||||||
#: netbox/netbox/tables/tables.py:269 netbox/templates/circuits/circuit.html:30
|
#: netbox/netbox/tables/tables.py:272 netbox/templates/circuits/circuit.html:30
|
||||||
#: netbox/templates/circuits/virtualcircuit.html:39
|
#: netbox/templates/circuits/virtualcircuit.html:39
|
||||||
#: netbox/templates/circuits/virtualcircuittermination.html:64
|
#: netbox/templates/circuits/virtualcircuittermination.html:64
|
||||||
#: netbox/templates/core/datasource.html:38 netbox/templates/dcim/cable.html:15
|
#: netbox/templates/core/datasource.html:38 netbox/templates/dcim/cable.html:15
|
||||||
@ -818,7 +819,7 @@ msgstr ""
|
|||||||
#: netbox/vpn/tables/l2vpn.py:27 netbox/vpn/tables/tunnels.py:48
|
#: netbox/vpn/tables/l2vpn.py:27 netbox/vpn/tables/tunnels.py:48
|
||||||
#: netbox/wireless/forms/bulk_edit.py:46 netbox/wireless/forms/bulk_edit.py:109
|
#: netbox/wireless/forms/bulk_edit.py:46 netbox/wireless/forms/bulk_edit.py:109
|
||||||
#: netbox/wireless/forms/bulk_import.py:45
|
#: netbox/wireless/forms/bulk_import.py:45
|
||||||
#: netbox/wireless/forms/bulk_import.py:89
|
#: netbox/wireless/forms/bulk_import.py:132
|
||||||
#: netbox/wireless/forms/filtersets.py:52
|
#: netbox/wireless/forms/filtersets.py:52
|
||||||
#: netbox/wireless/forms/filtersets.py:111
|
#: netbox/wireless/forms/filtersets.py:111
|
||||||
#: netbox/wireless/tables/wirelesslan.py:52
|
#: netbox/wireless/tables/wirelesslan.py:52
|
||||||
@ -898,7 +899,7 @@ msgstr ""
|
|||||||
#: netbox/vpn/forms/filtersets.py:219 netbox/wireless/forms/bulk_edit.py:66
|
#: netbox/vpn/forms/filtersets.py:219 netbox/wireless/forms/bulk_edit.py:66
|
||||||
#: netbox/wireless/forms/bulk_edit.py:114
|
#: netbox/wireless/forms/bulk_edit.py:114
|
||||||
#: netbox/wireless/forms/bulk_import.py:57
|
#: netbox/wireless/forms/bulk_import.py:57
|
||||||
#: netbox/wireless/forms/bulk_import.py:102
|
#: netbox/wireless/forms/bulk_import.py:137
|
||||||
#: netbox/wireless/forms/filtersets.py:38
|
#: netbox/wireless/forms/filtersets.py:38
|
||||||
#: netbox/wireless/forms/filtersets.py:103
|
#: netbox/wireless/forms/filtersets.py:103
|
||||||
msgid "Tenant"
|
msgid "Tenant"
|
||||||
@ -935,8 +936,8 @@ msgstr ""
|
|||||||
#: netbox/circuits/forms/bulk_import.py:108
|
#: netbox/circuits/forms/bulk_import.py:108
|
||||||
#: netbox/circuits/forms/filtersets.py:213
|
#: netbox/circuits/forms/filtersets.py:213
|
||||||
#: netbox/wireless/forms/bulk_edit.py:138
|
#: netbox/wireless/forms/bulk_edit.py:138
|
||||||
#: netbox/wireless/forms/bulk_import.py:121
|
#: netbox/wireless/forms/bulk_import.py:156
|
||||||
#: netbox/wireless/forms/bulk_import.py:124
|
#: netbox/wireless/forms/bulk_import.py:159
|
||||||
#: netbox/wireless/forms/filtersets.py:134
|
#: netbox/wireless/forms/filtersets.py:134
|
||||||
msgid "Distance unit"
|
msgid "Distance unit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -971,7 +972,7 @@ msgstr ""
|
|||||||
#: netbox/ipam/forms/filtersets.py:406 netbox/ipam/forms/filtersets.py:492
|
#: netbox/ipam/forms/filtersets.py:406 netbox/ipam/forms/filtersets.py:492
|
||||||
#: netbox/ipam/forms/filtersets.py:505 netbox/ipam/forms/filtersets.py:530
|
#: netbox/ipam/forms/filtersets.py:505 netbox/ipam/forms/filtersets.py:530
|
||||||
#: netbox/ipam/forms/filtersets.py:601 netbox/ipam/forms/filtersets.py:619
|
#: netbox/ipam/forms/filtersets.py:601 netbox/ipam/forms/filtersets.py:619
|
||||||
#: netbox/netbox/tables/tables.py:285 netbox/templates/dcim/moduletype.html:68
|
#: netbox/netbox/tables/tables.py:288 netbox/templates/dcim/moduletype.html:68
|
||||||
#: netbox/virtualization/forms/filtersets.py:46
|
#: netbox/virtualization/forms/filtersets.py:46
|
||||||
#: netbox/virtualization/forms/filtersets.py:109
|
#: netbox/virtualization/forms/filtersets.py:109
|
||||||
#: netbox/virtualization/forms/filtersets.py:204
|
#: netbox/virtualization/forms/filtersets.py:204
|
||||||
@ -1058,7 +1059,7 @@ msgstr ""
|
|||||||
#: netbox/circuits/forms/bulk_edit.py:289
|
#: netbox/circuits/forms/bulk_edit.py:289
|
||||||
#: netbox/circuits/forms/bulk_import.py:188
|
#: netbox/circuits/forms/bulk_import.py:188
|
||||||
#: netbox/circuits/forms/filtersets.py:305
|
#: netbox/circuits/forms/filtersets.py:305
|
||||||
#: netbox/circuits/tables/circuits.py:206 netbox/dcim/forms/model_forms.py:656
|
#: netbox/circuits/tables/circuits.py:207 netbox/dcim/forms/model_forms.py:656
|
||||||
#: netbox/templates/circuits/circuitgroupassignment.html:34
|
#: netbox/templates/circuits/circuitgroupassignment.html:34
|
||||||
#: netbox/templates/dcim/device.html:139
|
#: netbox/templates/dcim/device.html:139
|
||||||
#: netbox/templates/dcim/virtualchassis.html:68
|
#: netbox/templates/dcim/virtualchassis.html:68
|
||||||
@ -1173,7 +1174,7 @@ msgstr ""
|
|||||||
#: netbox/virtualization/forms/bulk_import.py:71
|
#: netbox/virtualization/forms/bulk_import.py:71
|
||||||
#: netbox/virtualization/forms/bulk_import.py:125
|
#: netbox/virtualization/forms/bulk_import.py:125
|
||||||
#: netbox/vpn/forms/bulk_import.py:63 netbox/wireless/forms/bulk_import.py:61
|
#: netbox/vpn/forms/bulk_import.py:63 netbox/wireless/forms/bulk_import.py:61
|
||||||
#: netbox/wireless/forms/bulk_import.py:106
|
#: netbox/wireless/forms/bulk_import.py:141
|
||||||
msgid "Assigned tenant"
|
msgid "Assigned tenant"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -1240,7 +1241,7 @@ msgstr ""
|
|||||||
#: netbox/circuits/forms/filtersets.py:130
|
#: netbox/circuits/forms/filtersets.py:130
|
||||||
#: netbox/circuits/forms/filtersets.py:188
|
#: netbox/circuits/forms/filtersets.py:188
|
||||||
#: netbox/circuits/forms/filtersets.py:246
|
#: netbox/circuits/forms/filtersets.py:246
|
||||||
#: netbox/circuits/tables/circuits.py:143 netbox/dcim/forms/bulk_edit.py:353
|
#: netbox/circuits/tables/circuits.py:144 netbox/dcim/forms/bulk_edit.py:353
|
||||||
#: netbox/dcim/forms/bulk_edit.py:466 netbox/dcim/forms/bulk_edit.py:735
|
#: netbox/dcim/forms/bulk_edit.py:466 netbox/dcim/forms/bulk_edit.py:735
|
||||||
#: netbox/dcim/forms/bulk_edit.py:790 netbox/dcim/forms/bulk_edit.py:944
|
#: netbox/dcim/forms/bulk_edit.py:790 netbox/dcim/forms/bulk_edit.py:944
|
||||||
#: netbox/dcim/forms/bulk_import.py:241 netbox/dcim/forms/bulk_import.py:343
|
#: netbox/dcim/forms/bulk_import.py:241 netbox/dcim/forms/bulk_import.py:343
|
||||||
@ -1304,7 +1305,7 @@ msgstr ""
|
|||||||
#: netbox/circuits/forms/filtersets.py:45
|
#: netbox/circuits/forms/filtersets.py:45
|
||||||
#: netbox/circuits/forms/filtersets.py:169
|
#: netbox/circuits/forms/filtersets.py:169
|
||||||
#: netbox/circuits/forms/filtersets.py:231
|
#: netbox/circuits/forms/filtersets.py:231
|
||||||
#: netbox/circuits/tables/circuits.py:138 netbox/dcim/forms/bulk_edit.py:121
|
#: netbox/circuits/tables/circuits.py:139 netbox/dcim/forms/bulk_edit.py:121
|
||||||
#: netbox/dcim/forms/bulk_edit.py:328 netbox/dcim/forms/bulk_edit.py:919
|
#: netbox/dcim/forms/bulk_edit.py:328 netbox/dcim/forms/bulk_edit.py:919
|
||||||
#: netbox/dcim/forms/bulk_import.py:96 netbox/dcim/forms/filtersets.py:75
|
#: netbox/dcim/forms/bulk_import.py:96 netbox/dcim/forms/filtersets.py:75
|
||||||
#: netbox/dcim/forms/filtersets.py:187 netbox/dcim/forms/filtersets.py:213
|
#: netbox/dcim/forms/filtersets.py:187 netbox/dcim/forms/filtersets.py:213
|
||||||
@ -1374,7 +1375,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: netbox/circuits/forms/filtersets.py:302
|
#: netbox/circuits/forms/filtersets.py:302
|
||||||
#: netbox/circuits/forms/model_forms.py:253
|
#: netbox/circuits/forms/model_forms.py:253
|
||||||
#: netbox/circuits/tables/circuits.py:190 netbox/dcim/forms/bulk_edit.py:126
|
#: netbox/circuits/tables/circuits.py:191 netbox/dcim/forms/bulk_edit.py:126
|
||||||
#: netbox/dcim/forms/bulk_import.py:103 netbox/dcim/forms/model_forms.py:125
|
#: netbox/dcim/forms/bulk_import.py:103 netbox/dcim/forms/model_forms.py:125
|
||||||
#: netbox/dcim/tables/sites.py:95 netbox/extras/forms/filtersets.py:544
|
#: netbox/dcim/tables/sites.py:95 netbox/extras/forms/filtersets.py:544
|
||||||
#: netbox/ipam/filtersets.py:994 netbox/ipam/forms/bulk_edit.py:488
|
#: netbox/ipam/filtersets.py:994 netbox/ipam/forms/bulk_edit.py:488
|
||||||
@ -1695,7 +1696,7 @@ msgstr ""
|
|||||||
msgid "virtual circuit terminations"
|
msgid "virtual circuit terminations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/circuits/tables/circuits.py:30 netbox/circuits/tables/circuits.py:167
|
#: netbox/circuits/tables/circuits.py:30 netbox/circuits/tables/circuits.py:168
|
||||||
#: netbox/circuits/tables/providers.py:18
|
#: netbox/circuits/tables/providers.py:18
|
||||||
#: netbox/circuits/tables/providers.py:67
|
#: netbox/circuits/tables/providers.py:67
|
||||||
#: netbox/circuits/tables/providers.py:97
|
#: netbox/circuits/tables/providers.py:97
|
||||||
@ -1820,14 +1821,14 @@ msgstr ""
|
|||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/circuits/tables/circuits.py:39 netbox/circuits/tables/circuits.py:173
|
#: netbox/circuits/tables/circuits.py:39 netbox/circuits/tables/circuits.py:174
|
||||||
#: netbox/circuits/tables/providers.py:43
|
#: netbox/circuits/tables/providers.py:43
|
||||||
#: netbox/circuits/tables/providers.py:77
|
#: netbox/circuits/tables/providers.py:77
|
||||||
#: netbox/circuits/tables/virtual_circuits.py:27
|
#: netbox/circuits/tables/virtual_circuits.py:27
|
||||||
#: netbox/netbox/navigation/menu.py:275 netbox/netbox/navigation/menu.py:279
|
#: netbox/netbox/navigation/menu.py:275 netbox/netbox/navigation/menu.py:279
|
||||||
#: netbox/netbox/navigation/menu.py:281
|
#: netbox/netbox/navigation/menu.py:281
|
||||||
#: netbox/templates/circuits/provider.html:57
|
#: netbox/templates/circuits/provider.html:57
|
||||||
#: netbox/templates/circuits/provideraccount.html:44
|
#: netbox/templates/circuits/provideraccount.html:48
|
||||||
#: netbox/templates/circuits/providernetwork.html:50
|
#: netbox/templates/circuits/providernetwork.html:50
|
||||||
msgid "Circuits"
|
msgid "Circuits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1905,12 +1906,12 @@ msgstr ""
|
|||||||
msgid "Termination Point"
|
msgid "Termination Point"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/circuits/tables/circuits.py:133 netbox/dcim/tables/devices.py:160
|
#: netbox/circuits/tables/circuits.py:134 netbox/dcim/tables/devices.py:160
|
||||||
#: netbox/templates/dcim/sitegroup.html:26
|
#: netbox/templates/dcim/sitegroup.html:26
|
||||||
msgid "Site Group"
|
msgid "Site Group"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/circuits/tables/circuits.py:148
|
#: netbox/circuits/tables/circuits.py:149
|
||||||
#: netbox/templates/circuits/providernetwork.html:17
|
#: netbox/templates/circuits/providernetwork.html:17
|
||||||
#: netbox/templates/circuits/virtualcircuit.html:27
|
#: netbox/templates/circuits/virtualcircuit.html:27
|
||||||
#: netbox/templates/circuits/virtualcircuittermination.html:30
|
#: netbox/templates/circuits/virtualcircuittermination.html:30
|
||||||
@ -2167,7 +2168,7 @@ msgid "Local"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/data_backends.py:50 netbox/core/tables/change_logging.py:20
|
#: netbox/core/data_backends.py:50 netbox/core/tables/change_logging.py:20
|
||||||
#: netbox/templates/account/profile.html:15 netbox/templates/users/user.html:17
|
#: netbox/templates/account/profile.html:13 netbox/templates/users/user.html:15
|
||||||
#: netbox/users/tables.py:31
|
#: netbox/users/tables.py:31
|
||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2342,7 +2343,7 @@ msgstr ""
|
|||||||
#: netbox/templates/extras/savedfilter.html:21
|
#: netbox/templates/extras/savedfilter.html:21
|
||||||
#: netbox/templates/extras/tableconfig.html:29
|
#: netbox/templates/extras/tableconfig.html:29
|
||||||
#: netbox/templates/inc/user_menu.html:33 netbox/templates/users/token.html:21
|
#: netbox/templates/inc/user_menu.html:33 netbox/templates/users/token.html:21
|
||||||
#: netbox/templates/users/user.html:6 netbox/templates/users/user.html:14
|
#: netbox/templates/users/user.html:4 netbox/templates/users/user.html:12
|
||||||
#: netbox/users/filtersets.py:107 netbox/users/filtersets.py:174
|
#: netbox/users/filtersets.py:107 netbox/users/filtersets.py:174
|
||||||
#: netbox/users/forms/filtersets.py:84 netbox/users/forms/filtersets.py:125
|
#: netbox/users/forms/filtersets.py:84 netbox/users/forms/filtersets.py:125
|
||||||
#: netbox/users/forms/model_forms.py:155 netbox/users/forms/model_forms.py:192
|
#: netbox/users/forms/model_forms.py:155 netbox/users/forms/model_forms.py:192
|
||||||
@ -2509,8 +2510,8 @@ msgstr ""
|
|||||||
#: netbox/core/models/config.py:18 netbox/core/models/data.py:269
|
#: netbox/core/models/config.py:18 netbox/core/models/data.py:269
|
||||||
#: netbox/core/models/files.py:30 netbox/core/models/jobs.py:52
|
#: netbox/core/models/files.py:30 netbox/core/models/jobs.py:52
|
||||||
#: netbox/extras/models/models.py:806 netbox/extras/models/notifications.py:39
|
#: netbox/extras/models/models.py:806 netbox/extras/models/notifications.py:39
|
||||||
#: netbox/extras/models/notifications.py:186
|
#: netbox/extras/models/notifications.py:192
|
||||||
#: netbox/netbox/models/features.py:53 netbox/users/models/tokens.py:32
|
#: netbox/netbox/models/features.py:54 netbox/users/models/tokens.py:32
|
||||||
msgid "created"
|
msgid "created"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2623,7 +2624,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/data.py:273 netbox/core/models/files.py:34
|
#: netbox/core/models/data.py:273 netbox/core/models/files.py:34
|
||||||
#: netbox/netbox/models/features.py:59
|
#: netbox/netbox/models/features.py:60
|
||||||
msgid "last updated"
|
msgid "last updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2753,7 +2754,7 @@ msgid "Deletion is prevented by a protection rule: {message}"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/tables/change_logging.py:25
|
#: netbox/core/tables/change_logging.py:25
|
||||||
#: netbox/templates/account/profile.html:19 netbox/templates/users/user.html:21
|
#: netbox/templates/account/profile.html:17 netbox/templates/users/user.html:19
|
||||||
msgid "Full Name"
|
msgid "Full Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2762,7 +2763,7 @@ msgstr ""
|
|||||||
#: netbox/extras/tables/tables.py:341 netbox/extras/tables/tables.py:373
|
#: netbox/extras/tables/tables.py:341 netbox/extras/tables/tables.py:373
|
||||||
#: netbox/extras/tables/tables.py:453 netbox/extras/tables/tables.py:514
|
#: netbox/extras/tables/tables.py:453 netbox/extras/tables/tables.py:514
|
||||||
#: netbox/extras/tables/tables.py:637 netbox/extras/tables/tables.py:677
|
#: netbox/extras/tables/tables.py:637 netbox/extras/tables/tables.py:677
|
||||||
#: netbox/extras/tables/tables.py:731 netbox/netbox/tables/tables.py:273
|
#: netbox/extras/tables/tables.py:731 netbox/netbox/tables/tables.py:276
|
||||||
#: netbox/templates/core/objectchange.html:58
|
#: netbox/templates/core/objectchange.html:58
|
||||||
#: netbox/templates/extras/eventrule.html:78
|
#: netbox/templates/extras/eventrule.html:78
|
||||||
#: netbox/templates/extras/journalentry.html:18
|
#: netbox/templates/extras/journalentry.html:18
|
||||||
@ -2800,7 +2801,7 @@ msgstr ""
|
|||||||
#: netbox/core/tables/jobs.py:10 netbox/core/tables/tasks.py:76
|
#: netbox/core/tables/jobs.py:10 netbox/core/tables/tasks.py:76
|
||||||
#: netbox/dcim/tables/devicetypes.py:169 netbox/extras/tables/tables.py:230
|
#: netbox/dcim/tables/devicetypes.py:169 netbox/extras/tables/tables.py:230
|
||||||
#: netbox/extras/tables/tables.py:504 netbox/extras/tables/tables.py:702
|
#: netbox/extras/tables/tables.py:504 netbox/extras/tables/tables.py:702
|
||||||
#: netbox/netbox/tables/tables.py:218
|
#: netbox/netbox/tables/tables.py:221
|
||||||
#: netbox/templates/dcim/virtualchassis_edit.html:56
|
#: netbox/templates/dcim/virtualchassis_edit.html:56
|
||||||
#: netbox/utilities/forms/forms.py:73 netbox/wireless/tables/wirelesslink.py:16
|
#: netbox/utilities/forms/forms.py:73 netbox/wireless/tables/wirelesslink.py:16
|
||||||
msgid "ID"
|
msgid "ID"
|
||||||
@ -3440,7 +3441,7 @@ msgstr ""
|
|||||||
#: netbox/dcim/filtersets.py:542 netbox/dcim/filtersets.py:707
|
#: netbox/dcim/filtersets.py:542 netbox/dcim/filtersets.py:707
|
||||||
#: netbox/dcim/filtersets.py:911 netbox/dcim/filtersets.py:985
|
#: netbox/dcim/filtersets.py:911 netbox/dcim/filtersets.py:985
|
||||||
#: netbox/dcim/filtersets.py:1025 netbox/dcim/filtersets.py:1368
|
#: netbox/dcim/filtersets.py:1025 netbox/dcim/filtersets.py:1368
|
||||||
#: netbox/dcim/filtersets.py:2093
|
#: netbox/dcim/filtersets.py:2108
|
||||||
msgid "Manufacturer (ID)"
|
msgid "Manufacturer (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3448,7 +3449,7 @@ msgstr ""
|
|||||||
#: netbox/dcim/filtersets.py:548 netbox/dcim/filtersets.py:713
|
#: netbox/dcim/filtersets.py:548 netbox/dcim/filtersets.py:713
|
||||||
#: netbox/dcim/filtersets.py:917 netbox/dcim/filtersets.py:991
|
#: netbox/dcim/filtersets.py:917 netbox/dcim/filtersets.py:991
|
||||||
#: netbox/dcim/filtersets.py:1031 netbox/dcim/filtersets.py:1374
|
#: netbox/dcim/filtersets.py:1031 netbox/dcim/filtersets.py:1374
|
||||||
#: netbox/dcim/filtersets.py:2099
|
#: netbox/dcim/filtersets.py:2114
|
||||||
msgid "Manufacturer (slug)"
|
msgid "Manufacturer (slug)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3461,14 +3462,14 @@ msgid "Rack type (ID)"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:414 netbox/dcim/filtersets.py:921
|
#: netbox/dcim/filtersets.py:414 netbox/dcim/filtersets.py:921
|
||||||
#: netbox/dcim/filtersets.py:1047 netbox/dcim/filtersets.py:2103
|
#: netbox/dcim/filtersets.py:1047 netbox/dcim/filtersets.py:2118
|
||||||
#: netbox/ipam/filtersets.py:376 netbox/ipam/filtersets.py:488
|
#: netbox/ipam/filtersets.py:376 netbox/ipam/filtersets.py:488
|
||||||
#: netbox/ipam/filtersets.py:998 netbox/virtualization/filtersets.py:177
|
#: netbox/ipam/filtersets.py:998 netbox/virtualization/filtersets.py:177
|
||||||
msgid "Role (ID)"
|
msgid "Role (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:420 netbox/dcim/filtersets.py:927
|
#: netbox/dcim/filtersets.py:420 netbox/dcim/filtersets.py:927
|
||||||
#: netbox/dcim/filtersets.py:1054 netbox/dcim/filtersets.py:2109
|
#: netbox/dcim/filtersets.py:1054 netbox/dcim/filtersets.py:2124
|
||||||
#: netbox/extras/filtersets.py:651 netbox/ipam/filtersets.py:382
|
#: netbox/extras/filtersets.py:651 netbox/ipam/filtersets.py:382
|
||||||
#: netbox/ipam/filtersets.py:494 netbox/ipam/filtersets.py:1004
|
#: netbox/ipam/filtersets.py:494 netbox/ipam/filtersets.py:1004
|
||||||
#: netbox/virtualization/filtersets.py:184
|
#: netbox/virtualization/filtersets.py:184
|
||||||
@ -3477,7 +3478,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: netbox/dcim/filtersets.py:450 netbox/dcim/filtersets.py:1123
|
#: netbox/dcim/filtersets.py:450 netbox/dcim/filtersets.py:1123
|
||||||
#: netbox/dcim/filtersets.py:1444 netbox/dcim/filtersets.py:1542
|
#: netbox/dcim/filtersets.py:1444 netbox/dcim/filtersets.py:1542
|
||||||
#: netbox/dcim/filtersets.py:2501
|
#: netbox/dcim/filtersets.py:2516
|
||||||
msgid "Rack (ID)"
|
msgid "Rack (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3575,7 +3576,7 @@ msgstr ""
|
|||||||
msgid "Power port (ID)"
|
msgid "Power port (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:907 netbox/dcim/filtersets.py:2089
|
#: netbox/dcim/filtersets.py:907 netbox/dcim/filtersets.py:2104
|
||||||
msgid "Parent inventory item (ID)"
|
msgid "Parent inventory item (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3610,8 +3611,8 @@ msgid "Platform (slug)"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:1105 netbox/dcim/filtersets.py:1428
|
#: netbox/dcim/filtersets.py:1105 netbox/dcim/filtersets.py:1428
|
||||||
#: netbox/dcim/filtersets.py:1526 netbox/dcim/filtersets.py:2191
|
#: netbox/dcim/filtersets.py:1526 netbox/dcim/filtersets.py:2206
|
||||||
#: netbox/dcim/filtersets.py:2433 netbox/dcim/filtersets.py:2492
|
#: netbox/dcim/filtersets.py:2448 netbox/dcim/filtersets.py:2507
|
||||||
msgid "Site name (slug)"
|
msgid "Site name (slug)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -3925,44 +3926,44 @@ msgstr ""
|
|||||||
msgid "Virtual circuit termination (ID)"
|
msgid "Virtual circuit termination (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:2056
|
#: netbox/dcim/filtersets.py:2071
|
||||||
msgid "Parent module bay (ID)"
|
msgid "Parent module bay (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:2061
|
#: netbox/dcim/filtersets.py:2076
|
||||||
msgid "Installed module (ID)"
|
msgid "Installed module (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:2072
|
#: netbox/dcim/filtersets.py:2087
|
||||||
msgid "Installed device (ID)"
|
msgid "Installed device (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:2078
|
#: netbox/dcim/filtersets.py:2093
|
||||||
msgid "Installed device (name)"
|
msgid "Installed device (name)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:2148
|
#: netbox/dcim/filtersets.py:2163
|
||||||
msgid "Master (ID)"
|
msgid "Master (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:2154
|
#: netbox/dcim/filtersets.py:2169
|
||||||
msgid "Master (name)"
|
msgid "Master (name)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:2196 netbox/tenancy/filtersets.py:250
|
#: netbox/dcim/filtersets.py:2211 netbox/tenancy/filtersets.py:250
|
||||||
msgid "Tenant (ID)"
|
msgid "Tenant (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:2202 netbox/extras/filtersets.py:711
|
#: netbox/dcim/filtersets.py:2217 netbox/extras/filtersets.py:711
|
||||||
#: netbox/tenancy/filtersets.py:256
|
#: netbox/tenancy/filtersets.py:256
|
||||||
msgid "Tenant (slug)"
|
msgid "Tenant (slug)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:2238 netbox/dcim/forms/filtersets.py:1145
|
#: netbox/dcim/filtersets.py:2253 netbox/dcim/forms/filtersets.py:1145
|
||||||
msgid "Unterminated"
|
msgid "Unterminated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/filtersets.py:2496
|
#: netbox/dcim/filtersets.py:2511
|
||||||
msgid "Power panel (ID)"
|
msgid "Power panel (ID)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5071,7 +5072,7 @@ msgid "Side B name"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/forms/bulk_import.py:1378
|
#: netbox/dcim/forms/bulk_import.py:1378
|
||||||
#: netbox/wireless/forms/bulk_import.py:91
|
#: netbox/wireless/forms/bulk_import.py:134
|
||||||
msgid "Connection status"
|
msgid "Connection status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5277,7 +5278,7 @@ msgid "Connection"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/forms/filtersets.py:1426 netbox/extras/forms/bulk_edit.py:382
|
#: netbox/dcim/forms/filtersets.py:1426 netbox/extras/forms/bulk_edit.py:382
|
||||||
#: netbox/extras/forms/bulk_import.py:253 netbox/extras/forms/filtersets.py:527
|
#: netbox/extras/forms/bulk_import.py:261 netbox/extras/forms/filtersets.py:527
|
||||||
#: netbox/extras/forms/model_forms.py:759 netbox/extras/tables/tables.py:640
|
#: netbox/extras/forms/model_forms.py:759 netbox/extras/tables/tables.py:640
|
||||||
#: netbox/templates/extras/journalentry.html:30
|
#: netbox/templates/extras/journalentry.html:30
|
||||||
msgid "Kind"
|
msgid "Kind"
|
||||||
@ -7179,11 +7180,13 @@ msgstr ""
|
|||||||
msgid "Termination B"
|
msgid "Termination B"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/tables/cables.py:66 netbox/wireless/tables/wirelesslink.py:22
|
#: netbox/dcim/tables/cables.py:66 netbox/wireless/forms/bulk_import.py:97
|
||||||
|
#: netbox/wireless/tables/wirelesslink.py:22
|
||||||
msgid "Device A"
|
msgid "Device A"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/tables/cables.py:72 netbox/wireless/tables/wirelesslink.py:31
|
#: netbox/dcim/tables/cables.py:72 netbox/wireless/forms/bulk_import.py:118
|
||||||
|
#: netbox/wireless/tables/wirelesslink.py:31
|
||||||
msgid "Device B"
|
msgid "Device B"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -7203,11 +7206,11 @@ msgstr ""
|
|||||||
msgid "Rack B"
|
msgid "Rack B"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/tables/cables.py:102
|
#: netbox/dcim/tables/cables.py:102 netbox/wireless/forms/bulk_import.py:90
|
||||||
msgid "Site A"
|
msgid "Site A"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/tables/cables.py:108
|
#: netbox/dcim/tables/cables.py:108 netbox/wireless/forms/bulk_import.py:111
|
||||||
msgid "Site B"
|
msgid "Site B"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -7375,7 +7378,7 @@ msgstr ""
|
|||||||
#: netbox/templates/ipam/ipaddress_bulk_add.html:15
|
#: netbox/templates/ipam/ipaddress_bulk_add.html:15
|
||||||
#: netbox/templates/ipam/service.html:42
|
#: netbox/templates/ipam/service.html:42
|
||||||
#: netbox/templates/virtualization/vminterface.html:107
|
#: netbox/templates/virtualization/vminterface.html:107
|
||||||
#: netbox/vpn/tables/tunnels.py:98
|
#: netbox/vpn/tables/tunnels.py:99
|
||||||
msgid "IP Addresses"
|
msgid "IP Addresses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -8191,7 +8194,8 @@ msgstr ""
|
|||||||
#: netbox/extras/forms/bulk_import.py:37 netbox/extras/forms/bulk_import.py:118
|
#: netbox/extras/forms/bulk_import.py:37 netbox/extras/forms/bulk_import.py:118
|
||||||
#: netbox/extras/forms/bulk_import.py:139
|
#: netbox/extras/forms/bulk_import.py:139
|
||||||
#: netbox/extras/forms/bulk_import.py:164
|
#: netbox/extras/forms/bulk_import.py:164
|
||||||
#: netbox/extras/forms/bulk_import.py:188 netbox/extras/forms/filtersets.py:141
|
#: netbox/extras/forms/bulk_import.py:188
|
||||||
|
#: netbox/extras/forms/bulk_import.py:242 netbox/extras/forms/filtersets.py:141
|
||||||
#: netbox/extras/forms/filtersets.py:235 netbox/extras/forms/filtersets.py:265
|
#: netbox/extras/forms/filtersets.py:235 netbox/extras/forms/filtersets.py:265
|
||||||
#: netbox/extras/forms/model_forms.py:50 netbox/extras/forms/model_forms.py:222
|
#: netbox/extras/forms/model_forms.py:50 netbox/extras/forms/model_forms.py:222
|
||||||
#: netbox/extras/forms/model_forms.py:254
|
#: netbox/extras/forms/model_forms.py:254
|
||||||
@ -8205,6 +8209,7 @@ msgstr ""
|
|||||||
#: netbox/extras/forms/bulk_import.py:141
|
#: netbox/extras/forms/bulk_import.py:141
|
||||||
#: netbox/extras/forms/bulk_import.py:166
|
#: netbox/extras/forms/bulk_import.py:166
|
||||||
#: netbox/extras/forms/bulk_import.py:190
|
#: netbox/extras/forms/bulk_import.py:190
|
||||||
|
#: netbox/extras/forms/bulk_import.py:244
|
||||||
#: netbox/tenancy/forms/bulk_import.py:95
|
#: netbox/tenancy/forms/bulk_import.py:95
|
||||||
msgid "One or more assigned object types"
|
msgid "One or more assigned object types"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -8281,15 +8286,15 @@ msgstr ""
|
|||||||
msgid "Script {name} not found"
|
msgid "Script {name} not found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/bulk_import.py:250
|
#: netbox/extras/forms/bulk_import.py:258
|
||||||
msgid "Assigned object type"
|
msgid "Assigned object type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/bulk_import.py:255
|
#: netbox/extras/forms/bulk_import.py:263
|
||||||
msgid "The classification of entry"
|
msgid "The classification of entry"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/bulk_import.py:267
|
#: netbox/extras/forms/bulk_import.py:275
|
||||||
#: netbox/extras/forms/model_forms.py:398 netbox/netbox/navigation/menu.py:413
|
#: netbox/extras/forms/model_forms.py:398 netbox/netbox/navigation/menu.py:413
|
||||||
#: netbox/templates/extras/notificationgroup.html:41
|
#: netbox/templates/extras/notificationgroup.html:41
|
||||||
#: netbox/templates/users/group.html:29 netbox/users/forms/model_forms.py:236
|
#: netbox/templates/users/group.html:29 netbox/users/forms/model_forms.py:236
|
||||||
@ -8298,11 +8303,11 @@ msgstr ""
|
|||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/bulk_import.py:271
|
#: netbox/extras/forms/bulk_import.py:279
|
||||||
msgid "User names separated by commas, encased with double quotes"
|
msgid "User names separated by commas, encased with double quotes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/bulk_import.py:274
|
#: netbox/extras/forms/bulk_import.py:282
|
||||||
#: netbox/extras/forms/model_forms.py:393 netbox/netbox/navigation/menu.py:295
|
#: netbox/extras/forms/model_forms.py:393 netbox/netbox/navigation/menu.py:295
|
||||||
#: netbox/netbox/navigation/menu.py:433
|
#: netbox/netbox/navigation/menu.py:433
|
||||||
#: netbox/templates/extras/notificationgroup.html:31
|
#: netbox/templates/extras/notificationgroup.html:31
|
||||||
@ -8315,7 +8320,7 @@ msgstr ""
|
|||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/bulk_import.py:278
|
#: netbox/extras/forms/bulk_import.py:286
|
||||||
msgid "Group names separated by commas, encased with double quotes"
|
msgid "Group names separated by commas, encased with double quotes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -8586,7 +8591,7 @@ msgstr ""
|
|||||||
msgid "Must specify either local data or a data file"
|
msgid "Must specify either local data or a data file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/reports.py:17 netbox/extras/forms/scripts.py:30
|
#: netbox/extras/forms/reports.py:17 netbox/extras/forms/scripts.py:25
|
||||||
msgid "Schedule at"
|
msgid "Schedule at"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -8594,7 +8599,7 @@ msgstr ""
|
|||||||
msgid "Schedule execution of report to a set time"
|
msgid "Schedule execution of report to a set time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/reports.py:23 netbox/extras/forms/scripts.py:36
|
#: netbox/extras/forms/reports.py:23 netbox/extras/forms/scripts.py:31
|
||||||
msgid "Recurs every"
|
msgid "Recurs every"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -8602,28 +8607,28 @@ msgstr ""
|
|||||||
msgid "Interval at which this report is re-run (in minutes)"
|
msgid "Interval at which this report is re-run (in minutes)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/reports.py:35 netbox/extras/forms/scripts.py:48
|
#: netbox/extras/forms/reports.py:35 netbox/extras/forms/scripts.py:43
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid " (current time: <strong>{now}</strong>)"
|
msgid " (current time: <strong>{now}</strong>)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/reports.py:45 netbox/extras/forms/scripts.py:58
|
#: netbox/extras/forms/reports.py:45 netbox/extras/forms/scripts.py:53
|
||||||
msgid "Scheduled time must be in the future."
|
msgid "Scheduled time must be in the future."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/scripts.py:24
|
#: netbox/extras/forms/scripts.py:19
|
||||||
msgid "Commit changes"
|
msgid "Commit changes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/scripts.py:25
|
#: netbox/extras/forms/scripts.py:20
|
||||||
msgid "Commit changes to the database (uncheck for a dry-run)"
|
msgid "Commit changes to the database (uncheck for a dry-run)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/scripts.py:31
|
#: netbox/extras/forms/scripts.py:26
|
||||||
msgid "Schedule execution of script to a set time"
|
msgid "Schedule execution of script to a set time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/forms/scripts.py:40
|
#: netbox/extras/forms/scripts.py:35
|
||||||
msgid "Interval at which this script is re-run (in minutes)"
|
msgid "Interval at which this script is re-run (in minutes)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -9338,7 +9343,7 @@ msgid "notifications"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/models/notifications.py:99
|
#: netbox/extras/models/notifications.py:99
|
||||||
#: netbox/extras/models/notifications.py:234
|
#: netbox/extras/models/notifications.py:240
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Objects of this type ({type}) do not support notifications."
|
msgid "Objects of this type ({type}) do not support notifications."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -9352,19 +9357,19 @@ msgstr ""
|
|||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/models/notifications.py:152
|
#: netbox/extras/models/notifications.py:158
|
||||||
msgid "notification group"
|
msgid "notification group"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/models/notifications.py:153
|
#: netbox/extras/models/notifications.py:159
|
||||||
msgid "notification groups"
|
msgid "notification groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/models/notifications.py:217
|
#: netbox/extras/models/notifications.py:223
|
||||||
msgid "subscription"
|
msgid "subscription"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/extras/models/notifications.py:218
|
#: netbox/extras/models/notifications.py:224
|
||||||
msgid "subscriptions"
|
msgid "subscriptions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -10042,8 +10047,8 @@ msgstr ""
|
|||||||
#: netbox/wireless/forms/bulk_edit.py:71 netbox/wireless/forms/bulk_edit.py:119
|
#: netbox/wireless/forms/bulk_edit.py:71 netbox/wireless/forms/bulk_edit.py:119
|
||||||
#: netbox/wireless/forms/bulk_import.py:64
|
#: netbox/wireless/forms/bulk_import.py:64
|
||||||
#: netbox/wireless/forms/bulk_import.py:67
|
#: netbox/wireless/forms/bulk_import.py:67
|
||||||
#: netbox/wireless/forms/bulk_import.py:109
|
#: netbox/wireless/forms/bulk_import.py:144
|
||||||
#: netbox/wireless/forms/bulk_import.py:112
|
#: netbox/wireless/forms/bulk_import.py:147
|
||||||
#: netbox/wireless/forms/filtersets.py:57
|
#: netbox/wireless/forms/filtersets.py:57
|
||||||
#: netbox/wireless/forms/filtersets.py:116
|
#: netbox/wireless/forms/filtersets.py:116
|
||||||
msgid "Authentication type"
|
msgid "Authentication type"
|
||||||
@ -11422,51 +11427,51 @@ msgstr ""
|
|||||||
msgid "{class_name} must specify a model class."
|
msgid "{class_name} must specify a model class."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/models/features.py:280
|
#: netbox/netbox/models/features.py:281
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Unknown field name '{name}' in custom field data."
|
msgid "Unknown field name '{name}' in custom field data."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/models/features.py:286
|
#: netbox/netbox/models/features.py:287
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Invalid value for custom field '{name}': {error}"
|
msgid "Invalid value for custom field '{name}': {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/models/features.py:295
|
#: netbox/netbox/models/features.py:296
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Custom field '{name}' must have a unique value."
|
msgid "Custom field '{name}' must have a unique value."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/models/features.py:302
|
#: netbox/netbox/models/features.py:303
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Missing required custom field '{name}'."
|
msgid "Missing required custom field '{name}'."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/models/features.py:492
|
#: netbox/netbox/models/features.py:493
|
||||||
msgid "Remote data source"
|
msgid "Remote data source"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/models/features.py:502
|
#: netbox/netbox/models/features.py:503
|
||||||
msgid "data path"
|
msgid "data path"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/models/features.py:506
|
#: netbox/netbox/models/features.py:507
|
||||||
msgid "Path to remote file (relative to data source root)"
|
msgid "Path to remote file (relative to data source root)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/models/features.py:509
|
#: netbox/netbox/models/features.py:510
|
||||||
msgid "auto sync enabled"
|
msgid "auto sync enabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/models/features.py:511
|
#: netbox/netbox/models/features.py:512
|
||||||
msgid "Enable automatic synchronization of data when the data file is updated"
|
msgid "Enable automatic synchronization of data when the data file is updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/models/features.py:514
|
#: netbox/netbox/models/features.py:515
|
||||||
msgid "date synced"
|
msgid "date synced"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/models/features.py:608
|
#: netbox/netbox/models/features.py:609
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{class_name} must implement a sync_data() method."
|
msgid "{class_name} must implement a sync_data() method."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -12057,12 +12062,12 @@ msgstr ""
|
|||||||
msgid "No {model_name} found"
|
msgid "No {model_name} found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/tables/tables.py:278
|
#: netbox/netbox/tables/tables.py:281
|
||||||
#: netbox/templates/generic/bulk_import.html:117
|
#: netbox/templates/generic/bulk_import.html:117
|
||||||
msgid "Field"
|
msgid "Field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/tables/tables.py:281
|
#: netbox/netbox/tables/tables.py:284
|
||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -12275,44 +12280,44 @@ msgstr ""
|
|||||||
msgid "None found"
|
msgid "None found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/account/profile.html:6
|
#: netbox/templates/account/profile.html:4
|
||||||
msgid "User Profile"
|
msgid "User Profile"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/account/profile.html:12
|
#: netbox/templates/account/profile.html:10
|
||||||
msgid "Account Details"
|
msgid "Account Details"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/account/profile.html:29
|
#: netbox/templates/account/profile.html:27
|
||||||
#: netbox/templates/tenancy/contact.html:53 netbox/templates/users/user.html:25
|
#: netbox/templates/tenancy/contact.html:53 netbox/templates/users/user.html:23
|
||||||
#: netbox/tenancy/forms/bulk_edit.py:116
|
#: netbox/tenancy/forms/bulk_edit.py:116
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/account/profile.html:33 netbox/templates/users/user.html:29
|
#: netbox/templates/account/profile.html:31 netbox/templates/users/user.html:27
|
||||||
msgid "Account Created"
|
msgid "Account Created"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/account/profile.html:37 netbox/templates/users/user.html:33
|
#: netbox/templates/account/profile.html:35 netbox/templates/users/user.html:31
|
||||||
msgid "Last Login"
|
msgid "Last Login"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/account/profile.html:41 netbox/templates/users/user.html:45
|
#: netbox/templates/account/profile.html:39 netbox/templates/users/user.html:43
|
||||||
msgid "Superuser"
|
msgid "Superuser"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/account/profile.html:45
|
#: netbox/templates/account/profile.html:43
|
||||||
#: netbox/templates/inc/user_menu.html:31 netbox/templates/users/user.html:41
|
#: netbox/templates/inc/user_menu.html:31 netbox/templates/users/user.html:39
|
||||||
msgid "Staff"
|
msgid "Staff"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/account/profile.html:53
|
#: netbox/templates/account/profile.html:51
|
||||||
#: netbox/templates/users/objectpermission.html:82
|
#: netbox/templates/users/objectpermission.html:82
|
||||||
#: netbox/templates/users/user.html:53
|
#: netbox/templates/users/user.html:51
|
||||||
msgid "Assigned Groups"
|
msgid "Assigned Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/account/profile.html:58
|
#: netbox/templates/account/profile.html:56
|
||||||
#: netbox/templates/circuits/circuit_terminations_swap.html:18
|
#: netbox/templates/circuits/circuit_terminations_swap.html:18
|
||||||
#: netbox/templates/circuits/circuit_terminations_swap.html:26
|
#: netbox/templates/circuits/circuit_terminations_swap.html:26
|
||||||
#: netbox/templates/circuits/circuittermination.html:34
|
#: netbox/templates/circuits/circuittermination.html:34
|
||||||
@ -12328,7 +12333,7 @@ msgstr ""
|
|||||||
#: netbox/templates/extras/configtemplate.html:77
|
#: netbox/templates/extras/configtemplate.html:77
|
||||||
#: netbox/templates/extras/eventrule.html:66
|
#: netbox/templates/extras/eventrule.html:66
|
||||||
#: netbox/templates/extras/exporttemplate.html:88
|
#: netbox/templates/extras/exporttemplate.html:88
|
||||||
#: netbox/templates/extras/htmx/script_result.html:60
|
#: netbox/templates/extras/htmx/script_result.html:69
|
||||||
#: netbox/templates/extras/webhook.html:65
|
#: netbox/templates/extras/webhook.html:65
|
||||||
#: netbox/templates/extras/webhook.html:75
|
#: netbox/templates/extras/webhook.html:75
|
||||||
#: netbox/templates/inc/panel_table.html:13
|
#: netbox/templates/inc/panel_table.html:13
|
||||||
@ -12338,14 +12343,10 @@ msgstr ""
|
|||||||
#: netbox/templates/users/group.html:34 netbox/templates/users/group.html:44
|
#: netbox/templates/users/group.html:34 netbox/templates/users/group.html:44
|
||||||
#: netbox/templates/users/objectpermission.html:77
|
#: netbox/templates/users/objectpermission.html:77
|
||||||
#: netbox/templates/users/objectpermission.html:87
|
#: netbox/templates/users/objectpermission.html:87
|
||||||
#: netbox/templates/users/user.html:58 netbox/templates/users/user.html:68
|
#: netbox/templates/users/user.html:56 netbox/templates/users/user.html:66
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/account/profile.html:68 netbox/templates/users/user.html:78
|
|
||||||
msgid "Recent Activity"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: netbox/templates/account/token.html:8
|
#: netbox/templates/account/token.html:8
|
||||||
#: netbox/templates/account/token_list.html:6
|
#: netbox/templates/account/token_list.html:6
|
||||||
msgid "My API Tokens"
|
msgid "My API Tokens"
|
||||||
@ -12557,7 +12558,7 @@ msgstr ""
|
|||||||
#: netbox/templates/dcim/frontport.html:102
|
#: netbox/templates/dcim/frontport.html:102
|
||||||
#: netbox/templates/dcim/interface.html:237
|
#: netbox/templates/dcim/interface.html:237
|
||||||
#: netbox/templates/dcim/interface.html:257
|
#: netbox/templates/dcim/interface.html:257
|
||||||
#: netbox/templates/dcim/powerfeed.html:127
|
#: netbox/templates/dcim/powerfeed.html:123
|
||||||
#: netbox/templates/dcim/poweroutlet.html:85
|
#: netbox/templates/dcim/poweroutlet.html:85
|
||||||
#: netbox/templates/dcim/poweroutlet.html:86
|
#: netbox/templates/dcim/poweroutlet.html:86
|
||||||
#: netbox/templates/dcim/powerport.html:73
|
#: netbox/templates/dcim/powerport.html:73
|
||||||
@ -14007,10 +14008,15 @@ msgstr ""
|
|||||||
msgid "Log"
|
msgid "Log"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/extras/htmx/script_result.html:56
|
#: netbox/templates/extras/htmx/script_result.html:57
|
||||||
msgid "Output"
|
msgid "Output"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/templates/extras/htmx/script_result.html:61
|
||||||
|
#: netbox/templates/extras/object_render_config.html:60
|
||||||
|
msgid "Download"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/extras/inc/result_pending.html:4
|
#: netbox/templates/extras/inc/result_pending.html:4
|
||||||
msgid "Loading"
|
msgid "Loading"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -14057,10 +14063,6 @@ msgstr ""
|
|||||||
msgid "Rendered Config"
|
msgid "Rendered Config"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/extras/object_render_config.html:60
|
|
||||||
msgid "Download"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: netbox/templates/extras/object_render_config.html:68
|
#: netbox/templates/extras/object_render_config.html:68
|
||||||
msgid "Error rendering template"
|
msgid "Error rendering template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -14849,10 +14851,18 @@ msgstr ""
|
|||||||
msgid "Add Tenant Group"
|
msgid "Add Tenant Group"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/users/group.html:39 netbox/templates/users/user.html:63
|
#: netbox/templates/users/group.html:39 netbox/templates/users/user.html:61
|
||||||
msgid "Assigned Permissions"
|
msgid "Assigned Permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/templates/users/inc/user_activity.html:6
|
||||||
|
msgid "Recent Activity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/templates/users/inc/user_activity.html:9
|
||||||
|
msgid "View All"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/users/objectpermission.html:6
|
#: netbox/templates/users/objectpermission.html:6
|
||||||
#: netbox/templates/users/objectpermission.html:14
|
#: netbox/templates/users/objectpermission.html:14
|
||||||
#: netbox/users/forms/filtersets.py:66
|
#: netbox/users/forms/filtersets.py:66
|
||||||
@ -15076,7 +15086,7 @@ msgstr ""
|
|||||||
#: netbox/templates/vpn/tunneltermination.html:35
|
#: netbox/templates/vpn/tunneltermination.html:35
|
||||||
#: netbox/vpn/forms/bulk_import.py:107 netbox/vpn/forms/model_forms.py:103
|
#: netbox/vpn/forms/bulk_import.py:107 netbox/vpn/forms/model_forms.py:103
|
||||||
#: netbox/vpn/forms/model_forms.py:139 netbox/vpn/forms/model_forms.py:248
|
#: netbox/vpn/forms/model_forms.py:139 netbox/vpn/forms/model_forms.py:248
|
||||||
#: netbox/vpn/tables/tunnels.py:101
|
#: netbox/vpn/tables/tunnels.py:102
|
||||||
msgid "Outside IP"
|
msgid "Outside IP"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -16627,8 +16637,8 @@ msgstr ""
|
|||||||
#: netbox/wireless/forms/bulk_edit.py:76 netbox/wireless/forms/bulk_edit.py:124
|
#: netbox/wireless/forms/bulk_edit.py:76 netbox/wireless/forms/bulk_edit.py:124
|
||||||
#: netbox/wireless/forms/bulk_import.py:70
|
#: netbox/wireless/forms/bulk_import.py:70
|
||||||
#: netbox/wireless/forms/bulk_import.py:73
|
#: netbox/wireless/forms/bulk_import.py:73
|
||||||
#: netbox/wireless/forms/bulk_import.py:115
|
#: netbox/wireless/forms/bulk_import.py:150
|
||||||
#: netbox/wireless/forms/bulk_import.py:118
|
#: netbox/wireless/forms/bulk_import.py:153
|
||||||
#: netbox/wireless/forms/filtersets.py:62
|
#: netbox/wireless/forms/filtersets.py:62
|
||||||
#: netbox/wireless/forms/filtersets.py:121
|
#: netbox/wireless/forms/filtersets.py:121
|
||||||
msgid "Authentication cipher"
|
msgid "Authentication cipher"
|
||||||
@ -16639,15 +16649,39 @@ msgid "Bridged VLAN"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/wireless/forms/bulk_import.py:94
|
#: netbox/wireless/forms/bulk_import.py:94
|
||||||
|
msgid "Site of parent device A (if any)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/wireless/forms/bulk_import.py:100
|
||||||
|
msgid "Parent device of assigned interface A"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/wireless/forms/bulk_import.py:103
|
||||||
#: netbox/wireless/tables/wirelesslink.py:27
|
#: netbox/wireless/tables/wirelesslink.py:27
|
||||||
msgid "Interface A"
|
msgid "Interface A"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/wireless/forms/bulk_import.py:98
|
#: netbox/wireless/forms/bulk_import.py:106
|
||||||
|
msgid "Assigned interface A"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/wireless/forms/bulk_import.py:115
|
||||||
|
msgid "Site of parent device B (if any)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/wireless/forms/bulk_import.py:121
|
||||||
|
msgid "Parent device of assigned interface B"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/wireless/forms/bulk_import.py:124
|
||||||
#: netbox/wireless/tables/wirelesslink.py:36
|
#: netbox/wireless/tables/wirelesslink.py:36
|
||||||
msgid "Interface B"
|
msgid "Interface B"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/wireless/forms/bulk_import.py:127
|
||||||
|
msgid "Assigned interface B"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/wireless/forms/model_forms.py:166
|
#: netbox/wireless/forms/model_forms.py:166
|
||||||
msgid "Side B"
|
msgid "Side B"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -75,8 +75,9 @@ class UserView(generic.ObjectView):
|
|||||||
template_name = 'users/user.html'
|
template_name = 'users/user.html'
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=instance)[:20]
|
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=instance)[:20]
|
||||||
changelog_table = ObjectChangeTable(changelog)
|
changelog_table = ObjectChangeTable(changelog)
|
||||||
|
changelog_table.orderable = False
|
||||||
changelog_table.configure(request)
|
changelog_table.configure(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@ -43,3 +44,20 @@ class Migration(migrations.Migration):
|
|||||||
# Copy over existing site assignments
|
# Copy over existing site assignments
|
||||||
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
|
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_cluster_scope(objectchange, reverting):
|
||||||
|
site_ct = ContentType.objects.get_by_natural_key('dcim', 'site').pk
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is None:
|
||||||
|
continue
|
||||||
|
if site_id := data.get('site'):
|
||||||
|
data.update({
|
||||||
|
'scope_type': site_ct,
|
||||||
|
'scope_id': site_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'virtualization.cluster': oc_cluster_scope,
|
||||||
|
}
|
||||||
|
@ -87,3 +87,14 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oc_cluster_remove_site(objectchange, reverting):
|
||||||
|
for data in (objectchange.prechange_data, objectchange.postchange_data):
|
||||||
|
if data is not None:
|
||||||
|
data.pop('site', None)
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'virtualization.cluster': oc_cluster_remove_site,
|
||||||
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.apps import apps
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@ -50,3 +52,43 @@ class Migration(migrations.Migration):
|
|||||||
name='mac_address',
|
name='mac_address',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# See peer migrator in dcim.0200_populate_mac_addresses before making changes
|
||||||
|
def oc_vminterface_primary_mac_address(objectchange, reverting):
|
||||||
|
MACAddress = apps.get_model('dcim', 'MACAddress')
|
||||||
|
vminterface_ct = ContentType.objects.get_by_natural_key('virtualization', 'vminterface')
|
||||||
|
|
||||||
|
# Swap data order if the change is being reverted
|
||||||
|
if not reverting:
|
||||||
|
before, after = objectchange.prechange_data, objectchange.postchange_data
|
||||||
|
else:
|
||||||
|
before, after = objectchange.postchange_data, objectchange.prechange_data
|
||||||
|
|
||||||
|
if after.get('mac_address') != before.get('mac_address'):
|
||||||
|
# Create & assign the new MACAddress (if any)
|
||||||
|
if after.get('mac_address'):
|
||||||
|
mac = MACAddress.objects.create(
|
||||||
|
mac_address=after['mac_address'],
|
||||||
|
assigned_object_type=vminterface_ct,
|
||||||
|
assigned_object_id=objectchange.changed_object_id,
|
||||||
|
)
|
||||||
|
after['primary_mac_address'] = mac.pk
|
||||||
|
else:
|
||||||
|
after['primary_mac_address'] = None
|
||||||
|
# Delete the old MACAddress (if any)
|
||||||
|
if before.get('mac_address'):
|
||||||
|
MACAddress.objects.filter(
|
||||||
|
mac_address=before['mac_address'],
|
||||||
|
assigned_object_type=vminterface_ct,
|
||||||
|
assigned_object_id=objectchange.changed_object_id,
|
||||||
|
).delete()
|
||||||
|
before['primary_mac_address'] = None
|
||||||
|
|
||||||
|
before.pop('mac_address', None)
|
||||||
|
after.pop('mac_address', None)
|
||||||
|
|
||||||
|
|
||||||
|
objectchange_migrators = {
|
||||||
|
'virtualization.vminterface': oc_vminterface_primary_mac_address,
|
||||||
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user