Merge branch 'main' into nav-menu-callables

This commit is contained in:
Brian Tiemann 2025-06-10 08:50:44 -04:00
commit fbf639fad1
101 changed files with 4274 additions and 3632 deletions

View File

@ -15,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.3.1
placeholder: v4.3.2
validations:
required: true
- type: dropdown

View File

@ -27,7 +27,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.3.1
placeholder: v4.3.2
validations:
required: true
- type: dropdown

View File

@ -4,7 +4,7 @@
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"
@ -16,7 +16,7 @@ SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
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
The `sentry-sdk` Python package is required to enable Sentry integration.

View File

@ -6,7 +6,7 @@
Default: `True`
Setting this to False will disable the GraphQL API.
Setting this to `False` will disable the GraphQL API.
---

View File

@ -57,7 +57,7 @@ Sets content for the top banner in the user interface.
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.
@ -102,7 +102,7 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
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`
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`
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.
---

View File

@ -1,6 +1,6 @@
# 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`
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`
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)
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`.)
---

View File

@ -2,12 +2,12 @@
## 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
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:

View File

@ -5,7 +5,7 @@
Default: `False`
!!! 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.
@ -52,7 +52,7 @@ Although it is not recommended, the default validation rules can be disabled by
Default: `False`
If True, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
If `True`, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
---
@ -62,7 +62,7 @@ If True, cross-origin resource sharing (CORS) requests will be accepted from all
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
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
CORS_ORIGIN_WHITELIST = [
@ -84,7 +84,7 @@ The name of the cookie to use for the cross-site request forgery (CSRF) authenti
Default: `False`
If true, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection.
If `True`, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection.
---
@ -92,7 +92,7 @@ If true, the cookie employed for cross-site request forgery (CSRF) protection wi
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
CSRF_TRUSTED_ORIGINS = (
@ -164,7 +164,7 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
Default: `False`
If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
If `True`, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely.
@ -212,7 +212,7 @@ The view name or URL to which a user is redirected after logging out.
Default: `False`
If true, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain.
If `True`, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain.
---
@ -220,7 +220,7 @@ If true, the `includeSubDomains` directive will be included in the HTTP Strict T
Default: `False`
If true, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar.
If `True`, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar.
---
@ -236,7 +236,7 @@ If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict
Default: `False`
If true, all non-HTTPS requests will be automatically redirected to use HTTPS.
If `True`, all non-HTTPS requests will be automatically redirected to use HTTPS.
!!! warning
Ensure that your frontend HTTP daemon has been configured to forward the HTTP scheme correctly before enabling this option. An incorrectly configured frontend may result in a looping redirect.
@ -255,7 +255,7 @@ The name used for the session cookie. See the [Django documentation](https://doc
Default: `False`
If true, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection.
If `True`, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection.
---

View File

@ -95,7 +95,7 @@ Default: `('127.0.0.1', '::1')`
A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For
example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
addresses (and [`DEBUG`](./development.md#debug) is true).
addresses (and [`DEBUG`](./development.md#debug) is `True`).
---
@ -103,7 +103,7 @@ addresses (and [`DEBUG`](./development.md#debug) is true).
Default: `False`
Set this configuration parameter to True for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
Set this configuration parameter to `True` for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
!!! note
If Internet access is available via a proxy, set [`HTTP_PROXIES`](#http_proxies) instead.
@ -114,7 +114,7 @@ Set this configuration parameter to True for NetBox deployments which do not hav
Default: `{}`
A dictionary of custom jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
A dictionary of custom Jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
```python
def uppercase(x):

View File

@ -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 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 and supported Python versions in the project metadata file (`pyproject.toml`)
### 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 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/`.
* 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
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.

View File

@ -9,7 +9,7 @@ NetBox is the leading solution for modeling and documenting modern networks. By
## :material-server-network: Built for Networks
Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
Unlike general-purpose configuration management databases (CMDBs), NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
* Hierarchical regions, sites, and locations
* Racks, devices, and device components

View File

@ -108,7 +108,7 @@ Open `configuration.py` with your preferred editor to begin configuring NetBox.
### 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
ALLOWED_HOSTS = ['netbox.example.com', '192.0.2.123']

View File

@ -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
```
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

View File

@ -22,7 +22,7 @@ from netbox.plugins import PluginConfig
### 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).

View File

@ -1,6 +1,6 @@
# Views
## Writing Views
## Writing Basic Views
If your plugin will provide its own page or pages within the NetBox web UI, you'll need to define views. A view is a piece of business logic which performs an action and/or renders a page when a request is made to a particular URL. HTML content is rendered using a [template](./templates.md). Views are typically defined in `views.py`, and URL patterns in `urls.py`.
@ -47,9 +47,13 @@ A URL pattern has three components:
This makes our view accessible at the URL `/plugins/animal-sounds/random/`. (Remember, our `AnimalSoundsConfig` class sets our plugin's base URL to `animal-sounds`.) Viewing this URL should show the base NetBox template with our custom content inside it.
## NetBox Model Views
NetBox provides several generic view classes and additional helper functions, to simplify the implementation of plugin logic. These are recommended to be used whenever possible to keep the maintenance overhead of plugins low.
### View Classes
NetBox provides several generic view classes (documented below) to facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
Generic view classes (documented below) facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
| View Class | Description |
|----------------------|--------------------------------------------------------|
@ -65,18 +69,51 @@ NetBox provides several generic view classes (documented below) to facilitate co
!!! warning
Please note that only the classes which appear in this documentation are currently supported. Although other classes may be present within the `views.generic` module, they are not yet supported for use by plugins.
#### Example Usage
### URL registration
The NetBox URL registration process has two parts:
1. View classes can be decorated with `@register_model_view()`. This registers a new URL for the model.
2. All of a model's URLs can be included in `urls.py` using the `get_model_urls()` function. This call is usually required twice: once to import general views for the model and again to import model detail views tied to the object's primary key.
::: utilities.views.register_model_view
!!! note "Changed in NetBox v4.2"
In NetBox v4.2, the `register_model_view()` function was extended to support the registration of list views by passing `detail=False`.
::: utilities.urls.get_model_urls
!!! note "Changed in NetBox v4.2"
In NetBox v4.2, the `get_model_urls()` function was extended to support retrieving registered general model views (e.g. for listing objects) by passing `detail=False`.
### Example Usage
```python
# views.py
from netbox.views.generic import ObjectEditView
from utilities.views import register_model_view
from .models import Thing
@register_model_view(Thing, name='add', detail=False)
@register_model_view(Thing, name='edit')
class ThingEditView(ObjectEditView):
queryset = Thing.objects.all()
template_name = 'myplugin/thing.html'
...
```
```python
# urls.py
from django.urls import include, path
from utilities.urls import get_model_urls
urlpatterns = [
path('thing/', include(get_model_urls('myplugin', 'thing', detail=False))),
path('thing/<int:pk>/', include(get_model_urls('myplugin', 'thing'))),
...
]
```
## Object Views
Below are the class definitions for NetBox's object views. These views handle CRUD actions for individual objects. The view, add/edit, and delete views each inherit from `BaseObjectView`, which is not intended to be used directly.
@ -143,6 +180,9 @@ Below are the class definitions for NetBox's multi-object views. These views han
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
!!! note
These feature views are automatically registered for all models that implement the respective feature. There is usually no need to override them. However, if that's the case, the URL must be registered manually in `urls.py` instead of using the `register_model_view()` function or decorator.
::: netbox.views.generic.ObjectChangeLogView
options:
members:
@ -157,7 +197,7 @@ These views are provided to enable or enhance certain NetBox model features, suc
### Additional Tabs
Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict:
Plugins can "attach" a custom view to a NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict:
```python
from dcim.models import Site
@ -185,11 +225,6 @@ class MyView(generic.ObjectView):
)
```
!!! note "Changed in NetBox v4.2"
The `register_model_view()` function was extended in NetBox v4.2 to support registration of list views by passing `detail=False`.
::: utilities.views.register_model_view
::: utilities.views.ViewTab
### Extra Template Content

View File

@ -86,3 +86,69 @@ netbox=> DELETE FROM django_migrations WHERE app='pluginname';
!!! warning
Exercise extreme caution when altering Django system tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.
## 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 plugins 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
```

View File

@ -1,5 +1,31 @@
# 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)
### Enhancements

View File

@ -191,12 +191,9 @@ class ProfileView(LoginRequiredMixin, View):
def get(self, request):
# Compile changelog table
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
user=request.user
).prefetch_related(
'changed_object_type'
)[:20]
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=request.user)[:20]
changelog_table = ObjectChangeTable(changelog)
changelog_table.orderable = False
changelog_table.configure(request)
return render(request, self.template_name, {

View File

@ -1,4 +1,5 @@
import django.db.models.deletion
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
@ -49,3 +50,26 @@ class Migration(migrations.Migration):
# Copy over existing site assignments
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,
}

View File

@ -86,3 +86,15 @@ class Migration(migrations.Migration):
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,
}

View File

@ -1,4 +1,5 @@
import django.db.models.deletion
from django.contrib.contenttypes.models import ContentType
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,
}

View File

@ -120,7 +120,8 @@ class CircuitTerminationTable(NetBoxTable):
)
termination = tables.Column(
verbose_name=_('Termination Point'),
linkify=True
linkify=True,
orderable=False,
)
# Termination types
@ -132,7 +133,7 @@ class CircuitTerminationTable(NetBoxTable):
site_group = tables.Column(
verbose_name=_('Site Group'),
linkify=True,
accessor='_sitegroup'
accessor='_site_group'
)
region = tables.Column(
verbose_name=_('Region'),

View 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)

View File

@ -2012,6 +2012,21 @@ class InterfaceFilterSet(
'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
}.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(
ModularDeviceComponentFilterSet,

View File

@ -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,
}

View File

@ -1,4 +1,6 @@
import django.db.models.deletion
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
@ -15,7 +17,7 @@ def populate_mac_addresses(apps, schema_editor):
assigned_object_type=interface_ct,
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)
@ -51,3 +53,43 @@ class Migration(migrations.Migration):
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,
}

View File

@ -85,7 +85,7 @@ class CachedScopeMixin(models.Model):
abstract = True
def clean(self):
if self.scope_type and not self.scope:
if self.scope_type and not (self.scope or self.scope_id):
scope_type = self.scope_type.model_class()
raise ValidationError({
'scope': _(

View File

@ -144,7 +144,7 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
super().clean()
# Validate any attributes against the assigned profile's schema
if self.profile:
if self.profile and self.profile.schema:
try:
jsonschema.validate(self.attribute_data, schema=self.profile.schema)
except JSONValidationError as e:

View File

@ -12,6 +12,7 @@ from users.models import User
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
from virtualization.models import Cluster, ClusterType, ClusterGroup, VMInterface, VirtualMachine
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
from wireless.models import WirelessLink
class DeviceComponentFilterSetTests:
@ -4496,7 +4497,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
# Cables
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save()
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save()
# Third pair is not connected
# Wireless links
WirelessLink(interface_a=interfaces[7], interface_b=interfaces[8]).save()
def test_name(self):
params = {'name': ['Interface 1', 'Interface 2']}
@ -4684,15 +4687,15 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
def test_occupied(self):
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}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_connected(self):
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}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_kind(self):
params = {'kind': 'physical'}

View File

@ -131,7 +131,7 @@ class RenderTemplateMixin(models.Model):
"""
context = self.get_context(context=context, queryset=queryset)
env_params = self.environment_params or {}
output = render_jinja2(self.template_code, context, env_params)
output = render_jinja2(self.template_code, context, env_params, getattr(self, 'data_file', None))
# Replace CRLF-style line terminators
output = output.replace('\r\n', '\n')

View File

@ -1,9 +1,12 @@
from django.forms import ValidationError
from django.test import TestCase
import tempfile
from pathlib import Path
from core.models import ObjectType
from django.forms import ValidationError
from django.test import tag, TestCase
from core.models import DataSource, ObjectType
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
from extras.models import ConfigContext, Tag
from extras.models import ConfigContext, ConfigTemplate, Tag
from tenancy.models import Tenant, TenantGroup
from utilities.exceptions import AbortRequest
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -33,8 +36,8 @@ class TagTest(TestCase):
]
site = Site.objects.create(name='Site 1')
for tag in tags:
site.tags.add(tag)
for _tag in tags:
site.tags.add(_tag)
site.save()
site = Site.objects.first()
@ -540,3 +543,66 @@ class ConfigContextTest(TestCase):
device.local_context_data = 'foo'
with self.assertRaises(ValidationError):
device.clean()
class ConfigTemplateTest(TestCase):
"""
TODO: These test cases deal with the weighting, ordering, and deep merge logic of config context data.
"""
MAIN_TEMPLATE = """
{%- include 'base.j2' %}
""".strip()
BASE_TEMPLATE = """
Hi
""".strip()
@classmethod
def _create_template_file(cls, templates_dir, file_name, content):
template_file_name = file_name
if not template_file_name.endswith('j2'):
template_file_name += '.j2'
temp_file_path = templates_dir / template_file_name
with open(temp_file_path, 'w') as f:
f.write(content)
@classmethod
def setUpTestData(cls):
temp_dir = tempfile.TemporaryDirectory()
templates_dir = Path(temp_dir.name) / "templates"
templates_dir.mkdir(parents=True, exist_ok=True)
cls._create_template_file(templates_dir, 'base.j2', cls.BASE_TEMPLATE)
cls._create_template_file(templates_dir, 'main.j2', cls.MAIN_TEMPLATE)
data_source = DataSource(
name="Test DataSource",
type="local",
source_url=str(templates_dir),
)
data_source.save()
data_source.sync()
base_config_template = ConfigTemplate(
name="BaseTemplate",
data_file=data_source.datafiles.filter(path__endswith='base.j2').first()
)
base_config_template.clean()
base_config_template.save()
cls.base_config_template = base_config_template
main_config_template = ConfigTemplate(
name="MainTemplate",
data_file=data_source.datafiles.filter(path__endswith='main.j2').first()
)
main_config_template.clean()
main_config_template.save()
cls.main_config_template = main_config_template
@tag('regression')
def test_config_template_with_data_source(self):
self.assertEqual(self.BASE_TEMPLATE, self.base_config_template.render({}))
@tag('regression')
def test_config_template_with_data_source_nested_templates(self):
self.assertEqual(self.BASE_TEMPLATE, self.main_config_template.render({}))

View File

@ -966,7 +966,7 @@ class ObjectRenderConfigView(generic.ObjectView):
# Render the config template
rendered_config = None
error_message = None
error_message = ''
if config_template := instance.get_config_template():
try:
rendered_config = config_template.render(context=context_data)

View File

@ -449,7 +449,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet, C
@extend_schema_field(OpenApiTypes.STR)
def filter_present_in_vrf(self, queryset, name, vrf):
if vrf is None:
return queryset.none
return queryset.none()
return queryset.filter(
Q(vrf=vrf) |
Q(vrf__export_targets__in=vrf.import_targets.all())
@ -729,7 +729,7 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFil
@extend_schema_field(OpenApiTypes.STR)
def filter_present_in_vrf(self, queryset, name, vrf):
if vrf is None:
return queryset.none
return queryset.none()
return queryset.filter(
Q(vrf=vrf) |
Q(vrf__export_targets__in=vrf.import_targets.all())

View File

@ -826,7 +826,7 @@ class ServiceForm(NetBoxModelForm):
except ObjectDoesNotExist:
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
def clean(self):

View File

@ -159,6 +159,14 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
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)
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):

View File

@ -1,4 +1,5 @@
import django.db.models.deletion
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
@ -44,3 +45,20 @@ class Migration(migrations.Migration):
# Copy over existing site assignments
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,
}

View File

@ -60,3 +60,14 @@ class Migration(migrations.Migration):
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,
}

View File

@ -1,3 +1,4 @@
from django.contrib.contenttypes.models import ContentType
from django.db import migrations
from django.db.models import F
@ -54,3 +55,26 @@ class Migration(migrations.Migration):
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,
}

View File

@ -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,
}

View File

@ -148,7 +148,7 @@ class VLANQuerySet(RestrictedQuerySet):
# Find all relevant VLANGroups
q = Q()
site = vm.site or vm.cluster._site
site = vm.site
if vm.cluster:
# Add VLANGroups scoped to the assigned cluster (or its group)
q |= Q(
@ -160,6 +160,30 @@ class VLANQuerySet(RestrictedQuerySet):
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'),
scope_id=vm.cluster.group_id
)
# Looking all possible cluster scopes
if vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'location'):
site = site or vm.cluster.scope.site
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'location'),
scope_id__in=vm.cluster.scope.get_ancestors(include_self=True)
)
elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'site'):
site = site or vm.cluster.scope
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'),
scope_id=vm.cluster.scope.pk
)
elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'sitegroup'):
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
scope_id__in=vm.cluster.scope.get_ancestors(include_self=True)
)
elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'region'):
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'),
scope_id__in=vm.cluster.scope.get_ancestors(include_self=True)
)
# VM can be assigned to a site without a cluster so checking assigned site independently
if site:
# Add VLANGroups scoped to the assigned site (or its group or region)
q |= Q(

View File

@ -1,6 +1,7 @@
import json
import logging
from django.test import tag
from django.urls import reverse
from netaddr import IPNetwork
from rest_framework import status
@ -383,6 +384,18 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
)
Prefix.objects.bulk_create(prefixes)
@tag('regression')
def test_clean_validates_scope(self):
prefix = Prefix.objects.first()
site = Site.objects.create(name='Test Site', slug='test-site')
data = {'scope_type': 'dcim.site', 'scope_id': site.id}
url = reverse('ipam-api:prefix-detail', kwargs={'pk': prefix.pk})
self.add_permissions('ipam.change_prefix')
response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
def test_list_available_prefixes(self):
"""
Test retrieval of all available prefixes within a parent prefix.

View File

@ -1849,6 +1849,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
Cluster(name='Cluster 1', type=cluster_type, group=cluster_groups[0], scope=sites[0]),
Cluster(name='Cluster 2', type=cluster_type, group=cluster_groups[1], scope=sites[1]),
Cluster(name='Cluster 3', type=cluster_type, group=cluster_groups[2], scope=sites[2]),
Cluster(name='Cluster 4', type=cluster_type, group=cluster_groups[0], scope=locations[0]),
)
for cluster in clusters:
cluster.save()
@ -1857,6 +1858,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0]),
VirtualMachine(name='Virtual Machine 2', cluster=clusters[1]),
VirtualMachine(name='Virtual Machine 3', cluster=clusters[2]),
VirtualMachine(name='Virtual Machine 4', cluster=clusters[3]),
)
VirtualMachine.objects.bulk_create(virtual_machines)
@ -1864,6 +1866,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
VMInterface(virtual_machine=virtual_machines[0], name='VM Interface 1'),
VMInterface(virtual_machine=virtual_machines[1], name='VM Interface 2'),
VMInterface(virtual_machine=virtual_machines[2], name='VM Interface 3'),
VMInterface(virtual_machine=virtual_machines[3], name='VM Interface 4'),
)
VMInterface.objects.bulk_create(vm_interfaces)
@ -1890,6 +1893,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
VLANGroup(name='Cluster 1', slug='cluster-1', scope=clusters[0]),
VLANGroup(name='Cluster 2', slug='cluster-2', scope=clusters[1]),
VLANGroup(name='Cluster 3', slug='cluster-3', scope=clusters[2]),
VLANGroup(name='Cluster 4', slug='cluster-4', scope=clusters[3]),
# General purpose VLAN groups
VLANGroup(name='VLAN Group 1', slug='vlan-group-1'),
@ -1944,11 +1948,12 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
VLAN(vid=19, name='Cluster 1', group=groups[18]),
VLAN(vid=20, name='Cluster 2', group=groups[19]),
VLAN(vid=21, name='Cluster 3', group=groups[20]),
VLAN(vid=22, name='Cluster 4', group=groups[21]),
VLAN(
vid=101,
name='VLAN 101',
site=sites[3],
group=groups[21],
group=groups[22],
role=roles[0],
tenant=tenants[0],
status=VLANStatusChoices.STATUS_ACTIVE,
@ -1957,7 +1962,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
vid=102,
name='VLAN 102',
site=sites[3],
group=groups[21],
group=groups[22],
role=roles[0],
tenant=tenants[0],
status=VLANStatusChoices.STATUS_ACTIVE,
@ -1966,7 +1971,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
vid=201,
name='VLAN 201',
site=sites[4],
group=groups[22],
group=groups[23],
role=roles[1],
tenant=tenants[1],
status=VLANStatusChoices.STATUS_DEPRECATED,
@ -1975,7 +1980,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
vid=202,
name='VLAN 202',
site=sites[4],
group=groups[22],
group=groups[23],
role=roles[1],
tenant=tenants[1],
status=VLANStatusChoices.STATUS_DEPRECATED,
@ -1984,7 +1989,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
vid=301,
name='VLAN 301',
site=sites[5],
group=groups[23],
group=groups[24],
role=roles[2],
tenant=tenants[2],
status=VLANStatusChoices.STATUS_RESERVED,
@ -1993,13 +1998,13 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
vid=302,
name='VLAN 302',
site=sites[5],
group=groups[23],
group=groups[24],
role=roles[2],
tenant=tenants[2],
status=VLANStatusChoices.STATUS_RESERVED,
),
# Create one globally available VLAN on a VLAN group
VLAN(vid=500, name='VLAN Group 1', group=groups[24]),
VLAN(vid=500, name='VLAN Group 1', group=groups[25]),
# Create one globally available VLAN
VLAN(vid=1000, name='Global VLAN'),
# Create some Q-in-Q service VLANs
@ -2130,6 +2135,9 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
vm_id = VirtualMachine.objects.first().pk
params = {'available_on_virtualmachine': vm_id}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7) # 5 scoped + 1 global group + 1 global
vm_id = VirtualMachine.objects.get(name='Virtual Machine 4').pk
params = {'available_on_virtualmachine': vm_id}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) # 6 scoped + 1 global group + 1 global
def test_available_at_site(self):
site_id = Site.objects.first().pk

