Compare commits

...

95 Commits

Author SHA1 Message Date
jeremystretch
2cd5fce62d Release v3.3.7 2022-11-01 17:09:55 -04:00
jeremystretch
ade307bc03 Fixes #10809: Permit nullifying site time_zone via REST API 2022-11-01 17:09:55 -04:00
jeremystretch
c8be4ef8e2 Fixes #10791: Permit nullifying VLAN group scope_type via REST API 2022-11-01 17:09:55 -04:00
jeremystretch
816214361d Fixes #10803: Fix exception when ordering contacts by number of assignments 2022-11-01 17:09:55 -04:00
jeremystretch
d1970ca85b Changelog for #10282, #10770 2022-11-01 17:09:55 -04:00
Arthur
8001694a4c 10282 fix race condition in API IP creation 2022-11-01 17:09:55 -04:00
Arthur
10e258739f 10770 fix social auth 2022-11-01 17:09:55 -04:00
jeremystretch
f3fdf03661 Changelog for #10666 (missed in v3.3.6) 2022-11-01 17:09:55 -04:00
jeremystretch
44814f759c PRVB 2022-11-01 17:09:55 -04:00
Jeremy Stretch
f1a7bceef2 Merge pull request #10758 from netbox-community/develop
Release v3.3.6
2022-10-26 10:18:44 -04:00
jeremystretch
eac2ace80b Release v3.3.6 2022-10-26 09:58:31 -04:00
Kevin Petremann
174ba6cf0f Fix LDAP auth: user never updated if inactive 2022-10-26 09:40:05 -04:00
jeremystretch
658c9347f3 Fixes #10682: Correct home view links to connection lists 2022-10-26 09:32:29 -04:00
jeremystretch
7b3ef2ade5 Fixes #10719: Prevent user without sufficient permission from creating an IP address via FHRP group creation 2022-10-26 08:44:20 -04:00
jeremystretch
2a62b628cf Fixes #10723: Distinguish between inside/outside NAT assignments for device/VM primary IPs 2022-10-26 08:23:50 -04:00
Arthur
d8c07abd68 10610 interface_id query on lag return vc interfaces 2022-10-26 08:10:03 -04:00
Arthur Hanson
8d486c5838 10716 add left-right plugins to tags page (#10744)
* 10716 add left-right plugins to tags page

* 10716 add back plugin_full_width
2022-10-26 08:05:15 -04:00
jeremystretch
eb91934d70 Fixes #10745: Correct display of status field in clusters list 2022-10-25 16:41:07 -04:00
jeremystretch
01654765e8 Fixes #10746: Add missing status attribute to cluster view 2022-10-25 16:38:32 -04:00
jeremystretch
4c504870e0 Tweak PR template language 2022-10-21 12:47:19 -04:00
jeremystretch
3d687a6c2d Closes #10718: Optimize object-based permissions enforcement 2022-10-21 12:43:36 -04:00
jeremystretch
96c4696417 Changelog for #9584, #10580, #10639 2022-10-20 16:31:52 -04:00
Arthur Hanson
e7659a5f99 9584 add device type (slug) to filter list (#10630)
* 9584 add device type (slug) to filter list

* 9584 add test
2022-10-20 16:27:51 -04:00
Craig Pund
53c9c3cf8d Fixes #10580 (#10687)
* change IP address accessor to parent object

* set IP assigned check to link to interface

* Fix Assigned not being orderable

Co-authored-by: Jeremy Stretch <jstretch@ns1.com>

Co-authored-by: Craig Pund <cpund@iuhealth.org>
Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
2022-10-20 16:26:26 -04:00
Patrick Rauscher
f60312febf Set *_COOKIE_PATH according to BASE_PATH
As discussed in #10639, all three `COOKIE_PATH`s should be set accordingly with the netbox-`BASE_PATH` to improve coexistance with other Django-projects probably hosted on the same Host
2022-10-20 16:24:38 -04:00
jeremystretch
7505baf3a1 Fixes #10712: Fix ModuleNotFoundError exception when generating API schema under Python 3.9+ 2022-10-20 15:49:13 -04:00
jeremystretch
33c6142365 Update documentation section options for new issues 2022-10-19 09:52:10 -04:00
jeremystretch
10e874039f Changelog for #10643, #10646 2022-10-19 09:02:09 -04:00
jeremystretch
060ee2dd96 Revert PR #10621 2022-10-19 08:55:30 -04:00
jeremystretch
43d1182b4b Fix styling for power input, rear port connection links 2022-10-19 08:47:14 -04:00
Arthur
d53da57f63 10646 fix cable power feed filter 2022-10-19 08:42:55 -04:00
Arthur Hanson
028b4b7ea7 10643 add fieldset to device role for improved add/edit form display (#10680)
* 10643 add fieldset to device role for improved add/edit form display

* 10643 update other forms

* 10643 update other forms

* Specify fieldsets for additional models

Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-10-19 08:35:23 -04:00
jeremystretch
4cb0230878 Closes #10685: Position A/Z termination cards above the fold under circuit view 2022-10-18 16:51:37 -04:00
Arthur Hanson
2fe8df3cbb 10655 fix contacts display in list views (#10681)
* 10655 fix contacts display in list views

* 10655 review changes
2022-10-18 16:47:14 -04:00
Arthur
64d67e3b00 10584 add clone fields to ipam-service 2022-10-18 16:06:31 -04:00
jeremystretch
aaf829898b Changelog for #10575, #10596 2022-10-12 08:41:41 -04:00
Arthur
8481cf66e3 10575 add requirements for openid connect packages 2022-10-12 08:39:14 -04:00
Arthur Hanson
bb150379a2 10571 replace deprecated mkdoc settings (#10622)
* 10571 replace deprecated mkdoc settings

* Omit landing page from docs nav menu

Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-10-12 08:36:02 -04:00
Arthur Hanson
cc811e5a56 10596 add overflow-auto to card-body (#10621)
* 10596 add overflow-auto to card-body

* 10596 add overflow-auto to card-body
2022-10-12 08:31:02 -04:00
jeremystretch
a9e583a693 Changelog for #9669, #9722 2022-10-05 12:04:47 -04:00
Jeremy Stretch
3a3ff474cb Merge pull request #10567 from scanplus/ldap-ca-cert
Added LDAP_CA_CERT_* to LDAP settings
2022-10-05 12:01:39 -04:00
Arthur Hanson
cc00789d35 9669 sanitize social auth usernames (#10549) 2022-10-05 11:50:47 -04:00
Jeremy Stretch
689f11a573 Merge pull request #10555 from kkthxbye-code/10527-update-js-dependencies
Fixes #10527 - Update JS dependencies
2022-10-05 11:04:37 -04:00
jeremystretch
ae90ad1fb7 PRVB 2022-10-05 10:13:02 -04:00
Jeremy Stretch
56d9725c39 Merge pull request #10570 from netbox-community/develop
Release v3.3.5
2022-10-05 10:10:44 -04:00
jeremystretch
1c69bfaf2c Release v3.3.5 2022-10-05 09:47:55 -04:00
Tobias Genannt
5e37f82b2f Added LDAP_CA_CERT_* to LDAP settings
These options can be used to specify a CA certificate to validate the LDAP
server certificate
2022-10-05 14:28:30 +02:00
jeremystretch
bdefd8ea8c Fixes #10562: Correct URL for contacts table tags column 2022-10-05 08:13:33 -04:00
kkthxbye-code
eabd405845 Fix graphiql by pinning esbuild 2022-10-04 22:00:32 +02:00
jeremystretch
03946f2ca8 Fixes #10559: Permit the pinning of a VM to a particular device within a cluster which has no site assignment 2022-10-04 15:46:55 -04:00
jeremystretch
fec8d1bc2f Fixes #10423: Enforce object type validation when creating journal entries 2022-10-04 15:26:52 -04:00
jeremystretch
53f5f46037 #10460: Fix PowerFeed details 2022-10-04 14:36:14 -04:00
kkthxbye-code
b227757b9a Update JS dependencies WIP 2022-10-04 15:02:37 +02:00
jeremystretch
eef5cefb5d Fixes #10460: Restore missing connection details for device components 2022-10-03 16:11:24 -04:00
jeremystretch
7712b81ab9 Fixes #10517: Automatically inherit site assignment from cluster when creating a virtual machine 2022-10-03 15:35:45 -04:00
jeremystretch
7feb86fe55 Changelog for #10352 2022-10-03 15:03:28 -04:00
PieterL75
d1efbf6620 Issue10352 removegetvariables (#10475)
* Add javascript to disable empty form fields

* add js cleanGetUrl

* use addEventListener submit

* use addEventListener

* update collectstatics

* Use FormData to remove empty fields

* optimeze ts-ignore

* update ts-ignore comment

* oneline of ts-ignore

* one line of ts-ingnore

* fix tsc errors by adding types (as per kkthxbye)

Co-authored-by: Pieter Lambrecht <pieter.lambrecht@sentia.com>
2022-10-03 14:32:01 -04:00
jeremystretch
aabee05a6a Changelog for #8424, #10491 2022-10-03 13:58:04 -04:00
jeremystretch
cf062b5b6a Closes #10346: Document how to access plugin config parameters 2022-10-03 13:56:46 -04:00
Arthur Hanson
0b6a3898fe 8424 device location (#10544)
* 8424 fix merge

* 8424 fix merge

* 8424 fix merge

* 8424 fix merge
2022-10-03 13:55:05 -04:00
Jeremy Stretch
517ebcfbcd Merge pull request #10525 from netbox-community/10491-delete-dependant
10491 improve error message for ProtectedError on contact assignment
2022-10-03 13:27:34 -04:00
jeremystretch
9ef24d3f43 Fixes #10513: Disable the reassignment of a module to a new device 2022-10-03 11:11:51 -04:00
Arthur
02ffc2ddee 10491 improve error message for ProtectedError on contact assignment 2022-09-30 09:09:21 -07:00
jeremystretch
62820ea2b8 Add workflow_dispatch event 2022-09-29 12:36:10 -04:00
jeremystretch
04738587e8 Move permissions block to root 2022-09-29 12:17:10 -04:00
jeremystretch
cbbfcd0e7b Bump stale to v6 2022-09-29 12:00:44 -04:00
jeremystretch
309a70df89 Tweak workflow permissions 2022-09-29 11:59:15 -04:00
Alex
4cb6984a65 GitHub Workflows security hardening (#10456)
* build: harden lock.yml permissions

Signed-off-by: Alex <aleksandrosansan@gmail.com>

* build: harden stale.yml permissions

Signed-off-by: Alex <aleksandrosansan@gmail.com>

* build: harden ci.yml permissions

Signed-off-by: Alex <aleksandrosansan@gmail.com>

Signed-off-by: Alex <aleksandrosansan@gmail.com>
2022-09-29 11:41:33 -04:00
jeremystretch
3c32c09a5a Fixes #10496: Use page.canonical_url to identify ReadTheDocs builds 2022-09-28 09:30:38 -04:00
jeremystretch
2d9852d6f1 Fixes #10408: Fix validation when attempting to add redundant contact assignments 2022-09-27 13:11:57 -04:00
jeremystretch
05542324fc Changelog for #10465, #10480 2022-09-27 11:53:11 -04:00
Patrick Hurrelmann
669e86f96e Fixes: #10465 Format all remaining displayed rackunits with floatformat (#10481)
* Fixes: #10465 Try to finish #10268 and format all remaining displayed rackunits with floatformat

* #10465: PEP8 fix

Co-authored-by: Patrick Hurrelmann <patrick.hurrelmann@nfon.com>
Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-09-27 11:24:19 -04:00
Jeremy Stretch
cbf928f363 Merge pull request #10482 from phurrelmann/10480-fix-link-target-on-cable-svg
Fixes: #10480 Fix link-target on cable-trace svg
2022-09-27 11:07:14 -04:00
Patrick Hurrelmann
43b18c13e3 Fixes: #10480 Fix link-target on cable-trace svg to open link in the same window. 2022-09-27 13:23:51 +02:00
jeremystretch
dda193247a Fixes #10470: Omit read-only custom fields from CSV import forms 2022-09-26 16:47:34 -04:00
jeremystretch
2463e4efd3 Fixes #10461: Enable filtering by read-only custom fields in the UI 2022-09-26 16:42:11 -04:00
jeremystretch
a0b17887fd Fixes #10445: Avoid rounding virtual machine memory values 2022-09-26 15:45:58 -04:00
jeremystretch
96784640e3 Changelog for #10435, #10439 2022-09-26 10:27:35 -04:00
Jeremy Stretch
b75d12fe05 Merge pull request #10442 from netbox-community/10435-untagged-vlan
10435 check if vm.cluster in qs
2022-09-26 10:25:48 -04:00
Jeremy Stretch
5e389c32ed Merge pull request #10463 from netbox-community/revert-10410-10408-add-contact
Revert "10408 add error message if already exists"
2022-09-26 10:24:54 -04:00
Jeremy Stretch
fd89ef04b6 Revert "10408 add error message if already exists" 2022-09-26 10:24:40 -04:00
Jeremy Stretch
abcc10e938 Merge pull request #10410 from netbox-community/10408-add-contact
10408 add error message if already exists
2022-09-26 10:24:23 -04:00
jeremystretch
3ad337dd15 Filter VLANs and VLANGroups by site or cluster site for VM 2022-09-26 10:08:54 -04:00
Jeremy Stretch
a527767caa Merge pull request #10455 from miaow2/10439-airlow-widget
10439 Add widget for Airflow field in DeviceTypeForm
2022-09-26 09:20:48 -04:00
Arthur Hanson
39129ecedf 10407 fix documentation link to requests (#10409)
* 10407 fix documentation link to requests

* Append page heading to URL

Co-authored-by: jeremystretch <jstretch@ns1.com>
2022-09-26 09:17:02 -04:00
Artem I. Kotik
c97d2d4fe9 Add widget for Airflow field in DeviceTypeForm 2022-09-24 15:49:23 +04:00
Arthur
7735634649 10435 check if vm.cluster in qs 2022-09-22 10:34:37 -07:00
Jonathan Senecal
148c6a6c23 Merge pull request #10432 from netbox-community/10431-pylance-is-no-longer-working-by-default-in-vscode
Add [tool.pyright] to pyproject.toml and fix #10431
2022-09-21 15:33:25 -04:00
Jonathan Senecal
360172cad0 Add [tool.pyright] to pyproject.toml 2022-09-21 15:19:40 -04:00
Daniel Sheppard
75c91232b4 Update changelog for #9497 2022-09-20 09:49:46 -05:00
Daniel Sheppard
0190c0225e Merge pull request #10420 from netbox-community/9497-fix-site-location-nonracked-device-display
Fixes #9497 - Change non-racked filter for sites/locations
2022-09-20 09:48:09 -05:00
Daniel Sheppard
86d366be4d Fixes #9651 - Document Pre-Change process for scripts 2022-09-20 09:46:23 -05:00
Daniel Sheppard
71d71a6b1b Fixes #9497 - Change filter for sites/locations 2022-09-20 09:26:40 -05:00
Arthur
695ad47fe9 10408 add error message if already exists 2022-09-19 10:46:16 -07:00
jeremystretch
1b62c11db5 PRVB 2022-09-16 13:41:09 -04:00
113 changed files with 2534 additions and 2987 deletions

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.3.4
placeholder: v3.3.7
validations:
required: true
- type: dropdown

View File

@@ -19,11 +19,15 @@ body:
label: Area
description: To what section of the documentation does this change primarily pertain?
options:
- Installation instructions
- Configuration parameters
- Functionality/features
- REST API
- Administration/development
- Features
- Installation/upgrade
- Getting started
- Configuration
- Customization
- Integrations/API
- Plugins
- Administration
- Development
- Other
validations:
required: true

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.3.4
placeholder: v3.3.7
validations:
required: true
- type: dropdown

View File

@@ -1,13 +1,14 @@
<!--
Thank you for your interest in contributing to NetBox! Please note that
our contribution policy requires that a feature request or bug report be
approved and assigned prior to filing a pull request. This helps avoid
wasting time and effort on something that we might not be able to accept.
approved and assigned prior to opening a pull request. This helps avoid
waste time and effort on a proposed change that we might not be able to
accept.
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
TO YOU, IT WE BE CLOSED AUTOMATICALLY.
TO YOU, IT WILL BE CLOSED AUTOMATICALLY.
Specify your assigned issue number on the line below.
Please specify your assigned issue number on the line below.
-->
### Fixes: #1234

View File

@@ -1,5 +1,7 @@
name: CI
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest

View File

@@ -4,6 +4,11 @@ name: 'Lock threads'
on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
lock:
@@ -11,7 +16,6 @@ jobs:
steps:
- uses: dessant/lock-threads@v3
with:
github-token: ${{ github.token }}
issue-inactive-days: 90
pr-inactive-days: 30
issue-lock-reason: 'resolved'

View File

@@ -1,14 +1,21 @@
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
name: 'Close stale issues/PRs'
on:
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
- uses: actions/stale@v6
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an

View File

@@ -68,7 +68,7 @@ drf-yasg[validation]
# Django wrapper for Graphene (GraphQL support)
# https://github.com/graphql-python/graphene-django
graphene_django
graphene_django<3.0
# WSGI HTTP server
# https://gunicorn.org/
@@ -80,7 +80,8 @@ Jinja2
# Simple markup language for rendering HTML
# https://github.com/Python-Markdown/markdown
Markdown
# mkdocs currently requires Markdown v3.3
Markdown<3.4
# File inclusion plugin for Python-Markdown
# https://github.com/cmacmackin/markdown-include

View File

@@ -2,8 +2,8 @@
{% block site_meta %}
{{ super() }}
{# Disable search indexing unless we're building for ReadTheDocs #}
{% if not config.extra.readthedocs %}
{# Disable search indexing unless we're building for ReadTheDocs (see #10496) #}
{% if page.canonical_url != 'https://docs.netbox.dev/' %}
<meta name="robots" content="noindex">
{% endif %}
{% endblock %}

View File

@@ -58,7 +58,7 @@ Email is sent from NetBox only for critical events or if configured for [logging
Default: None
A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://2.python-requests.org/en/master/user/advanced/). For example:
A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://requests.readthedocs.io/en/latest/user/advanced/#proxies). For example:
```python
HTTP_PROXIES = {

View File

@@ -129,6 +129,19 @@ The Script object provides a set of convenient functions for recording messages
Log messages are returned to the user upon execution of the script. Markdown rendering is supported for log messages.
## Change Logging
To generate the correct change log data when editing an existing object, a snapshot of the object must be taken before making any changes to the object.
```python
if obj.pk and hasattr(obj, 'snapshot'):
obj.snapshot()
obj.property = "New Value"
obj.full_clean()
obj.save()
```
## Variable Reference
### Default Options

View File

@@ -46,7 +46,7 @@ Next, create a file in the same directory as `configuration.py` (typically `/opt
### General Server Configuration
!!! info
When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure.
When using Active Directory you may need to specify a port on `AUTH_LDAP_SERVER_URI` to authenticate users from all domains in the forest. Use `3269` for secure, or `3268` for non-secure access to the GC (Global Catalog).
```python
import ldap
@@ -67,6 +67,16 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
# Note that this is a NetBox-specific setting which sets:
# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
LDAP_IGNORE_CERT_ERRORS = True
# Include this setting if you want to validate the LDAP server certificates against a CA certificate directory on your server
# Note that this is a NetBox-specific setting which sets:
# ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, LDAP_CA_CERT_DIR)
LDAP_CA_CERT_DIR = '/etc/ssl/certs'
# Include this setting if you want to validate the LDAP server certificates against your own CA.
# Note that this is a NetBox-specific setting which sets:
# ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, LDAP_CA_CERT_FILE)
LDAP_CA_CERT_FILE = '/path/to/example-CA.crt'
```
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.

View File

@@ -144,73 +144,73 @@ class MyModelFilterForm(NetBoxModelFilterSetForm):
In addition to the [form fields provided by Django](https://docs.djangoproject.com/en/stable/ref/forms/fields/), NetBox provides several field classes for use within forms to handle specific types of data. These can be imported from `utilities.forms.fields` and are documented below.
::: utilities.forms.ColorField
selection:
options:
members: false
::: utilities.forms.CommentField
selection:
options:
members: false
::: utilities.forms.JSONField
selection:
options:
members: false
::: utilities.forms.MACAddressField
selection:
options:
members: false
::: utilities.forms.SlugField
selection:
options:
members: false
## Choice Fields
::: utilities.forms.ChoiceField
selection:
options:
members: false
::: utilities.forms.MultipleChoiceField
selection:
options:
members: false
## Dynamic Object Fields
::: utilities.forms.DynamicModelChoiceField
selection:
options:
members: false
::: utilities.forms.DynamicModelMultipleChoiceField
selection:
options:
members: false
## Content Type Fields
::: utilities.forms.ContentTypeChoiceField
selection:
options:
members: false
::: utilities.forms.ContentTypeMultipleChoiceField
selection:
options:
members: false
## CSV Import Fields
::: utilities.forms.CSVChoiceField
selection:
options:
members: false
::: utilities.forms.CSVMultipleChoiceField
selection:
options:
members: false
::: utilities.forms.CSVModelChoiceField
selection:
options:
members: false
::: utilities.forms.CSVContentTypeField
selection:
options:
members: false
::: utilities.forms.CSVMultipleContentTypeField
selection:
options:
members: false

View File

@@ -32,11 +32,11 @@ schema = MyQuery
NetBox provides two object type classes for use by plugins.
::: netbox.graphql.types.BaseObjectType
selection:
options:
members: false
::: netbox.graphql.types.NetBoxObjectType
selection:
options:
members: false
## GraphQL Fields
@@ -44,9 +44,9 @@ NetBox provides two object type classes for use by plugins.
NetBox provides two field classes for use by plugins.
::: netbox.graphql.fields.ObjectField
selection:
options:
members: false
::: netbox.graphql.fields.ObjectListField
selection:
options:
members: false

View File

@@ -112,6 +112,14 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
!!! tip "Accessing Config Parameters"
Plugin configuration parameters can be accessed in `settings.PLUGINS_CONFIG`, mapped by plugin name. For example:
```python
from django.conf import settings
settings.PLUGINS_CONFIG['myplugin']['verbose_name']
```
## Create setup.py
`setup.py` is the [setup script](https://docs.python.org/3.8/distutils/setupscript.html) used to package and install our plugin once it's finished. The primary function of this script is to call the setuptools library's `setup()` function to create a Python distribution package. We can pass a number of keyword arguments to control the package creation as well as to provide metadata about the plugin. An example `setup.py` is below:

View File

@@ -52,38 +52,38 @@ This will automatically apply any user-specific preferences for the table. (If u
The table column classes listed below are supported for use in plugins. These classes can be imported from `netbox.tables.columns`.
::: netbox.tables.BooleanColumn
selection:
options:
members: false
::: netbox.tables.ChoiceFieldColumn
selection:
options:
members: false
::: netbox.tables.ColorColumn
selection:
options:
members: false
::: netbox.tables.ColoredLabelColumn
selection:
options:
members: false
::: netbox.tables.ContentTypeColumn
selection:
options:
members: false
::: netbox.tables.ContentTypesColumn
selection:
options:
members: false
::: netbox.tables.MarkdownColumn
selection:
options:
members: false
::: netbox.tables.TagColumn
selection:
options:
members: false
::: netbox.tables.TemplateColumn
selection:
options:
members:
- __init__

View File

@@ -84,24 +84,24 @@ Below are the class definitions for NetBox's object views. These views handle CR
::: netbox.views.generic.base.BaseObjectView
::: netbox.views.generic.ObjectView
selection:
options:
members:
- get_object
- get_template_name
::: netbox.views.generic.ObjectEditView
selection:
options:
members:
- get_object
- alter_object
::: netbox.views.generic.ObjectDeleteView
selection:
options:
members:
- get_object
::: netbox.views.generic.ObjectChildrenView
selection:
options:
members:
- get_children
- prep_table_data
@@ -113,22 +113,22 @@ Below are the class definitions for NetBox's multi-object views. These views han
::: netbox.views.generic.base.BaseMultiObjectView
::: netbox.views.generic.ObjectListView
selection:
options:
members:
- get_table
- export_table
- export_template
::: netbox.views.generic.BulkImportView
selection:
options:
members: false
::: netbox.views.generic.BulkEditView
selection:
options:
members: false
::: netbox.views.generic.BulkDeleteView
selection:
options:
members:
- get_form
@@ -137,12 +137,12 @@ Below are the class definitions for NetBox's multi-object views. These views han
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
::: netbox.views.generic.ObjectChangeLogView
selection:
options:
members:
- get_form
::: netbox.views.generic.ObjectJournalView
selection:
options:
members:
- get_form

View File

@@ -1,5 +1,75 @@
# NetBox v3.3
## v3.3.7 (2022-11-01)
### Bug Fixes
* [#10282](https://github.com/netbox-community/netbox/issues/10282) - Enforce advisory locks when allocating available IP addresses to prevent race conditions
* [#10770](https://github.com/netbox-community/netbox/issues/10282) - Fix social authentication for new users
* [#10791](https://github.com/netbox-community/netbox/issues/10791) - Permit nullifying VLAN group `scope_type` via REST API
* [#10803](https://github.com/netbox-community/netbox/issues/10803) - Fix exception when ordering contacts by number of assignments
* [#10809](https://github.com/netbox-community/netbox/issues/10809) - Permit nullifying site `time_zone` via REST API
---
## v3.3.6 (2022-10-26)
### Enhancements
* [#9584](https://github.com/netbox-community/netbox/issues/9584) - Enable filtering devices by device type slug
* [#9722](https://github.com/netbox-community/netbox/issues/9722) - Add LDAP configuration parameters to specify certificates
* [#10580](https://github.com/netbox-community/netbox/issues/10580) - Link "assigned" checkbox in IP address table to assigned interface
* [#10639](https://github.com/netbox-community/netbox/issues/10639) - Set cookie paths according to configured `BASE_PATH`
* [#10685](https://github.com/netbox-community/netbox/issues/10685) - Position A/Z termination cards above the fold under circuit view
### Bug Fixes
* [#9669](https://github.com/netbox-community/netbox/issues/9669) - Strip colons from usernames when using remote authentication
* [#10575](https://github.com/netbox-community/netbox/issues/10575) - Include OIDC dependencies for python-social-auth
* [#10584](https://github.com/netbox-community/netbox/issues/10584) - Fix service clone link
* [#10610](https://github.com/netbox-community/netbox/issues/10610) - Allow assignment of VC member to LAG on non-master peer
* [#10643](https://github.com/netbox-community/netbox/issues/10643) - Ensure consistent display of custom fields for all model forms
* [#10646](https://github.com/netbox-community/netbox/issues/10646) - Fix filtering of power feed by power panel when connecting a cable
* [#10655](https://github.com/netbox-community/netbox/issues/10655) - Correct display of assigned contacts in object tables
* [#10666](https://github.com/netbox-community/netbox/issues/10666) - Re-evaluate disabled LDAP user when processing API requests
* [#10682](https://github.com/netbox-community/netbox/issues/10682) - Correct home view links to connection lists
* [#10712](https://github.com/netbox-community/netbox/issues/10712) - Fix ModuleNotFoundError exception when generating API schema under Python 3.9+
* [#10716](https://github.com/netbox-community/netbox/issues/10716) - Add left/right page plugin content embeds for tag view
* [#10719](https://github.com/netbox-community/netbox/issues/10719) - Prevent user without sufficient permission from creating an IP address via FHRP group creation
* [#10723](https://github.com/netbox-community/netbox/issues/10723) - Distinguish between inside/outside NAT assignments for device/VM primary IPs
* [#10745](https://github.com/netbox-community/netbox/issues/10745) - Correct display of status field in clusters list
* [#10746](https://github.com/netbox-community/netbox/issues/10746) - Add missing status attribute to cluster view
---
## v3.3.5 (2022-10-05)
### Enhancements
* [#8424](https://github.com/netbox-community/netbox/issues/8424) - Include rack elevation under device view
* [#10352](https://github.com/netbox-community/netbox/issues/10352) - Omit extraneous URL query attributes during search
* [#10465](https://github.com/netbox-community/netbox/issues/10465) - Improve formatting of device heights and rack positions
### Bug Fixes
* [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view
* [#10408](https://github.com/netbox-community/netbox/issues/10408) - Fix validation when attempting to add redundant contact assignments
* [#10423](https://github.com/netbox-community/netbox/issues/10423) - Enforce object type validation when creating journal entries
* [#10435](https://github.com/netbox-community/netbox/issues/10435) - Fix exception when filtering VLANs by virtual machine with no cluster assigned
* [#10439](https://github.com/netbox-community/netbox/issues/10439) - Fix form widget styling for DeviceType airflow field
* [#10445](https://github.com/netbox-community/netbox/issues/10445) - Avoid rounding virtual machine memory values
* [#10460](https://github.com/netbox-community/netbox/issues/10460) - Restore missing connection details for device components
* [#10461](https://github.com/netbox-community/netbox/issues/10461) - Enable filtering by read-only custom fields in the UI
* [#10470](https://github.com/netbox-community/netbox/issues/10470) - Omit read-only custom fields from CSV import forms
* [#10480](https://github.com/netbox-community/netbox/issues/10480) - Cable trace SVG links should not force a new window
* [#10491](https://github.com/netbox-community/netbox/issues/10491) - Clarify representation of blocking contact assignments during contact deletion
* [#10513](https://github.com/netbox-community/netbox/issues/10513) - Disable the reassignment of a module to a new device
* [#10517](https://github.com/netbox-community/netbox/issues/10517) - Automatically inherit site assignment from cluster when creating a virtual machine
* [#10559](https://github.com/netbox-community/netbox/issues/10559) - Permit the pinning of a VM to a particular device within a cluster which has no site assignment
* [#10562](https://github.com/netbox-community/netbox/issues/10562) - Correct URL for contacts table tags column
---
## v3.3.4 (2022-09-16)
### Bug Fixes

View File

@@ -30,7 +30,7 @@ plugins:
- os.chdir('netbox/')
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
- django.setup()
rendering:
options:
heading_level: 3
members_order: source
show_root_heading: true
@@ -38,7 +38,6 @@ 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

View File

@@ -76,6 +76,12 @@ class ProviderNetworkForm(NetBoxModelForm):
class CircuitTypeForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Circuit Type', (
'name', 'slug', 'description', 'tags',
)),
)
class Meta:
model = CircuitType
fields = [

View File

@@ -1,8 +1,9 @@
import django_tables2 as tables
from circuits.models import *
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from .columns import CommitRateColumn
__all__ = (
@@ -39,7 +40,7 @@ class CircuitTypeTable(NetBoxTable):
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
class CircuitTable(TenancyColumnsMixin, NetBoxTable):
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
cid = tables.Column(
linkify=True,
verbose_name='Circuit ID'
@@ -58,9 +59,6 @@ class CircuitTable(TenancyColumnsMixin, NetBoxTable):
)
commit_rate = CommitRateColumn()
comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='circuits:circuit_list'
)

View File

@@ -1,7 +1,8 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from circuits.models import *
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin
from netbox.tables import NetBoxTable, columns
__all__ = (
@@ -10,7 +11,7 @@ __all__ = (
)
class ProviderTable(NetBoxTable):
class ProviderTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -31,9 +32,6 @@ class ProviderTable(NetBoxTable):
verbose_name='Circuits'
)
comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='circuits:provider_list'
)

View File

@@ -130,7 +130,7 @@ class SiteSerializer(NetBoxModelSerializer):
region = NestedRegionSerializer(required=False, allow_null=True)
group = NestedSiteGroupSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneSerializerField(required=False)
time_zone = TimeZoneSerializerField(required=False, allow_null=True)
asns = SerializedPKRelatedField(
queryset=ASN.objects.all(),
serializer=NestedASNSerializer,

View File

@@ -800,6 +800,12 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
to_field_name='slug',
label='Manufacturer (slug)',
)
device_type = django_filters.ModelMultipleChoiceFilter(
field_name='device_type__slug',
queryset=DeviceType.objects.all(),
to_field_name='slug',
label='Device type (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
label='Device type (ID)',
@@ -1357,7 +1363,7 @@ class InterfaceFilterSet(
try:
devices = Device.objects.filter(pk__in=id_list)
for device in devices:
vc_interface_ids += device.vc_interfaces().values_list('id', flat=True)
vc_interface_ids += device.vc_interfaces(if_master=False).values_list('id', flat=True)
return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist:
return queryset.none()

View File

@@ -108,7 +108,7 @@ def get_cable_form(a_type, b_type):
label='Power Feed',
disabled_indicator='_occupied',
query_params={
'powerpanel_id': f'$termination_{cable_end}_powerpanel',
'power_panel_id': f'$termination_{cable_end}_powerpanel',
}
)

View File

@@ -78,6 +78,12 @@ class RegionForm(NetBoxModelForm):
)
slug = SlugField()
fieldsets = (
('Region', (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
class Meta:
model = Region
fields = (
@@ -92,6 +98,12 @@ class SiteGroupForm(NetBoxModelForm):
)
slug = SlugField()
fieldsets = (
('Site Group', (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
class Meta:
model = SiteGroup
fields = (
@@ -213,6 +225,12 @@ class LocationForm(TenancyForm, NetBoxModelForm):
class RackRoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Rack Role', (
'name', 'slug', 'color', 'description', 'tags',
)),
)
class Meta:
model = RackRole
fields = [
@@ -340,6 +358,12 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
class ManufacturerForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Manufacturer', (
'name', 'slug', 'description', 'tags',
)),
)
class Meta:
model = Manufacturer
fields = [
@@ -373,6 +397,7 @@ class DeviceTypeForm(NetBoxModelForm):
'front_image', 'rear_image', 'comments', 'tags',
]
widgets = {
'airflow': StaticSelect(),
'subdevice_role': StaticSelect(),
'front_image': ClearableFileInput(attrs={
'accept': DEVICETYPE_IMAGE_FORMATS
@@ -405,6 +430,12 @@ class ModuleTypeForm(NetBoxModelForm):
class DeviceRoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Device Role', (
'name', 'slug', 'color', 'vm_role', 'description', 'tags',
)),
)
class Meta:
model = DeviceRole
fields = [
@@ -421,6 +452,13 @@ class PlatformForm(NetBoxModelForm):
max_length=64
)
fieldsets = (
('Platform', (
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
)),
)
class Meta:
model = Platform
fields = [
@@ -678,6 +716,7 @@ class ModuleForm(NetBoxModelForm):
super().__init__(*args, **kwargs)
if self.instance.pk:
self.fields['device'].disabled = True
self.fields['replicate_components'].initial = False
self.fields['replicate_components'].disabled = True
self.fields['adopt_components'].initial = False
@@ -1575,6 +1614,12 @@ class InventoryItemForm(DeviceComponentForm):
class InventoryItemRoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Inventory Item Role', (
'name', 'slug', 'color', 'description', 'tags',
)),
)
class Meta:
model = InventoryItemRole
fields = [

View File

@@ -987,6 +987,14 @@ class Module(NetBoxModel, ConfigContextModel):
def get_absolute_url(self):
return reverse('dcim:module', args=[self.pk])
def clean(self):
super().clean()
if self.module_bay.device != self.device:
raise ValidationError(
f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
)
def save(self, *args, **kwargs):
is_new = self.pk is None

View File

@@ -35,7 +35,7 @@ class Node(Hyperlink):
"""
def __init__(self, position, width, url, color, labels, radius=10, **extra):
super(Node, self).__init__(href=url, target='_blank', **extra)
super(Node, self).__init__(href=url, target='_parent', **extra)
x, y = position

View File

@@ -9,6 +9,7 @@ from svgwrite.text import Text
from django.conf import settings
from django.core.exceptions import FieldError
from django.db.models import Q
from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils.http import urlencode
@@ -41,7 +42,7 @@ def get_device_description(device):
device.device_role,
device.device_type.manufacturer.name,
device.device_type.model,
device.device_type.u_height,
floatformat(device.device_type.u_height),
device.asset_tag or '',
device.serial or ''
)

View File

@@ -1,12 +1,26 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem,
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
ConsolePort,
ConsoleServerPort,
Device,
DeviceBay,
DeviceRole,
FrontPort,
Interface,
InventoryItem,
InventoryItemRole,
ModuleBay,
Platform,
PowerOutlet,
PowerPort,
RearPort,
VirtualChassis,
)
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from .template_code import *
__all__ = (
@@ -137,7 +151,7 @@ class PlatformTable(NetBoxTable):
# Devices
#
class DeviceTable(TenancyColumnsMixin, NetBoxTable):
class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.TemplateColumn(
order_by=('_name',),
template_code=DEVICE_LINK
@@ -201,9 +215,6 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
verbose_name='VC Priority'
)
comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:device_list'
)

View File

@@ -1,10 +1,22 @@
import django_tables2 as tables
from dcim.models import (
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
ConsolePortTemplate,
ConsoleServerPortTemplate,
DeviceBayTemplate,
DeviceType,
FrontPortTemplate,
InterfaceTemplate,
InventoryItemTemplate,
Manufacturer,
ModuleBayTemplate,
PowerOutletTemplate,
PowerPortTemplate,
RearPortTemplate,
)
from tenancy.tables import ContactsColumnMixin
from netbox.tables import NetBoxTable, columns
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS
__all__ = (
@@ -27,7 +39,7 @@ __all__ = (
# Manufacturers
#
class ManufacturerTable(NetBoxTable):
class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -43,9 +55,6 @@ class ManufacturerTable(NetBoxTable):
verbose_name='Platforms'
)
slug = tables.Column()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:manufacturer_list'
)
@@ -85,6 +94,9 @@ class DeviceTypeTable(NetBoxTable):
tags = columns.TagColumn(
url_name='dcim:devicetype_list'
)
u_height = columns.TemplateColumn(
template_code='{{ value|floatformat }}'
)
class Meta(NetBoxTable.Meta):
model = DeviceType

View File

@@ -1,7 +1,9 @@
import django_tables2 as tables
from dcim.models import PowerFeed, PowerPanel
from tenancy.tables import ContactsColumnMixin
from netbox.tables import NetBoxTable, columns
from .devices import CableTerminationTable
__all__ = (
@@ -14,7 +16,7 @@ __all__ = (
# Power panels
#
class PowerPanelTable(NetBoxTable):
class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -29,9 +31,6 @@ class PowerPanelTable(NetBoxTable):
url_params={'power_panel_id': 'pk'},
verbose_name='Feeds'
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:powerpanel_list'
)

View File

@@ -1,9 +1,9 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from dcim.models import Rack, RackReservation, RackRole
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
__all__ = (
'RackTable',
@@ -37,7 +37,7 @@ class RackRoleTable(NetBoxTable):
# Racks
#
class RackTable(TenancyColumnsMixin, NetBoxTable):
class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column(
order_by=('_name',),
linkify=True
@@ -68,9 +68,6 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
orderable=False,
verbose_name='Power'
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:rack_list'
)

View File

@@ -1,8 +1,9 @@
import django_tables2 as tables
from dcim.models import Location, Region, Site, SiteGroup
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from .template_code import LOCATION_BUTTONS
__all__ = (
@@ -17,7 +18,7 @@ __all__ = (
# Regions
#
class RegionTable(NetBoxTable):
class RegionTable(ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
linkify=True
)
@@ -26,9 +27,6 @@ class RegionTable(NetBoxTable):
url_params={'region_id': 'pk'},
verbose_name='Sites'
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:region_list'
)
@@ -46,7 +44,7 @@ class RegionTable(NetBoxTable):
# Site groups
#
class SiteGroupTable(NetBoxTable):
class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
linkify=True
)
@@ -55,9 +53,6 @@ class SiteGroupTable(NetBoxTable):
url_params={'group_id': 'pk'},
verbose_name='Sites'
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:sitegroup_list'
)
@@ -75,7 +70,7 @@ class SiteGroupTable(NetBoxTable):
# Sites
#
class SiteTable(TenancyColumnsMixin, NetBoxTable):
class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -97,9 +92,6 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
verbose_name='ASN Count'
)
comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:site_list'
)
@@ -118,7 +110,7 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
# Locations
#
class LocationTable(TenancyColumnsMixin, NetBoxTable):
class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
linkify=True
)
@@ -136,9 +128,6 @@ class LocationTable(TenancyColumnsMixin, NetBoxTable):
url_params={'location_id': 'pk'},
verbose_name='Devices'
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='dcim:location_list'
)

View File

@@ -1643,6 +1643,8 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
device_types = DeviceType.objects.all()[:2]
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'device_type': [device_types[0].slug, device_types[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_devicerole(self):
device_roles = DeviceRole.objects.all()[:2]

View File

@@ -1778,10 +1778,12 @@ class ModuleTestCase(
ModuleBay(device=devices[0], name='Module Bay 2'),
ModuleBay(device=devices[0], name='Module Bay 3'),
ModuleBay(device=devices[0], name='Module Bay 4'),
ModuleBay(device=devices[0], name='Module Bay 5'),
ModuleBay(device=devices[1], name='Module Bay 1'),
ModuleBay(device=devices[1], name='Module Bay 2'),
ModuleBay(device=devices[1], name='Module Bay 3'),
ModuleBay(device=devices[1], name='Module Bay 4'),
ModuleBay(device=devices[1], name='Module Bay 5'),
)
ModuleBay.objects.bulk_create(module_bays)
@@ -1795,7 +1797,7 @@ class ModuleTestCase(
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'device': devices[1].pk,
'device': devices[0].pk,
'module_bay': module_bays[3].pk,
'module_type': module_types[0].pk,
'serial': 'A',
@@ -1867,7 +1869,6 @@ class ModuleTestCase(
self.assertIsNone(interface.module)
# Create a module with adopted components
form_data['module_bay'] = ModuleBay.objects.filter(device=device).first()
form_data['module_type'] = module_type
form_data['replicate_components'] = False
form_data['adopt_components'] = True

View File

@@ -355,7 +355,7 @@ class SiteView(generic.ObjectView):
nonracked_devices = Device.objects.filter(
site=instance,
position__isnull=True,
rack__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
@@ -450,7 +450,7 @@ class LocationView(generic.ObjectView):
nonracked_devices = Device.objects.filter(
location=instance,
position__isnull=True,
rack__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
@@ -1616,6 +1616,7 @@ class DeviceView(generic.ObjectView):
return {
'services': services,
'vc_members': vc_members,
'svg_extra': f'highlight=id:{instance.pk}'
}

View File

@@ -34,7 +34,9 @@ class CustomFieldsMixin:
return ContentType.objects.get_for_model(self.model)
def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type)
return CustomField.objects.filter(content_types=content_type).exclude(
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
)
def _get_form_field(self, customfield):
return customfield.to_form_field()
@@ -50,13 +52,6 @@ class CustomFieldsMixin:
field_name = f'cf_{customfield.name}'
self.fields[field_name] = self._get_form_field(customfield)
if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
self.fields[field_name].disabled = True
if self.fields[field_name].help_text:
self.fields[field_name].help_text += '<br />'
self.fields[field_name].help_text += '<i class="mdi mdi-alert-circle-outline"></i> ' \
'Field is set to read-only.'
# Annotate the field in the list of CustomField form fields
self.custom_fields[field_name] = customfield
if customfield.group_name not in self.custom_field_groups:

View File

@@ -297,12 +297,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
return model.objects.filter(pk__in=value)
return value
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibility=True, for_csv_import=False):
"""
Return a form field suitable for setting a CustomField's value for an object.
set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
enforce_visibility: Honor the value of CustomField.ui_visibility. Set to False for filtering.
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
"""
initial = self.default if set_initial else None
@@ -398,6 +399,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
if self.description:
field.help_text = escape(self.description)
# Annotate read-only fields
if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
field.disabled = True
prepend = '<br />' if field.help_text else ''
field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> Field is set to read-only.'
return field
def to_filter(self, lookup_expr=None):

View File

@@ -463,6 +463,14 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, WebhooksMixin
def get_absolute_url(self):
return reverse('extras:journalentry', args=[self.pk])
def clean(self):
super().clean()
# Prevent the creation of journal entries on unsupported models
permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
if self.assigned_object_type not in permitted_types:
raise ValidationError(f"Journaling is not supported for this object type ({self.assigned_object_type}).")
def get_kind_color(self):
return JournalEntryKindChoices.colors.get(self.kind)

View File

@@ -175,6 +175,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
queryset=ContentType.objects.filter(
model__in=VLANGROUP_SCOPE_TYPES
),
allow_null=True,
required=False,
default=None
)

View File

@@ -112,6 +112,18 @@ class IPAddressViewSet(NetBoxModelViewSet):
serializer_class = serializers.IPAddressSerializer
filterset_class = filtersets.IPAddressFilterSet
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def update(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs)
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
class FHRPGroupViewSet(NetBoxModelViewSet):
queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')

View File

@@ -88,6 +88,12 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm):
class RIRForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('RIR', (
'name', 'slug', 'is_private', 'description', 'tags',
)),
)
class Meta:
model = RIR
fields = [
@@ -164,6 +170,12 @@ class ASNForm(TenancyForm, NetBoxModelForm):
class RoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Role', (
'name', 'slug', 'weight', 'description', 'tags',
)),
)
class Meta:
model = Role
fields = [
@@ -540,6 +552,7 @@ class FHRPGroupForm(NetBoxModelForm):
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
user = getattr(instance, '_user', None) # Set under FHRPGroupEditView.alter_object()
# Check if we need to create a new IPAddress for the group
if self.cleaned_data.get('ip_address'):
@@ -553,7 +566,7 @@ class FHRPGroupForm(NetBoxModelForm):
ipaddress.save()
# Check that the new IPAddress conforms with any assigned object-level permissions
if not IPAddress.objects.filter(pk=ipaddress.pk).first():
if not IPAddress.objects.restrict(user, 'add').filter(pk=ipaddress.pk).first():
raise PermissionsViolation()
return instance
@@ -784,6 +797,12 @@ class ServiceTemplateForm(NetBoxModelForm):
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
)
fieldsets = (
('Service Template', (
'name', 'protocol', 'ports', 'description', 'tags',
)),
)
class Meta:
model = ServiceTemplate
fields = ('name', 'protocol', 'ports', 'description', 'tags')

View File

@@ -92,6 +92,8 @@ class Service(ServiceBase, NetBoxModel):
verbose_name='IP addresses'
)
clone_fields = ['protocol', 'ports', 'description', 'device', 'virtual_machine', 'ipaddresses', ]
class Meta:
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique

View File

@@ -81,30 +81,34 @@ class VLANQuerySet(RestrictedQuerySet):
# Find all relevant VLANGroups
q = Q()
if vm.cluster.site:
if vm.cluster.site.region:
site = vm.site or vm.cluster.site
if vm.cluster:
# Add VLANGroups scoped to the assigned cluster (or its group)
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'),
scope_id=vm.cluster_id
)
if vm.cluster.group:
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'),
scope_id__in=vm.cluster.site.region.get_ancestors(include_self=True)
)
if vm.cluster.site.group:
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
scope_id__in=vm.cluster.site.group.get_ancestors(include_self=True)
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'),
scope_id=vm.cluster.group_id
)
if site:
# Add VLANGroups scoped to the assigned site (or its group or region)
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'),
scope_id=vm.cluster.site_id
scope_id=site.pk
)
if vm.cluster.group:
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'),
scope_id=vm.cluster.group_id
)
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'),
scope_id=vm.cluster_id
)
if site.region:
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'),
scope_id__in=site.region.get_ancestors(include_self=True)
)
if site.group:
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
scope_id__in=site.group.get_ancestors(include_self=True)
)
vlan_groups = VLANGroup.objects.filter(q)
# Return all applicable VLANs
@@ -113,7 +117,7 @@ class VLANQuerySet(RestrictedQuerySet):
Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
Q(group__isnull=True, site__isnull=True) # Global VLANs
)
if vm.cluster.site:
q |= Q(site=vm.cluster.site)
if site:
q |= Q(site=site)
return self.filter(q)

View File

@@ -375,7 +375,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
)
assigned = columns.BooleanColumn(
accessor='assigned_object_id',
linkify=True,
linkify=lambda record: record.assigned_object.get_absolute_url(),
verbose_name='Assigned'
)
tags = columns.TagColumn(

View File

@@ -930,6 +930,12 @@ class FHRPGroupEditView(generic.ObjectEditView):
return return_url
def alter_object(self, obj, request, url_args, url_kwargs):
# Workaround to solve #10719. Capture the current user on the FHRPGroup instance so that
# we can evaluate permissions during the creation of a new IPAddress within the form.
obj._user = request.user
return obj
class FHRPGroupDeleteView(generic.ObjectDeleteView):
queryset = FHRPGroup.objects.all()

View File

@@ -58,22 +58,24 @@ class TokenAuthentication(authentication.TokenAuthentication):
if token.is_expired:
raise exceptions.AuthenticationFailed("Token expired")
if not token.user.is_active:
raise exceptions.AuthenticationFailed("User inactive")
user = token.user
# When LDAP authentication is active try to load user data from LDAP directory
if settings.REMOTE_AUTH_BACKEND == 'netbox.authentication.LDAPBackend':
from netbox.authentication import LDAPBackend
ldap_backend = LDAPBackend()
# Load from LDAP if FIND_GROUP_PERMS is active
if ldap_backend.settings.FIND_GROUP_PERMS:
user = ldap_backend.populate_user(token.user.username)
# Always query LDAP when user is not active, otherwise it is never activated again
if ldap_backend.settings.FIND_GROUP_PERMS or not token.user.is_active:
ldap_user = ldap_backend.populate_user(token.user.username)
# If the user is found in the LDAP directory use it, if not fallback to the local user
if user:
return user, token
if ldap_user:
user = ldap_user
return token.user, token
if not user.is_active:
raise exceptions.AuthenticationFailed("User inactive")
return user, token
class TokenPermissions(DjangoObjectPermissions):

View File

@@ -108,6 +108,5 @@ class ObjectValidationMixin:
conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
if conforming_count != len(instance):
raise ObjectDoesNotExist
else:
# Check that the instance is matched by the view's queryset
self.queryset.get(pk=instance.pk)
elif not self.queryset.filter(pk=instance.pk).exists():
raise ObjectDoesNotExist

View File

@@ -351,6 +351,14 @@ class LDAPBackend:
if getattr(ldap_config, 'LDAP_IGNORE_CERT_ERRORS', False):
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
# Optionally set CA cert directory
if ca_cert_dir := getattr(ldap_config, 'LDAP_CA_CERT_DIR', None):
ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, ca_cert_dir)
# Optionally set CA cert file
if ca_cert_file := getattr(ldap_config, 'LDAP_CA_CERT_FILE', None):
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, ca_cert_file)
return obj

View File

@@ -2,7 +2,7 @@ from django import forms
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
from extras.forms.customfields import CustomFieldsMixin
from extras.models import CustomField, Tag
from utilities.forms import BootstrapMixin, CSVModelForm
@@ -63,6 +63,11 @@ class NetBoxModelCSVForm(CSVModelForm, NetBoxModelForm):
"""
tags = None # Temporary fix in lieu of tag import support (see #9158)
def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type).filter(
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE
)
def _get_form_field(self, customfield):
return customfield.to_form_field(for_csv_import=True)
@@ -125,10 +130,10 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
)
def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type).exclude(
return super()._get_custom_fields(content_type).exclude(
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
Q(type=CustomFieldTypeChoices.TYPE_JSON)
)
def _get_form_field(self, customfield):
return customfield.to_form_field(set_initial=False, enforce_required=False)
return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False)

View File

@@ -20,7 +20,6 @@ class NetBoxFeatureSet(
CustomLinksMixin,
CustomValidationMixin,
ExportTemplatesMixin,
JournalingMixin,
TagsMixin,
WebhooksMixin
):
@@ -51,7 +50,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, models.Model)
abstract = True
class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model):
class NetBoxModel(CloningMixin, JournalingMixin, NetBoxFeatureSet, models.Model):
"""
Primary models represent real objects within the infrastructure being modeled.
"""

View File

@@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
# Environment setup
#
VERSION = '3.3.4'
VERSION = '3.3.7'
# Hostname
HOSTNAME = platform.node()
@@ -85,6 +85,7 @@ CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
CSRF_COOKIE_PATH = BASE_PATH or '/'
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
@@ -129,6 +130,8 @@ SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE',
SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
SESSION_COOKIE_PATH = BASE_PATH or '/'
LANGUAGE_COOKIE_PATH = BASE_PATH or '/'
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')
@@ -498,7 +501,7 @@ for param in dir(configuration):
# Force usage of PostgreSQL's JSONB field for extra data
SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'users.utils.clean_username'
#
# Django Prometheus

View File

@@ -1,5 +1,6 @@
import platform
import sys
from collections import namedtuple
from django.conf import settings
from django.core.cache import cache
@@ -8,6 +9,7 @@ from django.shortcuts import redirect, render
from django.template import loader
from django.template.exceptions import TemplateDoesNotExist
from django.urls import reverse
from django.utils.translation import gettext as _
from django.views.decorators.csrf import requires_csrf_token
from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
from django.views.generic import View
@@ -24,100 +26,90 @@ from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
from netbox.constants import SEARCH_MAX_RESULTS
from netbox.forms import SearchForm
from netbox.search import SEARCH_TYPES
from tenancy.models import Tenant
from tenancy.models import Contact, Tenant
from virtualization.models import Cluster, VirtualMachine
from wireless.models import WirelessLAN, WirelessLink
Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count'))
class HomeView(View):
template_name = 'home.html'
def get(self, request):
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
return redirect("login")
return redirect('login')
connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
console_connections = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
_path__is_complete=True
)
connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
).count
power_connections = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
_path__is_complete=True
)
connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
).count
interface_connections = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
_path__is_complete=True
)
).count
def get_count_queryset(model):
return model.objects.restrict(request.user, 'view').count
def build_stats():
org = (
("dcim.view_site", "Sites", Site.objects.restrict(request.user, 'view').count),
("tenancy.view_tenant", "Tenants", Tenant.objects.restrict(request.user, 'view').count),
Link(_('Sites'), 'dcim:site_list', 'dcim.view_site', get_count_queryset(Site)),
Link(_('Tenants'), 'tenancy:tenant_list', 'tenancy.view_tenant', get_count_queryset(Tenant)),
Link(_('Contacts'), 'tenancy:contact_list', 'tenancy.view_contact', get_count_queryset(Contact)),
)
dcim = (
("dcim.view_rack", "Racks", Rack.objects.restrict(request.user, 'view').count),
("dcim.view_devicetype", "Device Types", DeviceType.objects.restrict(request.user, 'view').count),
("dcim.view_device", "Devices", Device.objects.restrict(request.user, 'view').count),
Link(_('Racks'), 'dcim:rack_list', 'dcim.view_rack', get_count_queryset(Rack)),
Link(_('Device Types'), 'dcim:devicetype_list', 'dcim.view_devicetype', get_count_queryset(DeviceType)),
Link(_('Devices'), 'dcim:device_list', 'dcim.view_device', get_count_queryset(Device)),
)
ipam = (
("ipam.view_vrf", "VRFs", VRF.objects.restrict(request.user, 'view').count),
("ipam.view_aggregate", "Aggregates", Aggregate.objects.restrict(request.user, 'view').count),
("ipam.view_prefix", "Prefixes", Prefix.objects.restrict(request.user, 'view').count),
("ipam.view_iprange", "IP Ranges", IPRange.objects.restrict(request.user, 'view').count),
("ipam.view_ipaddress", "IP Addresses", IPAddress.objects.restrict(request.user, 'view').count),
("ipam.view_vlan", "VLANs", VLAN.objects.restrict(request.user, 'view').count)
Link(_('VRFs'), 'ipam:vrf_list', 'ipam.view_vrf', get_count_queryset(VRF)),
Link(_('Aggregates'), 'ipam:aggregate_list', 'ipam.view_aggregate', get_count_queryset(Aggregate)),
Link(_('Prefixes'), 'ipam:prefix_list', 'ipam.view_prefix', get_count_queryset(Prefix)),
Link(_('IP Ranges'), 'ipam:iprange_list', 'ipam.view_iprange', get_count_queryset(IPRange)),
Link(_('IP Addresses'), 'ipam:ipaddress_list', 'ipam.view_ipaddress', get_count_queryset(IPAddress)),
Link(_('VLANs'), 'ipam:vlan_list', 'ipam.view_vlan', get_count_queryset(VLAN)),
)
circuits = (
("circuits.view_provider", "Providers", Provider.objects.restrict(request.user, 'view').count),
("circuits.view_circuit", "Circuits", Circuit.objects.restrict(request.user, 'view').count),
Link(_('Providers'), 'circuits:provider_list', 'circuits.view_provider', get_count_queryset(Provider)),
Link(_('Circuits'), 'circuits:circuit_list', 'circuits.view_circuit', get_count_queryset(Circuit))
)
virtualization = (
("virtualization.view_cluster", "Clusters", Cluster.objects.restrict(request.user, 'view').count),
("virtualization.view_virtualmachine", "Virtual Machines", VirtualMachine.objects.restrict(request.user, 'view').count),
Link(_('Clusters'), 'virtualization:cluster_list', 'virtualization.view_cluster',
get_count_queryset(Cluster)),
Link(_('Virtual Machines'), 'virtualization:virtualmachine_list', 'virtualization.view_virtualmachine',
get_count_queryset(VirtualMachine)),
)
connections = (
("dcim.view_cable", "Cables", Cable.objects.restrict(request.user, 'view').count),
("dcim.view_consoleport", "Console", connected_consoleports.count),
("dcim.view_interface", "Interfaces", connected_interfaces.count),
("dcim.view_powerport", "Power Connections", connected_powerports.count),
Link(_('Cables'), 'dcim:cable_list', 'dcim.view_cable', get_count_queryset(Cable)),
Link(_('Interfaces'), 'dcim:interface_connections_list', 'dcim.view_interface', interface_connections),
Link(_('Console'), 'dcim:console_connections_list', 'dcim.view_consoleport', console_connections),
Link(_('Power'), 'dcim:power_connections_list', 'dcim.view_powerport', power_connections),
)
power = (
("dcim.view_powerpanel", "Power Panels", PowerPanel.objects.restrict(request.user, 'view').count),
("dcim.view_powerfeed", "Power Feeds", PowerFeed.objects.restrict(request.user, 'view').count),
Link(_('Power Panels'), 'dcim:powerpanel_list', 'dcim.view_powerpanel', get_count_queryset(PowerPanel)),
Link(_('Power Feeds'), 'dcim:powerfeed_list', 'dcim.view_powerfeed', get_count_queryset(PowerFeed)),
)
wireless = (
("wireless.view_wirelesslan", "Wireless LANs", WirelessLAN.objects.restrict(request.user, 'view').count),
("wireless.view_wirelesslink", "Wireless Links", WirelessLink.objects.restrict(request.user, 'view').count),
Link(_('Wireless LANs'), 'wireless:wirelesslan_list', 'wireless.view_wirelesslan',
get_count_queryset(WirelessLAN)),
Link(_('Wireless Links'), 'wireless:wirelesslink_list', 'wireless.view_wirelesslink',
get_count_queryset(WirelessLink)),
)
sections = (
("Organization", org, "domain"),
("IPAM", ipam, "counter"),
("Virtualization", virtualization, "monitor"),
("Inventory", dcim, "server"),
("Circuits", circuits, "transit-connection-variant"),
("Connections", connections, "cable-data"),
("Power", power, "flash"),
("Wireless", wireless, "wifi"),
stats = (
(_('Organization'), org, 'domain'),
(_('IPAM'), ipam, 'counter'),
(_('Virtualization'), virtualization, 'monitor'),
(_('Inventory'), dcim, 'server'),
(_('Circuits'), circuits, 'transit-connection-variant'),
(_('Connections'), connections, 'cable-data'),
(_('Power'), power, 'flash'),
(_('Wireless'), wireless, 'wifi'),
)
stats = []
for section_label, section_items, icon_class in sections:
items = []
for perm, item_label, get_count in section_items:
app, scope = perm.split(".")
url = ":".join((app, scope.replace("view_", "") + "_list"))
item = {
"label": item_label,
"count": None,
"url": url,
"disabled": True,
"icon": icon_class,
}
if request.user.has_perm(perm):
item["count"] = get_count()
item["disabled"] = False
items.append(item)
stats.append((section_label, items, icon_class))
return stats
# Compile changelog table

View File

@@ -173,7 +173,7 @@ class ObjectImportView(GetReturnURLMixin, BaseObjectView):
obj = model_form.save()
# Enforce object-level permissions
if not self.queryset.filter(pk=obj.pk).first():
if not self.queryset.filter(pk=obj.pk).exists():
raise PermissionsViolation()
# Iterate through the related object forms (if any), validating and saving each instance.
@@ -390,7 +390,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
obj = form.save()
# Check that the new object conforms with any assigned object-level permissions
if not self.queryset.filter(pk=obj.pk).first():
if not self.queryset.filter(pk=obj.pk).exists():
raise PermissionsViolation()
msg = '{} {}'.format(

View File

@@ -31,8 +31,7 @@
}
},
"rules": {
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-unused-vars-experimental": "error",
"@typescript-eslint/no-unused-vars": "error",
"no-unused-vars": "off",
"no-inner-declarations": "off",
"comma-dangle": ["error", "always-multiline"],

View File

@@ -1 +1 @@
:root{--nbx-trace-color: #000;--nbx-trace-node-bg: #e9ecef;--nbx-trace-termination-bg: #f8f9fa;--nbx-trace-cable-shadow: #343a40;--nbx-trace-attachment: #ced4da}:root[data-netbox-color-mode=dark]{--nbx-trace-color: #fff;--nbx-trace-node-bg: #212529;--nbx-trace-termination-bg: #343a40;--nbx-trace-cable-shadow: #e9ecef;--nbx-trace-attachment: #6c757d}*{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:.875rem}text{text-anchor:middle;dominant-baseline:middle}text:not([fill]){fill:var(--nbx-trace-color)}text.bold{font-weight:700}svg rect{fill:var(--nbx-trace-node-bg);stroke:#606060;stroke-width:1}svg rect .termination{fill:var(--nbx-trace-termination-bg)}svg .connector text{text-anchor:start}svg line{stroke-width:5px}svg polyline{fill:none;stroke-width:5px}svg .cable-shadow{stroke:var(--nbx-trace-cable-shadow);stroke-width:7px}svg line.wireless-link{stroke:var(--nbx-trace-attachment);stroke-dasharray:4px 12px;stroke-linecap:round}svg line.attachment{stroke:var(--nbx-trace-attachment);stroke-dasharray:5px}
:root{--nbx-trace-color: #000;--nbx-trace-node-bg: #e9ecef;--nbx-trace-termination-bg: #f8f9fa;--nbx-trace-cable-shadow: #343a40;--nbx-trace-attachment: #ced4da}:root[data-netbox-color-mode=dark]{--nbx-trace-color: #fff;--nbx-trace-node-bg: #212529;--nbx-trace-termination-bg: #343a40;--nbx-trace-cable-shadow: #e9ecef;--nbx-trace-attachment: #6c757d}*{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Liberation Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-size:.875rem}text{text-anchor:middle;dominant-baseline:middle}text:not([fill]){fill:var(--nbx-trace-color)}text.bold{font-weight:700}svg rect{fill:var(--nbx-trace-node-bg);stroke:#606060;stroke-width:1}svg rect .termination{fill:var(--nbx-trace-termination-bg)}svg .connector text{text-anchor:start}svg line{stroke-width:5px}svg polyline{fill:none;stroke-width:5px}svg .cable-shadow{stroke:var(--nbx-trace-cable-shadow);stroke-width:7px}svg line.wireless-link{stroke:var(--nbx-trace-attachment);stroke-dasharray:4px 12px;stroke-linecap:round}svg line.attachment{stroke:var(--nbx-trace-attachment);stroke-dasharray:5px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
svg{--nbx-rack-bg: #e9ecef;--nbx-rack-border: #000;--nbx-rack-slot-bg: #e9ecef;--nbx-rack-slot-border: #adb5bd;--nbx-rack-slot-hover-bg: #ced4da;--nbx-rack-link-color: #0d6efd;--nbx-rack-unit-color: #6c757d}svg[data-netbox-color-mode=dark]{--nbx-rack-bg: #343a40;--nbx-rack-border: #6c757d;--nbx-rack-slot-bg: #343a40;--nbx-rack-slot-border: #495057;--nbx-rack-slot-hover-bg: #212529;--nbx-rack-link-color: #9ec5fe;--nbx-rack-unit-color: #6c757d}*{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:.875rem}rect{box-sizing:border-box}text{text-anchor:middle;dominant-baseline:middle}svg .unit{margin:0;padding:5px 0;fill:var(--nbx-rack-unit-color)}svg .hidden{visibility:hidden}svg rect.shaded,svg image.shaded{opacity:25%}svg text.shaded{opacity:50%}svg .rack{fill:none;stroke-width:2px;stroke:var(--nbx-rack-border);background-color:var(--nbx-rack-bg)}svg .slot{fill:var(--nbx-rack-slot-bg);stroke:var(--nbx-rack-slot-border)}svg .slot:hover{fill:var(--nbx-rack-slot-hover-bg)}svg .slot+.add-device{fill:var(--nbx-rack-link-color);opacity:0;pointer-events:none}svg .slot:hover+.add-device{opacity:1}svg .slot.occupied[class],svg .slot.occupied:hover[class]{fill:url(#occupied)}svg .slot.blocked[class],svg .slot.blocked:hover[class]{fill:url(#blocked)}svg .slot.blocked:hover+.add-device{opacity:0}svg .reservation[class]{fill:url(#reserved)}
svg{--nbx-rack-bg: #e9ecef;--nbx-rack-border: #000;--nbx-rack-slot-bg: #e9ecef;--nbx-rack-slot-border: #adb5bd;--nbx-rack-slot-hover-bg: #ced4da;--nbx-rack-link-color: #0d6efd;--nbx-rack-unit-color: #6c757d}svg[data-netbox-color-mode=dark]{--nbx-rack-bg: #343a40;--nbx-rack-border: #6c757d;--nbx-rack-slot-bg: #343a40;--nbx-rack-slot-border: #495057;--nbx-rack-slot-hover-bg: #212529;--nbx-rack-link-color: #9ec5fe;--nbx-rack-unit-color: #6c757d}*{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Liberation Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-size:.875rem}rect{box-sizing:border-box}text{text-anchor:middle;dominant-baseline:middle}svg .unit{margin:0;padding:5px 0;fill:var(--nbx-rack-unit-color)}svg .hidden{visibility:hidden}svg rect.shaded,svg image.shaded{opacity:25%}svg text.shaded{opacity:50%}svg .rack{fill:none;stroke-width:2px;stroke:var(--nbx-rack-border);background-color:var(--nbx-rack-bg)}svg .slot{fill:var(--nbx-rack-slot-bg);stroke:var(--nbx-rack-slot-border)}svg .slot:hover{fill:var(--nbx-rack-slot-hover-bg)}svg .slot+.add-device{fill:var(--nbx-rack-link-color);opacity:0;pointer-events:none}svg .slot:hover+.add-device{opacity:1}svg .slot.occupied[class],svg .slot.occupied:hover[class]{fill:url(#occupied)}svg .slot.blocked[class],svg .slot.blocked:hover[class]{fill:url(#blocked)}svg .slot.blocked:hover+.add-device{opacity:0}svg .reservation[class]{fill:url(#reserved)}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -22,43 +22,38 @@
"validate:formatting:scripts": "prettier -c src/**/*.ts"
},
"dependencies": {
"@mdi/font": "^5.9.55",
"@popperjs/core": "^2.9.2",
"@mdi/font": "^7.0.96",
"@popperjs/core": "^2.11.6",
"bootstrap": "~5.0.2",
"clipboard": "^2.0.8",
"color2k": "^1.2.4",
"dayjs": "^1.10.4",
"flatpickr": "4.6.3",
"htmx.org": "^1.6.1",
"just-debounce-it": "^1.4.0",
"clipboard": "^2.0.11",
"color2k": "^2.0.0",
"dayjs": "^1.11.5",
"flatpickr": "4.6.13",
"htmx.org": "^1.8.0",
"just-debounce-it": "^3.1.1",
"masonry-layout": "^4.2.2",
"query-string": "^6.14.1",
"sass": "^1.32.8",
"simplebar": "^5.3.4",
"slim-select": "^1.27.0"
"query-string": "^7.1.1",
"sass": "^1.55.0",
"simplebar": "^5.3.9",
"slim-select": "^1.27.1"
},
"devDependencies": {
"@types/bootstrap": "^5.0.12",
"@types/cookie": "^0.4.0",
"@types/masonry-layout": "^4.2.2",
"@typescript-eslint/eslint-plugin": "^4.29.3",
"@typescript-eslint/parser": "^4.29.3",
"esbuild": "^0.12.24",
"esbuild-sass-plugin": "^1.5.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-typescript": "^2.4.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-prettier": "^3.4.1",
"prettier": "^2.3.2",
"typescript": "~4.3.5"
"@types/bootstrap": "^5.0.17",
"@types/cookie": "^0.5.1",
"@types/masonry-layout": "^4.2.5",
"@typescript-eslint/eslint-plugin": "^5.39.0",
"@typescript-eslint/parser": "^5.39.0",
"esbuild": "^0.13.15",
"esbuild-sass-plugin": "^2.3.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.7.1",
"typescript": "~4.8.4"
},
"resolutions": {
"eslint-import-resolver-typescript/**/path-parse": "^1.0.7",
"slim-select/**/trim-newlines": "^3.0.1",
"eslint/glob-parent": "^5.1.2",
"esbuild-sass-plugin/**/glob-parent": "^5.1.2",
"@typescript-eslint/**/glob-parent": "^5.1.2",
"eslint-plugin-import/**/hosted-git-info": "^2.8.9"
"@types/bootstrap/**/@popperjs/core": "^2.11.6"
}
}
}

View File

@@ -37,6 +37,18 @@ function initDocument(): void {
}
function initWindow(): void {
const documentForms = document.forms;
for (const documentForm of documentForms) {
if (documentForm.method.toUpperCase() == 'GET') {
documentForm.addEventListener('formdata', function (event: FormDataEvent) {
const formData: FormData = event.formData;
for (const [name, value] of Array.from(formData.entries())) {
if (value === '') formData.delete(name);
}
});
}
}
const contentContainer = document.querySelector<HTMLElement>('.content-container');
if (contentContainer !== null) {
// Focus the content container for accessible navigation.

View File

@@ -32,7 +32,7 @@ $spacing-s: $input-padding-x;
}
}
@import './node_modules/slim-select/src/slim-select/slimselect';
@import '../node_modules/slim-select/src/slim-select/slimselect';
.ss-main {
color: $form-select-color;

File diff suppressed because it is too large Load Diff

View File

@@ -60,23 +60,17 @@
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/comments.html' %}
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
{% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-6">
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
</div>
<div class="col col-md-6">
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}

View File

@@ -54,80 +54,40 @@
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Connection
</h5>
<div class="card-body">
{% if object.mark_connected %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
{% elif object.cable %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:consoleport_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>{{ object.connected_endpoint.device|linkify }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.connected_endpoint|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.connected_endpoint.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% else %}
<div class="text-muted">
Not Connected
{% if perms.dcim.add_cable %}
<div class="dropdown float-end">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Server Port</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
</li>
</ul>
</div>
{% endif %}
</div>
{% endif %}
<div class="card">
<h5 class="card-header">Connection</h5>
<div class="card-body">
{% if object.mark_connected %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
{% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' %}
{% else %}
<div class="text-muted">
Not Connected
{% if perms.dcim.add_cable %}
<div class="dropdown float-end">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Server Port</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
</li>
</ul>
</div>
{% endif %}
</div>
{% endif %}
</div>
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_right_page object %}
</div>
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">

View File

@@ -54,82 +54,40 @@
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Connection
</h5>
<div class="card-body">
{% if object.mark_connected %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
{% elif object.cable %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:consoleserverport_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>
{{ object.connected_endpoint.device|linkify }}
</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.connected_endpoint|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.connected_endpoint.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% else %}
<div class="text-muted">
Not Connected
{% if perms.dcim.add_cable %}
<div class="dropdown float-end">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Port</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
</li>
</ul>
</div>
{% endif %}
<div class="card">
<h5 class="card-header">Connection</h5>
<div class="card-body">
{% if object.mark_connected %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
{% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' %}
{% else %}
<div class="text-muted">
Not Connected
{% if perms.dcim.add_cable %}
<div class="dropdown float-end">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Port</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
</li>
</ul>
</div>
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_right_page object %}
</div>
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">

View File

@@ -7,7 +7,7 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="col col-12 col-xl-6">
<div class="card">
<h5 class="card-header">
Device
@@ -66,7 +66,7 @@
{% with object.parent_bay.device as parent %}
{{ parent|linkify }} / {{ object.parent_bay }}
{% if parent.position %}
(U{{ parent.position }} / {{ parent.get_face_display }})
(U{{ parent.position|floatformat }} / {{ parent.get_face_display }})
{% endif %}
{% endwith %}
{% elif object.rack and object.position %}
@@ -90,7 +90,7 @@
<tr>
<th scope="row">Device Type</th>
<td>
{{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height }}U)
{{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height|floatformat }}U)
</td>
</tr>
<tr>
@@ -153,7 +153,7 @@
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="col col-12 col-xl-6">
<div class="card">
<h5 class="card-header">Management</h5>
<div class="card-body">
@@ -178,7 +178,7 @@
{% if object.primary_ip4.nat_inside %}
(NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
{% elif object.primary_ip4.nat_outside.exists %}
(NAT for {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
(NAT: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %}
{% else %}
{{ ''|placeholder }}
@@ -193,7 +193,7 @@
{% if object.primary_ip6.nat_inside %}
(NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
{% elif object.primary_ip6.nat_outside.exists %}
(NAT for {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
(NAT: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %}
{% else %}
{{ ''|placeholder }}
@@ -286,6 +286,22 @@
</div>
{% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% if object.rack and object.position %}
<div class="row" style="margin-bottom: 20px">
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
<div style="margin-left: 30px">
<h4>Front</h4>
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' extra_params=svg_extra %}
</div>
</div>
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
<div style="margin-left: 30px">
<h4>Rear</h4>
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' extra_params=svg_extra %}
</div>
</div>
</div>
{% endif %}
{% plugin_right_page object %}
</div>
</div>

View File

@@ -29,7 +29,7 @@
</tr>
<tr>
<td>Height (U)</td>
<td>{{ object.u_height }}</td>
<td>{{ object.u_height|floatformat }}</td>
</tr>
<tr>
<td>Full Depth</td>

View File

@@ -1,14 +0,0 @@
<td>
{% if termination.parent_object.provider %}
<i class="mdi mdi-lightning-bolt" title="Circuit"></i>
<a href="{{ termination.parent_object.get_absolute_url }}">
{{ termination.parent_object.provider }}
{{ termination.parent_object }}
</a>
{% else %}
{{ termination.parent_object|linkify }}
{% endif %}
</td>
<td>
{{ termination|linkify }}
</td>

View File

@@ -0,0 +1,36 @@
<table class="table table-hover">
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_complete and object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Path Endpoints</th>
<td>
{% for endpoint in object.connected_endpoints %}
{% if endpoint.parent_object %}
{{ endpoint.parent_object|linkify }}
<i class="mdi mdi-chevron-right"></i>
{% endif %}
{{ endpoint|linkify }}
{% if not forloop.last %}<br />{% endif %}
{% empty %}
{{ ''|placeholder }}
{% endfor %}
</td>
</tr>
</table>

View File

@@ -144,89 +144,7 @@
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
</div>
{% elif object.cable %}
<table class="table table-hover">
{% if object.connected_endpoint.device %}
<tr>
<td colspan="2">
{% if object.connected_endpoint.enabled %}
<span class="badge bg-success">Enabled</span>
{% else %}
<span class="badge bg-danger">Disabled</span>
{% endif %}
</td>
</tr>
{% endif %}
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint.device %}
{% with iface=object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>{{ iface.device|linkify }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ iface|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ iface.get_type_display }}</td>
</tr>
<tr>
<th scope="row">LAG</th>
<td>{{ iface.lag|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ iface.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">MTU</th>
<td>{{ iface.mtu|placeholder }}</td>
</tr>
<tr>
<th scope="row">MAC Address</th>
<td>{{ iface.mac_address|placeholder }}</td>
</tr>
<tr>
<th scope="row">802.1Q Mode</th>
<td>{{ iface.get_mode_display }}</td>
</tr>
{% endwith %}
{% elif object.connected_endpoint.circuit %}
{% with ct=object.connected_endpoint %}
<tr>
<th scope="row">Provider</th>
<td>{{ ct.circuit.provider|linkify }}</td>
</tr>
<tr>
<th scope="row">Circuit</th>
<td>{{ ct.circuit|linkify }}</td>
</tr>
<tr>
<th scope="row">Side</th>
<td>{{ ct.term_side }}</td>
</tr>
{% endwith %}
{% endif %}
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_complete and object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
</table>
{% include 'dcim/inc/connection_endpoints.html' %}
{% elif object.wireless_link %}
<table class="table table-hover">
<tr>
@@ -238,7 +156,7 @@
</a>
</td>
</tr>
{% with peer_interface=object.connected_endpoint %}
{% with peer_interface=object.link_peers.0 %}
<tr>
<th scope="row">Device</th>
<td>{{ peer_interface.device|linkify }}</td>

View File

@@ -41,8 +41,8 @@
<tr>
<th scope="row">Connected Device</th>
<td>
{% if object.connected_endpoint %}
{{ object.connected_endpoint.device|linkify }} ({{ object.connected_endpoint }})
{% if object.connected_endpoints %}
{{ object.connected_endpoints.0.device|linkify }} ({{ object.connected_endpoints.0|linkify:"name" }})
{% else %}
{{ ''|placeholder }}
{% endif %}
@@ -50,7 +50,7 @@
</tr>
<tr>
<th scope="row">Utilization (Allocated)</th>
{% with utilization=object.connected_endpoint.get_power_draw %}
{% with utilization=object.connected_endpoints.0.get_power_draw %}
{% if utilization %}
<td>
{{ utilization.allocated }}VA / {{ object.available_power }}VA
@@ -100,73 +100,33 @@
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Connection
</h5>
<div class="card-body">
{% if object.mark_connected %}
<div class="text-muted">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
</div>
{% elif object.cable %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:powerfeed_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>{{ object.connected_endpoint.device|linkify }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.connected_endpoint|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.connected_endpoint.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% else %}
<div class="text-muted">
Not connected
</div>
{% endif %}
<div class="card">
<h5 class="card-header">Connection</h5>
<div class="card-body">
{% if object.mark_connected %}
<div class="text-muted">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected
</div>
{% if not object.mark_connected and not object.cable %}
<div class="card-footer">
{% if perms.dcim.add_cable %}
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm float-end">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
</a>
{% endif %}
{% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' %}
{% else %}
<div class="text-muted">
Not connected
</div>
{% endif %}
{% endif %}
</div>
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
{% if not object.mark_connected and not object.cable %}
<div class="card-footer">
{% if perms.dcim.add_cable %}
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm float-end">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
</a>
{% endif %}
</div>
{% endif %}
</div>
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">

View File

@@ -58,69 +58,29 @@
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Connection
</h5>
<div class="card-body">
{% if object.mark_connected %}
<div class="text-muted">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
</div>
{% elif object.cable %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:poweroutlet_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>{{ object.connected_endpoint.device|linkify }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.connected_endpoint|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.connected_endpoint.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% else %}
<div class="text-muted">
Not Connected
{% if perms.dcim.add_cable %}
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" title="Connect" class="btn btn-primary btn-sm float-end">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
</a>
{% endif %}
</div>
{% endif %}
<div class="card">
<h5 class="card-header">Connection</h5>
<div class="card-body">
{% if object.mark_connected %}
<div class="text-muted">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
</div>
{% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' %}
{% else %}
<div class="text-muted">
Not Connected
{% if perms.dcim.add_cable %}
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" title="Connect" class="btn btn-primary btn-sm float-end">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
</a>
{% endif %}
</div>
{% endif %}
</div>
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_right_page object %}
</div>
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">

View File

@@ -58,79 +58,39 @@
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Connection
</h5>
<div class="card-body">
{% if object.mark_connected %}
<div class="text-muted">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
</div>
{% elif object.cable %}
<table class="table table-hover attr-table">
<tr>
<th scope="row">Cable</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:powerport_trace' pk=object.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if object.connected_endpoint %}
<tr>
<th scope="row">Device</th>
<td>{{ object.connected_endpoint.device|linkify }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.connected_endpoint|linkify:"name" }}</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.connected_endpoint.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Path Status</th>
<td>
{% if object.path.is_active %}
<span class="badge bg-success">Reachable</span>
{% else %}
<span class="badge bg-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% else %}
<div class="text-muted">
Not Connected
{% if perms.dcim.add_cable %}
<span class="dropdown float-end">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Outlet</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Feed</a>
</li>
</ul>
</span>
{% endif %}
</div>
{% endif %}
<div class="card">
<h5 class="card-header">Connection</h5>
<div class="card-body">
{% if object.mark_connected %}
<div class="text-muted">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as Connected
</div>
{% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' %}
{% else %}
<div class="text-muted">
Not Connected
{% if perms.dcim.add_cable %}
<span class="dropdown float-end">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Power Outlet</a>
</li>
<li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Power Feed</a>
</li>
</ul>
</span>
{% endif %}
</div>
{% endif %}
</div>
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_right_page object %}
</div>
{% include 'dcim/inc/panels/inventory_items.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">

View File

@@ -105,16 +105,16 @@
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Interface</a>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Interface</a>
</li>
<li>
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Front Port</a>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
</li>
<li>
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Rear Port</a>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
</li>
<li>
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.device.site.pk }}&return_url={{ object.get_absolute_url }}">Circuit Termination</a>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.device.site.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Circuit Termination</a>
</li>
</ul>
</span>

View File

@@ -39,6 +39,7 @@
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
@@ -64,6 +65,7 @@
</table>
</div>
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">

View File

@@ -36,8 +36,8 @@
<div class="card-body">
<div class="list-group list-group-flush">
{% for item in items %}
{% if not item.disabled %}
<a href="{% url item.url %}" class="list-group-item list-group-item-action">
{% if item.permission in perms %}
<a href="{% url item.viewname %}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between align-items-center">
{{ item.label }}
<h4 class="mb-1">{{ item.count }}</h4>

View File

@@ -3,6 +3,9 @@
{% load form_helpers %}
{% block form %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Contact Assignment</h5>

View File

@@ -19,9 +19,13 @@
<th scope="row">Type</th>
<td>{{ object.type|linkify }}</td>
</tr>
<tr>
<th scope="row">Status</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr>
<th scope="row">Group</th>
<td>{{ object.group|linkify }}</td>
<td>{{ object.group|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">Tenant</th>
@@ -34,7 +38,7 @@
</tr>
<tr>
<th scope="row">Site</th>
<td>{{ object.site|linkify }}</td>
<td>{{ object.site|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">Virtual Machines</th>

View File

@@ -46,7 +46,7 @@
{% if object.primary_ip4.nat_inside %}
(NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
{% elif object.primary_ip4.nat_outside.exists %}
(NAT for {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
(NAT: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %}
{% else %}
{{ ''|placeholder }}
@@ -61,7 +61,7 @@
{% if object.primary_ip6.nat_inside %}
(NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
{% elif object.primary_ip6.nat_outside.exists %}
(NAT for {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
(NAT: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %}
{% else %}
{{ ''|placeholder }}

View File

@@ -27,6 +27,12 @@ class TenantGroupForm(NetBoxModelForm):
)
slug = SlugField()
fieldsets = (
('Tenant Group', (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
class Meta:
model = TenantGroup
fields = [
@@ -64,6 +70,12 @@ class ContactGroupForm(NetBoxModelForm):
)
slug = SlugField()
fieldsets = (
('Contact Group', (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
class Meta:
model = ContactGroup
fields = ('parent', 'name', 'slug', 'description', 'tags')
@@ -72,6 +84,12 @@ class ContactGroupForm(NetBoxModelForm):
class ContactRoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Contact Role', (
'name', 'slug', 'description', 'tags',
)),
)
class Meta:
model = ContactRole
fields = ('name', 'slug', 'description', 'tags')
@@ -119,8 +137,10 @@ class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ContactAssignment
fields = (
'group', 'contact', 'role', 'priority',
'content_type', 'object_id', 'group', 'contact', 'role', 'priority',
)
widgets = {
'content_type': forms.HiddenInput(),
'object_id': forms.HiddenInput(),
'priority': StaticSelect(),
}

View File

@@ -163,8 +163,8 @@ class ContactAssignment(WebhooksMixin, ChangeLoggedModel):
def __str__(self):
if self.priority:
return f"{self.contact} ({self.get_priority_display()})"
return str(self.contact)
return f"{self.contact} ({self.get_priority_display()}) -> {self.object}"
return str(f"{self.contact} -> {self.object}")
def get_absolute_url(self):
return reverse('tenancy:contact', args=[self.contact.pk])

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