mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-24 08:25:17 -06:00
Merge branch 'feature' into 11478-fix-cabling-interface-display
This commit is contained in:
commit
e9ec4b9644
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.5.7
|
||||
placeholder: v3.5.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.5.7
|
||||
placeholder: v3.5.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
@ -14,12 +14,25 @@
|
||||
</div>
|
||||
<h3></h3>
|
||||
|
||||
Some general tips for engaging here on GitHub:
|
||||
## :information_source: Welcome to the Stadium!
|
||||
|
||||
In her book [Working in Public](https://www.amazon.com/Working-Public-Making-Maintenance-Software/dp/0578675862), Nadia Eghbal defines four production models for open source projects, categorized by contributor and user growth: federations, clubs, toys, and stadiums. The NetBox project fits her definition of a stadium very well:
|
||||
|
||||
> Stadiums are projects with low contributor growth and high user growth. While they may receive casual contributions, their regular contributor base does not grow proportionately to their users. As a result, they tend to be powered by one or a few developers.
|
||||
|
||||
The bulk of NetBox's development is carried out by a handful of core maintainers, with occasional contributions from collaborators in the community. We find the stadium analogy very useful in conveying the roles and obligations of both contributors and users.
|
||||
|
||||
If you're a contributor, actively working on the center stage, you have an obligation to produce quality content that will benefit the project as a whole. Conversely, if you're in the audience consuming the work being produced, you have the option of making requests and suggestions, but must also recognize that contributors are under no obligation to act on them.
|
||||
|
||||
NetBox users are welcome to participate in either role, on stage or in the crowd. We ask only that you acknowledge the role you've chosen and respect the roles of others.
|
||||
|
||||
### General Tips for Working on GitHub
|
||||
|
||||
* Register for a free [GitHub account](https://github.com/signup) if you haven't already.
|
||||
* You can use [GitHub Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for formatting text and adding images.
|
||||
* To help mitigate notification spam, please avoid "bumping" issues with no activity. (To vote an issue up or down, use a :thumbsup: or :thumbsdown: reaction.)
|
||||
* Please avoid pinging members with `@` unless they've previously expressed interest or involvement with that particular issue.
|
||||
* Familiarize yourself with [this list of discussion anti-patterns](https://github.com/bradfitz/issue-tracker-behaviors) and make every effort to avoid them.
|
||||
|
||||
## :bug: Reporting Bugs
|
||||
|
||||
|
561
contrib/generated_schema.json
Normal file
561
contrib/generated_schema.json
Normal file
@ -0,0 +1,561 @@
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"airflow": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"front-to-rear",
|
||||
"rear-to-front",
|
||||
"left-to-right",
|
||||
"right-to-left",
|
||||
"side-to-rear",
|
||||
"passive",
|
||||
"mixed"
|
||||
]
|
||||
},
|
||||
"weight-unit": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"kg",
|
||||
"g",
|
||||
"lb",
|
||||
"oz"
|
||||
]
|
||||
},
|
||||
"subdevice-role": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"parent",
|
||||
"child"
|
||||
]
|
||||
},
|
||||
"console-port": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"de-9",
|
||||
"db-25",
|
||||
"rj-11",
|
||||
"rj-12",
|
||||
"rj-45",
|
||||
"mini-din-8",
|
||||
"usb-a",
|
||||
"usb-b",
|
||||
"usb-c",
|
||||
"usb-mini-a",
|
||||
"usb-mini-b",
|
||||
"usb-micro-a",
|
||||
"usb-micro-b",
|
||||
"usb-micro-ab",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"console-server-port": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"de-9",
|
||||
"db-25",
|
||||
"rj-11",
|
||||
"rj-12",
|
||||
"rj-45",
|
||||
"mini-din-8",
|
||||
"usb-a",
|
||||
"usb-b",
|
||||
"usb-c",
|
||||
"usb-mini-a",
|
||||
"usb-mini-b",
|
||||
"usb-micro-a",
|
||||
"usb-micro-b",
|
||||
"usb-micro-ab",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"power-port": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"iec-60320-c6",
|
||||
"iec-60320-c8",
|
||||
"iec-60320-c14",
|
||||
"iec-60320-c16",
|
||||
"iec-60320-c20",
|
||||
"iec-60320-c22",
|
||||
"iec-60309-p-n-e-4h",
|
||||
"iec-60309-p-n-e-6h",
|
||||
"iec-60309-p-n-e-9h",
|
||||
"iec-60309-2p-e-4h",
|
||||
"iec-60309-2p-e-6h",
|
||||
"iec-60309-2p-e-9h",
|
||||
"iec-60309-3p-e-4h",
|
||||
"iec-60309-3p-e-6h",
|
||||
"iec-60309-3p-e-9h",
|
||||
"iec-60309-3p-n-e-4h",
|
||||
"iec-60309-3p-n-e-6h",
|
||||
"iec-60309-3p-n-e-9h",
|
||||
"iec-60906-1",
|
||||
"nbr-14136-10a",
|
||||
"nbr-14136-20a",
|
||||
"nema-1-15p",
|
||||
"nema-5-15p",
|
||||
"nema-5-20p",
|
||||
"nema-5-30p",
|
||||
"nema-5-50p",
|
||||
"nema-6-15p",
|
||||
"nema-6-20p",
|
||||
"nema-6-30p",
|
||||
"nema-6-50p",
|
||||
"nema-10-30p",
|
||||
"nema-10-50p",
|
||||
"nema-14-20p",
|
||||
"nema-14-30p",
|
||||
"nema-14-50p",
|
||||
"nema-14-60p",
|
||||
"nema-15-15p",
|
||||
"nema-15-20p",
|
||||
"nema-15-30p",
|
||||
"nema-15-50p",
|
||||
"nema-15-60p",
|
||||
"nema-l1-15p",
|
||||
"nema-l5-15p",
|
||||
"nema-l5-20p",
|
||||
"nema-l5-30p",
|
||||
"nema-l5-50p",
|
||||
"nema-l6-15p",
|
||||
"nema-l6-20p",
|
||||
"nema-l6-30p",
|
||||
"nema-l6-50p",
|
||||
"nema-l10-30p",
|
||||
"nema-l14-20p",
|
||||
"nema-l14-30p",
|
||||
"nema-l14-50p",
|
||||
"nema-l14-60p",
|
||||
"nema-l15-20p",
|
||||
"nema-l15-30p",
|
||||
"nema-l15-50p",
|
||||
"nema-l15-60p",
|
||||
"nema-l21-20p",
|
||||
"nema-l21-30p",
|
||||
"nema-l22-30p",
|
||||
"cs6361c",
|
||||
"cs6365c",
|
||||
"cs8165c",
|
||||
"cs8265c",
|
||||
"cs8365c",
|
||||
"cs8465c",
|
||||
"ita-c",
|
||||
"ita-e",
|
||||
"ita-f",
|
||||
"ita-ef",
|
||||
"ita-g",
|
||||
"ita-h",
|
||||
"ita-i",
|
||||
"ita-j",
|
||||
"ita-k",
|
||||
"ita-l",
|
||||
"ita-m",
|
||||
"ita-n",
|
||||
"ita-o",
|
||||
"usb-a",
|
||||
"usb-b",
|
||||
"usb-c",
|
||||
"usb-mini-a",
|
||||
"usb-mini-b",
|
||||
"usb-micro-a",
|
||||
"usb-micro-b",
|
||||
"usb-micro-ab",
|
||||
"usb-3-b",
|
||||
"usb-3-micro-b",
|
||||
"dc-terminal",
|
||||
"saf-d-grid",
|
||||
"neutrik-powercon-20",
|
||||
"neutrik-powercon-32",
|
||||
"neutrik-powercon-true1",
|
||||
"neutrik-powercon-true1-top",
|
||||
"ubiquiti-smartpower",
|
||||
"hardwired",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"power-outlet": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"iec-60320-c5",
|
||||
"iec-60320-c7",
|
||||
"iec-60320-c13",
|
||||
"iec-60320-c15",
|
||||
"iec-60320-c19",
|
||||
"iec-60320-c21",
|
||||
"iec-60309-p-n-e-4h",
|
||||
"iec-60309-p-n-e-6h",
|
||||
"iec-60309-p-n-e-9h",
|
||||
"iec-60309-2p-e-4h",
|
||||
"iec-60309-2p-e-6h",
|
||||
"iec-60309-2p-e-9h",
|
||||
"iec-60309-3p-e-4h",
|
||||
"iec-60309-3p-e-6h",
|
||||
"iec-60309-3p-e-9h",
|
||||
"iec-60309-3p-n-e-4h",
|
||||
"iec-60309-3p-n-e-6h",
|
||||
"iec-60309-3p-n-e-9h",
|
||||
"iec-60906-1",
|
||||
"nbr-14136-10a",
|
||||
"nbr-14136-20a",
|
||||
"nema-1-15r",
|
||||
"nema-5-15r",
|
||||
"nema-5-20r",
|
||||
"nema-5-30r",
|
||||
"nema-5-50r",
|
||||
"nema-6-15r",
|
||||
"nema-6-20r",
|
||||
"nema-6-30r",
|
||||
"nema-6-50r",
|
||||
"nema-10-30r",
|
||||
"nema-10-50r",
|
||||
"nema-14-20r",
|
||||
"nema-14-30r",
|
||||
"nema-14-50r",
|
||||
"nema-14-60r",
|
||||
"nema-15-15r",
|
||||
"nema-15-20r",
|
||||
"nema-15-30r",
|
||||
"nema-15-50r",
|
||||
"nema-15-60r",
|
||||
"nema-l1-15r",
|
||||
"nema-l5-15r",
|
||||
"nema-l5-20r",
|
||||
"nema-l5-30r",
|
||||
"nema-l5-50r",
|
||||
"nema-l6-15r",
|
||||
"nema-l6-20r",
|
||||
"nema-l6-30r",
|
||||
"nema-l6-50r",
|
||||
"nema-l10-30r",
|
||||
"nema-l14-20r",
|
||||
"nema-l14-30r",
|
||||
"nema-l14-50r",
|
||||
"nema-l14-60r",
|
||||
"nema-l15-20r",
|
||||
"nema-l15-30r",
|
||||
"nema-l15-50r",
|
||||
"nema-l15-60r",
|
||||
"nema-l21-20r",
|
||||
"nema-l21-30r",
|
||||
"nema-l22-30r",
|
||||
"CS6360C",
|
||||
"CS6364C",
|
||||
"CS8164C",
|
||||
"CS8264C",
|
||||
"CS8364C",
|
||||
"CS8464C",
|
||||
"ita-e",
|
||||
"ita-f",
|
||||
"ita-g",
|
||||
"ita-h",
|
||||
"ita-i",
|
||||
"ita-j",
|
||||
"ita-k",
|
||||
"ita-l",
|
||||
"ita-m",
|
||||
"ita-n",
|
||||
"ita-o",
|
||||
"ita-multistandard",
|
||||
"usb-a",
|
||||
"usb-micro-b",
|
||||
"usb-c",
|
||||
"dc-terminal",
|
||||
"hdot-cx",
|
||||
"saf-d-grid",
|
||||
"neutrik-powercon-20a",
|
||||
"neutrik-powercon-32a",
|
||||
"neutrik-powercon-true1",
|
||||
"neutrik-powercon-true1-top",
|
||||
"ubiquiti-smartpower",
|
||||
"hardwired",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"feed-leg": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"A",
|
||||
"B",
|
||||
"C"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"interface": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"virtual",
|
||||
"bridge",
|
||||
"lag",
|
||||
"100base-fx",
|
||||
"100base-lfx",
|
||||
"100base-tx",
|
||||
"100base-t1",
|
||||
"1000base-t",
|
||||
"2.5gbase-t",
|
||||
"5gbase-t",
|
||||
"10gbase-t",
|
||||
"10gbase-cx4",
|
||||
"1000base-x-gbic",
|
||||
"1000base-x-sfp",
|
||||
"10gbase-x-sfpp",
|
||||
"10gbase-x-xfp",
|
||||
"10gbase-x-xenpak",
|
||||
"10gbase-x-x2",
|
||||
"25gbase-x-sfp28",
|
||||
"50gbase-x-sfp56",
|
||||
"40gbase-x-qsfpp",
|
||||
"50gbase-x-sfp28",
|
||||
"100gbase-x-cfp",
|
||||
"100gbase-x-cfp2",
|
||||
"200gbase-x-cfp2",
|
||||
"100gbase-x-cfp4",
|
||||
"100gbase-x-cxp",
|
||||
"100gbase-x-cpak",
|
||||
"100gbase-x-dsfp",
|
||||
"100gbase-x-sfpdd",
|
||||
"100gbase-x-qsfp28",
|
||||
"100gbase-x-qsfpdd",
|
||||
"200gbase-x-qsfp56",
|
||||
"200gbase-x-qsfpdd",
|
||||
"400gbase-x-qsfpdd",
|
||||
"400gbase-x-osfp",
|
||||
"400gbase-x-cdfp",
|
||||
"400gbase-x-cfp8",
|
||||
"800gbase-x-qsfpdd",
|
||||
"800gbase-x-osfp",
|
||||
"1000base-kx",
|
||||
"10gbase-kr",
|
||||
"10gbase-kx4",
|
||||
"25gbase-kr",
|
||||
"40gbase-kr4",
|
||||
"50gbase-kr",
|
||||
"100gbase-kp4",
|
||||
"100gbase-kr2",
|
||||
"100gbase-kr4",
|
||||
"ieee802.11a",
|
||||
"ieee802.11g",
|
||||
"ieee802.11n",
|
||||
"ieee802.11ac",
|
||||
"ieee802.11ad",
|
||||
"ieee802.11ax",
|
||||
"ieee802.11ay",
|
||||
"ieee802.15.1",
|
||||
"other-wireless",
|
||||
"gsm",
|
||||
"cdma",
|
||||
"lte",
|
||||
"sonet-oc3",
|
||||
"sonet-oc12",
|
||||
"sonet-oc48",
|
||||
"sonet-oc192",
|
||||
"sonet-oc768",
|
||||
"sonet-oc1920",
|
||||
"sonet-oc3840",
|
||||
"1gfc-sfp",
|
||||
"2gfc-sfp",
|
||||
"4gfc-sfp",
|
||||
"8gfc-sfpp",
|
||||
"16gfc-sfpp",
|
||||
"32gfc-sfp28",
|
||||
"64gfc-qsfpp",
|
||||
"128gfc-qsfp28",
|
||||
"infiniband-sdr",
|
||||
"infiniband-ddr",
|
||||
"infiniband-qdr",
|
||||
"infiniband-fdr10",
|
||||
"infiniband-fdr",
|
||||
"infiniband-edr",
|
||||
"infiniband-hdr",
|
||||
"infiniband-ndr",
|
||||
"infiniband-xdr",
|
||||
"t1",
|
||||
"e1",
|
||||
"t3",
|
||||
"e3",
|
||||
"xdsl",
|
||||
"docsis",
|
||||
"gpon",
|
||||
"xg-pon",
|
||||
"xgs-pon",
|
||||
"ng-pon2",
|
||||
"epon",
|
||||
"10g-epon",
|
||||
"cisco-stackwise",
|
||||
"cisco-stackwise-plus",
|
||||
"cisco-flexstack",
|
||||
"cisco-flexstack-plus",
|
||||
"cisco-stackwise-80",
|
||||
"cisco-stackwise-160",
|
||||
"cisco-stackwise-320",
|
||||
"cisco-stackwise-480",
|
||||
"cisco-stackwise-1t",
|
||||
"juniper-vcp",
|
||||
"extreme-summitstack",
|
||||
"extreme-summitstack-128",
|
||||
"extreme-summitstack-256",
|
||||
"extreme-summitstack-512",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"poe_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pd",
|
||||
"pse"
|
||||
]
|
||||
},
|
||||
"poe_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"type1-ieee802.3af",
|
||||
"type2-ieee802.3at",
|
||||
"type3-ieee802.3bt",
|
||||
"type4-ieee802.3bt",
|
||||
"passive-24v-2pair",
|
||||
"passive-24v-4pair",
|
||||
"passive-48v-2pair",
|
||||
"passive-48v-4pair"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"front-port": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"8p8c",
|
||||
"8p6c",
|
||||
"8p4c",
|
||||
"8p2c",
|
||||
"6p6c",
|
||||
"6p4c",
|
||||
"6p2c",
|
||||
"4p4c",
|
||||
"4p2c",
|
||||
"gg45",
|
||||
"tera-4p",
|
||||
"tera-2p",
|
||||
"tera-1p",
|
||||
"110-punch",
|
||||
"bnc",
|
||||
"f",
|
||||
"n",
|
||||
"mrj21",
|
||||
"fc",
|
||||
"lc",
|
||||
"lc-pc",
|
||||
"lc-upc",
|
||||
"lc-apc",
|
||||
"lsh",
|
||||
"lsh-pc",
|
||||
"lsh-upc",
|
||||
"lsh-apc",
|
||||
"lx5",
|
||||
"lx5-pc",
|
||||
"lx5-upc",
|
||||
"lx5-apc",
|
||||
"mpo",
|
||||
"mtrj",
|
||||
"sc",
|
||||
"sc-pc",
|
||||
"sc-upc",
|
||||
"sc-apc",
|
||||
"st",
|
||||
"cs",
|
||||
"sn",
|
||||
"sma-905",
|
||||
"sma-906",
|
||||
"urm-p2",
|
||||
"urm-p4",
|
||||
"urm-p8",
|
||||
"splice",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"rear-port": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"8p8c",
|
||||
"8p6c",
|
||||
"8p4c",
|
||||
"8p2c",
|
||||
"6p6c",
|
||||
"6p4c",
|
||||
"6p2c",
|
||||
"4p4c",
|
||||
"4p2c",
|
||||
"gg45",
|
||||
"tera-4p",
|
||||
"tera-2p",
|
||||
"tera-1p",
|
||||
"110-punch",
|
||||
"bnc",
|
||||
"f",
|
||||
"n",
|
||||
"mrj21",
|
||||
"fc",
|
||||
"lc",
|
||||
"lc-pc",
|
||||
"lc-upc",
|
||||
"lc-apc",
|
||||
"lsh",
|
||||
"lsh-pc",
|
||||
"lsh-upc",
|
||||
"lsh-apc",
|
||||
"lx5",
|
||||
"lx5-pc",
|
||||
"lx5-upc",
|
||||
"lx5-apc",
|
||||
"mpo",
|
||||
"mtrj",
|
||||
"sc",
|
||||
"sc-pc",
|
||||
"sc-upc",
|
||||
"sc-apc",
|
||||
"st",
|
||||
"cs",
|
||||
"sn",
|
||||
"sma-905",
|
||||
"sma-906",
|
||||
"urm-p2",
|
||||
"urm-p4",
|
||||
"urm-p8",
|
||||
"splice",
|
||||
"other"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
|
||||
Default: True
|
||||
|
||||
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token immediately upon its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
|
||||
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token prior to its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
|
||||
|
||||
---
|
||||
|
||||
|
123
docs/development/internationalization.md
Normal file
123
docs/development/internationalization.md
Normal file
@ -0,0 +1,123 @@
|
||||
# Internationalization
|
||||
|
||||
Beginning with NetBox v4.0, NetBox will leverage [Django's automatic translation](https://docs.djangoproject.com/en/stable/topics/i18n/translation/) to support languages other than English. This page details the areas of the project which require special attention to ensure functioning translation support. Briefly, these include:
|
||||
|
||||
* The `verbose_name` and `verbose_name_plural` Meta attributes for each model
|
||||
* The `verbose_name` and (if defined) `help_text` for each model field
|
||||
* The `label` for each form field
|
||||
* Headers for `fieldsets` on each form class
|
||||
* The `verbose_name` for each table column
|
||||
* All human-readable strings within templates must be wrapped with `{% trans %}` or `{% blocktrans %}`
|
||||
|
||||
The rest of this document elaborates on each of the items above.
|
||||
|
||||
## General Guidance
|
||||
|
||||
* Wrap human-readable strings with Django's `gettext()` or `gettext_lazy()` utility functions to enable automatic translation. Generally, `gettext_lazy()` is preferred (and sometimes required) to defer translation until the string is displayed.
|
||||
|
||||
* By convention, the preferred translation function is typically imported as an underscore (`_`) to minimize boilerplate code. Thus, you will often see translation as e.g. `_("Some text")`. It is still an option to import and use alternative translation functions (e.g. `pgettext()` and `ngettext()`) normally as needed.
|
||||
|
||||
* Avoid passing markup and other non-natural language where possible. Everything wrapped by a translation function gets exported to a messages file for translation by a human.
|
||||
|
||||
* Where the intended meaning of the translated string may not be obvious, use `pgettext()` or `pgettext_lazy()` to include assisting context for the translator. For example:
|
||||
|
||||
```python
|
||||
# Context, string
|
||||
pgettext("month name", "May")
|
||||
```
|
||||
|
||||
* **Format strings do not support translation.** Avoid "f" strings for messages that must support translation. Instead, use `format()` to accomplish variable replacement:
|
||||
|
||||
```python
|
||||
# Translation will not work
|
||||
f"There are {count} objects"
|
||||
|
||||
# Do this instead
|
||||
"There are {count} objects".format(count=count)
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
1. Import `gettext_lazy` as `_`.
|
||||
2. Ensure both `verbose_name` and `verbose_name_plural` are defined under the model's `Meta` class and wrapped with the `gettext_lazy()` shortcut.
|
||||
3. Ensure each model field specifies a `verbose_name` wrapped with `gettext_lazy()`.
|
||||
4. Ensure any `help_text` attributes on model fields are also wrapped with `gettext_lazy()`.
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
class Circuit(PrimaryModel):
|
||||
commit_rate = models.PositiveIntegerField(
|
||||
...
|
||||
verbose_name=_('commit rate (Kbps)'),
|
||||
help_text=_("Committed rate")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('circuit')
|
||||
verbose_name_plural = _('circuits')
|
||||
```
|
||||
|
||||
## Forms
|
||||
|
||||
1. Import `gettext_lazy` as `_`.
|
||||
2. All form fields must specify a `label` wrapped with `gettext_lazy()`.
|
||||
3. All headers under a form's `fieldsets` property must be wrapped with `gettext_lazy()`.
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
...
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
(_('Circuit'), ('provider', 'type', 'status', 'description')),
|
||||
)
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
1. Import `gettext_lazy` as `_`.
|
||||
2. All table columns must specify a `verbose_name` wrapped with `gettext_lazy()`.
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
provider = tables.Column(
|
||||
verbose_name=_('Provider'),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
1. Ensure translation support is enabled by including `{% load i18n %}` at the top of the template.
|
||||
2. Use the [`{% trans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#translate-template-tag) tag (short for "translate") to wrap short strings.
|
||||
3. Longer strings may be enclosed between [`{% blocktrans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#blocktranslate-template-tag) and `{% endblocktrans %}` tags to improve readability and to enable variable replacement.
|
||||
4. Avoid passing HTML within translated strings where possible, as this can complicate the work needed of human translators to develop message maps.
|
||||
|
||||
```
|
||||
{% load i18n %}
|
||||
|
||||
{# A short string #}
|
||||
<h5 class="card-header">{% trans "Circuit List" %}</h5>
|
||||
|
||||
{# A longer string with a context variable #}
|
||||
{% blocktrans with count=object.circuits.count %}
|
||||
There are {count} circuits. Would you like to continue?
|
||||
{% endblocktrans %}
|
||||
```
|
||||
|
||||
!!! warning
|
||||
The `{% blocktrans %}` tag supports only **limited variable replacement**, comparable to the `format()` method on Python strings. It does not permit access to object attributes or the use of other template tags or filters inside it. Ensure that any necessary context is passed as simple variables.
|
||||
|
||||
!!! info
|
||||
The `{% trans %}` and `{% blocktrans %}` support the inclusion of contextual hints for translators using the `context` argument:
|
||||
|
||||
```nohighlight
|
||||
{% trans "May" context "month name" %}
|
||||
```
|
@ -43,10 +43,22 @@ Follow these instructions to perform a new installation of NetBox in a temporary
|
||||
|
||||
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
|
||||
|
||||
### Rebuild Demo Data (After Release)
|
||||
|
||||
After the release of a new minor version, generate a new demo data snapshot compatible with the new release. See the [`netbox-demo-data`](https://github.com/netbox-community/netbox-demo-data) repository for instructions.
|
||||
|
||||
---
|
||||
|
||||
## Patch Releases
|
||||
|
||||
### Notify netbox-docker Project of Any Relevant Changes
|
||||
|
||||
Notify the [`netbox-docker`](https://github.com/netbox-community/netbox-docker) maintainers (in **#netbox-docker**) of any changes that may be relevant to their build process, including:
|
||||
|
||||
* Significant changes to `upgrade.sh`
|
||||
* Increases in minimum versions for service dependencies (PostgreSQL, Redis, etc.)
|
||||
* Any changes to the reference installation
|
||||
|
||||
### Update Requirements
|
||||
|
||||
Before each release, update each of NetBox's Python dependencies to its most recent stable version. These are defined in `requirements.txt`, which is updated from `base_requirements.txt` using `pip`. To do this:
|
||||
@ -58,6 +70,16 @@ Before each release, update each of NetBox's Python dependencies to its most rec
|
||||
|
||||
In cases where upgrading a dependency to its most recent release is breaking, it should be constrained to its current minor version in `base_requirements.txt` with an explanatory comment and revisited for the next major NetBox release (see the [Address Constrained Dependencies](#address-constrained-dependencies) section above).
|
||||
|
||||
### Rebuild the Device Type Definition Schema
|
||||
|
||||
Run the following command to update the device type definition validation schema:
|
||||
|
||||
```nohighlight
|
||||
./manage.py buildschema --write
|
||||
```
|
||||
|
||||
This will automatically update the schema file at `contrib/generated_schema.json`.
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
* Update the `VERSION` constant in `settings.py` to the new release version.
|
||||
|
@ -570,27 +570,26 @@ The NetBox REST API primarily employs token-based authentication. For convenienc
|
||||
|
||||
A token is a unique identifier mapped to a NetBox user account. Each user may have one or more tokens which he or she can use for authentication when making REST API requests. To create a token, navigate to the API tokens page under your user profile.
|
||||
|
||||
!!! note
|
||||
All users can create and manage REST API tokens under the user control panel in the UI. The ability to view, add, change, or delete tokens via the REST API itself is controlled by the relevant model permissions, assigned to users and/or groups in the admin UI. These permissions should be used with great care to avoid accidentally permitting a user to create tokens for other user accounts.
|
||||
By default, all users can create and manage their own REST API tokens under the user control panel in the UI or via the REST API. This ability can be disabled by overriding the [`DEFAULT_PERMISSIONS`](../configuration/security.md#default_permissions) configuration parameter.
|
||||
|
||||
Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation.
|
||||
|
||||
By default, a token can be used to perform all actions via the API that a user would be permitted to do via the web UI. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only.
|
||||
|
||||
Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.
|
||||
|
||||
!!! warning "Restricting Token Retrieval"
|
||||
!!! info "Restricting Token Retrieval"
|
||||
The ability to retrieve the key value of a previously-created API token can be restricted by disabling the [`ALLOW_TOKEN_RETRIEVAL`](../configuration/security.md#allow_token_retrieval) configuration parameter.
|
||||
|
||||
### Restricting Write Operations
|
||||
|
||||
By default, a token can be used to perform all actions via the API that a user would be permitted to do via the web UI. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only.
|
||||
|
||||
#### Client IP Restriction
|
||||
|
||||
Each API token can optionally be restricted by client IP address. If one or more allowed IP prefixes/addresses is defined for a token, authentication will fail for any client connecting from an IP address outside the defined range(s). This enables restricting the use a token to a specific client. (By default, any client IP address is permitted.)
|
||||
|
||||
#### Creating Tokens for Other Users
|
||||
|
||||
It is possible to provision authentication tokens for other users via the REST API. To do, so the requesting user must have the `users.grant_token` permission assigned. While all users have inherent permission to create their own tokens, this permission is required to enable the creation of tokens for other users.
|
||||
|
||||

|
||||
It is possible to provision authentication tokens for other users via the REST API. To do, so the requesting user must have the `users.grant_token` permission assigned. While all users have inherent permission by default to create their own tokens, this permission is required to enable the creation of tokens for other users.
|
||||
|
||||
!!! warning "Exercise Caution"
|
||||
The ability to create tokens on behalf of other users enables the requestor to access the created token. This ability is intended e.g. for the provisioning of tokens by automated services, and should be used with extreme caution to avoid a security compromise.
|
||||
@ -627,7 +626,7 @@ When a token is used to authenticate a request, its `last_updated` time updated
|
||||
|
||||
### Initial Token Provisioning
|
||||
|
||||
Ideally, each user should provision his or her own REST API token(s) via the web UI. However, you may encounter where a token must be created by a user via the REST API itself. NetBox provides a special endpoint to provision tokens using a valid username and password combination.
|
||||
Ideally, each user should provision his or her own API token(s) via the web UI. However, you may encounter a scenario where a token must be created by a user via the REST API itself. NetBox provides a special endpoint to provision tokens using a valid username and password combination. (Note that the user must have permission to create API tokens regardless of the interface used.)
|
||||
|
||||
To provision a token via the REST API, make a `POST` request to the `/api/users/tokens/provision/` endpoint:
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 33 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 22 KiB |
@ -26,7 +26,9 @@ Every model includes by default a numeric primary key. This value is generated a
|
||||
|
||||
Plugin models can leverage certain NetBox features by inheriting from NetBox's `NetBoxModel` class. This class extends the plugin model to enable features unique to NetBox, including:
|
||||
|
||||
* Bookmarks
|
||||
* Change logging
|
||||
* Cloning
|
||||
* Custom fields
|
||||
* Custom links
|
||||
* Custom validation
|
||||
@ -105,6 +107,8 @@ For more information about database migrations, see the [Django documentation](h
|
||||
!!! warning
|
||||
Please note that only the classes which appear in this documentation are currently supported. Although other classes may be present within the `features` module, they are not yet supported for use by plugins.
|
||||
|
||||
::: netbox.models.features.BookmarksMixin
|
||||
|
||||
::: netbox.models.features.ChangeLoggingMixin
|
||||
|
||||
::: netbox.models.features.CloningMixin
|
||||
|
@ -1,6 +1,33 @@
|
||||
# NetBox v3.5
|
||||
|
||||
## v3.5.8 (FUTURE)
|
||||
## v3.5.9 (FUTURE)
|
||||
|
||||
---
|
||||
|
||||
## v3.5.8 (2023-08-15)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#10030](https://github.com/netbox-community/netbox/issues/10030) - Ship a validation schema for the device type library with each release
|
||||
* [#11675](https://github.com/netbox-community/netbox/issues/11675) - Add support for specifying import/export route targets during VRF bulk import
|
||||
* [#11922](https://github.com/netbox-community/netbox/issues/11922) - Automatically populate any VDC assignments from the parent when adding a child interface via the UI
|
||||
* [#12889](https://github.com/netbox-community/netbox/issues/12889) - Add 400GE CFP2 interface type
|
||||
* [#13033](https://github.com/netbox-community/netbox/issues/13033) - Add human-friendly speed column to interfaces table
|
||||
* [#13151](https://github.com/netbox-community/netbox/issues/13151) - Add "assigned" filter for IP addresses
|
||||
* [#13368](https://github.com/netbox-community/netbox/issues/13368) - List installed plugins on the server error report page
|
||||
* [#13442](https://github.com/netbox-community/netbox/issues/13442) - Add 200 and 400 Gbps speeds to dropdown choices on interface form
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#11578](https://github.com/netbox-community/netbox/issues/11578) - Fix schema definition for available IP & VLAN REST API endpoints
|
||||
* [#12639](https://github.com/netbox-community/netbox/issues/12639) - Raise validation error for invalid alphanumeric ranges when creating objects
|
||||
* [#12665](https://github.com/netbox-community/netbox/issues/12665) - Avoid escaping semicolons when rendering custom links
|
||||
* [#12750](https://github.com/netbox-community/netbox/issues/12750) - Automatically delete an AutoSyncRecord when its object is deleted
|
||||
* [#13343](https://github.com/netbox-community/netbox/issues/13343) - Fix filtering of circuits under provider network view
|
||||
* [#13369](https://github.com/netbox-community/netbox/issues/13369) - Fix job termination status for failed reports
|
||||
* [#13414](https://github.com/netbox-community/netbox/issues/13414) - Fix support for "hide-if-unset" custom fields on bulk import forms
|
||||
* [#13446](https://github.com/netbox-community/netbox/issues/13446) - Don't disable bulk edit/delete buttons after deselecting "select all" checkbox
|
||||
* [#13451](https://github.com/netbox-community/netbox/issues/13451) - Disable table ordering for custom link columns
|
||||
|
||||
---
|
||||
|
||||
|
@ -1,12 +1,17 @@
|
||||
# NetBox v3.6
|
||||
|
||||
## v3.6-beta2 (FUTURE)
|
||||
## v3.6-beta2 (2023-08-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#13351](https://github.com/netbox-community/netbox/issues/13351) - Fix missing text due to incorrectly applied translation tags
|
||||
* [#13361](https://github.com/netbox-community/netbox/issues/13361) - Extra choices field on custom field choice set form should not be required
|
||||
* [#13363](https://github.com/netbox-community/netbox/issues/13363) - Fix API endpoint for custom field choice selector in forms
|
||||
* [#13376](https://github.com/netbox-community/netbox/issues/13376) - Restrict add/remove tag fields by model on bulk edit forms
|
||||
* [#13410](https://github.com/netbox-community/netbox/issues/13410) - Fix rendering of custom choice fields with large number of choices
|
||||
* [#13433](https://github.com/netbox-community/netbox/issues/13433) - User field on API token form should be required
|
||||
* [#13434](https://github.com/netbox-community/netbox/issues/13434) - Randomly generate initial keys prior to the creation of new tokens
|
||||
* [#13437](https://github.com/netbox-community/netbox/issues/13437) - Display bookmark button only for relevant objects
|
||||
|
||||
---
|
||||
|
||||
@ -19,6 +24,8 @@
|
||||
* The `choices` array field has been removed from the CustomField model. Any defined choices are automatically migrated to CustomFieldChoiceSets, accessible via the new `choice_set` field on the CustomField model.
|
||||
* The `napalm_driver` and `napalm_args` fields (which were deprecated in v3.5) have been removed from the Platform model.
|
||||
* The `device` and `device_id` filter for interfaces will now only filter the specific device. Two new filters `virtual_chassis_member` and `virtual_chassis_member_id` have been added to allow for retrieval of Virtual Chassis interfaces from any Virtual Chassis member.
|
||||
* Reports and scripts are now returned within a `results` list when fetched via the REST API, consistent with other models.
|
||||
* Superusers can no longer retrieve API token keys via the web UI if [`ALLOW_TOKEN_RETRIEVAL`](https://docs.netbox.dev/en/stable/configuration/security/#allow_token_retrieval) is disabled. (The admin view has been removed per [#13044](https://github.com/netbox-community/netbox/issues/13044).)
|
||||
|
||||
### New Features
|
||||
|
||||
@ -71,7 +78,10 @@ Tags may now be restricted to use with designated object types. Tags that have n
|
||||
* [#11936](https://github.com/netbox-community/netbox/issues/11936) - Introduce support for tags and custom fields on webhooks
|
||||
* [#12175](https://github.com/netbox-community/netbox/issues/12175) - Permit racks to start numbering at values greater than one
|
||||
* [#12210](https://github.com/netbox-community/netbox/issues/12210) - Add tenancy assignment for power feeds
|
||||
* [#12461](https://github.com/netbox-community/netbox/issues/12461) - Add config template rendering for virtual machines
|
||||
* [#12814](https://github.com/netbox-community/netbox/issues/12814) - Expose NetBox models within ConfigTemplate rendering context
|
||||
* [#12882](https://github.com/netbox-community/netbox/issues/12882) - Add tag support for contact assignments
|
||||
* [#13037](https://github.com/netbox-community/netbox/issues/13037) - Return reports & scripts within a `results` list when fetched via the REST API
|
||||
* [#13170](https://github.com/netbox-community/netbox/issues/13170) - Add `rf_role` to InterfaceTemplate
|
||||
* [#13269](https://github.com/netbox-community/netbox/issues/13269) - Cache the number of assigned component templates for device types
|
||||
|
||||
@ -123,6 +133,10 @@ Tags may now be restricted to use with designated object types. Tags that have n
|
||||
* extras.CustomField
|
||||
* Removed the `choices` array field
|
||||
* Added the `choice_set` foreign key field (to ChoiceSet)
|
||||
* extras.Report
|
||||
* Reports are now returned within a `results` list
|
||||
* extras.Script
|
||||
* Scripts are now returned within a `results` list
|
||||
* extras.Tag
|
||||
* Added the `object_types` field for optional restriction to specific object types
|
||||
* extras.Webhook
|
||||
|
@ -211,6 +211,7 @@ nav:
|
||||
- ConfigContext: 'models/extras/configcontext.md'
|
||||
- ConfigTemplate: 'models/extras/configtemplate.md'
|
||||
- CustomField: 'models/extras/customfield.md'
|
||||
- CustomFieldChoiceSet: 'models/extras/customfieldchoiceset.md'
|
||||
- CustomLink: 'models/extras/customlink.md'
|
||||
- ExportTemplate: 'models/extras/exporttemplate.md'
|
||||
- ImageAttachment: 'models/extras/imageattachment.md'
|
||||
@ -270,6 +271,7 @@ nav:
|
||||
- Application Registry: 'development/application-registry.md'
|
||||
- User Preferences: 'development/user-preferences.md'
|
||||
- Web UI: 'development/web-ui.md'
|
||||
- Internationalization: 'development/internationalization.md'
|
||||
- Release Checklist: 'development/release-checklist.md'
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
|
@ -163,7 +163,7 @@ class ProviderNetworkView(generic.ObjectView):
|
||||
related_models = (
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
|
||||
'providernetwork_id',
|
||||
'provider_network_id',
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -836,6 +836,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
||||
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
||||
TYPE_200GE_QSFP_DD = '200gbase-x-qsfpdd'
|
||||
TYPE_400GE_CFP2 = '400gbase-x-cfp2'
|
||||
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
|
||||
TYPE_400GE_OSFP = '400gbase-x-osfp'
|
||||
TYPE_400GE_CDFP = '400gbase-x-cdfp'
|
||||
@ -978,6 +979,7 @@ class InterfaceTypeChoices(ChoiceSet):
|
||||
(TYPE_100GE_CFP, 'CFP (100GE)'),
|
||||
(TYPE_100GE_CFP2, 'CFP2 (100GE)'),
|
||||
(TYPE_200GE_CFP2, 'CFP2 (200GE)'),
|
||||
(TYPE_400GE_CFP2, 'CFP2 (400GE)'),
|
||||
(TYPE_100GE_CFP4, 'CFP4 (100GE)'),
|
||||
(TYPE_100GE_CXP, 'CXP (100GE)'),
|
||||
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
||||
@ -1141,6 +1143,8 @@ class InterfaceSpeedChoices(ChoiceSet):
|
||||
(25000000, '25 Gbps'),
|
||||
(40000000, '40 Gbps'),
|
||||
(100000000, '100 Gbps'),
|
||||
(200000000, '200 Gbps'),
|
||||
(400000000, '400 Gbps'),
|
||||
]
|
||||
|
||||
|
||||
|
@ -1098,6 +1098,9 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
queryset=VirtualDeviceContext.objects.all(),
|
||||
required=False,
|
||||
label=_('Virtual device contexts'),
|
||||
initial_params={
|
||||
'interfaces': '$parent',
|
||||
},
|
||||
query_params={
|
||||
'device_id': '$device',
|
||||
}
|
||||
|
@ -55,7 +55,10 @@ class ComponentCreateForm(forms.Form):
|
||||
super().clean()
|
||||
|
||||
# Validate that all replication fields generate an equal number of values
|
||||
pattern_count = len(self.cleaned_data[self.replication_fields[0]])
|
||||
if not (patterns := self.cleaned_data.get(self.replication_fields[0])):
|
||||
return
|
||||
|
||||
pattern_count = len(patterns)
|
||||
for field_name in self.replication_fields:
|
||||
value_count = len(self.cleaned_data[field_name])
|
||||
if self.cleaned_data[field_name] and value_count != pattern_count:
|
||||
|
62
netbox/dcim/management/commands/buildschema.py
Normal file
62
netbox/dcim/management/commands/buildschema.py
Normal file
@ -0,0 +1,62 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from jinja2 import FileSystemLoader, Environment
|
||||
|
||||
from dcim.choices import *
|
||||
|
||||
TEMPLATE_FILENAME = 'devicetype_schema.jinja2'
|
||||
OUTPUT_FILENAME = 'contrib/generated_schema.json'
|
||||
|
||||
CHOICES_MAP = {
|
||||
'airflow_choices': DeviceAirflowChoices,
|
||||
'weight_unit_choices': WeightUnitChoices,
|
||||
'subdevice_role_choices': SubdeviceRoleChoices,
|
||||
'console_port_type_choices': ConsolePortTypeChoices,
|
||||
'console_server_port_type_choices': ConsolePortTypeChoices,
|
||||
'power_port_type_choices': PowerPortTypeChoices,
|
||||
'power_outlet_type_choices': PowerOutletTypeChoices,
|
||||
'power_outlet_feedleg_choices': PowerOutletFeedLegChoices,
|
||||
'interface_type_choices': InterfaceTypeChoices,
|
||||
'interface_poe_mode_choices': InterfacePoEModeChoices,
|
||||
'interface_poe_type_choices': InterfacePoETypeChoices,
|
||||
'front_port_type_choices': PortTypeChoices,
|
||||
'rear_port_type_choices': PortTypeChoices,
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generate JSON schema for validating NetBox device type definitions"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--write',
|
||||
action='store_true',
|
||||
help="Write the generated schema to file"
|
||||
)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# Initialize template
|
||||
template_loader = FileSystemLoader(searchpath=f'{settings.TEMPLATES_DIR}/extras/schema/')
|
||||
template_env = Environment(loader=template_loader)
|
||||
template = template_env.get_template(TEMPLATE_FILENAME)
|
||||
|
||||
# Render template
|
||||
context = {
|
||||
key: json.dumps(choices.values())
|
||||
for key, choices in CHOICES_MAP.items()
|
||||
}
|
||||
rendered = template.render(**context)
|
||||
|
||||
if kwargs['write']:
|
||||
# $root/contrib/generated_schema.json
|
||||
filename = os.path.join(os.path.split(settings.BASE_DIR)[0], OUTPUT_FILENAME)
|
||||
with open(filename, mode='w', encoding='UTF-8') as f:
|
||||
f.write(json.dumps(json.loads(rendered), indent=4))
|
||||
f.write('\n')
|
||||
f.close()
|
||||
self.stdout.write(self.style.SUCCESS(f"Schema written to {filename}."))
|
||||
else:
|
||||
self.stdout.write(rendered)
|
@ -13,7 +13,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='config_template',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='extras.configtemplate'),
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='%(class)ss', to='extras.configtemplate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicerole',
|
||||
|
@ -24,7 +24,7 @@ from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
|
||||
from utilities.tracking import TrackingModelMixin
|
||||
from .device_components import *
|
||||
from .mixins import WeightMixin
|
||||
from .mixins import RenderConfigMixin, WeightMixin
|
||||
|
||||
|
||||
__all__ = (
|
||||
@ -525,7 +525,14 @@ def update_interface_bridges(device, interface_templates, module=None):
|
||||
interface.save()
|
||||
|
||||
|
||||
class Device(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, ConfigContextModel, TrackingModelMixin):
|
||||
class Device(
|
||||
ContactsMixin,
|
||||
ImageAttachmentsMixin,
|
||||
RenderConfigMixin,
|
||||
ConfigContextModel,
|
||||
TrackingModelMixin,
|
||||
PrimaryModel
|
||||
):
|
||||
"""
|
||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
|
||||
@ -686,13 +693,6 @@ class Device(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, ConfigContextMo
|
||||
validators=[MaxValueValidator(255)],
|
||||
help_text=_('Virtual chassis master election priority')
|
||||
)
|
||||
config_template = models.ForeignKey(
|
||||
to='extras.ConfigTemplate',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='devices',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
latitude = models.DecimalField(
|
||||
verbose_name=_('latitude'),
|
||||
max_digits=8,
|
||||
@ -1070,17 +1070,6 @@ class Device(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, ConfigContextMo
|
||||
def interfaces_count(self):
|
||||
return self.vc_interfaces().count()
|
||||
|
||||
def get_config_template(self):
|
||||
"""
|
||||
Return the appropriate ConfigTemplate (if any) for this Device.
|
||||
"""
|
||||
if self.config_template:
|
||||
return self.config_template
|
||||
if self.role.config_template:
|
||||
return self.role.config_template
|
||||
if self.platform and self.platform.config_template:
|
||||
return self.platform.config_template
|
||||
|
||||
def get_vc_master(self):
|
||||
"""
|
||||
If this Device is a VirtualChassis member, return the VC master. Otherwise, return None.
|
||||
|
@ -4,6 +4,11 @@ from django.utils.translation import gettext_lazy as _
|
||||
from dcim.choices import *
|
||||
from utilities.utils import to_grams
|
||||
|
||||
__all__ = (
|
||||
'RenderConfigMixin',
|
||||
'WeightMixin',
|
||||
)
|
||||
|
||||
|
||||
class WeightMixin(models.Model):
|
||||
weight = models.DecimalField(
|
||||
@ -44,3 +49,27 @@ class WeightMixin(models.Model):
|
||||
# Validate weight and weight_unit
|
||||
if self.weight and not self.weight_unit:
|
||||
raise ValidationError(_("Must specify a unit when setting a weight"))
|
||||
|
||||
|
||||
class RenderConfigMixin(models.Model):
|
||||
config_template = models.ForeignKey(
|
||||
to='extras.ConfigTemplate',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='%(class)ss',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get_config_template(self):
|
||||
"""
|
||||
Return the appropriate ConfigTemplate (if any) for this Device.
|
||||
"""
|
||||
if self.config_template:
|
||||
return self.config_template
|
||||
if self.role.config_template:
|
||||
return self.role.config_template
|
||||
if self.platform and self.platform.config_template:
|
||||
return self.platform.config_template
|
||||
|
@ -591,7 +591,12 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
||||
}
|
||||
)
|
||||
mgmt_only = columns.BooleanColumn(
|
||||
verbose_name=_('Management Only'),
|
||||
verbose_name=_('Management Only')
|
||||
)
|
||||
speed_formatted = columns.TemplateColumn(
|
||||
template_code='{% load helpers %}{{ value|humanize_speed }}',
|
||||
accessor=Accessor('speed'),
|
||||
verbose_name=_('Speed')
|
||||
)
|
||||
wireless_link = tables.Column(
|
||||
verbose_name=_('Wireless link'),
|
||||
@ -618,7 +623,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
||||
model = models.Interface
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
||||
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
|
||||
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn',
|
||||
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
||||
|
@ -1,4 +1,5 @@
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@ -45,6 +46,15 @@ CABLE_TERMINATION_TYPES = {
|
||||
|
||||
|
||||
class DeviceComponentsView(generic.ObjectChildrenView):
|
||||
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename', 'bulk_disconnect')
|
||||
action_perms = defaultdict(set, **{
|
||||
'add': {'add'},
|
||||
'import': {'add'},
|
||||
'bulk_edit': {'change'},
|
||||
'bulk_delete': {'delete'},
|
||||
'bulk_rename': {'change'},
|
||||
'bulk_disconnect': {'change'},
|
||||
})
|
||||
queryset = Device.objects.all()
|
||||
|
||||
def get_children(self, request, parent):
|
||||
@ -1997,6 +2007,7 @@ class DeviceModuleBaysView(DeviceComponentsView):
|
||||
table = tables.DeviceModuleBayTable
|
||||
filterset = filtersets.ModuleBayFilterSet
|
||||
template_name = 'dcim/device/modulebays.html'
|
||||
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
|
||||
tab = ViewTab(
|
||||
label=_('Module Bays'),
|
||||
badge=lambda obj: obj.module_bay_count,
|
||||
@ -2012,6 +2023,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
|
||||
table = tables.DeviceDeviceBayTable
|
||||
filterset = filtersets.DeviceBayFilterSet
|
||||
template_name = 'dcim/device/devicebays.html'
|
||||
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
|
||||
tab = ViewTab(
|
||||
label=_('Device Bays'),
|
||||
badge=lambda obj: obj.device_bay_count,
|
||||
@ -2023,6 +2035,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
|
||||
|
||||
@register_model_view(Device, 'inventory')
|
||||
class DeviceInventoryView(DeviceComponentsView):
|
||||
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
|
||||
child_model = InventoryItem
|
||||
table = tables.DeviceInventoryItemTable
|
||||
filterset = filtersets.InventoryItemFilterSet
|
||||
|
@ -239,7 +239,7 @@ class BookmarkSerializer(ValidatedModelSerializer):
|
||||
class TagSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
|
||||
object_types = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
|
||||
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
|
||||
many=True,
|
||||
required=False
|
||||
)
|
||||
|
@ -19,6 +19,13 @@ WEBHOOK_EVENT_TYPES = {
|
||||
|
||||
# Dashboard
|
||||
DEFAULT_DASHBOARD = [
|
||||
{
|
||||
'widget': 'extras.BookmarksWidget',
|
||||
'width': 4,
|
||||
'height': 5,
|
||||
'title': 'Bookmarks',
|
||||
'color': 'orange',
|
||||
},
|
||||
{
|
||||
'widget': 'extras.ObjectCountsWidget',
|
||||
'width': 4,
|
||||
@ -32,22 +39,6 @@ DEFAULT_DASHBOARD = [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
'widget': 'extras.ObjectCountsWidget',
|
||||
'width': 4,
|
||||
'height': 3,
|
||||
'title': 'IPAM',
|
||||
'config': {
|
||||
'models': [
|
||||
'ipam.vrf',
|
||||
'ipam.aggregate',
|
||||
'ipam.prefix',
|
||||
'ipam.iprange',
|
||||
'ipam.ipaddress',
|
||||
'ipam.vlan',
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
'widget': 'extras.NoteWidget',
|
||||
'width': 4,
|
||||
@ -65,13 +56,16 @@ DEFAULT_DASHBOARD = [
|
||||
{
|
||||
'widget': 'extras.ObjectCountsWidget',
|
||||
'width': 4,
|
||||
'height': 2,
|
||||
'title': 'Circuits',
|
||||
'height': 3,
|
||||
'title': 'IPAM',
|
||||
'config': {
|
||||
'models': [
|
||||
'circuits.provider',
|
||||
'circuits.circuit',
|
||||
'circuits.providernetwork',
|
||||
'ipam.vrf',
|
||||
'ipam.aggregate',
|
||||
'ipam.prefix',
|
||||
'ipam.iprange',
|
||||
'ipam.ipaddress',
|
||||
'ipam.vlan',
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -86,6 +80,20 @@ DEFAULT_DASHBOARD = [
|
||||
'cache_timeout': 14400,
|
||||
}
|
||||
},
|
||||
{
|
||||
'widget': 'extras.ObjectCountsWidget',
|
||||
'width': 4,
|
||||
'height': 3,
|
||||
'title': 'Circuits',
|
||||
'config': {
|
||||
'models': [
|
||||
'circuits.provider',
|
||||
'circuits.circuit',
|
||||
'circuits.providernetwork',
|
||||
'circuits.provideraccount',
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
'widget': 'extras.ObjectCountsWidget',
|
||||
'width': 4,
|
||||
|
@ -346,13 +346,16 @@ class BookmarksWidget(DashboardWidget):
|
||||
def render(self, request):
|
||||
from extras.models import Bookmark
|
||||
|
||||
bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
|
||||
if object_types := self.config.get('object_types'):
|
||||
models = get_models_from_content_types(object_types)
|
||||
conent_types = ContentType.objects.get_for_models(*models).values()
|
||||
bookmarks = bookmarks.filter(object_type__in=conent_types)
|
||||
if max_items := self.config.get('max_items'):
|
||||
bookmarks = bookmarks[:max_items]
|
||||
if request.user.is_anonymous:
|
||||
bookmarks = list()
|
||||
else:
|
||||
bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
|
||||
if object_types := self.config.get('object_types'):
|
||||
models = get_models_from_content_types(object_types)
|
||||
conent_types = ContentType.objects.get_for_models(*models).values()
|
||||
bookmarks = bookmarks.filter(object_type__in=conent_types)
|
||||
if max_items := self.config.get('max_items'):
|
||||
bookmarks = bookmarks[:max_items]
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
'bookmarks': bookmarks,
|
||||
|
@ -180,7 +180,7 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
||||
)
|
||||
content_type_id = ContentTypeChoiceField(
|
||||
label=_('Content type'),
|
||||
queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
|
||||
queryset=ContentType.objects.filter(FeatureQuery('image_attachments').get_query()),
|
||||
required=False
|
||||
)
|
||||
name = forms.CharField(
|
||||
|
@ -1,3 +1,4 @@
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
@ -8,6 +9,7 @@ from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
from extras.querysets import ConfigContextQuerySet
|
||||
from netbox.config import get_config
|
||||
from netbox.registry import registry
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import CloningMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
|
||||
from utilities.jinja2 import ConfigTemplateLoader
|
||||
@ -255,7 +257,19 @@ class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLog
|
||||
"""
|
||||
Render the contents of the template.
|
||||
"""
|
||||
context = context or {}
|
||||
_context = dict()
|
||||
|
||||
# Populate the default template context with NetBox model classes, namespaced by app
|
||||
# TODO: Devise a canonical mechanism for identifying the models to include (see #13427)
|
||||
for app, model_names in registry['model_features']['custom_fields'].items():
|
||||
_context.setdefault(app, {})
|
||||
for model_name in model_names:
|
||||
model = apps.get_registered_model(app, model_name)
|
||||
_context[app][model.__name__] = model
|
||||
|
||||
# Add the provided context data, if any
|
||||
if context is not None:
|
||||
_context.update(context)
|
||||
|
||||
# Initialize the Jinja2 environment and instantiate the Template
|
||||
environment = self._get_environment()
|
||||
@ -263,7 +277,7 @@ class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLog
|
||||
template = environment.get_template(self.data_file.path)
|
||||
else:
|
||||
template = environment.from_string(self.template_code)
|
||||
output = template.render(**context)
|
||||
output = template.render(**_context)
|
||||
|
||||
# Replace CRLF-style line terminators
|
||||
return output.replace('\r\n', '\n')
|
||||
|
@ -316,7 +316,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
text = clean_html(text, allowed_schemes)
|
||||
|
||||
# Sanitize link
|
||||
link = urllib.parse.quote(link, safe='/:?&=%+[]@#,')
|
||||
link = urllib.parse.quote(link, safe='/:?&=%+[]@#,;')
|
||||
|
||||
# Verify link scheme is allowed
|
||||
result = urllib.parse.urlparse(link)
|
||||
|
@ -2,7 +2,6 @@ import collections
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.module_loading import import_string
|
||||
from packaging import version
|
||||
@ -146,23 +145,3 @@ class PluginConfig(AppConfig):
|
||||
for setting, value in cls.default_settings.items():
|
||||
if setting not in user_config:
|
||||
user_config[setting] = value
|
||||
|
||||
|
||||
#
|
||||
# Utilities
|
||||
#
|
||||
|
||||
def get_plugin_config(plugin_name, parameter, default=None):
|
||||
"""
|
||||
Return the value of the specified plugin configuration parameter.
|
||||
|
||||
Args:
|
||||
plugin_name: The name of the plugin
|
||||
parameter: The name of the configuration parameter
|
||||
default: The value to return if the parameter is not defined (default: None)
|
||||
"""
|
||||
try:
|
||||
plugin_config = settings.PLUGINS_CONFIG[plugin_name]
|
||||
return plugin_config.get(parameter, default)
|
||||
except KeyError:
|
||||
raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.")
|
||||
|
37
netbox/extras/plugins/utils.py
Normal file
37
netbox/extras/plugins/utils.py
Normal file
@ -0,0 +1,37 @@
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
__all__ = (
|
||||
'get_installed_plugins',
|
||||
'get_plugin_config',
|
||||
)
|
||||
|
||||
|
||||
def get_installed_plugins():
|
||||
"""
|
||||
Return a dictionary mapping the names of installed plugins to their versions.
|
||||
"""
|
||||
plugins = {}
|
||||
for plugin_name in settings.PLUGINS:
|
||||
plugin_name = plugin_name.rsplit('.', 1)[-1]
|
||||
plugin_config = apps.get_app_config(plugin_name)
|
||||
plugins[plugin_name] = getattr(plugin_config, 'version', None)
|
||||
|
||||
return dict(sorted(plugins.items()))
|
||||
|
||||
|
||||
def get_plugin_config(plugin_name, parameter, default=None):
|
||||
"""
|
||||
Return the value of the specified plugin configuration parameter.
|
||||
|
||||
Args:
|
||||
plugin_name: The name of the plugin
|
||||
parameter: The name of the configuration parameter
|
||||
default: The value to return if the parameter is not defined (default: None)
|
||||
"""
|
||||
try:
|
||||
plugin_config = settings.PLUGINS_CONFIG[plugin_name]
|
||||
return plugin_config.get(parameter, default)
|
||||
except KeyError:
|
||||
raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.")
|
@ -214,20 +214,18 @@ class Report(object):
|
||||
self.active_test = method_name
|
||||
test_method = getattr(self, method_name)
|
||||
test_method()
|
||||
job.data = self._results
|
||||
if self.failed:
|
||||
self.logger.warning("Report failed")
|
||||
job.status = JobStatusChoices.STATUS_FAILED
|
||||
job.terminate(status=JobStatusChoices.STATUS_FAILED)
|
||||
else:
|
||||
self.logger.info("Report completed successfully")
|
||||
job.status = JobStatusChoices.STATUS_COMPLETED
|
||||
job.terminate()
|
||||
except Exception as e:
|
||||
stacktrace = traceback.format_exc()
|
||||
self.log_failure(None, f"An exception occurred: {type(e).__name__}: {e} <pre>{stacktrace}</pre>")
|
||||
logger.error(f"Exception raised during report execution: {e}")
|
||||
job.terminate(status=JobStatusChoices.STATUS_ERRORED)
|
||||
finally:
|
||||
job.data = self._results
|
||||
job.terminate()
|
||||
|
||||
# Perform any post-run tasks
|
||||
self.post_run()
|
||||
|
@ -5,8 +5,9 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import Client, TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from extras.plugins import PluginMenu, get_plugin_config
|
||||
from extras.plugins import PluginMenu
|
||||
from extras.tests.dummy_plugin import config as dummy_config
|
||||
from extras.plugins.utils import get_plugin_config
|
||||
from netbox.graphql.schema import Query
|
||||
from netbox.registry import registry
|
||||
|
||||
|
@ -31,8 +31,8 @@ class WebhookTest(APITestCase):
|
||||
def setUpTestData(cls):
|
||||
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
DUMMY_URL = "http://localhost/"
|
||||
DUMMY_SECRET = "LOOKATMEIMASECRETSTRING"
|
||||
DUMMY_URL = 'http://localhost:9000/'
|
||||
DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING'
|
||||
|
||||
webhooks = Webhook.objects.bulk_create((
|
||||
Webhook(name='Webhook 1', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers='X-Foo: Bar'),
|
||||
@ -259,7 +259,7 @@ class WebhookTest(APITestCase):
|
||||
name='Conditional Webhook',
|
||||
type_create=True,
|
||||
type_update=True,
|
||||
payload_url='http://localhost/',
|
||||
payload_url='http://localhost:9000/',
|
||||
conditions={
|
||||
'and': [
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.paginator import EmptyPage
|
||||
from django.db.models import Count, Q
|
||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
@ -18,6 +19,7 @@ from netbox.config import get_config, PARAMS
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm, get_field_value
|
||||
from utilities.htmx import is_htmx
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
from utilities.rqworker import get_workers_for_queue
|
||||
from utilities.templatetags.builtins.filters import render_markdown
|
||||
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
|
||||
@ -89,6 +91,25 @@ class CustomFieldChoiceSetListView(generic.ObjectListView):
|
||||
class CustomFieldChoiceSetView(generic.ObjectView):
|
||||
queryset = CustomFieldChoiceSet.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
|
||||
# Paginate choices list
|
||||
per_page = get_paginate_count(request)
|
||||
try:
|
||||
page_number = request.GET.get('page', 1)
|
||||
except ValueError:
|
||||
page_number = 1
|
||||
paginator = EnhancedPaginator(instance.choices, per_page)
|
||||
try:
|
||||
choices = paginator.page(page_number)
|
||||
except EmptyPage:
|
||||
choices = paginator.page(paginator.num_pages)
|
||||
|
||||
return {
|
||||
'paginator': paginator,
|
||||
'choices': choices,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(CustomFieldChoiceSet, 'edit')
|
||||
class CustomFieldChoiceSetEditView(generic.ObjectEditView):
|
||||
|
@ -1,7 +1,5 @@
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Round
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_pglocks import advisory_lock
|
||||
from drf_spectacular.utils import extend_schema
|
||||
@ -16,6 +14,7 @@ from circuits.models import Provider
|
||||
from dcim.models import Site
|
||||
from ipam import filtersets
|
||||
from ipam.models import *
|
||||
from ipam.models import L2VPN, L2VPNTermination
|
||||
from ipam.utils import get_next_available_prefix
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from netbox.api.viewsets.mixins import ObjectValidationMixin
|
||||
@ -24,7 +23,6 @@ from netbox.constants import ADVISORY_LOCK_KEYS
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.utils import count_related
|
||||
from . import serializers
|
||||
from ipam.models import L2VPN, L2VPNTermination
|
||||
|
||||
|
||||
class IPAMRootView(APIRootView):
|
||||
@ -346,7 +344,11 @@ class AvailableASNsView(AvailableObjectsView):
|
||||
def get(self, request, pk):
|
||||
return super().get(request, pk)
|
||||
|
||||
@extend_schema(methods=["post"], responses={201: serializers.AvailableASNSerializer(many=True)})
|
||||
@extend_schema(
|
||||
methods=["post"],
|
||||
responses={201: serializers.ASNSerializer(many=True)},
|
||||
request=serializers.ASNSerializer(many=True),
|
||||
)
|
||||
def post(self, request, pk):
|
||||
return super().post(request, pk)
|
||||
|
||||
@ -395,7 +397,11 @@ class AvailablePrefixesView(AvailableObjectsView):
|
||||
def get(self, request, pk):
|
||||
return super().get(request, pk)
|
||||
|
||||
@extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=True)})
|
||||
@extend_schema(
|
||||
methods=["post"],
|
||||
responses={201: serializers.PrefixSerializer(many=True)},
|
||||
request=serializers.PrefixSerializer(many=True),
|
||||
)
|
||||
def post(self, request, pk):
|
||||
return super().post(request, pk)
|
||||
|
||||
@ -435,7 +441,11 @@ class AvailableIPAddressesView(AvailableObjectsView):
|
||||
def get(self, request, pk):
|
||||
return super().get(request, pk)
|
||||
|
||||
@extend_schema(methods=["post"], responses={201: serializers.IPAddressSerializer(many=True)})
|
||||
@extend_schema(
|
||||
methods=["post"],
|
||||
responses={201: serializers.IPAddressSerializer(many=True)},
|
||||
request=serializers.IPAddressSerializer(many=True),
|
||||
)
|
||||
def post(self, request, pk):
|
||||
return super().post(request, pk)
|
||||
|
||||
@ -482,6 +492,10 @@ class AvailableVLANsView(AvailableObjectsView):
|
||||
def get(self, request, pk):
|
||||
return super().get(request, pk)
|
||||
|
||||
@extend_schema(methods=["post"], responses={201: serializers.VLANSerializer(many=True)})
|
||||
@extend_schema(
|
||||
methods=["post"],
|
||||
responses={201: serializers.VLANSerializer(many=True)},
|
||||
request=serializers.VLANSerializer(many=True),
|
||||
)
|
||||
def post(self, request, pk):
|
||||
return super().post(request, pk)
|
||||
|
@ -591,6 +591,10 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
method='_assigned_to_interface',
|
||||
label=_('Is assigned to an interface'),
|
||||
)
|
||||
assigned = django_filters.BooleanFilter(
|
||||
method='_assigned',
|
||||
label=_('Is assigned'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=IPAddressStatusChoices,
|
||||
null_value=None
|
||||
@ -706,6 +710,18 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
assigned_object_id__isnull=False
|
||||
)
|
||||
|
||||
def _assigned(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.exclude(
|
||||
assigned_object_type__isnull=True,
|
||||
assigned_object_id__isnull=True
|
||||
)
|
||||
else:
|
||||
return queryset.filter(
|
||||
assigned_object_type__isnull=True,
|
||||
assigned_object_id__isnull=True
|
||||
)
|
||||
|
||||
|
||||
class FHRPGroupFilterSet(NetBoxModelFilterSet):
|
||||
protocol = django_filters.MultipleChoiceFilter(
|
||||
|
@ -1,7 +1,6 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import Device, Interface, Site
|
||||
@ -10,7 +9,9 @@ from ipam.constants import *
|
||||
from ipam.models import *
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
|
||||
from utilities.forms.fields import (
|
||||
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField
|
||||
)
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
|
||||
__all__ = (
|
||||
@ -42,10 +43,25 @@ class VRFImportForm(NetBoxModelImportForm):
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned tenant')
|
||||
)
|
||||
import_targets = CSVModelMultipleChoiceField(
|
||||
queryset=RouteTarget.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Import route targets')
|
||||
)
|
||||
export_targets = CSVModelMultipleChoiceField(
|
||||
queryset=RouteTarget.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Export route targets')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', 'tags')
|
||||
fields = (
|
||||
'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'comments',
|
||||
'tags',
|
||||
)
|
||||
|
||||
|
||||
class RouteTargetImportForm(NetBoxModelImportForm):
|
||||
|
@ -256,7 +256,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = IPRange
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
(_('Attriubtes'), ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')),
|
||||
(_('Attributes'), ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')),
|
||||
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
||||
)
|
||||
family = forms.ChoiceField(
|
||||
|
@ -992,6 +992,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'fhrpgroup_id': [fhrp_groups[0].pk, fhrp_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_assigned(self):
|
||||
params = {'assigned': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
||||
params = {'assigned': 'false'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_assigned_to_interface(self):
|
||||
params = {'assigned_to_interface': 'true'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
@ -216,7 +216,7 @@ class ASNRangeASNsView(generic.ObjectChildrenView):
|
||||
child_model = ASN
|
||||
table = tables.ASNTable
|
||||
filterset = filtersets.ASNFilterSet
|
||||
template_name = 'ipam/asnrange/asns.html'
|
||||
template_name = 'generic/object_children.html'
|
||||
tab = ViewTab(
|
||||
label=_('ASNs'),
|
||||
badge=lambda x: x.get_child_asns().count(),
|
||||
@ -816,7 +816,6 @@ class IPAddressAssignView(generic.ObjectView):
|
||||
table = None
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
addresses = self.queryset.prefetch_related('vrf', 'tenant')
|
||||
# Limit to 100 results
|
||||
addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100]
|
||||
@ -866,7 +865,7 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView):
|
||||
child_model = IPAddress
|
||||
table = tables.IPAddressTable
|
||||
filterset = filtersets.IPAddressFilterSet
|
||||
template_name = 'ipam/ipaddress/ip_addresses.html'
|
||||
template_name = 'generic/object_children.html'
|
||||
tab = ViewTab(
|
||||
label=_('Related IPs'),
|
||||
badge=lambda x: x.get_related_ips().count(),
|
||||
@ -963,7 +962,6 @@ class FHRPGroupView(generic.ObjectView):
|
||||
queryset = FHRPGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
|
||||
# Get assigned interfaces
|
||||
members_table = tables.FHRPGroupAssignmentTable(
|
||||
data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance),
|
||||
@ -1077,7 +1075,7 @@ class VLANInterfacesView(generic.ObjectChildrenView):
|
||||
child_model = Interface
|
||||
table = tables.VLANDevicesTable
|
||||
filterset = InterfaceFilterSet
|
||||
template_name = 'ipam/vlan/interfaces.html'
|
||||
template_name = 'generic/object_children.html'
|
||||
tab = ViewTab(
|
||||
label=_('Device Interfaces'),
|
||||
badge=lambda x: x.get_interfaces().count(),
|
||||
@ -1095,7 +1093,7 @@ class VLANVMInterfacesView(generic.ObjectChildrenView):
|
||||
child_model = VMInterface
|
||||
table = tables.VLANVirtualMachinesTable
|
||||
filterset = VMInterfaceFilterSet
|
||||
template_name = 'ipam/vlan/vminterfaces.html'
|
||||
template_name = 'generic/object_children.html'
|
||||
tab = ViewTab(
|
||||
label=_('VM Interfaces'),
|
||||
badge=lambda x: x.get_vminterfaces().count(),
|
||||
|
@ -11,6 +11,7 @@ from rest_framework.reverse import reverse
|
||||
from rest_framework.views import APIView
|
||||
from rq.worker import Worker
|
||||
|
||||
from extras.plugins.utils import get_installed_plugins
|
||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||
|
||||
|
||||
@ -61,19 +62,11 @@ class StatusView(APIView):
|
||||
installed_apps[app_config.name] = version
|
||||
installed_apps = {k: v for k, v in sorted(installed_apps.items())}
|
||||
|
||||
# Gather installed plugins
|
||||
plugins = {}
|
||||
for plugin_name in settings.PLUGINS:
|
||||
plugin_name = plugin_name.rsplit('.', 1)[-1]
|
||||
plugin_config = apps.get_app_config(plugin_name)
|
||||
plugins[plugin_name] = getattr(plugin_config, 'version', None)
|
||||
plugins = {k: v for k, v in sorted(plugins.items())}
|
||||
|
||||
return Response({
|
||||
'django-version': DJANGO_VERSION,
|
||||
'installed-apps': installed_apps,
|
||||
'netbox-version': settings.VERSION,
|
||||
'plugins': plugins,
|
||||
'plugins': get_installed_plugins(),
|
||||
'python-version': platform.python_version(),
|
||||
'rq-workers-running': Worker.count(get_connection('default')),
|
||||
})
|
||||
|
@ -88,7 +88,10 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
|
||||
|
||||
def _get_custom_fields(self, content_type):
|
||||
return CustomField.objects.filter(content_types=content_type).filter(
|
||||
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE
|
||||
ui_visibility__in=[
|
||||
CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
|
||||
CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET,
|
||||
]
|
||||
)
|
||||
|
||||
def _get_form_field(self, customfield):
|
||||
|
@ -22,6 +22,7 @@ __all__ = (
|
||||
class NetBoxFeatureSet(
|
||||
BookmarksMixin,
|
||||
ChangeLoggingMixin,
|
||||
CloningMixin,
|
||||
CustomFieldsMixin,
|
||||
CustomLinksMixin,
|
||||
CustomValidationMixin,
|
||||
@ -53,7 +54,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, WebhooksMixin
|
||||
abstract = True
|
||||
|
||||
|
||||
class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model):
|
||||
class NetBoxModel(NetBoxFeatureSet, models.Model):
|
||||
"""
|
||||
Base model for most object types. Suitable for use by plugins.
|
||||
"""
|
||||
@ -90,6 +91,10 @@ class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model):
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# NetBox internal base models
|
||||
#
|
||||
|
||||
class PrimaryModel(NetBoxModel):
|
||||
"""
|
||||
Primary models represent real objects within the infrastructure being modeled.
|
||||
@ -108,7 +113,7 @@ class PrimaryModel(NetBoxModel):
|
||||
abstract = True
|
||||
|
||||
|
||||
class NestedGroupModel(CloningMixin, NetBoxFeatureSet, MPTTModel):
|
||||
class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
|
||||
"""
|
||||
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
|
||||
recursively using MPTT. Within each parent, each child instance must have a unique name.
|
||||
|
@ -491,6 +491,19 @@ class SyncedDataMixin(models.Model):
|
||||
|
||||
return ret
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
from core.models import AutoSyncRecord
|
||||
|
||||
# Delete AutoSyncRecord
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
AutoSyncRecord.objects.filter(
|
||||
datafile=self.data_file,
|
||||
object_type=content_type,
|
||||
object_id=self.pk
|
||||
).delete()
|
||||
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def resolve_data_file(self):
|
||||
"""
|
||||
Determine the designated DataFile object identified by its parent DataSource and its path. Returns None if
|
||||
@ -525,11 +538,20 @@ class SyncedDataMixin(models.Model):
|
||||
raise NotImplementedError(f"{self.__class__} must implement a sync_data() method.")
|
||||
|
||||
|
||||
#
|
||||
# Feature registration
|
||||
#
|
||||
|
||||
FEATURES_MAP = {
|
||||
'bookmarks': BookmarksMixin,
|
||||
'change_logging': ChangeLoggingMixin,
|
||||
'cloning': CloningMixin,
|
||||
'contacts': ContactsMixin,
|
||||
'custom_fields': CustomFieldsMixin,
|
||||
'custom_links': CustomLinksMixin,
|
||||
'custom_validation': CustomValidationMixin,
|
||||
'export_templates': ExportTemplatesMixin,
|
||||
'image_attachments': ImageAttachmentsMixin,
|
||||
'jobs': JobsMixin,
|
||||
'journaling': JournalingMixin,
|
||||
'synced_data': SyncedDataMixin,
|
||||
@ -544,12 +566,13 @@ registry['model_features'].update({
|
||||
|
||||
@receiver(class_prepared)
|
||||
def _register_features(sender, **kwargs):
|
||||
# Record each applicable feature for the model in the registry
|
||||
features = {
|
||||
feature for feature, cls in FEATURES_MAP.items() if issubclass(sender, cls)
|
||||
}
|
||||
register_features(sender, features)
|
||||
|
||||
# Feature view registration
|
||||
# Register applicable feature views for the model
|
||||
if issubclass(sender, JournalingMixin):
|
||||
register_model_view(
|
||||
sender,
|
||||
|
@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.6-beta1'
|
||||
VERSION = '3.6-beta2'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
@ -474,8 +474,6 @@ LOGIN_REDIRECT_URL = f'/{BASE_PATH}'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
TEST_RUNNER = "django_rich.test.RichRunner"
|
||||
|
||||
# Exclude potentially sensitive models from wildcard view exemption. These may still be exempted
|
||||
# by specifying the model individually in the EXEMPT_VIEW_PERMISSIONS configuration parameter.
|
||||
EXEMPT_EXCLUDE_MODELS = (
|
||||
|
@ -511,9 +511,9 @@ class CustomLinkColumn(tables.Column):
|
||||
"""
|
||||
def __init__(self, customlink, *args, **kwargs):
|
||||
self.customlink = customlink
|
||||
kwargs['accessor'] = Accessor('pk')
|
||||
if 'verbose_name' not in kwargs:
|
||||
kwargs['verbose_name'] = customlink.name
|
||||
kwargs.setdefault('accessor', Accessor('pk'))
|
||||
kwargs.setdefault('orderable', False)
|
||||
kwargs.setdefault('verbose_name', customlink.name)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
@ -54,7 +54,7 @@ class BaseTable(tables.Table):
|
||||
# 3. Meta.fields
|
||||
selected_columns = None
|
||||
if user is not None and not isinstance(user, AnonymousUser):
|
||||
selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
|
||||
selected_columns = user.config.get(f"tables.{self.name}.columns")
|
||||
if not selected_columns:
|
||||
selected_columns = getattr(self.Meta, 'default_columns', self.Meta.fields)
|
||||
|
||||
@ -113,6 +113,10 @@ class BaseTable(tables.Table):
|
||||
columns.append((name, column.verbose_name))
|
||||
return columns
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
@property
|
||||
def available_columns(self):
|
||||
return self._get_columns(visible=False)
|
||||
@ -138,17 +142,16 @@ class BaseTable(tables.Table):
|
||||
"""
|
||||
# Save ordering preference
|
||||
if request.user.is_authenticated:
|
||||
table_name = self.__class__.__name__
|
||||
if self.prefixed_order_by_field in request.GET:
|
||||
if request.GET[self.prefixed_order_by_field]:
|
||||
# If an ordering has been specified as a query parameter, save it as the
|
||||
# user's preferred ordering for this table.
|
||||
ordering = request.GET.getlist(self.prefixed_order_by_field)
|
||||
request.user.config.set(f'tables.{table_name}.ordering', ordering, commit=True)
|
||||
request.user.config.set(f'tables.{self.name}.ordering', ordering, commit=True)
|
||||
else:
|
||||
# If the ordering has been set to none (empty), clear any existing preference.
|
||||
request.user.config.clear(f'tables.{table_name}.ordering', commit=True)
|
||||
elif ordering := request.user.config.get(f'tables.{table_name}.ordering'):
|
||||
request.user.config.clear(f'tables.{self.name}.ordering', commit=True)
|
||||
elif ordering := request.user.config.get(f'tables.{self.name}.ordering'):
|
||||
# If no ordering has been specified, set the preferred ordering (if any).
|
||||
self.order_by = ordering
|
||||
|
||||
|
@ -11,6 +11,8 @@ from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
|
||||
from django.views.generic import View
|
||||
from sentry_sdk import capture_message
|
||||
|
||||
from extras.plugins.utils import get_installed_plugins
|
||||
|
||||
__all__ = (
|
||||
'handler_404',
|
||||
'handler_500',
|
||||
@ -53,4 +55,5 @@ def handler_500(request, template_name=ERROR_500_TEMPLATE_NAME):
|
||||
'exception': str(type_),
|
||||
'netbox_version': settings.VERSION,
|
||||
'python_version': platform.python_version(),
|
||||
'plugins': get_installed_plugins(),
|
||||
}))
|
||||
|
@ -143,9 +143,12 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
||||
return render(request, self.get_template_name(), {
|
||||
'object': instance,
|
||||
'child_model': self.child_model,
|
||||
'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
|
||||
'table': table,
|
||||
'table_config': f'{table.name}_config',
|
||||
'actions': actions,
|
||||
'tab': self.tab,
|
||||
'return_url': request.get_full_path(),
|
||||
**self.get_extra_context(request, instance),
|
||||
})
|
||||
|
||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
import { getElement, getElements, findFirstAdjacent } from '../util';
|
||||
import { getElements, findFirstAdjacent } from '../util';
|
||||
|
||||
/**
|
||||
* If any PK checkbox is checked, uncheck the select all table checkbox and the select all
|
||||
@ -63,29 +63,6 @@ function handleSelectAllToggle(event: Event): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize the select all confirmation checkbox state with the select all confirmation button
|
||||
* disabled state. If the select all confirmation checkbox is checked, the buttons should be
|
||||
* enabled. If not, the buttons should be disabled.
|
||||
*
|
||||
* @param event Change Event
|
||||
*/
|
||||
function handleSelectAll(event: Event): void {
|
||||
const target = event.currentTarget as HTMLInputElement;
|
||||
const selectAllBox = getElement<HTMLDivElement>('select-all-box');
|
||||
if (selectAllBox !== null) {
|
||||
for (const button of selectAllBox.querySelectorAll<HTMLButtonElement>(
|
||||
'button[type="submit"]',
|
||||
)) {
|
||||
if (target.checked) {
|
||||
button.disabled = false;
|
||||
} else {
|
||||
button.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize table select all elements.
|
||||
*/
|
||||
@ -98,9 +75,4 @@ export function initSelectAll(): void {
|
||||
for (const element of getElements<HTMLInputElement>('input[type="checkbox"][name="pk"]')) {
|
||||
element.addEventListener('change', handlePkCheck);
|
||||
}
|
||||
const selectAll = getElement<HTMLInputElement>('select-all');
|
||||
|
||||
if (selectAll !== null) {
|
||||
selectAll.addEventListener('change', handleSelectAll);
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,10 @@
|
||||
{{ error }}
|
||||
|
||||
{% trans "Python version" %}: {{ python_version }}
|
||||
{% trans "NetBox version" %}: {{ netbox_version }}</pre>
|
||||
{% trans "NetBox version" %}: {{ netbox_version }}
|
||||
{% trans "Plugins" %}: {% for plugin, version in plugins.items %}
|
||||
{{ plugin }}: {{ version }}{% empty %}{% trans "None installed" %}{% endfor %}
|
||||
</pre>
|
||||
<p>
|
||||
{% trans "If further assistance is required, please post to the" %} <a href="https://github.com/netbox-community/netbox/discussions">{% trans "NetBox discussion forum" %}</a> {% trans "on GitHub" %}.
|
||||
</p>
|
||||
|
15
netbox/templates/dcim/device/components_base.html
Normal file
15
netbox/templates/dcim/device/components_base.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block bulk_edit_controls %}
|
||||
{{ block.super }}
|
||||
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
|
||||
{% if 'bulk_rename' in actions and bulk_rename_view %}
|
||||
<button type="submit" name="_rename"
|
||||
formaction="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock bulk_edit_controls %}
|
@ -1,58 +1,28 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% extends 'dcim/device/components_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
{% block bulk_delete_controls %}
|
||||
{{ block.super }}
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-sm btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Console Port" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endwith %}
|
||||
{% endblock bulk_delete_controls %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% block bulk_extra_controls %}
|
||||
{{ block.super }}
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Console Ports" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock bulk_extra_controls %}
|
||||
|
@ -1,58 +1,28 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% extends 'dcim/device/components_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
{% block bulk_delete_controls %}
|
||||
{{ block.super }}
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.add_consoleserverport %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Console Server Ports" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endwith %}
|
||||
{% endblock bulk_delete_controls %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% block bulk_extra_controls %}
|
||||
{{ block.super }}
|
||||
{% if perms.dcim.add_consoleserverport %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Console Server Ports" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock bulk_extra_controls %}
|
@ -1,51 +1,14 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% extends 'dcim/device/components_base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:devicebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
{% block bulk_extra_controls %}
|
||||
{{ block.super }}
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Device Bays" %}
|
||||
</a>
|
||||
<a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Device Bays" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% endif %}
|
||||
{% endblock bulk_extra_controls %}
|
||||
|
@ -1,58 +1,28 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% extends 'dcim/device/components_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
{% block bulk_delete_controls %}
|
||||
{{ block.super }}
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.add_frontport %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add front ports" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endwith %}
|
||||
{% endblock bulk_delete_controls %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% block bulk_extra_controls %}
|
||||
{{ block.super }}
|
||||
{% if perms.dcim.add_frontport %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Front Ports" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock bulk_extra_controls %}
|
||||
|
@ -1,67 +1,28 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% extends 'dcim/device/components_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" name="_edit"
|
||||
formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
|
||||
class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
<button type="submit" name="_rename"
|
||||
formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
|
||||
class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete"
|
||||
formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
|
||||
class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% block bulk_delete_controls %}
|
||||
{{ block.super }}
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
|
||||
class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endblock bulk_delete_controls %}
|
||||
|
||||
{% block bulk_extra_controls %}
|
||||
{{ block.super }}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Interfaces" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Interfaces" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% endblock bulk_extra_controls %}
|
||||
|
@ -1,51 +1,14 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% extends 'dcim/device/components_base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:inventoryitem_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:inventoryitem_bulk_delete' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
{% block bulk_extra_controls %}
|
||||
{{ block.super }}
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Inventory Item" %}
|
||||
</a>
|
||||
<a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Inventory Item" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% endif %}
|
||||
{% endblock bulk_extra_controls %}
|
||||
|
@ -1,47 +1,14 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% extends 'dcim/device/components_base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:modulebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:modulebay_bulk_rename' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" formaction="{% url 'dcim:modulebay_bulk_delete' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.add_modulebay %}
|
||||
{% block bulk_extra_controls %}
|
||||
{{ block.super }}
|
||||
{% if perms.dcim.add_modulebay %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Module Bays" %}
|
||||
</a>
|
||||
<a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Module Bays" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% table_config_form table %}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
{% endblock bulk_extra_controls %}
|
||||
|
@ -1,58 +1,28 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% extends 'dcim/device/components_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
{% block bulk_delete_controls %}
|
||||
{{ block.super }}
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.add_poweroutlet %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Power Outlets" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endwith %}
|
||||
{% endblock bulk_delete_controls %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% block bulk_extra_controls %}
|
||||
{{ block.super }}
|
||||
{% if perms.dcim.add_poweroutlet %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Power Outlets" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock bulk_extra_controls %}
|
||||
|
@ -1,58 +1,28 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% extends 'dcim/device/components_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
{% block bulk_delete_controls %}
|
||||
{{ block.super }}
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-sm btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Power Port" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endwith %}
|
||||
{% endblock bulk_delete_controls %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% block bulk_extra_controls %}
|
||||
{{ block.super }}
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Power Port" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock bulk_extra_controls %}
|
||||
|
@ -1,58 +1,28 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load static %}
|
||||
{% extends 'dcim/device/components_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
{% block bulk_delete_controls %}
|
||||
{{ block.super }}
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.add_rearport %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add rear ports" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endwith %}
|
||||
{% endblock bulk_delete_controls %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% block bulk_extra_controls %}
|
||||
{{ block.super }}
|
||||
{% if perms.dcim.add_rearport %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Rear Ports" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock bulk_extra_controls %}
|
@ -1,5 +1,4 @@
|
||||
{% extends 'dcim/rack/base.html' %}
|
||||
{% load helpers %}
|
||||
{% extends 'generic/object_children.html' %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.dcim.add_device %}
|
||||
@ -10,42 +9,4 @@
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_edit"
|
||||
formaction="{% url 'dcim:device_bulk_edit' %}?return_url={% url 'dcim:rack_nonracked_devices' pk=object.pk %}"
|
||||
class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit"
|
||||
formaction="{% url 'dcim:device_bulk_delete' %}?return_url={% url 'dcim:rack_nonracked_devices' pk=object.pk %}"
|
||||
class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% endblock extra_controls %}
|
||||
|
@ -1,44 +1,13 @@
|
||||
{% extends 'dcim/rack/base.html' %}
|
||||
{% load helpers %}
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="RackReservationTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:rackreservation_bulk_edit' %}?return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" formaction="{% url 'dcim:rackreservation_bulk_delete' %}?return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.add_rackreservation %}
|
||||
{% block extra_controls %}
|
||||
{% if perms.dcim.add_rackreservation %}
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:rackreservation_add' %}?rack={{ object.pk }}&return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add reservation" %}
|
||||
</a>
|
||||
<a href="{% url 'dcim:rackreservation_add' %}?rack={{ object.pk }}&return_url={% url 'dcim:rack_reservations' pk=object.pk %}"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add reservation" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
@ -55,18 +55,14 @@
|
||||
<th>Label</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for value, label in object.choices|slice:":50" %}
|
||||
{% for value, label in choices %}
|
||||
<tr>
|
||||
<td>{{ value }}</td>
|
||||
<td>{{ label }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if object.choices|length > 50 %}
|
||||
<tr>
|
||||
<td colspan="2" class="text-muted">(Additional choices not displayed)</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% include 'inc/paginator.html' with page=choices %}
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
|
@ -1,3 +1,5 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if bookmarks %}
|
||||
<div class="list-group list-group-flush">
|
||||
{% for bookmark in bookmarks %}
|
||||
@ -6,4 +8,9 @@
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center text-muted">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
{% blocktrans %}No bookmarks have been added yet.{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
4
netbox/templates/extras/imageattachment.html
Normal file
4
netbox/templates/extras/imageattachment.html
Normal file
@ -0,0 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
|
||||
{% block tabs %}
|
||||
{% endblock %}
|
93
netbox/templates/extras/schema/devicetype_schema.jinja2
Normal file
93
netbox/templates/extras/schema/devicetype_schema.jinja2
Normal file
@ -0,0 +1,93 @@
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"airflow": {
|
||||
"type": "string",
|
||||
"enum": {{ airflow_choices }}
|
||||
},
|
||||
"weight-unit": {
|
||||
"type": "string",
|
||||
"enum": {{ weight_unit_choices }}
|
||||
},
|
||||
"subdevice-role": {
|
||||
"type": "string",
|
||||
"enum": {{ subdevice_role_choices }}
|
||||
},
|
||||
"console-port": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": {{ console_port_type_choices }}
|
||||
}
|
||||
}
|
||||
},
|
||||
"console-server-port": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": {{ console_server_port_type_choices }}
|
||||
}
|
||||
}
|
||||
},
|
||||
"power-port": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": {{ power_port_type_choices }}
|
||||
}
|
||||
}
|
||||
},
|
||||
"power-outlet": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": {{ power_outlet_type_choices }}
|
||||
},
|
||||
"feed-leg": {
|
||||
"type": "string",
|
||||
"enum": {{ power_outlet_feedleg_choices }}
|
||||
}
|
||||
}
|
||||
},
|
||||
"interface": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": {{ interface_type_choices }}
|
||||
},
|
||||
"poe_mode": {
|
||||
"type": "string",
|
||||
"enum": {{ interface_poe_mode_choices }}
|
||||
},
|
||||
"poe_type": {
|
||||
"type": "string",
|
||||
"enum": {{ interface_poe_type_choices }}
|
||||
}
|
||||
}
|
||||
},
|
||||
"front-port": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": {{ front_port_type_choices }}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rear-port": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": {{ rear_port_type_choices}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -60,7 +60,7 @@ Context:
|
||||
{# Extra buttons #}
|
||||
{% block extra_controls %}{% endblock %}
|
||||
|
||||
{% if perms.extras.add_bookmark %}
|
||||
{% if perms.extras.add_bookmark and object.bookmarks %}
|
||||
{% bookmark_button object %}
|
||||
{% endif %}
|
||||
{% if request.user|can_add:object %}
|
||||
|
58
netbox/templates/generic/object_children.html
Normal file
58
netbox/templates/generic/object_children.html
Normal file
@ -0,0 +1,58 @@
|
||||
{% extends base_template %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="noprint bulk-buttons">
|
||||
{% block bulk_controls %}
|
||||
<div class="bulk-button-group">
|
||||
<div class="btn-group" role="group">
|
||||
{# Bulk edit buttons #}
|
||||
{% block bulk_edit_controls %}
|
||||
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
||||
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
||||
<button type="submit" name="_edit"
|
||||
formaction="{% url bulk_edit_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock bulk_edit_controls %}
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
{# Bulk delete buttons #}
|
||||
{% block bulk_delete_controls %}
|
||||
{% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %}
|
||||
{% if 'bulk_delete' in actions and bulk_delete_view %}
|
||||
<button type="submit"
|
||||
formaction="{% url bulk_delete_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock bulk_delete_controls %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bulk-button-group">
|
||||
{# Other bulk action buttons #}
|
||||
{% block bulk_extra_controls %}{% endblock %}
|
||||
</div>
|
||||
{% endblock bulk_controls %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
@ -1,5 +1,4 @@
|
||||
{% extends 'ipam/aggregate/base.html' %}
|
||||
{% load helpers %}
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
@ -10,38 +9,4 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:aggregate_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:aggregate_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% endblock extra_controls %}
|
||||
|
@ -1,37 +0,0 @@
|
||||
{% extends 'ipam/asnrange/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="ASNTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'ipam:asn_bulk_edit' %}?return_url={% url 'ipam:asnrange_asns' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'ipam:asn_bulk_delete' %}?return_url={% url 'ipam:asnrange_asns' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
@ -1,19 +0,0 @@
|
||||
{% extends 'ipam/ipaddress/base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
@ -1,45 +1,10 @@
|
||||
{% extends 'ipam/iprange/base.html' %}
|
||||
{% load helpers %}
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and object.first_available_ip %}
|
||||
{% if perms.ipam.add_ipaddress and object.first_available_ip %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ object.first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-sm btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add IP Address" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% endblock extra_controls %}
|
||||
|
@ -1,5 +1,4 @@
|
||||
{% extends 'ipam/prefix/base.html' %}
|
||||
{% load helpers %}
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
@ -8,38 +7,4 @@
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add IP Address" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% endblock extra_controls %}
|
||||
|
@ -1,5 +1,4 @@
|
||||
{% extends 'ipam/prefix/base.html' %}
|
||||
{% load helpers %}
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
@ -8,38 +7,4 @@
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add IP Range" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'ipam:iprange_bulk_edit' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'ipam:iprange_bulk_delete' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% endblock extra_controls %}
|
||||
|
@ -1,5 +1,4 @@
|
||||
{% extends 'ipam/prefix/base.html' %}
|
||||
{% load helpers %}
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
@ -9,39 +8,4 @@
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Prefix" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% endblock extra_controls %}
|
||||
|
@ -1,20 +0,0 @@
|
||||
{% extends 'ipam/vlan/base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
@ -1,20 +0,0 @@
|
||||
{% extends 'ipam/vlan/base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
@ -1,4 +1,4 @@
|
||||
{% extends base_template %}
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
@ -11,20 +11,3 @@
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="ContactAssignmentTable_config" %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
|
@ -1,31 +1,13 @@
|
||||
{% extends 'virtualization/cluster/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
|
||||
|
||||
<form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if perms.virtualization.change_cluster %}
|
||||
<button type="submit" name="_remove" class="btn btn-danger btn-sm">
|
||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Remove Devices" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% block bulk_delete_controls %}
|
||||
{{ block.super }}
|
||||
{% if 'bulk_remove_devices' in actions %}
|
||||
<button type="submit" name="_remove"
|
||||
formaction="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}?return_url={{ return_url }}"
|
||||
class="btn btn-danger btn-sm">
|
||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Remove Selected" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endblock bulk_delete_controls %}
|
||||
|
@ -1,36 +0,0 @@
|
||||
{% extends 'virtualization/cluster/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'virtualization:virtualmachine_bulk_edit' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'virtualization:virtualmachine_bulk_delete' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
@ -43,6 +43,10 @@
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Config Template" %}</th>
|
||||
<td>{{ object.config_template|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Primary IPv4" %}</th>
|
||||
<td>
|
||||
|
@ -1,48 +1,14 @@
|
||||
{% extends 'virtualization/virtualmachine/base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% extends 'generic/object_children.html' %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noprint">
|
||||
{% if perms.virtualization.change_vminterface %}
|
||||
<div class="btn-group" role="group">
|
||||
<button type="submit" name="_edit" formaction="{% url 'virtualization:vminterface_bulk_edit' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">
|
||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
|
||||
</button>
|
||||
<button type="submit" name="_rename" formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
|
||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.delete_vminterface %}
|
||||
<button type="submit" name="_delete" formaction="{% url 'virtualization:vminterface_bulk_delete' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-danger btn-sm">
|
||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Delete" %}
|
||||
{% block bulk_edit_controls %}
|
||||
{{ block.super }}
|
||||
{% if 'bulk_rename' in actions %}
|
||||
<button type="submit" name="_rename"
|
||||
formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-warning btn-sm">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.add_vminterface %}
|
||||
<div class="float-end">
|
||||
<a href="{% url 'virtualization:vminterface_add' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add interfaces" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% table_config_form table %}
|
||||
{% endblock modals %}
|
||||
{% endif %}
|
||||
{% endblock bulk_edit_controls %}
|
||||
|
@ -0,0 +1,70 @@
|
||||
{% extends 'virtualization/virtualmachine/base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ object }} - {% trans "Config" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-5">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Config Template" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Config Template" %}</th>
|
||||
<td>{{ config_template|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Data Source" %}</th>
|
||||
<td>{{ config_template.data_file.source|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Data File" %}</th>
|
||||
<td>{{ config_template.data_file|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<div class="card">
|
||||
<div class="accordion accordion-flush" id="renderConfig">
|
||||
<div class="card-body">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="renderConfigHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsedRenderConfig" aria-expanded="false" aria-controls="collapsedRenderConfig">
|
||||
{% trans "Context Data" %}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapsedRenderConfig" class="accordion-collapse collapse" aria-labelledby="renderConfigHeading" data-bs-parent="#renderConfig">
|
||||
<div class="accordion-body">
|
||||
<pre class="card-body">{{ context_data|pprint }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="float-end">
|
||||
<a href="?export=True" class="btn btn-sm btn-primary" role="button">
|
||||
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
|
||||
</a>
|
||||
</div>
|
||||
<h5>{% trans "Rendered Config" %}</h5>
|
||||
</div>
|
||||
{% if config_template %}
|
||||
<pre class="card-body">{{ rendered_config }}</pre>
|
||||
{% else %}
|
||||
<div class="card-body text-muted">{% trans "No configuration template found" %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -88,7 +88,7 @@ class ContactAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
limit_choices_to=FeatureQuery('custom_fields'),
|
||||
limit_choices_to=FeatureQuery('contacts'),
|
||||
required=False,
|
||||
label=_('Object type')
|
||||
)
|
||||
|
@ -41,11 +41,6 @@ class ObjectContactsView(generic.ObjectChildrenView):
|
||||
|
||||
return table
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
|
||||
}
|
||||
|
||||
#
|
||||
# Tenant groups
|
||||
#
|
||||
|
@ -111,8 +111,10 @@ class UserConfigForm(BootstrapMixin, forms.ModelForm, metaclass=UserConfigFormMe
|
||||
class UserTokenForm(BootstrapMixin, forms.ModelForm):
|
||||
key = forms.CharField(
|
||||
label=_('Key'),
|
||||
required=False,
|
||||
help_text=_("If no key is provided, one will be generated automatically.")
|
||||
help_text=_(
|
||||
'Keys must be at least 40 characters in length. <strong>Be sure to record your key</strong> prior to '
|
||||
'submitting this form, as it may no longer be accessible once the token has been created.'
|
||||
)
|
||||
)
|
||||
allowed_ips = SimpleArrayField(
|
||||
base_field=IPNetworkFormField(validators=[prefix_validator]),
|
||||
@ -140,13 +142,15 @@ class UserTokenForm(BootstrapMixin, forms.ModelForm):
|
||||
if self.instance.pk and not settings.ALLOW_TOKEN_RETRIEVAL:
|
||||
del self.fields['key']
|
||||
|
||||
# Generate an initial random key if none has been specified
|
||||
if not self.instance.pk and not self.initial.get('key'):
|
||||
self.initial['key'] = Token.generate_key()
|
||||
|
||||
|
||||
class TokenForm(UserTokenForm):
|
||||
user = forms.ModelChoiceField(
|
||||
queryset=get_user_model().objects.order_by(
|
||||
'username'
|
||||
),
|
||||
required=False
|
||||
queryset=get_user_model().objects.order_by('username'),
|
||||
label=_('User')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -171,22 +171,23 @@ class TokenTestCase(
|
||||
create_test_user('User 2'),
|
||||
)
|
||||
tokens = (
|
||||
Token(key='123456790123456789012345678901234567890A', user=users[0]),
|
||||
Token(key='123456790123456789012345678901234567890B', user=users[0]),
|
||||
Token(key='123456790123456789012345678901234567890C', user=users[1]),
|
||||
Token(key='123456789012345678901234567890123456789A', user=users[0]),
|
||||
Token(key='123456789012345678901234567890123456789B', user=users[0]),
|
||||
Token(key='123456789012345678901234567890123456789C', user=users[1]),
|
||||
)
|
||||
Token.objects.bulk_create(tokens)
|
||||
|
||||
cls.form_data = {
|
||||
'user': users[0].pk,
|
||||
'key': '1234567890123456789012345678901234567890',
|
||||
'description': 'testdescription',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"key,user,description",
|
||||
f"123456790123456789012345678901234567890D,{users[0].pk},testdescriptionD",
|
||||
f"123456790123456789012345678901234567890E,{users[1].pk},testdescriptionE",
|
||||
f"123456790123456789012345678901234567890F,{users[1].pk},testdescriptionF",
|
||||
f"123456789012345678901234567890123456789D,{users[0].pk},testdescriptionD",
|
||||
f"123456789012345678901234567890123456789E,{users[1].pk},testdescriptionE",
|
||||
f"123456789012345678901234567890123456789F,{users[1].pk},testdescriptionF",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
|
@ -60,6 +60,9 @@ def parse_alphanumeric_range(string):
|
||||
except ValueError:
|
||||
begin, end = dash_range, dash_range
|
||||
if begin.isdigit() and end.isdigit():
|
||||
if int(begin) >= int(end):
|
||||
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
|
||||
|
||||
for n in list(range(int(begin), int(end) + 1)):
|
||||
values.append(n)
|
||||
else:
|
||||
@ -71,6 +74,10 @@ def parse_alphanumeric_range(string):
|
||||
# Not a valid range (more than a single character)
|
||||
if not len(begin) == len(end) == 1:
|
||||
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
|
||||
|
||||
if ord(begin) >= ord(end):
|
||||
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
|
||||
|
||||
for n in list(range(ord(begin), ord(end) + 1)):
|
||||
values.append(chr(n))
|
||||
return values
|
||||
|
@ -264,8 +264,9 @@ class ExpandAlphanumeric(TestCase):
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern('r[a-9]a')), [])
|
||||
|
||||
def test_invalid_range_bounds(self):
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern('r[9-8]a')), [])
|
||||
self.assertEqual(sorted(expand_alphanumeric_pattern('r[b-a]a')), [])
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
sorted(expand_alphanumeric_pattern('r[9-8]a'))
|
||||
sorted(expand_alphanumeric_pattern('r[b-a]a'))
|
||||
|
||||
def test_invalid_range_len(self):
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
|
@ -5,6 +5,7 @@ from dcim.api.nested_serializers import (
|
||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer,
|
||||
)
|
||||
from dcim.choices import InterfaceModeChoices
|
||||
from extras.api.nested_serializers import NestedConfigTemplateSerializer
|
||||
from ipam.api.nested_serializers import (
|
||||
NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer, NestedVRFSerializer,
|
||||
)
|
||||
@ -79,6 +80,7 @@ class VirtualMachineSerializer(NetBoxModelSerializer):
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
|
||||
|
||||
# Counter fields
|
||||
interface_count = serializers.IntegerField(read_only=True)
|
||||
@ -88,7 +90,8 @@ class VirtualMachineSerializer(NetBoxModelSerializer):
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments',
|
||||
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'interface_count',
|
||||
'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'interface_count',
|
||||
]
|
||||
validators = []
|
||||
|
||||
|
@ -5,6 +5,7 @@ from django.utils.translation import gettext as _
|
||||
from dcim.filtersets import CommonInterfaceFilterSet
|
||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from extras.models import ConfigTemplate
|
||||
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
||||
@ -228,6 +229,10 @@ class VirtualMachineFilterSet(
|
||||
method='_has_primary_ip',
|
||||
label=_('Has a primary IP'),
|
||||
)
|
||||
config_template_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
label=_('Config template (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
|
@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from dcim.choices import InterfaceModeChoices
|
||||
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
|
||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.models import VLAN, VLANGroup, VRF
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
@ -174,12 +175,17 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
config_template = DynamicModelChoiceField(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = VirtualMachine
|
||||
fieldsets = (
|
||||
(None, ('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform', 'description')),
|
||||
(_('Resources'), ('vcpus', 'memory', 'disk'))
|
||||
(_('Resources'), ('vcpus', 'memory', 'disk')),
|
||||
('Configuration', ('config_template',)),
|
||||
)
|
||||
nullable_fields = (
|
||||
'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments',
|
||||
|
@ -2,6 +2,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.choices import InterfaceModeChoices
|
||||
from dcim.models import Device, DeviceRole, Platform, Site
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.models import VRF
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
@ -123,12 +124,19 @@ class VirtualMachineImportForm(NetBoxModelImportForm):
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned platform')
|
||||
)
|
||||
config_template = CSVModelChoiceField(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
label=_('Config template'),
|
||||
help_text=_('Config template')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualMachine
|
||||
fields = (
|
||||
'name', 'status', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
|
||||
'description', 'comments', 'tags',
|
||||
'description', 'config_template', 'comments', 'tags',
|
||||
)
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||
from extras.forms import LocalConfigContextFilterForm
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.models import L2VPN, VRF
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||
@ -93,7 +94,7 @@ class VirtualMachineFilterForm(
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
(_('Cluster'), ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
|
||||
(_('Location'), ('region_id', 'site_group_id', 'site_id')),
|
||||
(_('Attributes'), ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
|
||||
(_('Attributes'), ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'config_template_id', 'local_context_data')),
|
||||
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||
)
|
||||
@ -170,6 +171,11 @@ class VirtualMachineFilterForm(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
config_template_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
required=False,
|
||||
label=_('Config template')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user