View File

@ -231,14 +231,19 @@ SESSION_FILE_PATH = None
# DISK_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
# class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example:
# STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage'
# STORAGE_CONFIG = {
# 'AWS_ACCESS_KEY_ID': 'Key ID',
# 'AWS_SECRET_ACCESS_KEY': 'Secret',
# 'AWS_STORAGE_BUCKET_NAME': 'netbox',
# 'AWS_S3_REGION_NAME': 'eu-west-1',
# Within the STORAGES dictionary, "default" is used for image uploads, "staticfiles" is for static files and "scripts"
# is used for custom scripts. See django-storages and django-storage-swift libraries for more details. By default the
# following configuration is used:
# STORAGES = {
# "default": {
# "BACKEND": "django.core.files.storage.FileSystemStorage",
# },
# "staticfiles": {
# "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
# },
# "scripts": {
# "BACKEND": "extras.storage.ScriptFileSystemStorage",
# },
# }
# Time zone (default: UTC)

View File

@ -66,6 +66,9 @@ class BaseTable(tables.Table):
if column.visible:
model = getattr(self.Meta, 'model')
accessor = column.accessor
if accessor.startswith('custom_field_data__'):
# Ignore custom field references
continue
prefetch_path = []
for field_name in accessor.split(accessor.SEPARATOR):
try:

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -23,14 +23,14 @@
},
"dependencies": {
"@mdi/font": "7.4.47",
"@tabler/core": "1.2.0",
"@tabler/core": "1.3.2",
"bootstrap": "5.3.6",
"clipboard": "2.0.11",
"flatpickr": "4.6.13",
"gridstack": "12.1.2",
"gridstack": "12.2.1",
"htmx.org": "2.0.4",
"query-string": "9.1.2",
"sass": "1.88.0",
"query-string": "9.2.0",
"sass": "1.89.1",
"tom-select": "2.4.3",
"typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13"

View File

@ -757,13 +757,13 @@
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
"@tabler/core@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@tabler/core/-/core-1.2.0.tgz#cc61cd60d0bc644709bd708f1dd917e760203b4e"
integrity sha512-Zrisg/pMi3c/X8AFbmwY6GNlWS/XPlW/jzt6grMar8ICOT7jO0weU9f/KCVgA49I1jMg2ev0uGxcpI5DP3CNdQ==
"@tabler/core@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@tabler/core/-/core-1.3.2.tgz#85e4a47b661bca4cd50e26039fc25c4bdb4aff34"
integrity sha512-QDVJbv48YJrahBLdxYkLi6NutQv4jGbkUWyzxE2NcNJ3s3GGpRx98JmbAoN92NZKNmf26vZdW6k2Q5haVKlS4A==
dependencies:
"@popperjs/core" "^2.11.8"
bootstrap "5.3.5"
bootstrap "5.3.6"
"@tanstack/react-virtual@^3.0.0-beta.60":
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"
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:
version "5.3.6"
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"
integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==
gridstack@12.1.2:
version "12.1.2"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.1.2.tgz#784f6d55873bb48fa9230c1284f769c9fbf785a8"
integrity sha512-IC1mkm5xonhAnftwIxsG+B3bawxC61ciKWEvX15ExpVQPbNVN7O9aZZhM7Y/eE4JaIR8PXrdkjd12gMnwNYRLQ==
gridstack@12.2.1:
version "12.2.1"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.2.1.tgz#0e82e3d9d11e5229388d73bd57f8ef1a0e7059c4"
integrity sha512-xU69tThmmVxgMHTuM/z3rIKiiGm0zW4tcB6yRcuwiOUUBiwb3tslzFOrUjWz+PwaxoAW+JChT4fqOLl+oKAxZA==
has-bigints@^1.0.1, has-bigints@^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"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
query-string@9.1.2:
version "9.1.2"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.1.2.tgz#1e4c6a17e2eaab7a282240cf716dec5e72c36cba"
integrity sha512-s3UlTyjxRux4KjwWaJsjh1Mp8zoCkSGKirbD9H89pEM9UOZsfpRZpdfzvsy2/mGlLfC3NnYVpy2gk7jXITHEtA==
query-string@9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.2.0.tgz#bf9909412689117865aac4e05c10422c4839828f"
integrity sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ==
dependencies:
decode-uri-component "^0.4.1"
filter-obj "^5.1.0"
@ -2665,10 +2660,10 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0"
is-regex "^1.1.4"
sass@1.88.0:
version "1.88.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.88.0.tgz#cd1495749bebd9e4aca86e93ee60b3904a107789"
integrity sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==
sass@1.89.1:
version "1.89.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.1.tgz#9281c52c85b4be54264d310fef63a811dfcfb9d9"
integrity sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==
dependencies:
chokidar "^4.0.0"
immutable "^5.0.2"

View File

@ -1,3 +1,3 @@
version: "4.3.1"
version: "4.3.2"
edition: "Community"
published: "2025-05-13"
published: "2025-06-05"

View File

@ -1,12 +1,10 @@
{% extends 'account/base.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block title %}{% trans "User Profile" %}{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="row">
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Account Details" %}</h2>
@ -64,12 +62,7 @@
{% if perms.core.view_objectchange %}
<div class="row">
<div class="col-md-12">
<div class="card">
<h2 class="card-header text-center">{% trans "Recent Activity" %}</h2>
<div class="table-responsive">
{% render_table changelog_table 'inc/table.html' %}
</div>
</div>
{% include 'users/inc/user_activity.html' with user=user table=changelog_table %}
</div>
</div>
{% endif %}

View File

@ -45,7 +45,7 @@
</div>
{% elif perms.dcim.add_cable %}
<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" %}
</button>
<ul class="dropdown-menu">

View File

@ -28,6 +28,10 @@
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}

View File

@ -65,7 +65,7 @@
{% trans "Not Connected" %}
{% if perms.dcim.add_cable %}
<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" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">

View File

@ -65,7 +65,7 @@
{% trans "Not Connected" %}
{% if perms.dcim.add_cable %}
<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" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">

View File

@ -308,7 +308,7 @@
{% trans "Services" %}
{% if perms.ipam.add_service %}
<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" %}
</a>
</div>

View File

@ -118,10 +118,6 @@
{% else %}
<div class="card-body text-muted">
{% trans "Not connected" %}
</div>
{% endif %}
{% if not object.mark_connected and not object.cable %}
<div class="card-footer">
{% 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">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Connect" %}

View File

@ -63,11 +63,15 @@
</h2>
<pre class="card-body" id="rendered_config">{{ rendered_config }}</pre>
</div>
{% else %}
{% elif error_message %}
<div class="alert alert-warning">
<h4 class="alert-title mb-1">{% trans "Error rendering template" %}</h4>
{% trans error_message %}
</div>
{% else %}
<div class="alert alert-warning">
<h4 class="alert-title mb-1">{% trans "Template output is empty" %}</h4>
</div>
{% endif %}
{% else %}
<div class="alert alert-info">

View 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>

View File

@ -1,14 +1,12 @@
{% extends 'generic/object.html' %}
{% load i18n %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% block title %}{% trans "User" %} {{ object.username }}{% endblock %}
{% block subtitle %}{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="row">
<div class="col-md-6">
<div class="card">
<h2 class="card-header">{% trans "User" %}</h2>
@ -74,12 +72,7 @@
{% if perms.core.view_objectchange %}
<div class="row">
<div class="col-md-12">
<div class="card">
<h2 class="text-center">{% trans "Recent Activity" %}</h2>
<div class="card-body table-responsive">
{% render_table changelog_table 'inc/table.html' %}
</div>
</div>
{% include 'users/inc/user_activity.html' with user=object table=changelog_table %}
</div>
</div>
{% endif %}

View File

@ -154,7 +154,7 @@
{% trans "Services" %}
{% if perms.ipam.add_service %}
<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" %}
</a>
</div>

View File

@ -66,3 +66,17 @@ class Migration(migrations.Migration):
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,
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-20 05:01+0000\n"
"POT-Creation-Date: 2025-06-10 05:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -70,15 +70,15 @@ msgstr ""
msgid "You have logged out."
msgstr ""
#: netbox/account/views.py:227
#: netbox/account/views.py:224
msgid "Your preferences have been updated."
msgstr ""
#: netbox/account/views.py:255
#: netbox/account/views.py:252
msgid "LDAP-authenticated user credentials cannot be changed within NetBox."
msgstr ""
#: netbox/account/views.py:270
#: netbox/account/views.py:267
msgid "Your password has been changed successfully."
msgstr ""
@ -104,7 +104,7 @@ msgstr ""
#: 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: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/vpn/choices.py:19 netbox/vpn/choices.py:280
#: netbox/wireless/choices.py:25
@ -165,8 +165,8 @@ msgstr ""
#: netbox/dcim/filtersets.py:215 netbox/dcim/filtersets.py:336
#: netbox/dcim/filtersets.py:467 netbox/dcim/filtersets.py:1075
#: netbox/dcim/filtersets.py:1397 netbox/dcim/filtersets.py:1495
#: netbox/dcim/filtersets.py:2160 netbox/dcim/filtersets.py:2403
#: netbox/dcim/filtersets.py:2461 netbox/ipam/filtersets.py:954
#: netbox/dcim/filtersets.py:2175 netbox/dcim/filtersets.py:2418
#: netbox/dcim/filtersets.py:2476 netbox/ipam/filtersets.py:954
#: netbox/virtualization/filtersets.py:139 netbox/vpn/filtersets.py:361
msgid "Region (ID)"
msgstr ""
@ -177,8 +177,8 @@ msgstr ""
#: netbox/dcim/filtersets.py:222 netbox/dcim/filtersets.py:343
#: netbox/dcim/filtersets.py:474 netbox/dcim/filtersets.py:1082
#: netbox/dcim/filtersets.py:1404 netbox/dcim/filtersets.py:1502
#: netbox/dcim/filtersets.py:2167 netbox/dcim/filtersets.py:2410
#: netbox/dcim/filtersets.py:2468 netbox/extras/filtersets.py:602
#: netbox/dcim/filtersets.py:2182 netbox/dcim/filtersets.py:2425
#: netbox/dcim/filtersets.py:2483 netbox/extras/filtersets.py:602
#: netbox/ipam/filtersets.py:961 netbox/virtualization/filtersets.py:146
#: netbox/vpn/filtersets.py:356
msgid "Region (slug)"
@ -189,8 +189,8 @@ msgstr ""
#: netbox/dcim/filtersets.py:131 netbox/dcim/filtersets.py:228
#: netbox/dcim/filtersets.py:349 netbox/dcim/filtersets.py:480
#: netbox/dcim/filtersets.py:1088 netbox/dcim/filtersets.py:1410
#: netbox/dcim/filtersets.py:1508 netbox/dcim/filtersets.py:2173
#: netbox/dcim/filtersets.py:2416 netbox/dcim/filtersets.py:2474
#: netbox/dcim/filtersets.py:1508 netbox/dcim/filtersets.py:2188
#: netbox/dcim/filtersets.py:2431 netbox/dcim/filtersets.py:2489
#: netbox/ipam/filtersets.py:239 netbox/ipam/filtersets.py:967
#: netbox/virtualization/filtersets.py:152
msgid "Site group (ID)"
@ -201,8 +201,8 @@ msgstr ""
#: netbox/dcim/filtersets.py:138 netbox/dcim/filtersets.py:235
#: netbox/dcim/filtersets.py:356 netbox/dcim/filtersets.py:487
#: netbox/dcim/filtersets.py:1095 netbox/dcim/filtersets.py:1417
#: netbox/dcim/filtersets.py:1515 netbox/dcim/filtersets.py:2180
#: netbox/dcim/filtersets.py:2423 netbox/dcim/filtersets.py:2481
#: netbox/dcim/filtersets.py:1515 netbox/dcim/filtersets.py:2195
#: netbox/dcim/filtersets.py:2438 netbox/dcim/filtersets.py:2496
#: netbox/extras/filtersets.py:608 netbox/ipam/filtersets.py:246
#: netbox/ipam/filtersets.py:974 netbox/virtualization/filtersets.py:159
msgid "Site group (slug)"
@ -211,7 +211,7 @@ msgstr ""
#: netbox/circuits/filtersets.py:62 netbox/circuits/forms/filtersets.py:59
#: netbox/circuits/forms/filtersets.py:183
#: 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:935 netbox/dcim/forms/bulk_import.py:134
#: 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/filtersets.py:360 netbox/dcim/filtersets.py:455
#: netbox/dcim/filtersets.py:1099 netbox/dcim/filtersets.py:1422
#: netbox/dcim/filtersets.py:1520 netbox/dcim/filtersets.py:2185
#: netbox/dcim/filtersets.py:2427 netbox/dcim/filtersets.py:2486
#: netbox/dcim/filtersets.py:1520 netbox/dcim/filtersets.py:2200
#: netbox/dcim/filtersets.py:2442 netbox/dcim/filtersets.py:2501
#: netbox/ipam/filtersets.py:251 netbox/ipam/filtersets.py:978
#: netbox/virtualization/filtersets.py:163 netbox/vpn/filtersets.py:371
msgid "Site (ID)"
@ -329,7 +329,7 @@ msgstr ""
#: 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: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)"
msgstr ""
@ -341,7 +341,7 @@ msgstr ""
#: netbox/circuits/filtersets.py:537 netbox/core/filtersets.py:81
#: netbox/core/filtersets.py:140 netbox/core/filtersets.py:177
#: 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:136 netbox/extras/filtersets.py:185
#: 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:263
#: 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/circuitgroupassignment.html:30
#: netbox/templates/circuits/circuittermination.html:19
@ -503,6 +503,7 @@ msgstr ""
#: netbox/templates/circuits/circuittype.html:26
#: netbox/templates/circuits/inc/circuit_termination_fields.html:83
#: netbox/templates/circuits/provider.html:33
#: netbox/templates/circuits/provideraccount.html:32
#: netbox/templates/circuits/providernetwork.html:32
#: netbox/templates/circuits/virtualcircuit.html:56
#: netbox/templates/circuits/virtualcircuittermination.html:68
@ -624,7 +625,7 @@ msgstr ""
#: netbox/circuits/forms/model_forms.py:77
#: netbox/circuits/forms/model_forms.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:101
#: netbox/circuits/tables/virtual_circuits.py:46
@ -678,7 +679,7 @@ msgstr ""
#: netbox/circuits/forms/bulk_import.py:221
#: netbox/circuits/forms/filtersets.py:138
#: 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/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
@ -704,7 +705,7 @@ msgstr ""
#: 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/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/virtualcircuittermination.html:64
#: netbox/templates/core/datasource.html:38 netbox/templates/dcim/cable.html:15
@ -971,7 +972,7 @@ msgstr ""
#: 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: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:109
#: netbox/virtualization/forms/filtersets.py:204
@ -1058,7 +1059,7 @@ msgstr ""
#: netbox/circuits/forms/bulk_edit.py:289
#: netbox/circuits/forms/bulk_import.py:188
#: 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/dcim/device.html:139
#: netbox/templates/dcim/virtualchassis.html:68
@ -1240,7 +1241,7 @@ msgstr ""
#: netbox/circuits/forms/filtersets.py:130
#: netbox/circuits/forms/filtersets.py:188
#: 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:790 netbox/dcim/forms/bulk_edit.py:944
#: 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:169
#: 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_import.py:96 netbox/dcim/forms/filtersets.py:75
#: 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/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/tables/sites.py:95 netbox/extras/forms/filtersets.py:544
#: netbox/ipam/filtersets.py:994 netbox/ipam/forms/bulk_edit.py:488
@ -1695,7 +1696,7 @@ msgstr ""
msgid "virtual circuit terminations"
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:67
#: netbox/circuits/tables/providers.py:97
@ -1820,14 +1821,14 @@ msgstr ""
msgid "Name"
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:77
#: netbox/circuits/tables/virtual_circuits.py:27
#: netbox/netbox/navigation/menu.py:275 netbox/netbox/navigation/menu.py:279
#: netbox/netbox/navigation/menu.py:281
#: netbox/templates/circuits/provider.html:57
#: netbox/templates/circuits/provideraccount.html:44
#: netbox/templates/circuits/provideraccount.html:48
#: netbox/templates/circuits/providernetwork.html:50
msgid "Circuits"
msgstr ""
@ -1905,12 +1906,12 @@ msgstr ""
msgid "Termination Point"
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
msgid "Site Group"
msgstr ""
#: netbox/circuits/tables/circuits.py:148
#: netbox/circuits/tables/circuits.py:149
#: netbox/templates/circuits/providernetwork.html:17
#: netbox/templates/circuits/virtualcircuit.html:27
#: netbox/templates/circuits/virtualcircuittermination.html:30
@ -2167,7 +2168,7 @@ msgid "Local"
msgstr ""
#: 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
msgid "Username"
msgstr ""
@ -2342,7 +2343,7 @@ msgstr ""
#: netbox/templates/extras/savedfilter.html:21
#: netbox/templates/extras/tableconfig.html:29
#: 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/forms/filtersets.py:84 netbox/users/forms/filtersets.py:125
#: netbox/users/forms/model_forms.py:155 netbox/users/forms/model_forms.py:192
@ -2753,7 +2754,7 @@ msgid "Deletion is prevented by a protection rule: {message}"
msgstr ""
#: 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"
msgstr ""
@ -2762,7 +2763,7 @@ msgstr ""
#: 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: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/extras/eventrule.html:78
#: netbox/templates/extras/journalentry.html:18
@ -2800,7 +2801,7 @@ msgstr ""
#: 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/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/utilities/forms/forms.py:73 netbox/wireless/tables/wirelesslink.py:16
msgid "ID"
@ -3440,7 +3441,7 @@ msgstr ""
#: netbox/dcim/filtersets.py:542 netbox/dcim/filtersets.py:707
#: netbox/dcim/filtersets.py:911 netbox/dcim/filtersets.py:985
#: netbox/dcim/filtersets.py:1025 netbox/dcim/filtersets.py:1368
#: netbox/dcim/filtersets.py:2093
#: netbox/dcim/filtersets.py:2108
msgid "Manufacturer (ID)"
msgstr ""
@ -3448,7 +3449,7 @@ msgstr ""
#: netbox/dcim/filtersets.py:548 netbox/dcim/filtersets.py:713
#: netbox/dcim/filtersets.py:917 netbox/dcim/filtersets.py:991
#: netbox/dcim/filtersets.py:1031 netbox/dcim/filtersets.py:1374
#: netbox/dcim/filtersets.py:2099
#: netbox/dcim/filtersets.py:2114
msgid "Manufacturer (slug)"
msgstr ""
@ -3461,14 +3462,14 @@ msgid "Rack type (ID)"
msgstr ""
#: 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:998 netbox/virtualization/filtersets.py:177
msgid "Role (ID)"
msgstr ""
#: 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/ipam/filtersets.py:494 netbox/ipam/filtersets.py:1004
#: netbox/virtualization/filtersets.py:184
@ -3477,7 +3478,7 @@ msgstr ""
#: netbox/dcim/filtersets.py:450 netbox/dcim/filtersets.py:1123
#: netbox/dcim/filtersets.py:1444 netbox/dcim/filtersets.py:1542
#: netbox/dcim/filtersets.py:2501
#: netbox/dcim/filtersets.py:2516
msgid "Rack (ID)"
msgstr ""
@ -3575,7 +3576,7 @@ msgstr ""
msgid "Power port (ID)"
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)"
msgstr ""
@ -3610,8 +3611,8 @@ msgid "Platform (slug)"
msgstr ""
#: netbox/dcim/filtersets.py:1105 netbox/dcim/filtersets.py:1428
#: netbox/dcim/filtersets.py:1526 netbox/dcim/filtersets.py:2191
#: netbox/dcim/filtersets.py:2433 netbox/dcim/filtersets.py:2492
#: netbox/dcim/filtersets.py:1526 netbox/dcim/filtersets.py:2206
#: netbox/dcim/filtersets.py:2448 netbox/dcim/filtersets.py:2507
msgid "Site name (slug)"
msgstr ""
@ -3925,44 +3926,44 @@ msgstr ""
msgid "Virtual circuit termination (ID)"
msgstr ""
#: netbox/dcim/filtersets.py:2056
#: netbox/dcim/filtersets.py:2071
msgid "Parent module bay (ID)"
msgstr ""
#: netbox/dcim/filtersets.py:2061
#: netbox/dcim/filtersets.py:2076
msgid "Installed module (ID)"
msgstr ""
#: netbox/dcim/filtersets.py:2072
#: netbox/dcim/filtersets.py:2087
msgid "Installed device (ID)"
msgstr ""
#: netbox/dcim/filtersets.py:2078
#: netbox/dcim/filtersets.py:2093
msgid "Installed device (name)"
msgstr ""
#: netbox/dcim/filtersets.py:2148
#: netbox/dcim/filtersets.py:2163
msgid "Master (ID)"
msgstr ""
#: netbox/dcim/filtersets.py:2154
#: netbox/dcim/filtersets.py:2169
msgid "Master (name)"
msgstr ""
#: netbox/dcim/filtersets.py:2196 netbox/tenancy/filtersets.py:250
#: netbox/dcim/filtersets.py:2211 netbox/tenancy/filtersets.py:250
msgid "Tenant (ID)"
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
msgid "Tenant (slug)"
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"
msgstr ""
#: netbox/dcim/filtersets.py:2496
#: netbox/dcim/filtersets.py:2511
msgid "Power panel (ID)"
msgstr ""
@ -7375,7 +7376,7 @@ msgstr ""
#: netbox/templates/ipam/ipaddress_bulk_add.html:15
#: netbox/templates/ipam/service.html:42
#: netbox/templates/virtualization/vminterface.html:107
#: netbox/vpn/tables/tunnels.py:98
#: netbox/vpn/tables/tunnels.py:99
msgid "IP Addresses"
msgstr ""
@ -12057,12 +12058,12 @@ msgstr ""
msgid "No {model_name} found"
msgstr ""
#: netbox/netbox/tables/tables.py:278
#: netbox/netbox/tables/tables.py:281
#: netbox/templates/generic/bulk_import.html:117
msgid "Field"
msgstr ""
#: netbox/netbox/tables/tables.py:281
#: netbox/netbox/tables/tables.py:284
msgid "Value"
msgstr ""
@ -12275,44 +12276,44 @@ msgstr ""
msgid "None found"
msgstr ""
#: netbox/templates/account/profile.html:6
#: netbox/templates/account/profile.html:4
msgid "User Profile"
msgstr ""
#: netbox/templates/account/profile.html:12
#: netbox/templates/account/profile.html:10
msgid "Account Details"
msgstr ""
#: netbox/templates/account/profile.html:29
#: netbox/templates/tenancy/contact.html:53 netbox/templates/users/user.html:25
#: netbox/templates/account/profile.html:27
#: netbox/templates/tenancy/contact.html:53 netbox/templates/users/user.html:23
#: netbox/tenancy/forms/bulk_edit.py:116
msgid "Email"
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"
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"
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"
msgstr ""
#: netbox/templates/account/profile.html:45
#: netbox/templates/inc/user_menu.html:31 netbox/templates/users/user.html:41
#: netbox/templates/account/profile.html:43
#: netbox/templates/inc/user_menu.html:31 netbox/templates/users/user.html:39
msgid "Staff"
msgstr ""
#: netbox/templates/account/profile.html:53
#: netbox/templates/account/profile.html:51
#: netbox/templates/users/objectpermission.html:82
#: netbox/templates/users/user.html:53
#: netbox/templates/users/user.html:51
msgid "Assigned Groups"
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:26
#: netbox/templates/circuits/circuittermination.html:34
@ -12338,14 +12339,10 @@ msgstr ""
#: netbox/templates/users/group.html:34 netbox/templates/users/group.html:44
#: netbox/templates/users/objectpermission.html:77
#: 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"
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_list.html:6
msgid "My API Tokens"
@ -12557,7 +12554,7 @@ msgstr ""
#: netbox/templates/dcim/frontport.html:102
#: netbox/templates/dcim/interface.html:237
#: 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:86
#: netbox/templates/dcim/powerport.html:73
@ -14065,7 +14062,11 @@ msgstr ""
msgid "Error rendering template"
msgstr ""
#: netbox/templates/extras/object_render_config.html:74
#: netbox/templates/extras/object_render_config.html:73
msgid "Template output is empty"
msgstr ""
#: netbox/templates/extras/object_render_config.html:78
msgid "No configuration template has been assigned."
msgstr ""
@ -14845,10 +14846,18 @@ msgstr ""
msgid "Add Tenant Group"
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"
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:14
#: netbox/users/forms/filtersets.py:66
@ -15072,7 +15081,7 @@ msgstr ""
#: netbox/templates/vpn/tunneltermination.html:35
#: 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/tables/tunnels.py:101
#: netbox/vpn/tables/tunnels.py:102
msgid "Outside IP"
msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -75,8 +75,9 @@ class UserView(generic.ObjectView):
template_name = 'users/user.html'
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.orderable = False
changelog_table.configure(request)
return {

View File

@ -49,11 +49,27 @@ class DataFileLoader(BaseLoader):
# Utility functions
#
def render_jinja2(template_code, context, environment_params=None):
def render_jinja2(template_code, context, environment_params=None, data_file=None):
"""
Render a Jinja2 template with the provided context. Return the rendered content.
"""
environment_params = environment_params or {}
if 'loader' not in environment_params:
if data_file:
loader = DataFileLoader(data_file.source)
loader.cache_templates({
data_file.path: template_code
})
else:
loader = BaseLoader()
environment_params['loader'] = loader
environment = SandboxedEnvironment(**environment_params)
environment.filters.update(get_config().JINJA2_FILTERS)
return environment.from_string(source=template_code).render(**context)
if data_file:
template = environment.get_template(data_file.path)
else:
template = environment.from_string(source=template_code)
return template.render(**context)

View File

@ -1,4 +1,5 @@
import django.db.models.deletion
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
@ -43,3 +44,20 @@ class Migration(migrations.Migration):
# Copy over existing site assignments
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,
}

View File

@ -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,
}

View File

@ -1,4 +1,6 @@
import django.db.models.deletion
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
@ -50,3 +52,43 @@ class Migration(migrations.Migration):
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,
}

View File

@ -73,7 +73,7 @@ class TunnelTable(TenancyColumnsMixin, NetBoxTable):
default_columns = ('pk', 'name', 'group', 'status', 'encapsulation', 'tenant', 'terminations_count')
class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
class TunnelTerminationTable(NetBoxTable):
tunnel = tables.Column(
verbose_name=_('Tunnel'),
linkify=True
@ -89,7 +89,8 @@ class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
)
termination = tables.Column(
verbose_name=_('Tunnel interface'),
linkify=True
linkify=True,
orderable=False,
)
ip_addresses = columns.ManyToManyColumn(
accessor=tables.A('termination__ip_addresses'),

View File

@ -0,0 +1,23 @@
from django.test import RequestFactory, tag, TestCase
from vpn.models import TunnelTermination
from vpn.tables import TunnelTerminationTable
@tag('regression')
class TunnelTerminationTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
terminations = TunnelTermination.objects.all()
fake_request = RequestFactory().get("/")
disallowed = {'actions'}
orderable_columns = [
column.name for column in TunnelTerminationTable(terminations).columns
if column.orderable and column.name not in disallowed
]
for col in orderable_columns:
for dir in ('-', ''):
table = TunnelTerminationTable(terminations)
table.order_by = f'{dir}{col}'
table.as_html(fake_request)

View File

@ -206,7 +206,7 @@ class WirelessLink(WirelessAuthenticationBase, DistanceMixin, PrimaryModel):
})
if self.interface_b.type not in WIRELESS_IFACE_TYPES:
raise ValidationError({
'interface_a': _(
'interface_b': _(
"{type} is not a wireless interface."
).format(type=self.interface_b.get_type_display())
})

View File

@ -1,6 +1,40 @@
# See PEP 518 for the spec of this file
# https://www.python.org/dev/peps/pep-0518/
[project]
name = "netbox"
version = "4.3.1"
requires-python = ">=3.10"
authors = [
{ name = "NetBox Community" }
]
maintainers = [
{ name = "NetBox Community" }
]
description = "The premier source of truth powering network automation."
readme = "README.md"
license = "Apache-2.0"
license-files = ["LICENSE.txt"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: Django",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"Natural Language :: English",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
[project.urls]
Homepage = "https://netboxlabs.com/products/netbox/"
Documentation = "https://netboxlabs.com/docs/netbox/"
Source = "https://github.com/netbox-community/netbox"
Issues = "https://github.com/netbox-community/netbox/issues"
[tool.black]
line-length = 120
target_version = ['py310', 'py311', 'py312']

Some files were not shown because too many files have changed in this diff Show More