Compare commits

...

77 Commits

Author SHA1 Message Date
Arthur Hanson 094597e2f6
Merge 80802a4357 into 5af3c659a5 2024-04-26 13:36:57 -03:00
Tobias Genannt 5af3c659a5 Fix #15826: Added new group and user models 2024-04-25 09:23:27 -04:00
Arthur Hanson 4923025fec
15541 Add component selector to InventoryItemTemplate (#15691)
* 15541 update InventoryItemTemplateForm

* 15541 update InventoryItemTemplateForm

* Remove custom template

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-04-25 09:22:32 -04:00
Arthur Hanson ded2fe9471
15809 Mark unions as nullable in GraphQL where appropriate (#15824)
* 15809 mark unions as nullable where appropriate

* 15809 fix tests

* 15809 fix tests
2024-04-25 09:19:19 -04:00
Jeremy Stretch e05ca710ae Flag HTMX navigation as an experimental feature 2024-04-23 10:38:49 -04:00
Jeremy Stretch c32dff5649 Release v4.0-beta2 2024-04-22 15:35:34 -04:00
Jeremy Stretch 8364e632b7 Remove obsolete type definitions 2024-04-22 15:10:37 -04:00
Jeremy Stretch c43b929542 Fixes #15580: Fix rendering of modals with HTMX enabled 2024-04-22 15:10:28 -04:00
Jeremy Stretch e3c418263e Fixes #15778: Fix bulk edit/delete functionality when HTMX is enabled 2024-04-22 14:31:39 -04:00
Jeremy Stretch 46bd62fdc9 Merge branch 'develop' into feature 2024-04-22 13:23:42 -04:00
Jeremy Stretch 0b0dab42eb PRVB 2024-04-22 12:23:31 -04:00
Jeremy Stretch d115601da3
Merge pull request #15805 from netbox-community/develop
Release v3.7.6
2024-04-22 12:18:27 -04:00
Jeremy Stretch a61e20849b Release v3.7.6 2024-04-22 11:46:03 -04:00
Arthur Hanson 1eca1c3d17
15803 localize help_text (#15804) 2024-04-22 11:42:20 -04:00
transifex-integration[bot] 5d95d49268
Update translations 2024-04-22 11:28:04 -04:00
Jeremy Stretch 6b8bfe9947 Changelog for #14690, #15541, #15588, #15761, #15771, #15790 2024-04-22 11:25:21 -04:00
Jeremy Stretch e87877b6ea Fixes #15771: Show id field as supported on all bulk import forms 2024-04-22 11:08:36 -04:00
Jeremy Stretch ebe504c825 Closes #15664: Restore usage of READTHEDOCS env variable 2024-04-22 09:52:03 -04:00
Markku Leiniö b6e38b2ebe
Closes #14690: Pretty-format JSON fields in the config form (#15623)
* Closes #14690: Pretty-format JSON fields in the config form

* Revert changes

* Use our own JSONField for config parameters for pretty editor outputs

* Compare identity instead of equality
2024-04-22 09:25:16 -04:00
Arthur Hanson 90d0104359
15541 Add component selector to InventoryItemTemplate (#15759)
* 15541 make inventoryitemtemplateform match inventoryitemform

* 15541 set tab active
2024-04-22 08:22:53 -04:00
Arthur 781d932b2a 15789 make sure job completed before including config_form 2024-04-21 13:57:15 -04:00
Jeremy Stretch 781409b5ae Fixes #15787: Convert User ID column to 64-bit integer 2024-04-21 13:55:39 -04:00
Arthur Hanson 88facbafbb
15761 filter IKE Proposals on IKE Policy detail view (#15766)
* 15761 filter IKEAProposals on IKEAPolicy detail view

* Add test for ike_policy filter

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-04-19 17:09:55 -04:00
Jeremy Stretch c9de3128ca Fixes #15790: Fix live preview support for EventRule comments 2024-04-19 17:09:02 -04:00
Arthur 94c31622ac 15588 set readonly nullable fields as allow_null=True 2024-04-19 16:17:28 -04:00
Jeremy Stretch 3d3c1c315b Update documentation for the DEFAULT_LANGUAGE configuration parameter 2024-04-19 16:15:32 -04:00
Jeremy Stretch f42d0336c2 Clean up layout of global search results 2024-04-19 15:27:25 -04:00
Jeremy Stretch db87fe96b7 Clean up bulk import view 2024-04-19 15:23:09 -04:00
Jeremy Stretch 0f0ab1a3be Closes #15547: Add comments field to CustomField model 2024-04-19 15:10:06 -04:00
Jeremy Stretch 824d66a54c Dissuade non-superusers from creating API tokens via the admin view 2024-04-19 14:34:25 -04:00
Jeremy Stretch 3551f3e021 Remove the is_staff restriction for admin menu items 2024-04-19 14:34:25 -04:00
Florian Derler 1a1300716c #15712: add imageattachments to vms 2024-04-19 14:15:50 -04:00
Arthur 4b83b5d0e1 15764 change vc_position from PositiveSmallInteger to PositiveInteger 2024-04-19 13:22:44 -04:00
Jeremy Stretch 174865b9aa Fixes #15760: Permit breaking of long words for wrap within object attribute tables 2024-04-19 13:18:25 -04:00
Jeremy Stretch c9bd59ab02 Fixes #15641: Fix adding/removing filters on advanced object selector widget 2024-04-19 13:15:57 -04:00
Jeremy Stretch 1efd80954e Formatting cleanup 2024-04-19 10:50:00 -04:00
Jeff Gehlbach f4c8f5f5b6 Add link to plugin certification program details in Plugin module of docs. Fixes #15769 2024-04-19 08:49:13 -04:00
Jeremy Stretch 480b36d65e Fixes #15698: Drop and recreate FK constraints on ObjectPermission M2M tables 2024-04-19 08:17:19 -04:00
Jeremy Stretch d0f0782bc0 Update changelog 2024-04-17 16:24:04 -04:00
Jeremy Stretch 19fe5ef25c Changelog for #15427, #15582, #15635 2024-04-17 16:18:57 -04:00
Arthur e303ccfd12 15636 change content_type_id to object_type_id for imageattachment 2024-04-17 16:13:29 -04:00
Arthur Hanson 928014c766
5509 Add Test cases for Custom Fields (#12312)
* 5509 add content type data to model tests create and update

* 5509 update use cf form data

* 5509 update tests to use CustomFieldTypeChoices

* 5509 update tests to check custom fields

* Simplify custom fields used for testing

* Move custom field data functions to testing.utils

* Move validate_custom_field_data() into assertInstanceEqual()

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-04-17 16:05:05 -04:00
Jeremy Stretch 75d6bfe42f Closes #15736: Remove annotated_date() template filter and annotated_now() template tag 2024-04-17 14:09:07 -04:00
Jeremy Stretch b5bb732031
Closes #10696: Break out instructions for installing & removing plugins (#15757)
* Closes #10696: Break out instructions for installing & rmeoving plugins

* Misc cleanup
2024-04-17 11:58:14 -04:00
Jeremy Stretch 95cc29d898 Closes #15752: Remove the ENABLE_LOCALIZATION configuration parameter 2024-04-17 11:54:29 -04:00
Jeremy Stretch 157df069e8 Closes #15738: Remove configuration parameters date & time formatting 2024-04-17 11:50:14 -04:00
Jeremy Stretch 77a4300888
Closes #15618: Always use ISO 8601 date & time formatting (#15737)
* Introduce the isodate(), isotime(), and isodatetime() template filters

* Display the relative time on mouse hover

* Render journal entry times in ISO 8601 format

* Use ISO 8601 format when displaying dates & times in a table

* Standardize the use of DateTimeColumn across all tables
2024-04-17 11:46:47 -04:00
Arthur Hanson b8cedfcc08
15582 check permissions on specific object when sync request (#15704)
* 15582 check permissions on specific object when sync request

* 15582 move permission check

* Enable translation of error message

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-04-17 10:09:50 -04:00
Javier de la Puente c5ae89ad03 Use endpoint_url in S3Backend 2024-04-17 09:59:39 -04:00
Markku Leiniö 4284028bb0 Closes #15727: Add tab template context variable in the plugin doc 2024-04-17 08:30:39 -04:00
Jeremy Stretch 3c3943c809 Convert "needs triage" label to a status indicator 2024-04-15 12:12:35 -04:00
Jeremy Stretch 17e8773c8c Changelog for #15640, #15644, #15654, #15668, #15685 2024-04-15 12:10:33 -04:00
Arthur Hanson f47b158863
15685 Allow decimal for cable length filter form (#15703)
* 15685 allow decimal for cable length filter

* 15685 allow decimal for cable length filter

* 15685 remove minlenth

* 15685 remove minlenth
2024-04-15 11:24:32 -04:00
Wrage, Florian f7e4fe2a9c Fixes #15640: add identifier field to search index of l2vpn 2024-04-15 10:53:53 -04:00
Julio Oliveira at Encora 5098422f68
Fixes #15644 - Add the ability to configure HSTS in NetBox (#15683)
* Added SECURE_HSTS_SECONDSm SECURE_HSTS_INCLUDE_SUBDOMAINS, and SECURE_HSTS_PRELOAD to settings.py

* Addressed some PR comments.

* Apply suggestions from code review

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-04-15 10:19:15 -04:00
Julio-Oliveira-Encora d7922a68d8 Fixed line 391 in netbox/virtualization/views.py. It was reeplaced "view_virtual_disk" with "view_virtualdisk" 2024-04-15 09:28:21 -04:00
Arthur 54c6d95fbb 15654 check for no termination in TunnelTerminationSerializer 2024-04-15 09:22:58 -04:00
Jeremy Stretch b7668fbfc3 PRVB 2024-04-04 16:23:16 -04:00
Jeremy Stretch 1c76034069
Merge pull request #15631 from netbox-community/develop
Release v3.7.5
2024-04-04 16:20:14 -04:00
Jeremy Stretch ad0e476788 Release v3.7.5 2024-04-04 16:06:42 -04:00
Jeremy Stretch e10f5ec3b4 Update source strings for translation 2024-04-04 15:12:51 -04:00
Jeremy Stretch 48a3f3cb70 Changelog for #14707, #15039, #15598, #15608, #15609 2024-04-04 15:05:49 -04:00
muTeREdO 238fa704b9
add example showing how to order results. (#15627)
* add example showing how to order results.

This addresses issue 15622 by building off filtering example to
show how to order results on a named field.

* Apply suggestions from code review

---------

Co-authored-by: Frank Clements <fclements@scoore.net>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-04-04 14:21:26 -04:00
padthaitofuhot 3b3511c43c Refactor 32264ac3 to re-separate bulk and single device creation. Fixes #15598. 2024-04-04 14:01:55 -04:00
Markku Leiniö da13fa5569 Closes #15039: Add Clone button in API token 2024-04-04 13:32:43 -04:00
Markku Leiniö 5b50920c61 Closes #14707: Change 'Interface' to 'Tunnel interface' in VPN tunnel forms 2024-04-04 12:57:35 -04:00
Jeremy Stretch d9a7b4ee0e Fixes #15609: Fix filtering providers list by assigned ASN 2024-04-04 10:45:57 -04:00
Jeremy Stretch 282dc7a705 Fixes #15608: Avoid caching values of null fields in search index 2024-04-04 10:45:19 -04:00
Jeremy Stretch e1753c0f9b Fix formatting 2024-04-04 10:03:12 -04:00
Jeremy Stretch 0e94f2e05d Simplify auto-assignment qualification 2024-04-04 09:53:49 -04:00
Jeremy Stretch 1c370f45d0 Add weighted assignments & enable for documentation issue 2024-04-04 09:20:20 -04:00
Jeremy Stretch 24e2fc253a Changelog for #15029, #15102, #15435, #15597 2024-04-03 14:12:35 -04:00
Arthur fca23c6419 15029 check if duplicate FHRP group assignment 2024-04-03 14:09:32 -04:00
Abhimanyu Saharan e4984d2883 fixed user and group filter form name #15102 2024-04-03 14:02:11 -04:00
Iain Buclaw 6030c521f4 Fix typo in Add Components dropdown 2024-04-03 13:29:32 -04:00
Arthur 83dad6f771 15597 add button_class choices to import form 2024-04-03 13:06:53 -04:00
Arthur 80802a4357 15353 add better script error message 2024-03-14 15:06:26 -07:00
133 changed files with 4252 additions and 3824 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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).

View File

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

View File

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

View File

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

View File

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

View File

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

72
docs/plugins/removal.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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('/')

View File

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

View File

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

View File

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

View File

@ -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 = [

View File

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

View File

@ -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'),

View File

@ -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'),

View File

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

View File

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

View File

@ -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),
),
]

View File

@ -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):

View File

@ -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')

View File

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

View File

@ -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()

View File

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

View File

@ -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')),

View File

@ -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),
),
]

View File

@ -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()

View File

@ -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])

View File

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

View File

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

View File

@ -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')

View File

@ -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):

View File

@ -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):

View File

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

View File

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

View File

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

View File

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

View File

@ -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):

View File

@ -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')
),
),
),

View File

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

View File

@ -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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -85,6 +85,7 @@
</tr>
</table>
</div>
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">&middot;</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 %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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