Compare commits
77 Commits
8158be3139
...
094597e2f6
Author | SHA1 | Date |
---|---|---|
Arthur Hanson | 094597e2f6 | |
Tobias Genannt | 5af3c659a5 | |
Arthur Hanson | 4923025fec | |
Arthur Hanson | ded2fe9471 | |
Jeremy Stretch | e05ca710ae | |
Jeremy Stretch | c32dff5649 | |
Jeremy Stretch | 8364e632b7 | |
Jeremy Stretch | c43b929542 | |
Jeremy Stretch | e3c418263e | |
Jeremy Stretch | 46bd62fdc9 | |
Jeremy Stretch | 0b0dab42eb | |
Jeremy Stretch | d115601da3 | |
Jeremy Stretch | a61e20849b | |
Arthur Hanson | 1eca1c3d17 | |
transifex-integration[bot] | 5d95d49268 | |
Jeremy Stretch | 6b8bfe9947 | |
Jeremy Stretch | e87877b6ea | |
Jeremy Stretch | ebe504c825 | |
Markku Leiniö | b6e38b2ebe | |
Arthur Hanson | 90d0104359 | |
Arthur | 781d932b2a | |
Jeremy Stretch | 781409b5ae | |
Arthur Hanson | 88facbafbb | |
Jeremy Stretch | c9de3128ca | |
Arthur | 94c31622ac | |
Jeremy Stretch | 3d3c1c315b | |
Jeremy Stretch | f42d0336c2 | |
Jeremy Stretch | db87fe96b7 | |
Jeremy Stretch | 0f0ab1a3be | |
Jeremy Stretch | 824d66a54c | |
Jeremy Stretch | 3551f3e021 | |
Florian Derler | 1a1300716c | |
Arthur | 4b83b5d0e1 | |
Jeremy Stretch | 174865b9aa | |
Jeremy Stretch | c9bd59ab02 | |
Jeremy Stretch | 1efd80954e | |
Jeff Gehlbach | f4c8f5f5b6 | |
Jeremy Stretch | 480b36d65e | |
Jeremy Stretch | d0f0782bc0 | |
Jeremy Stretch | 19fe5ef25c | |
Arthur | e303ccfd12 | |
Arthur Hanson | 928014c766 | |
Jeremy Stretch | 75d6bfe42f | |
Jeremy Stretch | b5bb732031 | |
Jeremy Stretch | 95cc29d898 | |
Jeremy Stretch | 157df069e8 | |
Jeremy Stretch | 77a4300888 | |
Arthur Hanson | b8cedfcc08 | |
Javier de la Puente | c5ae89ad03 | |
Markku Leiniö | 4284028bb0 | |
Jeremy Stretch | 3c3943c809 | |
Jeremy Stretch | 17e8773c8c | |
Arthur Hanson | f47b158863 | |
Wrage, Florian | f7e4fe2a9c | |
Julio Oliveira at Encora | 5098422f68 | |
Julio-Oliveira-Encora | d7922a68d8 | |
Arthur | 54c6d95fbb | |
Jeremy Stretch | b7668fbfc3 | |
Jeremy Stretch | 1c76034069 | |
Jeremy Stretch | ad0e476788 | |
Jeremy Stretch | e10f5ec3b4 | |
Jeremy Stretch | 48a3f3cb70 | |
muTeREdO | 238fa704b9 | |
padthaitofuhot | 3b3511c43c | |
Markku Leiniö | da13fa5569 | |
Markku Leiniö | 5b50920c61 | |
Jeremy Stretch | d9a7b4ee0e | |
Jeremy Stretch | 282dc7a705 | |
Jeremy Stretch | e1753c0f9b | |
Jeremy Stretch | 0e94f2e05d | |
Jeremy Stretch | 1c370f45d0 | |
Jeremy Stretch | 24e2fc253a | |
Arthur | fca23c6419 | |
Abhimanyu Saharan | e4984d2883 | |
Iain Buclaw | 6030c521f4 | |
Arthur | 83dad6f771 | |
Arthur | 80802a4357 |
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: 🐛 Bug Report
|
||||
description: Report a reproducible bug in the current release of NetBox
|
||||
labels: ["type: bug", "needs triage"]
|
||||
labels: ["type: bug", "status: needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
@ -26,7 +26,7 @@ body:
|
|||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.7.4
|
||||
placeholder: v3.7.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: 📖 Documentation Change
|
||||
description: Suggest an addition or modification to the NetBox documentation
|
||||
labels: ["type: documentation", "needs triage"]
|
||||
labels: ["type: documentation", "status: needs triage"]
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: ✨ Feature Request
|
||||
description: Propose a new NetBox feature or enhancement
|
||||
labels: ["type: feature", "needs triage"]
|
||||
labels: ["type: feature", "status: needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
@ -14,7 +14,7 @@ body:
|
|||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.7.4
|
||||
placeholder: v3.7.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
@ -13,8 +13,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: pozil/auto-assign-issue@v1
|
||||
if: "contains(github.event.issue.labels.*.name, 'type: bug') || contains(github.event.issue.labels.*.name, 'type: feature')"
|
||||
if: "contains(github.event.issue.labels.*.name, 'status: needs triage')"
|
||||
with:
|
||||
assignees: abhi1693,arthanson,DanSheps,jeffgdotorg,jeremystretch
|
||||
# Weighted assignments
|
||||
assignees: arthanson:3, jeffgdotorg:3, jeremystretch:3, abhi1693, DanSheps
|
||||
numOfAssignee: 1
|
||||
abortIfPreviousAssignees: true
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
{% block site_meta %}
|
||||
{{ super() }}
|
||||
{# Disable search indexing unless we're building for ReadTheDocs (see #10496) #}
|
||||
{% if page.canonical_url != 'https://docs.netbox.dev/' %}
|
||||
{# Disable search indexing unless we're building for ReadTheDocs #}
|
||||
{% if not config.extra.readthedocs %}
|
||||
<meta name="robots" content="noindex">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
# Date & Time Parameters
|
||||
|
||||
## TIME_ZONE
|
||||
|
||||
Default: UTC
|
||||
|
||||
The time zone NetBox will use when dealing with dates and times. It is recommended to use UTC time unless you have a specific need to use a local time zone. Please see the [list of available time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
|
||||
|
||||
## Date and Time Formatting
|
||||
|
||||
You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date). Default formats are listed below.
|
||||
|
||||
!!! note
|
||||
These system defaults will be overridden by a user's selected language/locale when [localization](./system.md#enable_localization) is enabled.
|
||||
|
||||
```python
|
||||
DATE_FORMAT = 'N j, Y' # June 26, 2016
|
||||
SHORT_DATE_FORMAT = 'Y-m-d' # 2016-06-26
|
||||
TIME_FORMAT = 'g:i a' # 1:23 p.m.
|
||||
SHORT_TIME_FORMAT = 'H:i:s' # 13:23:00
|
||||
DATETIME_FORMAT = 'N j, Y g:i a' # June 26, 2016 1:23 p.m.
|
||||
SHORT_DATETIME_FORMAT = 'Y-m-d H:i' # 2016-06-26 13:23
|
||||
```
|
|
@ -181,6 +181,30 @@ The view name or URL to which a user is redirected after logging out.
|
|||
|
||||
---
|
||||
|
||||
## SECURE_HSTS_INCLUDE_SUBDOMAINS
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## SECURE_HSTS_PRELOAD
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## SECURE_HSTS_SECONDS
|
||||
|
||||
Default: 0
|
||||
|
||||
If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict Transport Security (HSTS) header on all responses that do not already have it. This will instruct the browser that the website must be accessed via HTTPS, blocking any HTTP request.
|
||||
|
||||
---
|
||||
|
||||
## SECURE_SSL_REDIRECT
|
||||
|
||||
Default: False
|
||||
|
|
|
@ -16,10 +16,7 @@ BASE_PATH = 'netbox/'
|
|||
|
||||
Default: `en-us` (US English)
|
||||
|
||||
Defines the default preferred language/locale for requests that do not specify one. This is used to alter e.g. the display of dates and numbers to fit the user's locale. See [this list](http://www.i18nguy.com/unicode/language-identifiers.html) of standard language codes. (This parameter maps to Django's [`LANGUAGE_CODE`](https://docs.djangoproject.com/en/stable/ref/settings/#language-code) internal setting.)
|
||||
|
||||
!!! note
|
||||
Altering this parameter will *not* change the language used in NetBox. We hope to provide translation support in a future NetBox release.
|
||||
Defines the default preferred language/locale for requests that do not specify one. (This parameter maps to Django's [`LANGUAGE_CODE`](https://docs.djangoproject.com/en/stable/ref/settings/#language-code) internal setting.)
|
||||
|
||||
---
|
||||
|
||||
|
@ -65,14 +62,6 @@ Email is sent from NetBox only for critical events or if configured for [logging
|
|||
|
||||
---
|
||||
|
||||
## ENABLE_LOCALIZATION
|
||||
|
||||
Default: False
|
||||
|
||||
Determines if localization features are enabled or not. This should only be enabled for development or testing purposes as netbox is not yet fully localized. Turning this on will localize numeric and date formats (overriding any configured [system defaults](./date-time.md#date-and-time-formatting)) based on the browser locale as well as translate certain strings from third party modules.
|
||||
|
||||
---
|
||||
|
||||
## HTTP_PROXIES
|
||||
|
||||
Default: None
|
||||
|
@ -203,3 +192,9 @@ A dictionary of configuration parameters for the storage backend configured as `
|
|||
If `STORAGE_BACKEND` is not defined, this setting will be ignored.
|
||||
|
||||
---
|
||||
|
||||
## TIME_ZONE
|
||||
|
||||
Default: UTC
|
||||
|
||||
The time zone NetBox will use when dealing with dates and times. It is recommended to use UTC time unless you have a specific need to use a local time zone. Please see the [list of available time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
|
||||
|
|
|
@ -85,13 +85,19 @@ Each model generally has two views associated with it: a list view and a detail
|
|||
* `/api/dcim/devices/` - List existing devices or create a new device
|
||||
* `/api/dcim/devices/123/` - Retrieve, update, or delete the device with ID 123
|
||||
|
||||
Lists of objects can be filtered using a set of query parameters. For example, to find all interfaces belonging to the device with ID 123:
|
||||
Lists of objects can be filtered and ordered using a set of query parameters. For example, to find all interfaces belonging to the device with ID 123:
|
||||
|
||||
```
|
||||
GET /api/dcim/interfaces/?device_id=123
|
||||
```
|
||||
|
||||
See the [filtering documentation](../reference/filtering.md) for more details.
|
||||
An optional `ordering` parameter can be used to define how to sort the results. Building off the previous example, to sort all the interfaces in reverse order of creation (newest to oldest) for a device with ID 123:
|
||||
|
||||
```
|
||||
GET /api/dcim/interfaces/?device_id=123&ordering=-created
|
||||
```
|
||||
|
||||
See the [filtering documentation](../reference/filtering.md) for more details on topics related to filtering, ordering and lookup expressions.
|
||||
|
||||
## Serialization
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
!!! tip "Plugins Development Tutorial"
|
||||
Just getting started with plugins? Check out our [**NetBox Plugin Tutorial**](https://github.com/netbox-community/netbox-plugin-tutorial) on GitHub! This in-depth guide will walk you through the process of creating an entire plugin from scratch. It even includes a companion [demo plugin repo](https://github.com/netbox-community/netbox-plugin-demo) to ensure you can jump in at any step along the way. This will get you up and running with plugins in no time!
|
||||
|
||||
!!! tip "Plugin Certification Program"
|
||||
NetBox Labs offers a [**Plugin Certification Program**](https://github.com/netbox-community/netbox/wiki/Plugin-Certification-Program) for plugin developers interested in establishing a co-maintainer relationship. The program aims to assure ongoing compatibility, maintainability, and commercial supportability of key plugins.
|
||||
|
||||
NetBox can be extended to support additional data models and functionality through the use of plugins. A plugin is essentially a self-contained [Django app](https://docs.djangoproject.com/en/stable/) which gets installed alongside NetBox to provide custom functionality. Multiple plugins can be installed in a single NetBox instance, and each plugin can be enabled and configured independently.
|
||||
|
||||
!!! info "Django Development"
|
||||
|
|
|
@ -157,7 +157,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`:
|
||||
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:
|
||||
|
||||
```python
|
||||
from dcim.models import Site
|
||||
|
@ -173,6 +173,16 @@ class MyView(generic.ObjectView):
|
|||
badge=lambda obj: Stuff.objects.filter(site=obj).count(),
|
||||
permission='myplugin.view_stuff'
|
||||
)
|
||||
|
||||
def get(self, request, pk):
|
||||
...
|
||||
return render(
|
||||
request,
|
||||
"myplugin/mytabview.html",
|
||||
context={
|
||||
"tab": self.tab,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
::: utilities.views.register_model_view
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
Plugins are packaged [Django](https://docs.djangoproject.com/) apps that can be installed alongside NetBox to provide custom functionality not present in the core application. Plugins can introduce their own models and views, but cannot interfere with existing components. A NetBox user may opt to install plugins provided by the community or build his or her own.
|
||||
|
||||
Please see the documented instructions for [installing a plugin](./installation.md) to get started.
|
||||
|
||||
## Capabilities
|
||||
|
||||
The NetBox plugin architecture allows for the following:
|
||||
|
@ -23,122 +25,3 @@ Either by policy or by technical limitation, the interaction of plugins with Net
|
|||
* **Override core templates.** Plugins can inject additional content where supported, but may not manipulate or remove core content.
|
||||
* **Modify core settings.** A configuration registry is provided for plugins, however they cannot alter or delete the core configuration.
|
||||
* **Disable core components.** Plugins are not permitted to disable or hide core NetBox components.
|
||||
|
||||
## Installing Plugins
|
||||
|
||||
The instructions below detail the process for installing and enabling a NetBox plugin.
|
||||
|
||||
### Install Package
|
||||
|
||||
Download and install the plugin package per its installation instructions. Plugins published via PyPI are typically installed using pip. Be sure to install the plugin within NetBox's virtual environment.
|
||||
|
||||
```no-highlight
|
||||
$ source /opt/netbox/venv/bin/activate
|
||||
(venv) $ pip install <package>
|
||||
```
|
||||
|
||||
Alternatively, you may wish to install the plugin manually by running `python setup.py install`. If you are developing a plugin and want to install it only temporarily, run `python setup.py develop` instead.
|
||||
|
||||
### Enable the Plugin
|
||||
|
||||
In `configuration.py`, add the plugin's name to the `PLUGINS` list:
|
||||
|
||||
```python
|
||||
PLUGINS = [
|
||||
'plugin_name',
|
||||
]
|
||||
```
|
||||
|
||||
### Configure Plugin
|
||||
|
||||
If the plugin requires any configuration, define it in `configuration.py` under the `PLUGINS_CONFIG` parameter. The available configuration parameters should be detailed in the plugin's README file.
|
||||
|
||||
```no-highlight
|
||||
PLUGINS_CONFIG = {
|
||||
'plugin_name': {
|
||||
'foo': 'bar',
|
||||
'buzz': 'bazz'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Run Database Migrations
|
||||
|
||||
If the plugin introduces new database models, run the provided schema migrations:
|
||||
|
||||
```no-highlight
|
||||
(venv) $ cd /opt/netbox/netbox/
|
||||
(venv) $ python3 manage.py migrate
|
||||
```
|
||||
|
||||
### Collect Static Files
|
||||
|
||||
Plugins may package static files to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command:
|
||||
|
||||
```no-highlight
|
||||
(venv) $ cd /opt/netbox/netbox/
|
||||
(venv) $ python3 manage.py collectstatic
|
||||
```
|
||||
|
||||
### Restart WSGI Service
|
||||
|
||||
Restart the WSGI service and RQ workers to load the new plugin:
|
||||
|
||||
```no-highlight
|
||||
# sudo systemctl restart netbox netbox-rq
|
||||
```
|
||||
|
||||
## Removing Plugins
|
||||
|
||||
Follow these steps to completely remove a plugin.
|
||||
|
||||
### Update Configuration
|
||||
|
||||
Remove the plugin from the `PLUGINS` list in `configuration.py`. Also remove any relevant configuration parameters from `PLUGINS_CONFIG`.
|
||||
|
||||
### Remove the Python Package
|
||||
|
||||
Use `pip` to remove the installed plugin:
|
||||
|
||||
```no-highlight
|
||||
$ source /opt/netbox/venv/bin/activate
|
||||
(venv) $ pip uninstall <package>
|
||||
```
|
||||
|
||||
### Restart WSGI Service
|
||||
|
||||
Restart the WSGI service:
|
||||
|
||||
```no-highlight
|
||||
# sudo systemctl restart netbox
|
||||
```
|
||||
|
||||
### Drop Database Tables
|
||||
|
||||
!!! note
|
||||
This step is necessary only for plugin which have created one or more database tables (generally through the introduction of new models). Check your plugin's documentation if unsure.
|
||||
|
||||
Enter the PostgreSQL database shell to determine if the plugin has created any SQL tables. Substitute `pluginname` in the example below for the name of the plugin being removed. (You can also run the `\dt` command without a pattern to list _all_ tables.)
|
||||
|
||||
```no-highlight
|
||||
netbox=> \dt pluginname_*
|
||||
List of relations
|
||||
List of relations
|
||||
Schema | Name | Type | Owner
|
||||
--------+----------------+-------+--------
|
||||
public | pluginname_foo | table | netbox
|
||||
public | pluginname_bar | table | netbox
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Exercise extreme caution when removing tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.
|
||||
|
||||
Drop each of the listed tables to remove it from the database:
|
||||
|
||||
```no-highlight
|
||||
netbox=> DROP TABLE pluginname_foo;
|
||||
DROP TABLE
|
||||
netbox=> DROP TABLE pluginname_bar;
|
||||
DROP TABLE
|
||||
```
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
# Installing a Plugin
|
||||
|
||||
!!! warning
|
||||
The instructions below detail the general process for installing and configuring a NetBox plugin. However, each plugin is different and may require additional tasks or modifications to the steps below. Always consult the documentation for a specific plugin **before** attempting to install it.
|
||||
|
||||
## Install the Python Package
|
||||
|
||||
Download and install the plugin's Python package per its installation instructions. Plugins published via PyPI are typically installed using the [`pip`](https://packaging.python.org/en/latest/tutorials/installing-packages/) command line utility. Be sure to install the plugin within NetBox's virtual environment.
|
||||
|
||||
```no-highlight
|
||||
$ source /opt/netbox/venv/bin/activate
|
||||
(venv) $ pip install <package>
|
||||
```
|
||||
|
||||
Alternatively, you may wish to install the plugin manually by running `python setup.py install`. If you are developing a plugin and want to install it only temporarily, run `python setup.py develop` instead.
|
||||
|
||||
## Enable the Plugin
|
||||
|
||||
In `configuration.py`, add the plugin's name to the `PLUGINS` list:
|
||||
|
||||
```python
|
||||
PLUGINS = [
|
||||
# ...
|
||||
'plugin_name',
|
||||
]
|
||||
```
|
||||
|
||||
## Configure the Plugin
|
||||
|
||||
If the plugin requires any configuration, define it in `configuration.py` under the `PLUGINS_CONFIG` parameter. The available configuration parameters should be detailed in the plugin's `README` file or other documentation.
|
||||
|
||||
```no-highlight
|
||||
PLUGINS_CONFIG = {
|
||||
'plugin_name': {
|
||||
'foo': 'bar',
|
||||
'buzz': 'bazz'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Run Database Migrations
|
||||
|
||||
If the plugin introduces new database models, run the provided schema migrations:
|
||||
|
||||
```no-highlight
|
||||
(venv) $ cd /opt/netbox/netbox/
|
||||
(venv) $ python3 manage.py migrate
|
||||
```
|
||||
|
||||
!!! tip
|
||||
It's okay to run the `migrate` management command even if the plugin does not include any migration files.
|
||||
|
||||
## Collect Static Files
|
||||
|
||||
Plugins may package static resources like images or scripts to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command:
|
||||
|
||||
```no-highlight
|
||||
(venv) $ cd /opt/netbox/netbox/
|
||||
(venv) $ python3 manage.py collectstatic
|
||||
```
|
||||
|
||||
### Restart WSGI Service
|
||||
|
||||
Finally, restart the WSGI service and RQ workers to load the new plugin:
|
||||
|
||||
```no-highlight
|
||||
# sudo systemctl restart netbox netbox-rq
|
||||
```
|
|
@ -0,0 +1,72 @@
|
|||
# Removing a Plugin
|
||||
|
||||
!!! warning
|
||||
The instructions below detail the general process for removing a NetBox plugin. However, each plugin is different and may require additional tasks or modifications to the steps below. Always consult the documentation for a specific plugin **before** attempting to remove it.
|
||||
|
||||
## Disable the Plugin
|
||||
|
||||
Disable the plugin by removing it from the `PLUGINS` list in `configuration.py`.
|
||||
|
||||
## Remove its Configuration
|
||||
|
||||
Delete the plugin's entry (if any) in the `PLUGINS_CONFIG` dictionary in `configuration.py`.
|
||||
|
||||
!!! tip
|
||||
If there's a chance you may reinstall the plugin, consider commenting out any configuration parameters instead of deleting them.
|
||||
|
||||
## Re-index Search Entries
|
||||
|
||||
Run the `reindex` management command to reindex the global search engine. This will remove any stale entries pertaining to objects provided by the plugin.
|
||||
|
||||
```no-highlight
|
||||
$ cd /opt/netbox/netbox/
|
||||
$ source /opt/netbox/venv/bin/activate
|
||||
(venv) $ python3 manage.py reindex
|
||||
```
|
||||
|
||||
## Uninstall its Python Package
|
||||
|
||||
Use `pip` to remove the installed plugin:
|
||||
|
||||
```no-highlight
|
||||
$ source /opt/netbox/venv/bin/activate
|
||||
(venv) $ pip uninstall <package>
|
||||
```
|
||||
|
||||
## Restart WSGI Service
|
||||
|
||||
Restart the WSGI service:
|
||||
|
||||
```no-highlight
|
||||
# sudo systemctl restart netbox
|
||||
```
|
||||
|
||||
## Drop Database Tables
|
||||
|
||||
!!! note
|
||||
This step is necessary only for plugins which have created one or more database tables (generally through the introduction of new models). Check your plugin's documentation if unsure.
|
||||
|
||||
Enter the PostgreSQL database shell (`manage.py dbshell`) to determine if the plugin has created any SQL tables. Substitute `pluginname` in the example below for the name of the plugin being removed. (You can also run the `\dt` command without a pattern to list _all_ tables.)
|
||||
|
||||
```no-highlight
|
||||
netbox=> \dt pluginname_*
|
||||
List of relations
|
||||
List of relations
|
||||
Schema | Name | Type | Owner
|
||||
--------+----------------+-------+--------
|
||||
public | pluginname_foo | table | netbox
|
||||
public | pluginname_bar | table | netbox
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Exercise extreme caution when removing tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.
|
||||
|
||||
Drop each of the listed tables to remove it from the database:
|
||||
|
||||
```no-highlight
|
||||
netbox=> DROP TABLE pluginname_foo;
|
||||
DROP TABLE
|
||||
netbox=> DROP TABLE pluginname_bar;
|
||||
DROP TABLE
|
||||
```
|
|
@ -1,11 +1,51 @@
|
|||
# NetBox v3.7
|
||||
|
||||
## v3.7.5 (FUTURE)
|
||||
## v3.7.7 (FUTURE)
|
||||
|
||||
---
|
||||
|
||||
## v3.7.6 (2024-04-22)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#14690](https://github.com/netbox-community/netbox/issues/14690) - Improve rendering of JSON data in configuration form
|
||||
* [#15427](https://github.com/netbox-community/netbox/issues/15427) - Enable compatibility with non-Amazon S3 providers for remote data sources
|
||||
* [#15640](https://github.com/netbox-community/netbox/issues/15640) - Add global search support for L2VPN identifiers
|
||||
* [#15644](https://github.com/netbox-community/netbox/issues/15644) - Introduce new configuration parameters for enabling HTTP Strict Transport Security (HSTS)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#15541](https://github.com/netbox-community/netbox/issues/15541) - Restore ability to modify assigned component template when adding/modifying an inventory item template
|
||||
* [#15582](https://github.com/netbox-community/netbox/issues/15582) - Fix permission constraints for synchronization of remote data sources
|
||||
* [#15588](https://github.com/netbox-community/netbox/issues/15588) - Correct OpenAPI schema definitions for read-only fields which may return null values
|
||||
* [#15635](https://github.com/netbox-community/netbox/issues/15635) - Extend plugin removal instruction to include reindexing the global search cache
|
||||
* [#15654](https://github.com/netbox-community/netbox/issues/15654) - Fix `AttributeError` exception when attempting to save an incomplete tunnel termination
|
||||
* [#15668](https://github.com/netbox-community/netbox/issues/15668) - Fix permission required to display virtual disks tab on virtual machine UI view
|
||||
* [#15685](https://github.com/netbox-community/netbox/issues/15685) - Allow filtering cables by decimal values using UI filter form
|
||||
* [#15761](https://github.com/netbox-community/netbox/issues/15761) - Add missing `ike_policy` & `ike_policy_id` filters for IKE proposals
|
||||
* [#15771](https://github.com/netbox-community/netbox/issues/15771) - Include `id` in list of supported fields for all bulk import forms
|
||||
* [#15790](https://github.com/netbox-community/netbox/issues/15790) - Fix live preview support for EventRule comments
|
||||
|
||||
---
|
||||
|
||||
## v3.7.5 (2024-04-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#14707](https://github.com/netbox-community/netbox/issues/14707) - Clarify interface designation when creating tunnel terminations
|
||||
* [#15039](https://github.com/netbox-community/netbox/issues/15039) - Allow API tokens to be cloned
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#14799](https://github.com/netbox-community/netbox/issues/14799) - Avoid caching modified reports & scripts
|
||||
* [#15029](https://github.com/netbox-community/netbox/issues/15029) - Raise a clean validation error when attempting to make duplicate FHRP group assignments
|
||||
* [#15102](https://github.com/netbox-community/netbox/issues/15102) - Fix usage of selector widget for form fields referencing users/groups
|
||||
* [#15435](https://github.com/netbox-community/netbox/issues/15435) - Correct permissions name to allow adding a module bay to a device via the UI
|
||||
* [#15502](https://github.com/netbox-community/netbox/issues/15502) - Fix KeyError exception when modifying an IP address assigned to a virtual machine
|
||||
* [#15597](https://github.com/netbox-community/netbox/issues/15597) - Restore help modal for `button_class` field on custom link bulk import form
|
||||
* [#15598](https://github.com/netbox-community/netbox/issues/15598) - Fix exception when creating a device from a device type with one or more child inventory items
|
||||
* [#15608](https://github.com/netbox-community/netbox/issues/15608) - Avoid caching values of null fields in search index
|
||||
* [#15609](https://github.com/netbox-community/netbox/issues/15609) - Fix filtering of the providers list by assigned ASN
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# NetBox v4.0
|
||||
|
||||
## v4.0-beta2 (FUTURE)
|
||||
## v4.0-beta2 (2024-04-22)
|
||||
|
||||
**WARNING:** This is a beta release of NetBox intended for testing and evaluation. **Do not use this software in production.** Also be aware that no upgrade path is provided to future releases.
|
||||
|
||||
|
@ -18,6 +18,7 @@
|
|||
* The `object_type` field on the CustomField model has been renamed to `related_object_type`.
|
||||
* The `utilities.utils` module has been removed and its resources reorganized into separate modules organized by function.
|
||||
* The obsolete `NullableCharField` class has been removed. (Use Django's stock `CharField` class with `null=True` instead.)
|
||||
* The `annotated_date` template filter and `annotated_now` template tag have been removed.
|
||||
|
||||
### New Features
|
||||
|
||||
|
@ -86,15 +87,31 @@ The legacy admin user interface is now disabled by default, and the few remainin
|
|||
* [#15383](https://github.com/netbox-community/netbox/issues/15383) - Standardize filtering logic for the parents of recursively-nested models (parent & ancestor filters)
|
||||
* [#15413](https://github.com/netbox-community/netbox/issues/15413) - The global search engine now supports caching of non-field object attributes
|
||||
* [#15490](https://github.com/netbox-community/netbox/issues/15490) - Custom validators can now reference related object attributes via dotted paths
|
||||
* [#15547](https://github.com/netbox-community/netbox/issues/15547) - Add comments field to CustomField model
|
||||
* [#15712](https://github.com/netbox-community/netbox/issues/15712) - Enable image attachments for virtual machines
|
||||
* [#15735](https://github.com/netbox-community/netbox/issues/15735) - Display all dates & times in ISO 8601 format consistently
|
||||
* [#15754](https://github.com/netbox-community/netbox/issues/15754) - Remove `is_staff` restriction on admin menu items
|
||||
* [#15764](https://github.com/netbox-community/netbox/issues/15764) - Increase maximum value of Device `vc_position` field
|
||||
|
||||
### Bug Fixes (from Beta1)
|
||||
|
||||
* [#15580](https://github.com/netbox-community/netbox/issues/15580) - Fix rendering of modals with HTMX navigation enabled
|
||||
* [#15605](https://github.com/netbox-community/netbox/issues/15605) - Fix `ProgrammingError` exception when applying migrations to older databases
|
||||
* [#15613](https://github.com/netbox-community/netbox/issues/15613) - Restore the login button/user menu on mobile view
|
||||
* [#15616](https://github.com/netbox-community/netbox/issues/15616) - Fix button style for invalid custom links
|
||||
* [#15617](https://github.com/netbox-community/netbox/issues/15617) - Fix rack elevation styling under dark mode
|
||||
* [#15619](https://github.com/netbox-community/netbox/issues/15619) - Enforce a minimum width for progress bars
|
||||
* [#15636](https://github.com/netbox-community/netbox/issues/15636) - Fix filtering of attached images when viewing an object in the UI
|
||||
* [#15637](https://github.com/netbox-community/netbox/issues/15637) - Correct nonfunctional links within embedded tables when HTMX enabled
|
||||
* [#15638](https://github.com/netbox-community/netbox/issues/15638) - Correct parameter used to retrieve saved filters for a model
|
||||
* [#15641](https://github.com/netbox-community/netbox/issues/15641) - Fix adding/removing filters on the advanced object selector widget
|
||||
* [#15652](https://github.com/netbox-community/netbox/issues/15652) - Fix the display of error messages after attempting to delete an object
|
||||
* [#15671](https://github.com/netbox-community/netbox/issues/15671) - Fix `ValueError` exception when uploading a custom script
|
||||
* [#15695](https://github.com/netbox-community/netbox/issues/15695) - Fix `ForeignKeyViolation` exception when applying migration `users.0006_custom_group_model` on older databases
|
||||
* [#15698](https://github.com/netbox-community/netbox/issues/15698) - Fix ProgrammingError exception when applying the `users.0008_flip_objectpermission_assignments` migration to older databases
|
||||
* [#15760](https://github.com/netbox-community/netbox/issues/15760) - Permit breaking of long words for wrap within object attribute tables
|
||||
* [#15778](https://github.com/netbox-community/netbox/issues/15778) - Fix bulk edit/delete functionality when HTMX is enabled
|
||||
* [#15789](https://github.com/netbox-community/netbox/issues/15789) - Avoid AttributeError exception when attempting to view script results before job execution has completed
|
||||
|
||||
### Other Changes
|
||||
|
||||
|
@ -119,6 +136,9 @@ The legacy admin user interface is now disabled by default, and the few remainin
|
|||
* [#15401](https://github.com/netbox-community/netbox/issues/15401) - PostgreSQL indexes and sequence tables for the relocated L2VPN models (see [#14311](https://github.com/netbox-community/netbox/issues/14311)) have been renamed
|
||||
* [#15462](https://github.com/netbox-community/netbox/issues/15462) - Relocate resources from the `utilities.utils` module
|
||||
* [#15464](https://github.com/netbox-community/netbox/issues/15464) - The many-to-many relationships for ObjectPermission are now defined on the custom User and Group models
|
||||
* [#15736](https://github.com/netbox-community/netbox/issues/15736) - Remove obsolete `annotated_date` template filter & `annotated_now` template tag
|
||||
* [#15738](https://github.com/netbox-community/netbox/issues/15738) - Remove obsolete configuration parameters for date & time formatting
|
||||
* [#15752](https://github.com/netbox-community/netbox/issues/15752) - Remove the obsolete `ENABLE_LOCALIZATION` configuration parameter
|
||||
|
||||
### REST API Changes
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ plugins:
|
|||
show_root_toc_entry: false
|
||||
show_source: false
|
||||
extra:
|
||||
readthedocs: !ENV READTHEDOCS
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/netbox-community/netbox
|
||||
|
@ -112,7 +113,6 @@ nav:
|
|||
- Default Values: 'configuration/default-values.md'
|
||||
- Error Reporting: 'configuration/error-reporting.md'
|
||||
- Plugins: 'configuration/plugins.md'
|
||||
- Date & Time: 'configuration/date-time.md'
|
||||
- Miscellaneous: 'configuration/miscellaneous.md'
|
||||
- Development: 'configuration/development.md'
|
||||
- Customization:
|
||||
|
@ -129,7 +129,9 @@ nav:
|
|||
- Synchronized Data: 'integrations/synchronized-data.md'
|
||||
- Prometheus Metrics: 'integrations/prometheus-metrics.md'
|
||||
- Plugins:
|
||||
- Using Plugins: 'plugins/index.md'
|
||||
- About Plugins: 'plugins/index.md'
|
||||
- Installing a Plugin: 'plugins/installation.md'
|
||||
- Removing a Plugin: 'plugins/removal.md'
|
||||
- Developing Plugins:
|
||||
- Getting Started: 'plugins/development/index.md'
|
||||
- Models: 'plugins/development/models.md'
|
||||
|
|
|
@ -30,10 +30,12 @@ class UserTokenTable(NetBoxTable):
|
|||
write_enabled = columns.BooleanColumn(
|
||||
verbose_name=_('Write Enabled')
|
||||
)
|
||||
created = columns.DateColumn(
|
||||
created = columns.DateTimeColumn(
|
||||
timespec='minutes',
|
||||
verbose_name=_('Created'),
|
||||
)
|
||||
expires = columns.DateColumn(
|
||||
expires = columns.DateTimeColumn(
|
||||
timespec='minutes',
|
||||
verbose_name=_('Expires'),
|
||||
)
|
||||
last_used = columns.DateTimeColumn(
|
||||
|
|
|
@ -64,6 +64,12 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
|||
queryset=ASN.objects.all(),
|
||||
label=_('ASN (ID)'),
|
||||
)
|
||||
asn = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='asns__asn',
|
||||
queryset=ASN.objects.all(),
|
||||
to_field_name='asn',
|
||||
label=_('ASN'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
|
|
|
@ -25,7 +25,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('asn', name=_('ASN')),
|
||||
FieldSet('asn_id', name=_('ASN')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
|
@ -47,10 +47,6 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||
},
|
||||
label=_('Site')
|
||||
)
|
||||
asn = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('ASN (legacy)')
|
||||
)
|
||||
asn_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ASN.objects.all(),
|
||||
required=False,
|
||||
|
|
|
@ -90,10 +90,12 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_asn_id(self): # ASN object assignment
|
||||
def test_asn(self):
|
||||
asns = ASN.objects.all()[:2]
|
||||
params = {'asn_id': [asns[0].pk, asns[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'asn': [asns[0].asn, asns[1].asn]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
|
@ -30,10 +30,11 @@ class DataSourceViewSet(NetBoxModelViewSet):
|
|||
"""
|
||||
Enqueue a job to synchronize the DataSource.
|
||||
"""
|
||||
if not request.user.has_perm('core.sync_datasource'):
|
||||
raise PermissionDenied("Syncing data sources requires the core.sync_datasource permission.")
|
||||
|
||||
datasource = get_object_or_404(DataSource, pk=pk)
|
||||
|
||||
if not request.user.has_perm('core.sync_datasource', obj=datasource):
|
||||
raise PermissionDenied(_("This user does not have permission to synchronize this data source."))
|
||||
|
||||
datasource.enqueue_sync_job(request)
|
||||
serializer = serializers.DataSourceSerializer(datasource, context={'request': request})
|
||||
|
||||
|
|
|
@ -149,7 +149,8 @@ class S3Backend(DataBackend):
|
|||
region_name=self._region_name,
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
config=self.config
|
||||
config=self.config,
|
||||
endpoint_url=self._endpoint_url
|
||||
)
|
||||
bucket = s3.Bucket(self._bucket_name)
|
||||
|
||||
|
@ -176,6 +177,11 @@ class S3Backend(DataBackend):
|
|||
url_path = urlparse(self.url).path.lstrip('/')
|
||||
return url_path.split('/')[0]
|
||||
|
||||
@property
|
||||
def _endpoint_url(self):
|
||||
url_path = urlparse(self.url)
|
||||
return url_path._replace(params="", fragment="", query="", path="").geturl()
|
||||
|
||||
@property
|
||||
def _remote_path(self):
|
||||
url_path = urlparse(self.url).path.lstrip('/')
|
||||
|
|
|
@ -3,6 +3,7 @@ import json
|
|||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.forms.fields import JSONField as _JSONField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.forms.mixins import SyncedDataMixin
|
||||
|
@ -12,7 +13,7 @@ from netbox.forms import NetBoxModelForm
|
|||
from netbox.registry import registry
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import CommentField
|
||||
from utilities.forms.fields import CommentField, JSONField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import HTMXSelect
|
||||
|
||||
|
@ -133,6 +134,9 @@ class ConfigFormMetaclass(forms.models.ModelFormMetaclass):
|
|||
'help_text': param.description,
|
||||
}
|
||||
field_kwargs.update(**param.field_kwargs)
|
||||
if param.field is _JSONField:
|
||||
# Replace with our own JSONField to get pretty JSON in config editor
|
||||
param.field = JSONField
|
||||
param_fields[param.name] = param.field(**field_kwargs)
|
||||
attrs.update(param_fields)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django_tables2.utils import A
|
||||
|
||||
from core.tables.columns import RQJobStatusColumn
|
||||
from netbox.tables import BaseTable
|
||||
from netbox.tables import BaseTable, columns
|
||||
|
||||
|
||||
class BackgroundQueueTable(BaseTable):
|
||||
|
@ -75,13 +75,13 @@ class BackgroundTaskTable(BaseTable):
|
|||
linkify=("core:background_task", [A("id")]),
|
||||
verbose_name=_("ID")
|
||||
)
|
||||
created_at = tables.DateTimeColumn(
|
||||
created_at = columns.DateTimeColumn(
|
||||
verbose_name=_("Created")
|
||||
)
|
||||
enqueued_at = tables.DateTimeColumn(
|
||||
enqueued_at = columns.DateTimeColumn(
|
||||
verbose_name=_("Enqueued")
|
||||
)
|
||||
ended_at = tables.DateTimeColumn(
|
||||
ended_at = columns.DateTimeColumn(
|
||||
verbose_name=_("Ended")
|
||||
)
|
||||
status = RQJobStatusColumn(
|
||||
|
@ -117,7 +117,7 @@ class WorkerTable(BaseTable):
|
|||
state = tables.Column(
|
||||
verbose_name=_("State")
|
||||
)
|
||||
birth_date = tables.DateTimeColumn(
|
||||
birth_date = columns.DateTimeColumn(
|
||||
verbose_name=_("Birth")
|
||||
)
|
||||
pid = tables.Column(
|
||||
|
|
|
@ -347,7 +347,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
|||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
component = serializers.SerializerMethodField(read_only=True)
|
||||
component = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -53,7 +53,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
|||
)
|
||||
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
||||
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
||||
primary_ip = IPAddressSerializer(nested=True, read_only=True)
|
||||
primary_ip = IPAddressSerializer(nested=True, read_only=True, allow_null=True)
|
||||
primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
||||
primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
||||
oob_ip = IPAddressSerializer(nested=True, required=False, allow_null=True)
|
||||
|
@ -101,7 +101,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
|||
|
||||
|
||||
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
config_context = serializers.SerializerMethodField(read_only=True)
|
||||
config_context = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
fields = [
|
||||
|
|
|
@ -307,7 +307,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
|||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
component = serializers.SerializerMethodField(read_only=True)
|
||||
component = serializers.SerializerMethodField(read_only=True, allow_null=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -1373,14 +1373,14 @@ class VirtualDeviceContextImportForm(NetBoxModelImportForm):
|
|||
label=_('Device'),
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Assigned role'
|
||||
help_text=_('Assigned role')
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Assigned tenant'
|
||||
help_text=_('Assigned tenant')
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
label=_('Status'),
|
||||
|
|
|
@ -975,9 +975,9 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||
label=_('Color'),
|
||||
required=False
|
||||
)
|
||||
length = forms.IntegerField(
|
||||
length = forms.DecimalField(
|
||||
label=_('Length'),
|
||||
required=False
|
||||
required=False,
|
||||
)
|
||||
length_unit = forms.ChoiceField(
|
||||
label=_('Length unit'),
|
||||
|
|
|
@ -13,8 +13,7 @@ from netbox.forms import NetBoxModelForm
|
|||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import (
|
||||
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
|
||||
NumericArrayField, SlugField,
|
||||
CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
|
||||
|
@ -1003,31 +1002,128 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
|
|||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
component_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=MODULAR_COMPONENT_TEMPLATE_MODELS,
|
||||
|
||||
# Assigned component selectors
|
||||
consoleporttemplate = DynamicModelChoiceField(
|
||||
queryset=ConsolePortTemplate.objects.all(),
|
||||
required=False,
|
||||
widget=forms.HiddenInput
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Console port template')
|
||||
)
|
||||
component_id = forms.IntegerField(
|
||||
consoleserverporttemplate = DynamicModelChoiceField(
|
||||
queryset=ConsoleServerPortTemplate.objects.all(),
|
||||
required=False,
|
||||
widget=forms.HiddenInput
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Console server port template')
|
||||
)
|
||||
frontporttemplate = DynamicModelChoiceField(
|
||||
queryset=FrontPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Front port template')
|
||||
)
|
||||
interfacetemplate = DynamicModelChoiceField(
|
||||
queryset=InterfaceTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Interface template')
|
||||
)
|
||||
poweroutlettemplate = DynamicModelChoiceField(
|
||||
queryset=PowerOutletTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Power outlet template')
|
||||
)
|
||||
powerporttemplate = DynamicModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Power port template')
|
||||
)
|
||||
rearporttemplate = DynamicModelChoiceField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'device_type_id': '$device_type'
|
||||
},
|
||||
label=_('Rear port template')
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
|
||||
'component_type', 'component_id',
|
||||
),
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('interfacetemplate', name=_('Interface')),
|
||||
FieldSet('consoleporttemplate', name=_('Console Port')),
|
||||
FieldSet('consoleserverporttemplate', name=_('Console Server Port')),
|
||||
FieldSet('frontporttemplate', name=_('Front Port')),
|
||||
FieldSet('rearporttemplate', name=_('Rear Port')),
|
||||
FieldSet('powerporttemplate', name=_('Power Port')),
|
||||
FieldSet('poweroutlettemplate', name=_('Power Outlet')),
|
||||
),
|
||||
name=_('Component Assignment')
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemTemplate
|
||||
fields = [
|
||||
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
|
||||
'component_type', 'component_id',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
component_type = initial.get('component_type')
|
||||
component_id = initial.get('component_id')
|
||||
|
||||
if instance:
|
||||
# When editing set the initial value for component selection
|
||||
for component_model in ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS):
|
||||
if type(instance.component) is component_model.model_class():
|
||||
initial[component_model.model] = instance.component
|
||||
break
|
||||
elif component_type and component_id:
|
||||
# When adding the InventoryItem from a component page
|
||||
if content_type := ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS).filter(pk=component_type).first():
|
||||
if component := content_type.model_class().objects.filter(pk=component_id).first():
|
||||
initial[content_type.model] = component
|
||||
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Handle object assignment
|
||||
selected_objects = [
|
||||
field for field in (
|
||||
'consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate',
|
||||
'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate'
|
||||
) if self.cleaned_data[field]
|
||||
]
|
||||
if len(selected_objects) > 1:
|
||||
raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
|
||||
elif selected_objects:
|
||||
self.instance.component = self.cleaned_data[selected_objects[0]]
|
||||
else:
|
||||
self.instance.component = None
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
|
|
|
@ -130,7 +130,7 @@ class CableTerminationType(NetBoxObjectType):
|
|||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
], strawberry.union("CableTerminationTerminationType")]
|
||||
], strawberry.union("CableTerminationTerminationType")] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
@ -302,7 +302,7 @@ class InventoryItemTemplateType(ComponentTemplateType):
|
|||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
], strawberry.union("InventoryItemTemplateComponentType")]
|
||||
], strawberry.union("InventoryItemTemplateComponentType")] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
@ -431,7 +431,7 @@ class InventoryItemType(ComponentType):
|
|||
Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')],
|
||||
], strawberry.union("InventoryItemComponentType")]
|
||||
], strawberry.union("InventoryItemComponentType")] | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.0.3 on 2024-04-19 16:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0186_location_facility'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='vc_position',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -689,11 +689,10 @@ class Device(
|
|||
blank=True,
|
||||
null=True
|
||||
)
|
||||
vc_position = models.PositiveSmallIntegerField(
|
||||
vc_position = models.PositiveIntegerField(
|
||||
verbose_name=_('VC position'),
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[MaxValueValidator(255)],
|
||||
help_text=_('Virtual chassis position')
|
||||
)
|
||||
vc_priority = models.PositiveSmallIntegerField(
|
||||
|
@ -982,17 +981,16 @@ class Device(
|
|||
bulk_create: If True, bulk_create() will be called to create all components in a single query
|
||||
(default). Otherwise, save() will be called on each instance individually.
|
||||
"""
|
||||
components = [obj.instantiate(device=self) for obj in queryset]
|
||||
if not components:
|
||||
return
|
||||
|
||||
# Set default values for any applicable custom fields
|
||||
model = queryset.model.component_model
|
||||
if cf_defaults := CustomField.objects.get_defaults_for_model(model):
|
||||
for component in components:
|
||||
component.custom_field_data = cf_defaults
|
||||
|
||||
if bulk_create:
|
||||
components = [obj.instantiate(device=self) for obj in queryset]
|
||||
if not components:
|
||||
return
|
||||
# Set default values for any applicable custom fields
|
||||
if cf_defaults := CustomField.objects.get_defaults_for_model(model):
|
||||
for component in components:
|
||||
component.custom_field_data = cf_defaults
|
||||
model.objects.bulk_create(components)
|
||||
# Manually send the post_save signal for each of the newly created components
|
||||
for component in components:
|
||||
|
@ -1005,7 +1003,11 @@ class Device(
|
|||
update_fields=None
|
||||
)
|
||||
else:
|
||||
for component in components:
|
||||
for obj in queryset:
|
||||
component = obj.instantiate(device=self)
|
||||
# Set default values for any applicable custom fields
|
||||
if cf_defaults := CustomField.objects.get_defaults_for_model(model):
|
||||
component.custom_field_data = cf_defaults
|
||||
component.save()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
|
|
@ -65,7 +65,7 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||
'id', 'url', 'display', 'object_types', 'type', 'related_object_type', 'data_type', 'name', 'label',
|
||||
'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable',
|
||||
'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
||||
'choice_set', 'created', 'last_updated',
|
||||
'choice_set', 'comments', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
|
|
@ -165,7 +165,8 @@ class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
|
|||
Q(name__icontains=value) |
|
||||
Q(label__icontains=value) |
|
||||
Q(group_name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from extras.choices import *
|
|||
from extras.models import *
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from utilities.forms import BulkEditForm, add_blank_choice
|
||||
from utilities.forms.fields import ColorField, DynamicModelChoiceField
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect
|
||||
|
||||
__all__ = (
|
||||
|
@ -64,6 +64,7 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
|||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
nullable_fields = ('group_name', 'description', 'choice_set')
|
||||
|
||||
|
@ -316,8 +317,4 @@ class JournalEntryBulkEditForm(BulkEditForm):
|
|||
choices=add_blank_choice(JournalEntryKindChoices),
|
||||
required=False
|
||||
)
|
||||
comments = forms.CharField(
|
||||
label=_('Comments'),
|
||||
required=False,
|
||||
widget=forms.Textarea()
|
||||
)
|
||||
comments = CommentField()
|
||||
|
|
|
@ -71,7 +71,7 @@ class CustomFieldImportForm(CSVModelForm):
|
|||
fields = (
|
||||
'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description',
|
||||
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
|
||||
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable',
|
||||
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'comments',
|
||||
)
|
||||
|
||||
|
||||
|
@ -116,6 +116,12 @@ class CustomLinkImportForm(CSVModelForm):
|
|||
queryset=ObjectType.objects.with_feature('custom_links'),
|
||||
help_text=_("One or more assigned object types")
|
||||
)
|
||||
button_class = CSVChoiceField(
|
||||
label=_('button class'),
|
||||
required=False,
|
||||
choices=CustomLinkButtonClassChoices,
|
||||
help_text=_('The class of the first link in a group will be used for the dropdown button')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
|
|
|
@ -53,6 +53,7 @@ class CustomFieldForm(forms.ModelForm):
|
|||
queryset=CustomFieldChoiceSet.objects.all(),
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
|
@ -272,6 +273,7 @@ class EventRuleForm(NetBoxModelForm):
|
|||
required=False,
|
||||
help_text=_('Enter parameters to pass to the action in <a href="https://json.org/">JSON</a> format.')
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'description', 'object_types', 'enabled', 'tags', name=_('Event Rule')),
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.0.3 on 2024-04-19 18:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0113_customfield_rename_object_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customfield',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
|
@ -205,6 +205,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||
verbose_name=_('is cloneable'),
|
||||
help_text=_('Replicate this value when cloning objects')
|
||||
)
|
||||
comments = models.TextField(
|
||||
verbose_name=_('comments'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = CustomFieldManager()
|
||||
|
||||
|
|
|
@ -732,7 +732,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
|||
|
||||
def __str__(self):
|
||||
created = timezone.localtime(self.created)
|
||||
return f"{date_format(created, format='SHORT_DATETIME_FORMAT')} ({self.get_kind_display()})"
|
||||
return f"{created.date().isoformat()} {created.time().isoformat(timespec='minutes')} ({self.get_kind_display()})"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:journalentry', args=[self.pk])
|
||||
|
|
|
@ -96,6 +96,7 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
|||
Proxy model for script module files.
|
||||
"""
|
||||
objects = ScriptModuleManager()
|
||||
error = None
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
@ -118,6 +119,7 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
|
|||
try:
|
||||
module = self.get_module()
|
||||
except Exception as e:
|
||||
self.error = e
|
||||
logger.debug(f"Failed to load script: {self.python_name} error: {e}")
|
||||
module = None
|
||||
|
||||
|
|
|
@ -2,6 +2,18 @@ from netbox.search import SearchIndex, register_search
|
|||
from . import models
|
||||
|
||||
|
||||
@register_search
|
||||
class CustomFieldIndex(SearchIndex):
|
||||
model = models.CustomField
|
||||
fields = (
|
||||
('name', 100),
|
||||
('label', 100),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@register_search
|
||||
class JournalEntryIndex(SearchIndex):
|
||||
model = models.JournalEntry
|
||||
|
|
|
@ -78,7 +78,7 @@ class CustomFieldTable(NetBoxTable):
|
|||
fields = (
|
||||
'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required',
|
||||
'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
|
||||
'weight', 'choice_set', 'choices', 'created', 'last_updated',
|
||||
'weight', 'choice_set', 'choices', 'comments', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description')
|
||||
|
||||
|
@ -432,10 +432,10 @@ class ConfigTemplateTable(NetBoxTable):
|
|||
|
||||
|
||||
class ObjectChangeTable(NetBoxTable):
|
||||
time = tables.DateTimeColumn(
|
||||
time = columns.DateTimeColumn(
|
||||
verbose_name=_('Time'),
|
||||
linkify=True,
|
||||
format=settings.SHORT_DATETIME_FORMAT
|
||||
timespec='minutes',
|
||||
linkify=True
|
||||
)
|
||||
user_name = tables.Column(
|
||||
verbose_name=_('Username')
|
||||
|
@ -475,10 +475,10 @@ class ObjectChangeTable(NetBoxTable):
|
|||
|
||||
|
||||
class JournalEntryTable(NetBoxTable):
|
||||
created = tables.DateTimeColumn(
|
||||
created = columns.DateTimeColumn(
|
||||
verbose_name=_('Created'),
|
||||
linkify=True,
|
||||
format=settings.SHORT_DATETIME_FORMAT
|
||||
timespec='minutes',
|
||||
linkify=True
|
||||
)
|
||||
assigned_object_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Object Type')
|
||||
|
|
|
@ -1043,12 +1043,27 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
|
|||
})
|
||||
|
||||
|
||||
class ScriptView(generic.ObjectView):
|
||||
class BaseScriptView(generic.ObjectView):
|
||||
queryset = Script.objects.all()
|
||||
|
||||
def _get_script_class(self, script):
|
||||
script_class = script.python_class
|
||||
if script_class:
|
||||
script_class = script_class()
|
||||
|
||||
return script_class
|
||||
|
||||
|
||||
class ScriptView(BaseScriptView):
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
script = self.get_object(**kwargs)
|
||||
script_class = script.python_class()
|
||||
script_class = self._get_script_class(script)
|
||||
if not script_class:
|
||||
return render(request, 'extras/script.html', {
|
||||
'script': script,
|
||||
})
|
||||
|
||||
form = script_class.as_form(initial=normalize_querydict(request.GET))
|
||||
|
||||
return render(request, 'extras/script.html', {
|
||||
|
@ -1060,11 +1075,16 @@ class ScriptView(generic.ObjectView):
|
|||
|
||||
def post(self, request, **kwargs):
|
||||
script = self.get_object(**kwargs)
|
||||
script_class = script.python_class()
|
||||
|
||||
if not request.user.has_perm('extras.run_script', obj=script):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
script_class = self._get_script_class(script)
|
||||
if not script_class:
|
||||
return render(request, 'extras/script.html', {
|
||||
'script': script,
|
||||
})
|
||||
|
||||
form = script_class.as_form(request.POST, request.FILES)
|
||||
|
||||
# Allow execution only if RQ worker process is running
|
||||
|
@ -1094,21 +1114,22 @@ class ScriptView(generic.ObjectView):
|
|||
})
|
||||
|
||||
|
||||
class ScriptSourceView(generic.ObjectView):
|
||||
class ScriptSourceView(BaseScriptView):
|
||||
queryset = Script.objects.all()
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
script = self.get_object(**kwargs)
|
||||
script_class = self._get_script_class(script)
|
||||
|
||||
return render(request, 'extras/script/source.html', {
|
||||
'script': script,
|
||||
'script_class': script.python_class(),
|
||||
'script_class': script_class,
|
||||
'job_count': script.jobs.count(),
|
||||
'tab': 'source',
|
||||
})
|
||||
|
||||
|
||||
class ScriptJobsView(generic.ObjectView):
|
||||
class ScriptJobsView(BaseScriptView):
|
||||
queryset = Script.objects.all()
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
|
|
|
@ -100,7 +100,7 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
|||
"""
|
||||
family = serializers.IntegerField(read_only=True)
|
||||
prefix = serializers.CharField(read_only=True)
|
||||
vrf = VRFSerializer(nested=True, read_only=True)
|
||||
vrf = VRFSerializer(nested=True, read_only=True, allow_null=True)
|
||||
|
||||
def to_representation(self, instance):
|
||||
if self.context.get('vrf'):
|
||||
|
@ -183,7 +183,7 @@ class AvailableIPSerializer(serializers.Serializer):
|
|||
"""
|
||||
family = serializers.IntegerField(read_only=True)
|
||||
address = serializers.CharField(read_only=True)
|
||||
vrf = VRFSerializer(nested=True, read_only=True)
|
||||
vrf = VRFSerializer(nested=True, read_only=True, allow_null=True)
|
||||
description = serializers.CharField(required=False)
|
||||
|
||||
def to_representation(self, instance):
|
||||
|
|
|
@ -82,7 +82,7 @@ class AvailableVLANSerializer(serializers.Serializer):
|
|||
Representation of a VLAN which does not exist in the database.
|
||||
"""
|
||||
vid = serializers.IntegerField(read_only=True)
|
||||
group = VLANGroupSerializer(nested=True, read_only=True)
|
||||
group = VLANGroupSerializer(nested=True, read_only=True, allow_null=True)
|
||||
|
||||
def to_representation(self, instance):
|
||||
return {
|
||||
|
|
|
@ -533,6 +533,24 @@ class FHRPGroupAssignmentForm(forms.ModelForm):
|
|||
for ipaddress in ipaddresses:
|
||||
self.fields['group'].widget.add_query_param('related_ip', ipaddress.pk)
|
||||
|
||||
def clean_group(self):
|
||||
group = self.cleaned_data['group']
|
||||
|
||||
conflicting_assignments = FHRPGroupAssignment.objects.filter(
|
||||
interface_type=self.instance.interface_type,
|
||||
interface_id=self.instance.interface_id,
|
||||
group=group
|
||||
)
|
||||
if self.instance.id:
|
||||
conflicting_assignments = conflicting_assignments.exclude(id=self.instance.id)
|
||||
|
||||
if conflicting_assignments.exists():
|
||||
raise forms.ValidationError(
|
||||
_('Assignment already exists')
|
||||
)
|
||||
|
||||
return group
|
||||
|
||||
|
||||
class VLANGroupForm(NetBoxModelForm):
|
||||
scope_type = ContentTypeChoiceField(
|
||||
|
|
|
@ -133,7 +133,7 @@ class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType):
|
|||
Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["FHRPGroupType", strawberry.lazy('ipam.graphql.types')],
|
||||
Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')],
|
||||
], strawberry.union("IPAddressAssignmentType")]:
|
||||
], strawberry.union("IPAddressAssignmentType")] | None:
|
||||
return self.assigned_object
|
||||
|
||||
|
||||
|
@ -261,7 +261,7 @@ class VLANGroupType(OrganizationalObjectType):
|
|||
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
|
||||
], strawberry.union("VLANGroupScopeType")]:
|
||||
], strawberry.union("VLANGroupScopeType")] | None:
|
||||
return self.scope
|
||||
|
||||
|
||||
|
|
|
@ -131,9 +131,6 @@ EMAIL = {
|
|||
'FROM_EMAIL': '',
|
||||
}
|
||||
|
||||
# Localization
|
||||
ENABLE_LOCALIZATION = False
|
||||
|
||||
# Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and
|
||||
# by anonymous users. List models in the form `<app>.<model>`. Add '*' to this list to exempt all models.
|
||||
EXEMPT_VIEW_PERMISSIONS = [
|
||||
|
@ -237,12 +234,3 @@ SESSION_FILE_PATH = None
|
|||
|
||||
# Time zone (default: UTC)
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
# Date/time formatting. See the following link for supported formats:
|
||||
# https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date
|
||||
DATE_FORMAT = 'N j, Y'
|
||||
SHORT_DATE_FORMAT = 'Y-m-d'
|
||||
TIME_FORMAT = 'g:i a'
|
||||
SHORT_TIME_FORMAT = 'H:i:s'
|
||||
DATETIME_FORMAT = 'N j, Y g:i a'
|
||||
SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
|
||||
|
|
|
@ -74,17 +74,12 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
|
|||
"""
|
||||
Base form for creating a NetBox objects from CSV data. Used for bulk importing.
|
||||
"""
|
||||
id = forms.IntegerField(
|
||||
label=_('Id'),
|
||||
required=False,
|
||||
help_text='Numeric ID of an existing object to update (if not creating a new object)'
|
||||
)
|
||||
tags = CSVModelMultipleChoiceField(
|
||||
label=_('Tags'),
|
||||
queryset=Tag.objects.all(),
|
||||
required=False,
|
||||
to_field_name='slug',
|
||||
help_text='Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")'
|
||||
help_text=_('Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")')
|
||||
)
|
||||
|
||||
def _get_custom_fields(self, content_type):
|
||||
|
|
|
@ -368,12 +368,10 @@ ADMIN_MENU = Menu(
|
|||
MenuGroup(
|
||||
label=_('Authentication'),
|
||||
items=(
|
||||
# Proxy model for auth.User
|
||||
MenuItem(
|
||||
link=f'users:user_list',
|
||||
link_text=_('Users'),
|
||||
permissions=[f'auth.view_user'],
|
||||
staff_only=True,
|
||||
buttons=(
|
||||
MenuItemButton(
|
||||
link=f'users:user_add',
|
||||
|
@ -389,12 +387,10 @@ ADMIN_MENU = Menu(
|
|||
)
|
||||
)
|
||||
),
|
||||
# Proxy model for auth.Group
|
||||
MenuItem(
|
||||
link=f'users:group_list',
|
||||
link_text=_('Groups'),
|
||||
permissions=[f'auth.view_group'],
|
||||
staff_only=True,
|
||||
buttons=(
|
||||
MenuItemButton(
|
||||
link=f'users:group_add',
|
||||
|
@ -414,14 +410,12 @@ ADMIN_MENU = Menu(
|
|||
link=f'users:token_list',
|
||||
link_text=_('API Tokens'),
|
||||
permissions=[f'users.view_token'],
|
||||
staff_only=True,
|
||||
buttons=get_model_buttons('users', 'token')
|
||||
),
|
||||
MenuItem(
|
||||
link=f'users:objectpermission_list',
|
||||
link_text=_('Permissions'),
|
||||
permissions=[f'users.view_objectpermission'],
|
||||
staff_only=True,
|
||||
buttons=get_model_buttons('users', 'objectpermission', actions=['add'])
|
||||
),
|
||||
),
|
||||
|
@ -432,14 +426,12 @@ ADMIN_MENU = Menu(
|
|||
MenuItem(
|
||||
link='core:config',
|
||||
link_text=_('Current Config'),
|
||||
permissions=['core.view_configrevision'],
|
||||
staff_only=True
|
||||
permissions=['core.view_configrevision']
|
||||
),
|
||||
MenuItem(
|
||||
link='core:configrevision_list',
|
||||
link_text=_('Config Revisions'),
|
||||
permissions=['core.view_configrevision'],
|
||||
staff_only=True
|
||||
permissions=['core.view_configrevision']
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -448,13 +440,11 @@ ADMIN_MENU = Menu(
|
|||
items=(
|
||||
MenuItem(
|
||||
link='core:plugin_list',
|
||||
link_text=_('Plugins'),
|
||||
staff_only=True
|
||||
link_text=_('Plugins')
|
||||
),
|
||||
MenuItem(
|
||||
link='core:background_queue_list',
|
||||
link_text=_('Background Tasks'),
|
||||
staff_only=True
|
||||
link_text=_('Background Tasks')
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -22,6 +22,7 @@ PREFERENCES = {
|
|||
('dark', _('Dark')),
|
||||
),
|
||||
default='light',
|
||||
description=_('Preferred default UI theme')
|
||||
),
|
||||
'ui.htmx_navigation': UserPreference(
|
||||
label=_('HTMX Navigation'),
|
||||
|
@ -29,14 +30,17 @@ PREFERENCES = {
|
|||
('', _('Disabled')),
|
||||
('true', _('Enabled')),
|
||||
),
|
||||
default=False
|
||||
description=_('Enable dynamic UI navigation'),
|
||||
default=False,
|
||||
experimental=True
|
||||
),
|
||||
'locale.language': UserPreference(
|
||||
label=_('Language'),
|
||||
choices=(
|
||||
('', _('Auto')),
|
||||
*settings.LANGUAGES,
|
||||
)
|
||||
),
|
||||
description=_('Forces UI translation to the specified language.')
|
||||
),
|
||||
'pagination.per_page': UserPreference(
|
||||
label=_('Page length'),
|
||||
|
@ -51,8 +55,8 @@ PREFERENCES = {
|
|||
('top', _('Top')),
|
||||
('both', _('Both')),
|
||||
),
|
||||
description=_('Where the paginator controls will be displayed relative to a table'),
|
||||
default='bottom'
|
||||
default='bottom',
|
||||
description=_('Where the paginator controls will be displayed relative to a table')
|
||||
),
|
||||
|
||||
# Miscellaneous
|
||||
|
@ -62,6 +66,7 @@ PREFERENCES = {
|
|||
('json', 'JSON'),
|
||||
('yaml', 'YAML'),
|
||||
),
|
||||
description=_('The preferred syntax for displaying generic data within the UI')
|
||||
),
|
||||
|
||||
}
|
||||
|
|
|
@ -80,9 +80,10 @@ class SearchIndex:
|
|||
@staticmethod
|
||||
def get_field_value(instance, field_name):
|
||||
"""
|
||||
Return the value of the specified model field as a string.
|
||||
Return the value of the specified model field as a string (or None).
|
||||
"""
|
||||
return str(getattr(instance, field_name))
|
||||
if value := getattr(instance, field_name):
|
||||
return str(value)
|
||||
|
||||
@classmethod
|
||||
def get_category(cls):
|
||||
|
|
|
@ -24,7 +24,7 @@ from utilities.string import trailing_slash
|
|||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '4.0-beta1'
|
||||
VERSION = '4.0-beta2'
|
||||
HOSTNAME = platform.node()
|
||||
# Set the base directory two levels up
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
@ -73,8 +73,6 @@ CSRF_COOKIE_SECURE = getattr(configuration, 'CSRF_COOKIE_SECURE', False)
|
|||
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE = getattr(configuration, 'DATA_UPLOAD_MAX_MEMORY_SIZE', 2621440)
|
||||
DATABASE = getattr(configuration, 'DATABASE') # Required
|
||||
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
||||
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||
DEBUG = getattr(configuration, 'DEBUG', False)
|
||||
DEFAULT_DASHBOARD = getattr(configuration, 'DEFAULT_DASHBOARD', None)
|
||||
DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', {
|
||||
|
@ -93,7 +91,6 @@ DEVELOPER = getattr(configuration, 'DEVELOPER', False)
|
|||
DJANGO_ADMIN_ENABLED = getattr(configuration, 'DJANGO_ADMIN_ENABLED', False)
|
||||
DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
|
||||
EMAIL = getattr(configuration, 'EMAIL', {})
|
||||
ENABLE_LOCALIZATION = getattr(configuration, 'ENABLE_LOCALIZATION', False)
|
||||
EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', (
|
||||
'extras.events.process_event_queue',
|
||||
))
|
||||
|
@ -142,6 +139,9 @@ RQ_RETRY_MAX = getattr(configuration, 'RQ_RETRY_MAX', 0)
|
|||
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
||||
SEARCH_BACKEND = getattr(configuration, 'SEARCH_BACKEND', 'netbox.search.backends.CachedValueSearchBackend')
|
||||
SECRET_KEY = getattr(configuration, 'SECRET_KEY') # Required
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = getattr(configuration, 'SECURE_HSTS_INCLUDE_SUBDOMAINS', False)
|
||||
SECURE_HSTS_PRELOAD = getattr(configuration, 'SECURE_HSTS_PRELOAD', False)
|
||||
SECURE_HSTS_SECONDS = getattr(configuration, 'SECURE_HSTS_SECONDS', 0)
|
||||
SECURE_SSL_REDIRECT = getattr(configuration, 'SECURE_SSL_REDIRECT', False)
|
||||
SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', None)
|
||||
SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False)
|
||||
|
@ -152,12 +152,8 @@ SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
|
|||
SESSION_COOKIE_PATH = CSRF_COOKIE_PATH
|
||||
SESSION_COOKIE_SECURE = getattr(configuration, 'SESSION_COOKIE_SECURE', False)
|
||||
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||
STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
|
||||
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
|
||||
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
|
||||
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
||||
|
||||
# Load any dynamic configuration parameters which have been hard-coded in the configuration file
|
||||
|
@ -391,8 +387,6 @@ MIDDLEWARE = [
|
|||
'netbox.middleware.MaintenanceModeMiddleware',
|
||||
'django_prometheus.middleware.PrometheusAfterMiddleware',
|
||||
]
|
||||
if not ENABLE_LOCALIZATION:
|
||||
MIDDLEWARE.remove('django.middleware.locale.LocaleMiddleware')
|
||||
|
||||
# URLs
|
||||
ROOT_URLCONF = 'netbox.urls'
|
||||
|
@ -483,11 +477,11 @@ SERIALIZATION_MODULES = {
|
|||
# Exclude potentially sensitive models from wildcard view exemption. These may still be exempted
|
||||
# by specifying the model individually in the EXEMPT_VIEW_PERMISSIONS configuration parameter.
|
||||
EXEMPT_EXCLUDE_MODELS = (
|
||||
('auth', 'group'),
|
||||
('auth', 'user'),
|
||||
('extras', 'configrevision'),
|
||||
('users', 'group'),
|
||||
('users', 'objectpermission'),
|
||||
('users', 'token'),
|
||||
('users', 'user'),
|
||||
)
|
||||
|
||||
# All URLs starting with a string listed here are exempt from login enforcement
|
||||
|
@ -717,8 +711,6 @@ LANGUAGES = (
|
|||
LOCALE_PATHS = (
|
||||
BASE_DIR + '/translations',
|
||||
)
|
||||
if not ENABLE_LOCALIZATION:
|
||||
USE_I18N = False
|
||||
|
||||
#
|
||||
# Strawberry (GraphQL)
|
||||
|
|
|
@ -10,7 +10,6 @@ from django.db.models import DateField, DateTimeField
|
|||
from django.template import Context, Template
|
||||
from django.urls import reverse
|
||||
from django.utils.dateparse import parse_date
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
@ -52,18 +51,17 @@ __all__ = (
|
|||
#
|
||||
|
||||
@library.register
|
||||
class DateColumn(tables.DateColumn):
|
||||
class DateColumn(tables.Column):
|
||||
"""
|
||||
Overrides the default implementation of DateColumn to better handle null values, returning a default value for
|
||||
tables and null when exporting data. It is registered in the tables library to use this class instead of the
|
||||
default, making this behavior consistent in all fields of type DateField.
|
||||
Render a datetime.date in ISO 8601 format.
|
||||
"""
|
||||
def render(self, value):
|
||||
if value:
|
||||
return date_format(value, format="SHORT_DATE_FORMAT")
|
||||
return value.isoformat()
|
||||
|
||||
def value(self, value):
|
||||
return value
|
||||
if value:
|
||||
return value.isoformat()
|
||||
|
||||
@classmethod
|
||||
def from_field(cls, field, **kwargs):
|
||||
|
@ -72,16 +70,24 @@ class DateColumn(tables.DateColumn):
|
|||
|
||||
|
||||
@library.register
|
||||
class DateTimeColumn(tables.DateTimeColumn):
|
||||
class DateTimeColumn(tables.Column):
|
||||
"""
|
||||
Overrides the default implementation of DateTimeColumn to better handle null values, returning a default value for
|
||||
tables and null when exporting data. It is registered in the tables library to use this class instead of the
|
||||
default, making this behavior consistent in all fields of type DateTimeField.
|
||||
Render a datetime.datetime in ISO 8601 format.
|
||||
|
||||
Args:
|
||||
timespec: Granularity specification; passed through to datetime.isoformat()
|
||||
"""
|
||||
def __init__(self, *args, timespec='seconds', **kwargs):
|
||||
self.timespec = timespec
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def render(self, value):
|
||||
if value:
|
||||
return f"{value.date().isoformat()} {value.time().isoformat(timespec=self.timespec)}"
|
||||
|
||||
def value(self, value):
|
||||
if value:
|
||||
return date_format(value, format="SHORT_DATETIME_FORMAT")
|
||||
return None
|
||||
return value.isoformat()
|
||||
|
||||
@classmethod
|
||||
def from_field(cls, field, **kwargs):
|
||||
|
@ -498,7 +504,7 @@ class CustomFieldColumn(tables.Column):
|
|||
if self.customfield.type == CustomFieldTypeChoices.TYPE_LONGTEXT and value:
|
||||
return render_markdown(value)
|
||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_DATE and value:
|
||||
return date_format(parse_date(value), format="SHORT_DATE_FORMAT")
|
||||
return parse_date(value).isoformat()
|
||||
if value is not None:
|
||||
obj = self.customfield.deserialize(value)
|
||||
return mark_safe(self._linkify_item(obj))
|
||||
|
|
|
@ -314,11 +314,20 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
|||
return data
|
||||
|
||||
def _get_form_fields(self):
|
||||
# Exclude any fields which use a HiddenInput widget
|
||||
return {
|
||||
name: field for name, field in self.model_form().fields.items()
|
||||
if type(field.widget) is not HiddenInput
|
||||
}
|
||||
form = self.model_form()
|
||||
required_fields = {}
|
||||
optional_fields = {}
|
||||
|
||||
# Return only visible fields, with required fields listed first
|
||||
for field in form.visible_fields():
|
||||
if field.is_hidden:
|
||||
continue
|
||||
elif field.field.required:
|
||||
required_fields[field.name] = field.field
|
||||
else:
|
||||
optional_fields[field.name] = field.field
|
||||
|
||||
return {**required_fields, **optional_fields}
|
||||
|
||||
def _save_object(self, import_form, model_form, request):
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -68,123 +68,14 @@ type APIObjectBase = {
|
|||
[k: string]: JSONAble;
|
||||
};
|
||||
|
||||
type APIKeyPair = {
|
||||
public_key: string;
|
||||
private_key: string;
|
||||
};
|
||||
|
||||
type APIReference = {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
_depth: number;
|
||||
};
|
||||
|
||||
type APISecret = {
|
||||
assigned_object: APIObjectBase;
|
||||
assigned_object_id: number;
|
||||
assigned_object_type: string;
|
||||
created: string;
|
||||
custom_fields: Record<string, unknown>;
|
||||
display: string;
|
||||
hash: string;
|
||||
id: number;
|
||||
last_updated: string;
|
||||
name: string;
|
||||
plaintext: Nullable<string>;
|
||||
role: APIObjectBase;
|
||||
tags: number[];
|
||||
url: string;
|
||||
};
|
||||
|
||||
type APIUserConfig = {
|
||||
tables: { [k: string]: { columns: string[]; available_columns: string[] } };
|
||||
[k: string]: unknown;
|
||||
};
|
||||
|
||||
type LLDPInterface = {
|
||||
parent_interface: string | null;
|
||||
remote_chassis_id: string | null;
|
||||
remote_port: string | null;
|
||||
remote_port_description: string | null;
|
||||
remote_system_capab: string[];
|
||||
remote_system_description: string | null;
|
||||
remote_system_enable_capab: string[];
|
||||
remote_system_name: string | null;
|
||||
};
|
||||
|
||||
type LLDPNeighborDetail = {
|
||||
get_lldp_neighbors_detail: { [interface: string]: LLDPInterface[] };
|
||||
};
|
||||
|
||||
type DeviceConfig = {
|
||||
get_config: {
|
||||
candidate: string | Record<string, unknown>;
|
||||
running: string | Record<string, unknown>;
|
||||
startup: string | Record<string, unknown>;
|
||||
error?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type DeviceConfigType = Exclude<keyof DeviceConfig['get_config'], 'error'>;
|
||||
|
||||
type DeviceEnvironment = {
|
||||
cpu?: {
|
||||
[core: string]: { '%usage': number };
|
||||
};
|
||||
memory?: {
|
||||
available_ram: number;
|
||||
used_ram: number;
|
||||
};
|
||||
power?: {
|
||||
[psu: string]: { capacity: number; output: number; status: boolean };
|
||||
};
|
||||
temperature?: {
|
||||
[sensor: string]: {
|
||||
is_alert: boolean;
|
||||
is_critical: boolean;
|
||||
temperature: number;
|
||||
};
|
||||
};
|
||||
fans?: {
|
||||
[fan: string]: {
|
||||
status: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type DeviceFacts = {
|
||||
fqdn: string;
|
||||
hostname: string;
|
||||
interface_list: string[];
|
||||
model: string;
|
||||
os_version: string;
|
||||
serial_number: string;
|
||||
uptime: number;
|
||||
vendor: string;
|
||||
};
|
||||
|
||||
type DeviceStatus = {
|
||||
get_environment: DeviceEnvironment | ErrorBase;
|
||||
get_facts: DeviceFacts | ErrorBase;
|
||||
};
|
||||
|
||||
interface ObjectWithGroup extends APIObjectBase {
|
||||
group: Nullable<APIReference>;
|
||||
}
|
||||
|
||||
declare const messages: string[];
|
||||
|
||||
type FormControls = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
|
||||
type ColorMode = 'light' | 'dark';
|
||||
type ColorModePreference = ColorMode | 'none';
|
||||
type ConfigContextFormat = 'json' | 'yaml';
|
||||
|
||||
type UserPreferences = {
|
||||
ui: {
|
||||
colorMode: ColorMode;
|
||||
showRackImages: boolean;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -37,6 +37,11 @@ table.attr-table {
|
|||
border-bottom-style: hidden;
|
||||
}
|
||||
|
||||
// Permit breaking of words which don't fit on one line in the cell
|
||||
td {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Remove bottom margin from <pre> elements inside table cells
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
{% block title %}{% trans "Change Password" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal col-md-8 offset-md-2">
|
||||
<form action="." method="post" class="object-edit">
|
||||
{% csrf_token %}
|
||||
<div class="field-group">
|
||||
<h5 class="text-center">{% trans "Password" %}</h5>
|
||||
<div class="field-group mb-5">
|
||||
<h5 class="col-9 offset-3">{% trans "Password" %}</h5>
|
||||
{% render_field form.old_password %}
|
||||
{% render_field form.new_password1 %}
|
||||
{% render_field form.new_password2 %}
|
||||
|
|
|
@ -39,30 +39,32 @@
|
|||
{% trans "Clear table preferences" %}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<table class="table table-hover object-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" class="toggle form-check-input" title="{% trans "Toggle All" %}">
|
||||
</th>
|
||||
<th>{% trans "Table" %}</th>
|
||||
<th>{% trans "Ordering" %}</th>
|
||||
<th>{% trans "Columns" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for table, prefs in request.user.config.data.tables.items %}
|
||||
<div class="card">
|
||||
<table class="table table-hover object-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="pk" value="tables.{{ table }}" class="form-check-input" />
|
||||
</td>
|
||||
<td>{{ table }}</td>
|
||||
<td>{{ prefs.ordering|join:", "|placeholder }}</td>
|
||||
<td>{{ prefs.columns|join:", "|placeholder }}</td>
|
||||
<th>
|
||||
<input type="checkbox" class="toggle form-check-input" title="{% trans "Toggle All" %}">
|
||||
</th>
|
||||
<th>{% trans "Table" %}</th>
|
||||
<th>{% trans "Ordering" %}</th>
|
||||
<th>{% trans "Columns" %}</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for table, prefs in request.user.config.data.tables.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="pk" value="tables.{{ table }}" class="form-check-input" />
|
||||
</td>
|
||||
<td>{{ table }}</td>
|
||||
<td>{{ prefs.ordering|join:", "|placeholder }}</td>
|
||||
<td>{{ prefs.columns|join:", "|placeholder }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-9 offset-3">
|
||||
|
|
|
@ -31,11 +31,11 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Account Created" %}</th>
|
||||
<td>{{ request.user.date_joined|annotated_date }}</td>
|
||||
<td>{{ request.user.date_joined|isodate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Last Login" %}</th>
|
||||
<td>{{ request.user.last_login|annotated_date }}</td>
|
||||
<td>{{ request.user.last_login|isodatetime:"minutes"|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Superuser" %}</th>
|
||||
|
@ -66,7 +66,7 @@
|
|||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header text-center">{% trans "Recent Activity" %}</h5>
|
||||
<div class="card-body table-responsive">
|
||||
<div class="table-responsive">
|
||||
{% render_table changelog_table 'inc/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -41,15 +41,15 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Created" %}</th>
|
||||
<td>{{ object.created|annotated_date }}</td>
|
||||
<td>{{ object.created|isodatetime }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Expires" %}</th>
|
||||
<td>{{ object.expires|placeholder }}</td>
|
||||
<td>{{ object.expires|isodatetime|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Last used" %}</th>
|
||||
<td>{{ object.last_used|placeholder }}</td>
|
||||
<td>{{ object.last_used|isodatetime|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Allowed IPs" %}</th>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body table-responsive">
|
||||
<div class="table-responsive">
|
||||
{% render_table table 'inc/table.html' %}
|
||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||
</div>
|
||||
|
|
|
@ -126,6 +126,9 @@ Blocks:
|
|||
{% endif %}
|
||||
{# /Bottom banner #}
|
||||
|
||||
{# BS5 pop-up modals #}
|
||||
{% block modals %}{% endblock %}
|
||||
|
||||
</div>
|
||||
|
||||
{# Page footer #}
|
||||
|
@ -179,9 +182,9 @@ Blocks:
|
|||
{# /Footer links #}
|
||||
|
||||
{# Footer text #}
|
||||
<ul class="list-inline list-inline-dots mb-0" id="footer-stamp" hx-swap-oob="true">
|
||||
<ul class="list-inline list-inline-dots fs-5 mb-0" id="footer-stamp" hx-swap-oob="true">
|
||||
<li class="list-inline-item">
|
||||
{% annotated_now %} {% now 'T' %}
|
||||
{% now 'Y-m-d H:i:s T' %}
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
{{ settings.HOSTNAME }} (v{{ settings.VERSION }})
|
||||
|
@ -197,8 +200,5 @@ Blocks:
|
|||
{# /Page content #}
|
||||
</div>
|
||||
|
||||
{# BS5 pop-up modals #}
|
||||
{% block modals %}{% endblock %}
|
||||
|
||||
</div>
|
||||
{% endblock layout %}
|
||||
|
|
|
@ -45,11 +45,11 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Install Date" %}</th>
|
||||
<td>{{ object.install_date|annotated_date|placeholder }}</td>
|
||||
<td>{{ object.install_date|isodate|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Termination Date" %}</th>
|
||||
<td>{{ object.termination_date|annotated_date|placeholder }}</td>
|
||||
<td>{{ object.termination_date|isodate|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Commit Rate" %}</th>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
{% block subtitle %}
|
||||
{% if object.created %}
|
||||
<div class="text-secondary fs-5">
|
||||
{% trans "Created" %} {{ object.created|annotated_date }}
|
||||
{% trans "Created" %} {{ object.created|isodatetime }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock subtitle %}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
{% block subtitle %}
|
||||
<div class="text-secondary fs-5">
|
||||
{% trans "Created" %} {{ object.created|annotated_date }}
|
||||
{% trans "Created" %} {{ object.created|isodatetime }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -49,12 +49,12 @@
|
|||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Created" %}</th>
|
||||
<td>{{ object.created|annotated_date }}</td>
|
||||
<td>{{ object.created|isodatetime }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Scheduled" %}</th>
|
||||
<td>
|
||||
{{ object.scheduled|annotated_date|placeholder }}
|
||||
{{ object.scheduled|isodatetime|placeholder }}
|
||||
{% if object.interval %}
|
||||
({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %})
|
||||
{% endif %}
|
||||
|
@ -62,11 +62,11 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Started" %}</th>
|
||||
<td>{{ object.started|annotated_date|placeholder }}</td>
|
||||
<td>{{ object.started|isodatetime|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Completed" %}</th>
|
||||
<td>{{ object.completed|annotated_date|placeholder }}</td>
|
||||
<td>{{ object.completed|isodatetime|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
{% block subtitle %}
|
||||
<div class="text-secondary fs-5">
|
||||
<span>{% trans "Created" %} {{ job.created_at|annotated_date }}</span>
|
||||
<span>{% trans "Created" %} {{ job.created_at|isodatetime }}</span>
|
||||
</div>
|
||||
{% endblock subtitle %}
|
||||
|
||||
|
@ -71,11 +71,11 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Created" %}</th>
|
||||
<td>{{ job.created_at|annotated_date }}</td>
|
||||
<td>{{ job.created_at|isodatetime }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Queued" %}</th>
|
||||
<td>{{ job.enqueued_at|annotated_date }}</td>
|
||||
<td>{{ job.enqueued_at|isodatetime }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
{% block subtitle %}
|
||||
<div class="text-secondary fs-5">
|
||||
<span>{% trans "Created" %} {{ worker.birth_date|annotated_date }}</span>
|
||||
<span>{% trans "Created" %} {{ worker.birth_date|isodatetime }}</span>
|
||||
</div>
|
||||
{% endblock subtitle %}
|
||||
|
||||
|
@ -49,7 +49,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Birth" %}</th>
|
||||
<td>{{ worker.birth_date|annotated_date }}</td>
|
||||
<td>{{ worker.birth_date|isodatetime }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Queues" %}</th>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
{% if perms.dcim.add_rearport %}
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">{% trans "Rear Ports" %}</a></li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
{% if perms.dcim.add_modulebay %}
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}">{% trans "Module Bays" %}</a></li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
<td>
|
||||
{% if object.time_zone %}
|
||||
{{ object.time_zone }} ({% trans "UTC" %} {{ object.time_zone|tzoffset }})<br />
|
||||
<small class="text-muted">{% trans "Site time" %}: {% timezone object.time_zone %}{% annotated_now %}{% endtimezone %}</small>
|
||||
<small class="text-muted">{% trans "Site time" %}: {% timezone object.time_zone %}{% now 'Y-m-d H:i' %}{% endtimezone %}</small>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
{% load humanize %}
|
||||
{% load helpers %}
|
||||
{% load log_levels %}
|
||||
{% load i18n %}
|
||||
|
@ -6,11 +5,11 @@
|
|||
<div class="htmx-container">
|
||||
<p>
|
||||
{% if job.started %}
|
||||
{% trans "Started" %}: <strong>{{ job.started|annotated_date }}</strong>
|
||||
{% trans "Started" %}: <strong>{{ job.started|isodatetime }}</strong>
|
||||
{% elif job.scheduled %}
|
||||
{% trans "Scheduled for" %}: <strong>{{ job.scheduled|annotated_date }}</strong> ({{ job.scheduled|naturaltime }})
|
||||
{% trans "Scheduled for" %}: <strong>{{ job.scheduled|isodatetime }}</strong>
|
||||
{% else %}
|
||||
{% trans "Created" %}: <strong>{{ job.created|annotated_date }}</strong>
|
||||
{% trans "Created" %}: <strong>{{ job.created|isodatetime }}</strong>
|
||||
{% endif %}
|
||||
{% if job.completed %}
|
||||
{% trans "Duration" %}: <strong>{{ job.duration }}</strong>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Created" %}</th>
|
||||
<td>{{ object.created|annotated_date }}</td>
|
||||
<td>{{ object.created|isodatetime:"minutes" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Created By" %}</th>
|
||||
|
|
|
@ -29,9 +29,7 @@
|
|||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Time" %}</th>
|
||||
<td>
|
||||
{{ object.time|annotated_date }}
|
||||
</td>
|
||||
<td>{{ object.time|isodatetime }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "User" %}</th>
|
||||
|
|
|
@ -14,38 +14,43 @@
|
|||
{% trans "You do not have permission to run scripts" %}.
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="" method="post" enctype="multipart/form-data" class="object-edit">
|
||||
{% csrf_token %}
|
||||
<div class="field-group my-4">
|
||||
{# Render grouped fields according to declared fieldsets #}
|
||||
{% for group, fields in script_class.get_fieldsets %}
|
||||
{% if fields %}
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h5 class="col-9 offset-3">{{ group }}</h5>
|
||||
{% if form %}
|
||||
<form action="" method="post" enctype="multipart/form-data" class="object-edit">
|
||||
{% csrf_token %}
|
||||
<div class="field-group my-4">
|
||||
{# Render grouped fields according to declared fieldsets #}
|
||||
{% for group, fields in script_class.get_fieldsets %}
|
||||
{% if fields %}
|
||||
<div class="field-group mb-5">
|
||||
<div class="row">
|
||||
<h5 class="col-9 offset-3">{{ group }}</h5>
|
||||
</div>
|
||||
{% for name in fields %}
|
||||
{% with field=form|getfield:name %}
|
||||
{% render_field field %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for name in fields %}
|
||||
{% with field=form|getfield:name %}
|
||||
{% render_field field %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<a href="{% url 'extras:script_list' %}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
||||
{% if not request.user|can_run:script or not script.is_executable %}
|
||||
<button class="btn btn-primary" disabled>
|
||||
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_run" class="btn btn-primary">
|
||||
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<a href="{% url 'extras:script_list' %}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
|
||||
{% if not request.user|can_run:script or not script.is_executable %}
|
||||
<button class="btn btn-primary" disabled>
|
||||
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_run" class="btn btn-primary">
|
||||
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>{% trans "Error loading script" %}.</p>
|
||||
<pre class="block">{{ script.module.error }}</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
{% extends 'extras/script/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<code class="h6 my-3 d-block">{{ script_class.filename }}</code>
|
||||
<pre class="block">{{ script_class.source }}</pre>
|
||||
|
||||
{% if script_class %}
|
||||
<code class="h6 my-3 d-block">{{ script_class.filename }}</code>
|
||||
<pre class="block">{{ script_class.source }}</pre>
|
||||
{% else %}
|
||||
<p>{% trans "Error loading script" %}.</p>
|
||||
<pre class="block">{{ script.module.error }}</pre>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
<td>{{ script.description|markdown|placeholder }}</td>
|
||||
{% if last_job %}
|
||||
<td>
|
||||
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|annotated_date }}</a>
|
||||
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% badge last_job.get_status_display last_job.get_status_color %}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">{% trans "Scripts" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ script.get_absolute_url }}">{{ script }}</a></li>
|
||||
<li class="breadcrumb-item">{{ job.created|annotated_date }}</li>
|
||||
<li class="breadcrumb-item">{{ job.created|isodatetime }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -101,5 +101,7 @@
|
|||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
{% if job.completed %}
|
||||
{% table_config_form table table_name="ObjectTable" %}
|
||||
{% endif %}
|
||||
{% endblock modals %}
|
||||
|
|
|
@ -103,87 +103,87 @@ Context:
|
|||
|
||||
{% if fields %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Field Options" %}</h5>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{% trans "Field" %}</th>
|
||||
<th>{% trans "Required" %}</th>
|
||||
<th>{% trans "Accessor" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
</tr>
|
||||
{% for name, field in fields.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{% if field.required %}<strong>{% endif %}{{ name }}{% if field.required %}</strong>{% endif %}</code>
|
||||
</td>
|
||||
<td>
|
||||
{% if field.required %}
|
||||
{% checkmark True true="Required" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if field.to_field_name %}
|
||||
<code>{{ field.to_field_name }}</code>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if field.STATIC_CHOICES %}
|
||||
<button type="button" class="btn btn-link float-end" data-bs-toggle="modal" data-bs-target="#{{ name }}_choices">
|
||||
<i class="mdi mdi-help-circle"></i>
|
||||
</button>
|
||||
<div class="modal fade" id="{{ name }}_choices" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><code>{{ name }}</code> {% trans "Choices" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Import Value" %}</th>
|
||||
<th>{% trans "Label" %}</th>
|
||||
</tr>
|
||||
{% for value, label in field.choices %}
|
||||
{% if value %}
|
||||
<tr>
|
||||
<td>
|
||||
<samp>{{ value }}</samp>
|
||||
</td>
|
||||
<td>
|
||||
{{ label }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if field.help_text %}
|
||||
{{ field.help_text }}<br />
|
||||
{% elif field.label %}
|
||||
{{ field.label }}<br />
|
||||
{% endif %}
|
||||
{% if field|widget_type == 'dateinput' %}
|
||||
<small class="text-muted">{% trans "Format: YYYY-MM-DD" %}</small>
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
<small class="text-muted">{% trans "Specify true or false" %}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Field Options" %}</h5>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Field" %}</th>
|
||||
<th>{% trans "Required" %}</th>
|
||||
<th>{% trans "Accessor" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for name, field in fields.items %}
|
||||
<tr>
|
||||
<td class="font-monospace{% if field.required %} fw-bold{% endif %}">
|
||||
{{ name }}
|
||||
</td>
|
||||
<td>
|
||||
{% if field.required %}
|
||||
{% checkmark True true="Required" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if field.to_field_name %}
|
||||
<td class="font-monospace">{{ field.to_field_name }}</td>
|
||||
{% else %}
|
||||
<td>{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if field.help_text %}
|
||||
{{ field.help_text }}
|
||||
{% elif field.label %}
|
||||
{{ field.label }}
|
||||
{% endif %}
|
||||
{% if field.STATIC_CHOICES %}
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#{{ name }}_choices"><i class="mdi mdi-help-circle"></i></a>
|
||||
<div class="modal fade" id="{{ name }}_choices" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<span class="font-monospace">{{ name }}</span> {% trans "Choices" %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<table class="table table-striped modal-body">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Import Value" %}</th>
|
||||
<th>{% trans "Label" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for value, label in field.choices %}
|
||||
{% if value %}
|
||||
<tr>
|
||||
<td><samp>{{ value }}</samp></td>
|
||||
<td>{{ label }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if field|widget_type == 'dateinput' %}
|
||||
<br /><small class="text-muted">{% trans "Format: YYYY-MM-DD" %}</small>
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
<br /><small class="text-muted">{% trans "Specify true or false" %}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="small text-muted">
|
||||
<i class="mdi mdi-check-bold text-success"></i>
|
||||
|
|
|
@ -48,10 +48,10 @@ Context:
|
|||
|
||||
{% block subtitle %}
|
||||
<div class="text-secondary fs-5">
|
||||
<span>{% trans "Created" %} {{ object.created|annotated_date }}</span>
|
||||
{% trans "Created" %} {{ object.created|isodatetime:"minutes" }}
|
||||
{% if object.last_updated %}
|
||||
<span class="separator">·</span>
|
||||
<span>{% trans "Updated" %} <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> {% trans "ago" %}</span>
|
||||
{% trans "Updated" %} {{ object.last_updated|isodatetime:"minutes" }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock subtitle %}
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
<div class="col-3">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for field in form.visible_fields %}
|
||||
<a href="#" class="list-group-item list-group-item-action px-0 py-1" data-bs-toggle="collapse" data-bs-target="#checkmark{{ forloop.counter }}, #selector{{ forloop.counter }}">
|
||||
<span id="checkmark{{ forloop.counter }}" class="collapse{% if forloop.counter < 3 or field.name in form.selector_fields %} show{% endif %}"><i class="mdi mdi-check-bold"></i></span>
|
||||
<a href="#" class="list-group-item list-group-item-action px-0 py-1" data-bs-toggle="collapse" data-bs-target=".selector{{ forloop.counter }}">
|
||||
<span class="collapse selector{{ forloop.counter }}{% if forloop.counter < 3 or field.name in form.selector_fields %} show{% endif %}"><i class="mdi mdi-check-bold"></i></span>
|
||||
{{ field.label }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
@ -21,7 +21,7 @@
|
|||
<input type="hidden" name="_search" value="true" />
|
||||
<div class="tab-content p-1">
|
||||
{% for field in form.visible_fields %}
|
||||
<div class="collapse{% if field.name in form.selector_fields %} show{% endif %}" id="selector{{ forloop.counter }}">{% render_field field %}</div>
|
||||
<div class="collapse selector{{ forloop.counter }}{% if field.name in form.selector_fields %} show{% endif %}">{% render_field field %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<div class="list-group">
|
||||
{% for object in results %}
|
||||
<a href="#" class="list-group-item list-group-item-action" data-label="{{ object }}" data-value="{{ object.pk }}" data-bs-dismiss="modal">
|
||||
<h6 class="mb-1">
|
||||
{{ object }}
|
||||
{% if object.status %}{% badge object.get_status_display bg_color=object.get_status_color %}{% endif %}
|
||||
</h6>
|
||||
<a href="#" class="list-group-item list-group-item-action p-2" data-label="{{ object }}" data-value="{{ object.pk }}" data-bs-dismiss="modal">
|
||||
{{ object }}
|
||||
{% if object.status %}{% badge object.get_status_display bg_color=object.get_status_color %}{% endif %}
|
||||
{% if object.description %}
|
||||
<small>{{ object.description }}</small>
|
||||
<small class="d-block text-muted">{{ object.description }}</small>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
|
|
@ -12,5 +12,5 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% htmx_table 'extras:imageattachment_list' content_type_id=object|content_type_id object_id=object.pk %}
|
||||
{% htmx_table 'extras:imageattachment_list' object_type_id=object|content_type_id object_id=object.pk %}
|
||||
</div>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Date Added" %}</th>
|
||||
<td>{{ object.date_added|annotated_date|placeholder }}</td>
|
||||
<td>{{ object.date_added|isodate|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
{% endblock tabs %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row px-3">
|
||||
<div class="row">
|
||||
<div class="col col-6 offset-3 py-3">
|
||||
<form action="{% url 'search' %}" method="get" class="form form-horizontal">
|
||||
{% render_form form %}
|
||||
|
@ -29,10 +29,12 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3">
|
||||
<div class="card">
|
||||
<div class="htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends 'generic/object.html' %}
|
||||
{% load i18n %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}{% trans "Token" %} {{ object }}{% endblock %}
|
||||
|
@ -33,15 +33,15 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Created" %}</th>
|
||||
<td>{{ object.created|annotated_date }}</td>
|
||||
<td>{{ object.created|isodatetime }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Expires" %}</th>
|
||||
<td>{{ object.expires|placeholder }}</td>
|
||||
<td>{{ object.expires|isodatetime|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Last used" %}</th>
|
||||
<td>{{ object.last_used|placeholder }}</td>
|
||||
<td>{{ object.last_used|isodatetime|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Allowed IPs" %}</th>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{% extends 'generic/object_edit.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% if not request.user.is_superuser %}
|
||||
{% include 'inc/alerts/warning.html' with title="Creating API Tokens" message="Non-superusers should generally create and modify API tokens under their user profile." %}
|
||||
{% endif %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -27,11 +27,11 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Account Created" %}</th>
|
||||
<td>{{ object.date_joined|annotated_date }}</td>
|
||||
<td>{{ object.date_joined|isodate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Last Login" %}</th>
|
||||
<td>{{ object.last_login|annotated_date }}</td>
|
||||
<td>{{ object.last_login|isodatetime:"minutes"|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Active" %}</th>
|
||||
|
|
|
@ -158,6 +158,7 @@
|
|||
</h5>
|
||||
{% htmx_table 'ipam:service_list' virtual_machine_id=object.pk %}
|
||||
</div>
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue