Compare commits

...

81 Commits

Author SHA1 Message Date
Jeremy Stretch
63c6687a87 Merge pull request #10026 from netbox-community/develop
Release v3.2.9
2022-08-16 12:29:59 -04:00
jeremystretch
e01b7951f2 Release v3.2.9 2022-08-16 11:50:14 -04:00
jeremystretch
8c220cc04f Fixes #9491: Remove button for adding inventory item templates to module type components 2022-08-16 11:39:51 -04:00
jeremystretch
9e9e90f88b Closes #9933: Add DOCSIS interface type 2022-08-16 10:11:40 -04:00
jeremystretch
6d328a82e9 Cleanup for #9505 2022-08-16 10:04:47 -04:00
jeremystretch
dedee0f9d9 #9979: Fix fallback to default value 2022-08-16 09:53:13 -04:00
jeremystretch
0ef1bc8490 Clean up bulk edit buttons 2022-08-16 09:49:51 -04:00
jeremystretch
30ab1e5a5e Changelog for #8723, #9505, #9979 2022-08-16 09:14:19 -04:00
Jeremy Stretch
14821eed44 Merge pull request #9639 from cpund/8723-branch
PR for #8723
2022-08-16 09:10:24 -04:00
Jeremy Stretch
c8ecee9682 Merge pull request #9712 from renatoalmeidaoliveira/develop
Include Network information in Prefix Template Issue:#9505
2022-08-16 09:06:56 -04:00
Jeremy Stretch
a8dd809f8e Merge pull request #9981 from chcon/develop
re-enable markdown in custom columns
2022-08-16 08:57:37 -04:00
Christoph Schneider
15f4b1fd5d add newline 2022-08-13 14:02:26 +02:00
Christoph Schneider
36491b13d8 remove class definition 2022-08-13 14:01:07 +02:00
Christoph Schneider
ac540b6183 remove import 2022-08-13 13:59:19 +02:00
Christoph Schneider
6f09d94e2a remove commented line 2022-08-13 13:56:51 +02:00
Christoph Schneider
f942216f3f re-enable markup in longtext custom columns 2022-08-13 13:54:38 +02:00
jeremystretch
ca0b21bef5 Closes #9980: Use standard table controls template for device interfaces list 2022-08-12 11:25:03 -04:00
jeremystretch
e4fa8af47f Changelog for #8595 2022-08-12 10:48:16 -04:00
Jeremy Stretch
6cf898fa13 Merge pull request #9982 from DorianXGH/pon_if_types
Closes #8595: Added new PON interface types
2022-08-12 10:43:15 -04:00
jeremystretch
41ad9b242c Fixes #9986: Workaround for upstream timezone data bug 2022-08-12 10:12:01 -04:00
Dorian Bourgeoisat
693ad700e8 Swapping NG-PON2 as main name instead of TWDM-PON 2022-08-12 00:49:13 +02:00
Craig Pund
5873ad95dc handle objects without names 2022-08-11 15:16:42 -04:00
Craig Pund
6a687a9ed1 not necessary to prefetch 2022-08-11 15:16:01 -04:00
jeremystretch
e2d5313940 Changelog for #9857 2022-08-11 13:02:37 -04:00
Jeremy Stretch
a59169fa96 Merge pull request #9964 from jsenecal/feat9857
Add a "clear" button for quick search
2022-08-11 12:20:33 -04:00
Jonathan Senecal
f74b7aa7ac Add a "clear" button for quick search
Fixes #9857
2022-08-11 08:26:25 -04:00
Christoph Schneider
9a80a491c9 re-enable markdown in custom columns 2022-08-11 14:11:41 +02:00
jeremystretch
aabe8f7c5b Changelog for #9625 2022-08-10 16:18:30 -04:00
Jeremy Stretch
10af44c12a Merge pull request #9970 from barnebyte-timewarp/develop
Closes #9625: Add Contact Phone/Email to quick view panes to save time
2022-08-10 16:16:04 -04:00
jeremystretch
a9aaa8939c Closes #9161: Pretty print JSON custom field data when editing 2022-08-10 16:12:04 -04:00
jeremystretch
8f1e70f01d Fixes #9961: Correct typo 2022-08-10 15:24:45 -04:00
Dorian Bourgeoisat
1c7ef73d1f Closes #8595: Added new PON interface types 2022-08-10 15:33:33 +02:00
Barnabas Lovas
c24f1f14ec Closes #9625: Add Contact Phone/Email to quick view panes to save time 2022-08-10 13:22:58 +02:00
Jeremy Stretch
b318b79027 Merge pull request #9958 from threadedstream/fix_typo_virt_filtersets
fix typo in virtualization/forms/filtersets.py
2022-08-09 14:29:58 -04:00
gildarov
c7faca9480 fix typo in virtualization/forms/filtersets.py 2022-08-09 11:56:19 +03:00
jeremystretch
064d7f3bd0 PRVB 2022-08-08 15:34:13 -04:00
Jeremy Stretch
f1877c0c5f Merge pull request #9955 from netbox-community/develop
Release v3.2.8
2022-08-08 15:32:38 -04:00
jeremystretch
ce7fb8ab17 Release v3.2.8 2022-08-08 15:17:36 -04:00
jeremystretch
caca074161 Fixes #9950: Prevent redirection to arbitrary URLs via 'next' parameter on login URL 2022-08-08 14:21:42 -04:00
jeremystretch
8721ad987c Fixes #9952: Prevent InvalidMove when attempting to assign a nested child object as parent 2022-08-08 12:22:22 -04:00
jeremystretch
876251c1cf Fixes #9948: Fix TypeError exception when requesting API tokens list as non-authenticated user 2022-08-08 12:22:01 -04:00
jeremystretch
36ac83a319 Fixes #9949: Fix KeyError exception resulting from invalid API token provisioning request 2022-08-08 11:43:27 -04:00
jeremystretch
90317adae7 Clean up usages of mark_safe() 2022-08-08 10:47:07 -04:00
jeremystretch
135543683d Changelog for #9919 2022-08-08 10:24:49 -04:00
Jeremy Stretch
38350a1023 Merge pull request #9940 from osamu-kj/develop
Fixes #9919: XSS Bypass in custom fields displayed in tables
2022-08-08 10:10:11 -04:00
jeremystretch
0e1947bc4b PEP8 fix 2022-08-08 09:58:58 -04:00
Osamu-kj
7141fc8eb0 Custom fields - removed the debug lines 2022-08-06 17:17:43 +02:00
Osamu-kj
db38ed4f19 Fixed the XSS protection code inside custom fields 2022-08-06 15:10:31 +02:00
Osamu-kj
f874e9932d Added HTML Sanitization to the custom fields 2022-08-04 18:52:25 +02:00
jeremystretch
a2e84dd279 Changelog for #9827, #9906 2022-08-03 15:22:51 -04:00
Jeremy Stretch
a397ce234a Merge pull request #9850 from sleepinggenius2/issue_9827
Adds patterned_fields support for bulk component creation
2022-08-03 15:22:16 -04:00
Jeremy Stretch
3694e5e846 Merge pull request #9911 from oasys/9906-support-color-on-frontrearport-import-export
Fixes #9906 import/export front/rearport color field for module- and device-types
2022-08-03 14:58:02 -04:00
Jason Lavoie
c6e25f068d import/export color field on front- and rear-ports for module-types and device-types
Closes: #9906

- Adds `color` field to front and rearport template import forms
- Adds `color` field to `to_yaml` export for front and rearport
  templates
2022-08-03 09:22:06 -04:00
jeremystretch
ff3fcb8134 #9871: Tweak display of utilization graph value 2022-08-01 12:38:12 -04:00
jeremystretch
d4d73674fc Fixes #9871: Fix utilization graph value alignments 2022-08-01 11:54:39 -04:00
jeremystretch
984d15d7fb Closes #9893: Move YAML serialization logic for component templates under the individual models 2022-08-01 11:39:07 -04:00
jeremystretch
efa449faff Closes #9882: Add manufacturer column to modules table 2022-08-01 10:36:53 -04:00
jeremystretch
3af989763e Closes #9883: Linkify location column in power panels table 2022-08-01 10:32:05 -04:00
jeremystretch
9646f88384 Fixes #9885: Fix child prefix counts when editing/deleting aggregates in bulk 2022-08-01 10:28:21 -04:00
jeremystretch
1bbf5d214b Closes #9881: Increase granularity in utilization graph values 2022-08-01 10:23:18 -04:00
jeremystretch
8a075bcff9 Fixes #9884: Prevent querying assigned VRF on prefix object init 2022-08-01 09:47:18 -04:00
jeremystretch
9fe5f09742 Fixes #9891: Ensure consistent ordering for tags during object serialization 2022-08-01 09:32:52 -04:00
jeremystretch
84f2225f42 PEP8 cleanup 2022-08-01 09:16:58 -04:00
jeremystretch
728ad51624 Changelog & cleanup for #9637 2022-08-01 09:12:15 -04:00
atownson
5ab03b7e92 Closes #9637: Add existing fields to the Rack Reservation user interface pages (#9870)
* Closes netbox-community#9637

Added site_group to RackReservationFrom class fieldsets
Added location to RackReservationTable class
2022-08-01 09:01:18 -04:00
Daniel Sheppard
890efa5400 Fixes #9062 - Add/edit {module} substitution to help text for component template name 2022-07-29 11:55:26 -05:00
jeremystretch
07620db027 Changelog for #9762 2022-07-28 12:45:27 -04:00
Jeremy Stretch
f8a3ffae4e Merge pull request #9868 from atownson/issue_9762
Closes #9762: Add nat_outside to the IPAddress table
2022-07-28 12:43:13 -04:00
atownson
62d1510c55 Closes netbox-community#9762
Added nat_outside to IPAddressTable class
2022-07-28 10:23:04 -05:00
jeremystretch
498b655cb7 Changelog and cleanup for #9825 2022-07-27 16:50:31 -04:00
Jeremy Stretch
fa94d9c82c Merge pull request #9826 from viroge/develop
Closes #9825: Add Contacts to VM table view
2022-07-27 16:48:08 -04:00
jeremystretch
6cee12b153 Fix formatting of webhook conditions form field 2022-07-27 15:40:25 -04:00
sleepinggenius2
bbf4b906e4 Adds patterned_fields support for bulk components 2022-07-26 17:16:03 -04:00
Renato Almeida de Oliveira
7d6882bec2 Change display to Modal 2022-07-23 20:24:33 +00:00
Gabor SOMOGYVARI
451a0067c7 Closes #9825: Add Contacts to VM table view 2022-07-22 10:42:20 +02:00
jeremystretch
383918d83b PRVB 2022-07-20 11:15:02 -04:00
Renato Almeida de Oliveira
e135f8e74d Include Network information in Prefix Template Issue:#9505 2022-07-13 02:49:14 +00:00
Craig Pund
76e634330f draft for error handling on device with no name 2022-06-30 16:00:03 -04:00
Craig Pund
ef03a2f383 fix return url to account 4 filtered device lists 2022-06-30 14:13:56 -04:00
Craig Pund
5dff7433e8 add bulk device rename button to device_list 2022-06-30 01:38:53 -04:00
Craig Pund
fa014fcbf0 add device bulk rename view and url 2022-06-30 01:38:38 -04:00
71 changed files with 874 additions and 596 deletions

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ bleach
# The Python web framework on which NetBox is built
# https://github.com/django/django
Django
Django<4.1
# Django middleware which permits cross-domain API requests
# https://github.com/OttoYiu/django-cors-headers

View File

@@ -1,3 +1,3 @@
## Front Ports
Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple rear ports, using numeric positions to annotate the specific alignment of each.
Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple front ports, using numeric positions to annotate the specific alignment of each.

View File

@@ -1,5 +1,53 @@
# NetBox v3.2
## v3.2.9 (2022-08-16)
### Enhancements
* [#8595](https://github.com/netbox-community/netbox/issues/8595) - Add PON interface types
* [#8723](https://github.com/netbox-community/netbox/issues/8723) - Enable bulk renaming of devices
* [#9161](https://github.com/netbox-community/netbox/issues/9161) - Pretty print JSON custom field data when editing
* [#9505](https://github.com/netbox-community/netbox/issues/9505) - Display extra addressing details for IPv4 prefixes
* [#9625](https://github.com/netbox-community/netbox/issues/9625) - Add phone & email details to contacts panel
* [#9857](https://github.com/netbox-community/netbox/issues/9857) - Add clear button to quick search fields
* [#9933](https://github.com/netbox-community/netbox/issues/9933) - Add DOCSIS interface type
### Bug Fixes
* [#9491](https://github.com/netbox-community/netbox/issues/9491) - Remove button for adding inventory item templates to module type components
* [#9979](https://github.com/netbox-community/netbox/issues/9979) - Fix Markdown rendering for custom fields in table columns
* [#9986](https://github.com/netbox-community/netbox/issues/9986) - Workaround for upstream timezone data bug
---
## v3.2.8 (2022-08-08)
### Enhancements
* [#9062](https://github.com/netbox-community/netbox/issues/9062) - Add/edit {module} substitution to help text for component template name
* [#9637](https://github.com/netbox-community/netbox/issues/9637) - Add site group field to rack reservation form
* [#9762](https://github.com/netbox-community/netbox/issues/9762) - Add `nat_outside` column to the IPAddress table
* [#9825](https://github.com/netbox-community/netbox/issues/9825) - Add contacts column to virtual machines table
* [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values
* [#9882](https://github.com/netbox-community/netbox/issues/9882) - Add manufacturer column to modules table
* [#9883](https://github.com/netbox-community/netbox/issues/9883) - Linkify location column in power panels table
* [#9906](https://github.com/netbox-community/netbox/issues/9906) - Include `color` attribute in front & rear port YAML import/export
### Bug Fixes
* [#9827](https://github.com/netbox-community/netbox/issues/9827) - Fix assignment of module bay position during bulk creation
* [#9871](https://github.com/netbox-community/netbox/issues/9871) - Fix utilization graph value alignments
* [#9884](https://github.com/netbox-community/netbox/issues/9884) - Prevent querying assigned VRF on prefix object init
* [#9885](https://github.com/netbox-community/netbox/issues/9885) - Fix child prefix counts when editing/deleting aggregates in bulk
* [#9891](https://github.com/netbox-community/netbox/issues/9891) - Ensure consistent ordering for tags during object serialization
* [#9919](https://github.com/netbox-community/netbox/issues/9919) - Fix potential XSS avenue via linked objects in tables
* [#9948](https://github.com/netbox-community/netbox/issues/9948) - Fix TypeError exception when requesting API tokens list as non-authenticated user
* [#9949](https://github.com/netbox-community/netbox/issues/9949) - Fix KeyError exception resulting from invalid API token provisioning request
* [#9950](https://github.com/netbox-community/netbox/issues/9950) - Prevent redirection to arbitrary URLs via `next` parameter on login URL
* [#9952](https://github.com/netbox-community/netbox/issues/9952) - Prevent InvalidMove when attempting to assign a nested child object as parent
---
## v3.2.7 (2022-07-20)
### Enhancements

View File

@@ -814,6 +814,17 @@ class InterfaceTypeChoices(ChoiceSet):
# ATM/DSL
TYPE_XDSL = 'xdsl'
# Coaxial
TYPE_DOCSIS = 'docsis'
# PON
TYPE_GPON = 'gpon'
TYPE_XG_PON = 'xg-pon'
TYPE_XGS_PON = 'xgs-pon'
TYPE_NG_PON2 = 'ng-pon2'
TYPE_EPON = 'epon'
TYPE_10G_EPON = '10g-epon'
# Stacking
TYPE_STACKWISE = 'cisco-stackwise'
TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus'
@@ -950,6 +961,23 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_XDSL, 'xDSL'),
)
),
(
'Coaxial',
(
(TYPE_DOCSIS, 'DOCSIS'),
)
),
(
'PON',
(
(TYPE_GPON, 'GPON (2.5 Gbps / 1.25 Gps)'),
(TYPE_XG_PON, 'XG-PON (10 Gbps / 2.5 Gbps)'),
(TYPE_XGS_PON, 'XGS-PON (10 Gbps)'),
(TYPE_NG_PON2, 'NG-PON2 (TWDM-PON) (4x10 Gbps)'),
(TYPE_EPON, 'EPON (1 Gbps)'),
(TYPE_10G_EPON, '10G-EPON (10 Gbps)'),
)
),
(
'Stacking',
(

View File

@@ -287,7 +287,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'tag')),
('User', ('user_id',)),
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
region_id = DynamicModelMultipleChoiceField(
@@ -295,25 +295,38 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
required=False,
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id'
},
label=_('Site')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.prefetch_related('site'),
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site_id',
},
label=_('Location'),
null_option='None'
)
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
required=False,
query_params={
'site_id': '$site_id',
'location_id': '$location_id',
},
label=_('Rack')
)
user_id = DynamicModelMultipleChoiceField(
queryset=User.objects.all(),
required=False,

View File

@@ -321,7 +321,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
)
fieldsets = (
('Reservation', ('region', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
)

View File

@@ -64,6 +64,14 @@ class ModularComponentTemplateCreateForm(ComponentCreateForm):
"""
Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType.
"""
name_pattern = ExpandableNameField(
label='Name',
help_text="""
Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
are not supported. Example: <code>[ge,xe]-0/0/[0-9]</code>. {module} is accepted as a substitution for
the module bay position.
"""
)
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(),
required=False

View File

@@ -146,7 +146,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = FrontPortTemplate
fields = [
'device_type', 'module_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label', 'description',
]
@@ -158,7 +158,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = RearPortTemplate
fields = [
'device_type', 'module_type', 'name', 'type', 'positions', 'label', 'description',
'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
]

View File

@@ -39,7 +39,10 @@ class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel):
related_name='%(class)ss'
)
name = models.CharField(
max_length=64
max_length=64,
help_text="""
{module} is accepted as a substitution for the module bay position when attached to a module type.
"""
)
_name = NaturalOrderingField(
target_field='name',
@@ -157,6 +160,14 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'label': self.label,
'description': self.description,
}
class ConsoleServerPortTemplate(ModularComponentTemplateModel):
"""
@@ -185,6 +196,14 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'label': self.label,
'description': self.description,
}
class PowerPortTemplate(ModularComponentTemplateModel):
"""
@@ -236,6 +255,16 @@ class PowerPortTemplate(ModularComponentTemplateModel):
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
})
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'maximum_draw': self.maximum_draw,
'allocated_draw': self.allocated_draw,
'label': self.label,
'description': self.description,
}
class PowerOutletTemplate(ModularComponentTemplateModel):
"""
@@ -298,6 +327,16 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'power_port': self.power_port.name if self.power_port else None,
'feed_leg': self.feed_leg,
'label': self.label,
'description': self.description,
}
class InterfaceTemplate(ModularComponentTemplateModel):
"""
@@ -337,6 +376,15 @@ class InterfaceTemplate(ModularComponentTemplateModel):
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'mgmt_only': self.mgmt_only,
'label': self.label,
'description': self.description,
}
class FrontPortTemplate(ModularComponentTemplateModel):
"""
@@ -410,6 +458,17 @@ class FrontPortTemplate(ModularComponentTemplateModel):
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'color': self.color,
'rear_port': self.rear_port.name,
'rear_port_position': self.rear_port_position,
'label': self.label,
'description': self.description,
}
class RearPortTemplate(ModularComponentTemplateModel):
"""
@@ -449,6 +508,16 @@ class RearPortTemplate(ModularComponentTemplateModel):
**kwargs
)
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'color': self.color,
'positions': self.positions,
'label': self.label,
'description': self.description,
}
class ModuleBayTemplate(ComponentTemplateModel):
"""
@@ -474,6 +543,14 @@ class ModuleBayTemplate(ComponentTemplateModel):
position=self.position
)
def to_yaml(self):
return {
'name': self.name,
'label': self.label,
'position': self.position,
'description': self.description,
}
class DeviceBayTemplate(ComponentTemplateModel):
"""
@@ -498,6 +575,13 @@ class DeviceBayTemplate(ComponentTemplateModel):
f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
)
def to_yaml(self):
return {
'name': self.name,
'label': self.label,
'description': self.description,
}
class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
"""

View File

@@ -1,5 +1,3 @@
from collections import OrderedDict
import yaml
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
@@ -161,115 +159,54 @@ class DeviceType(NetBoxModel):
return reverse('dcim:devicetype', args=[self.pk])
def to_yaml(self):
data = OrderedDict((
('manufacturer', self.manufacturer.name),
('model', self.model),
('slug', self.slug),
('part_number', self.part_number),
('u_height', self.u_height),
('is_full_depth', self.is_full_depth),
('subdevice_role', self.subdevice_role),
('airflow', self.airflow),
('comments', self.comments),
))
data = {
'manufacturer': self.manufacturer.name,
'model': self.model,
'slug': self.slug,
'part_number': self.part_number,
'u_height': self.u_height,
'is_full_depth': self.is_full_depth,
'subdevice_role': self.subdevice_role,
'airflow': self.airflow,
'comments': self.comments,
}
# Component templates
if self.consoleporttemplates.exists():
data['console-ports'] = [
{
'name': c.name,
'type': c.type,
'label': c.label,
'description': c.description,
}
for c in self.consoleporttemplates.all()
c.to_yaml() for c in self.consoleporttemplates.all()
]
if self.consoleserverporttemplates.exists():
data['console-server-ports'] = [
{
'name': c.name,
'type': c.type,
'label': c.label,
'description': c.description,
}
for c in self.consoleserverporttemplates.all()
c.to_yaml() for c in self.consoleserverporttemplates.all()
]
if self.powerporttemplates.exists():
data['power-ports'] = [
{
'name': c.name,
'type': c.type,
'maximum_draw': c.maximum_draw,
'allocated_draw': c.allocated_draw,
'label': c.label,
'description': c.description,
}
for c in self.powerporttemplates.all()
c.to_yaml() for c in self.powerporttemplates.all()
]
if self.poweroutlettemplates.exists():
data['power-outlets'] = [
{
'name': c.name,
'type': c.type,
'power_port': c.power_port.name if c.power_port else None,
'feed_leg': c.feed_leg,
'label': c.label,
'description': c.description,
}
for c in self.poweroutlettemplates.all()
c.to_yaml() for c in self.poweroutlettemplates.all()
]
if self.interfacetemplates.exists():
data['interfaces'] = [
{
'name': c.name,
'type': c.type,
'mgmt_only': c.mgmt_only,
'label': c.label,
'description': c.description,
}
for c in self.interfacetemplates.all()
c.to_yaml() for c in self.interfacetemplates.all()
]
if self.frontporttemplates.exists():
data['front-ports'] = [
{
'name': c.name,
'type': c.type,
'rear_port': c.rear_port.name,
'rear_port_position': c.rear_port_position,
'label': c.label,
'description': c.description,
}
for c in self.frontporttemplates.all()
c.to_yaml() for c in self.frontporttemplates.all()
]
if self.rearporttemplates.exists():
data['rear-ports'] = [
{
'name': c.name,
'type': c.type,
'positions': c.positions,
'label': c.label,
'description': c.description,
}
for c in self.rearporttemplates.all()
c.to_yaml() for c in self.rearporttemplates.all()
]
if self.modulebaytemplates.exists():
data['module-bays'] = [
{
'name': c.name,
'label': c.label,
'position': c.position,
'description': c.description,
}
for c in self.modulebaytemplates.all()
c.to_yaml() for c in self.modulebaytemplates.all()
]
if self.devicebaytemplates.exists():
data['device-bays'] = [
{
'name': c.name,
'label': c.label,
'description': c.description,
}
for c in self.devicebaytemplates.all()
c.to_yaml() for c in self.devicebaytemplates.all()
]
return yaml.dump(dict(data), sort_keys=False)
@@ -395,91 +332,41 @@ class ModuleType(NetBoxModel):
return reverse('dcim:moduletype', args=[self.pk])
def to_yaml(self):
data = OrderedDict((
('manufacturer', self.manufacturer.name),
('model', self.model),
('part_number', self.part_number),
('comments', self.comments),
))
data = {
'manufacturer': self.manufacturer.name,
'model': self.model,
'part_number': self.part_number,
'comments': self.comments,
}
# Component templates
if self.consoleporttemplates.exists():
data['console-ports'] = [
{
'name': c.name,
'type': c.type,
'label': c.label,
'description': c.description,
}
for c in self.consoleporttemplates.all()
c.to_yaml() for c in self.consoleporttemplates.all()
]
if self.consoleserverporttemplates.exists():
data['console-server-ports'] = [
{
'name': c.name,
'type': c.type,
'label': c.label,
'description': c.description,
}
for c in self.consoleserverporttemplates.all()
c.to_yaml() for c in self.consoleserverporttemplates.all()
]
if self.powerporttemplates.exists():
data['power-ports'] = [
{
'name': c.name,
'type': c.type,
'maximum_draw': c.maximum_draw,
'allocated_draw': c.allocated_draw,
'label': c.label,
'description': c.description,
}
for c in self.powerporttemplates.all()
c.to_yaml() for c in self.powerporttemplates.all()
]
if self.poweroutlettemplates.exists():
data['power-outlets'] = [
{
'name': c.name,
'type': c.type,
'power_port': c.power_port.name if c.power_port else None,
'feed_leg': c.feed_leg,
'label': c.label,
'description': c.description,
}
for c in self.poweroutlettemplates.all()
c.to_yaml() for c in self.poweroutlettemplates.all()
]
if self.interfacetemplates.exists():
data['interfaces'] = [
{
'name': c.name,
'type': c.type,
'mgmt_only': c.mgmt_only,
'label': c.label,
'description': c.description,
}
for c in self.interfacetemplates.all()
c.to_yaml() for c in self.interfacetemplates.all()
]
if self.frontporttemplates.exists():
data['front-ports'] = [
{
'name': c.name,
'type': c.type,
'rear_port': c.rear_port.name,
'rear_port_position': c.rear_port_position,
'label': c.label,
'description': c.description,
}
for c in self.frontporttemplates.all()
c.to_yaml() for c in self.frontporttemplates.all()
]
if self.rearporttemplates.exists():
data['rear-ports'] = [
{
'name': c.name,
'type': c.type,
'positions': c.positions,
'label': c.label,
'description': c.description,
}
for c in self.rearporttemplates.all()
c.to_yaml() for c in self.rearporttemplates.all()
]
return yaml.dump(dict(data), sort_keys=False)

View File

@@ -14,6 +14,9 @@ class ModuleTypeTable(NetBoxTable):
linkify=True,
verbose_name='Module Type'
)
manufacturer = tables.Column(
linkify=True
)
instance_count = columns.LinkedCountColumn(
viewname='dcim:module_list',
url_params={'module_type_id': 'pk'},
@@ -41,6 +44,10 @@ class ModuleTable(NetBoxTable):
module_bay = tables.Column(
linkify=True
)
manufacturer = tables.Column(
accessor=tables.A('module_type__manufacturer'),
linkify=True
)
module_type = tables.Column(
linkify=True
)
@@ -52,8 +59,9 @@ class ModuleTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Module
fields = (
'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'comments', 'tags',
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag', 'comments',
'tags',
)
default_columns = (
'pk', 'id', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag',
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'serial', 'asset_tag',
)

View File

@@ -21,6 +21,9 @@ class PowerPanelTable(NetBoxTable):
site = tables.Column(
linkify=True
)
location = tables.Column(
linkify=True
)
powerfeed_count = columns.LinkedCountColumn(
viewname='dcim:powerfeed_list',
url_params={'power_panel_id': 'pk'},
@@ -35,7 +38,9 @@ class PowerPanelTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = PowerPanel
fields = ('pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',)
fields = (
'pk', 'id', 'name', 'site', 'location', 'powerfeed_count', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')

View File

@@ -109,6 +109,10 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
accessor=Accessor('rack__site'),
linkify=True
)
location = tables.Column(
accessor=Accessor('rack__location'),
linkify=True
)
rack = tables.Column(
linkify=True
)
@@ -123,7 +127,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = RackReservation
fields = (
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')

View File

@@ -100,7 +100,7 @@ LOCATION_BUTTONS = """
MODULAR_COMPONENT_TEMPLATE_BUTTONS = """
{% load helpers %}
{% if perms.dcim.add_inventoryitemtemplate %}
{% if perms.dcim.add_inventoryitemtemplate and record.device_type_id %}
<a href="{% url 'dcim:inventoryitemtemplate_add' %}?device_type={{ record.device_type_id }}&component_type={{ record|content_type_id }}&component_id={{ record.pk }}&return_url={{ request.path }}" title="Add inventory item" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
</a>

View File

@@ -194,14 +194,14 @@ class RackTestCase(TestCase):
# Validate inventory (front face)
rack1_inventory_front = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT)
self.assertEqual(rack1_inventory_front[-10]['device'], device1)
del(rack1_inventory_front[-10])
del rack1_inventory_front[-10]
for u in rack1_inventory_front:
self.assertIsNone(u['device'])
# Validate inventory (rear face)
rack1_inventory_rear = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR)
self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
del(rack1_inventory_rear[-10])
del rack1_inventory_rear[-10]
for u in rack1_inventory_rear:
self.assertIsNone(u['device'])

View File

@@ -248,6 +248,7 @@ urlpatterns = [
path('devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'),
path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
path('devices/rename/', views.DeviceBulkRenameView.as_view(), name='device_bulk_rename'),
path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
path('devices/<int:pk>/', views.DeviceView.as_view(), name='device'),
path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'),

View File

@@ -1784,6 +1784,12 @@ class DeviceBulkDeleteView(generic.BulkDeleteView):
table = tables.DeviceTable
class DeviceBulkRenameView(generic.BulkRenameView):
queryset = Device.objects.all()
filterset = filtersets.DeviceFilterSet
table = tables.DeviceTable
#
# Devices
#
@@ -2707,6 +2713,7 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
filterset = filtersets.DeviceFilterSet
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
patterned_fields = ('name', 'label', 'position')
class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
@@ -3082,7 +3089,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
if membership_form.is_valid():
membership_form.save()
msg = 'Added member <a href="{}">{}</a>'.format(device.get_absolute_url(), escape(device))
msg = f'Added member <a href="{device.get_absolute_url()}">{escape(device)}</a>'
messages.success(request, mark_safe(msg))
if '_addanother' in request.POST:
@@ -3127,8 +3134,7 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
# Protect master device from being removed
virtual_chassis = VirtualChassis.objects.filter(master=device).first()
if virtual_chassis is not None:
msg = 'Unable to remove master device {} from the virtual chassis.'.format(escape(device))
messages.error(request, mark_safe(msg))
messages.error(request, f'Unable to remove master device {device} from the virtual chassis.')
return redirect(device.get_absolute_url())
if form.is_valid():

View File

@@ -133,6 +133,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
'http_method': StaticSelect(),
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
'body_template': forms.Textarea(attrs={'class': 'font-monospace'}),
'conditions': forms.Textarea(attrs={'class': 'font-monospace'}),
}

View File

@@ -18,7 +18,7 @@ from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
from utilities import filters
from utilities.forms import (
CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice,
JSONField, LaxURLField, StaticSelectMultiple, StaticSelect, add_blank_choice,
)
from utilities.querysets import RestrictedQuerySet
from utilities.validators import validate_regex
@@ -169,7 +169,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
model = ct.model_class()
instances = model.objects.filter(**{f'custom_field_data__{self.name}__isnull': False})
for instance in instances:
del(instance.custom_field_data[self.name])
del instance.custom_field_data[self.name]
model.objects.bulk_update(instances, ['custom_field_data'], batch_size=100)
def rename_object_data(self, old_name, new_name):
@@ -343,7 +343,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
# JSON
elif self.type == CustomFieldTypeChoices.TYPE_JSON:
field = forms.JSONField(required=required, initial=initial)
field = JSONField(required=required, initial=initial)
# Object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:

View File

@@ -992,7 +992,7 @@ class CustomFieldModelTest(TestCase):
with self.assertRaises(ValidationError):
site.clean()
del(site.cf['bar'])
del site.cf['bar']
site.clean()
def test_missing_required_field(self):

View File

@@ -30,4 +30,4 @@ class RegistryTest(TestCase):
reg['foo'] = 123
with self.assertRaises(TypeError):
del(reg['foo'])
del reg['foo']

View File

@@ -848,7 +848,7 @@ class ServiceCreateForm(ServiceForm):
# Fields which may be populated from a ServiceTemplate are not required
for field in ('name', 'protocol', 'ports'):
self.fields[field].required = False
del(self.fields[field].widget.attrs['required'])
del self.fields[field].widget.attrs['required']
def clean(self):
if self.cleaned_data['service_template']:

View File

@@ -373,7 +373,7 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
# Cache the original prefix and VRF so we can check if they have changed on post_save
self._prefix = self.prefix
self._vrf = self.vrf
self._vrf_id = self.vrf_id
def __str__(self):
return str(self.prefix)

View File

@@ -30,14 +30,14 @@ def update_children_depth(prefix):
def handle_prefix_saved(instance, created, **kwargs):
# Prefix has changed (or new instance has been created)
if created or instance.vrf != instance._vrf or instance.prefix != instance._prefix:
if created or instance.vrf_id != instance._vrf_id or instance.prefix != instance._prefix:
update_parents_children(instance)
update_children_depth(instance)
# If this is not a new prefix, clean up parent/children of previous prefix
if not created:
old_prefix = Prefix(vrf=instance._vrf, prefix=instance._prefix)
old_prefix = Prefix(vrf_id=instance._vrf_id, prefix=instance._prefix)
update_parents_children(old_prefix)
update_children_depth(old_prefix)

View File

@@ -369,6 +369,11 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
orderable=False,
verbose_name='NAT (Inside)'
)
nat_outside = tables.Column(
linkify=True,
orderable=False,
verbose_name='NAT (Outside)'
)
assigned = columns.BooleanColumn(
accessor='assigned_object_id',
linkify=True,
@@ -381,7 +386,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = IPAddress
fields = (
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'assigned', 'dns_name', 'description',
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside', 'assigned', 'dns_name', 'description',
'tags', 'created', 'last_updated',
)
default_columns = (

View File

@@ -333,14 +333,18 @@ class AggregateBulkImportView(generic.BulkImportView):
class AggregateBulkEditView(generic.BulkEditView):
queryset = Aggregate.objects.prefetch_related('rir')
queryset = Aggregate.objects.annotate(
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
)
filterset = filtersets.AggregateFilterSet
table = tables.AggregateTable
form = forms.AggregateBulkEditForm
class AggregateBulkDeleteView(generic.BulkDeleteView):
queryset = Aggregate.objects.prefetch_related('rir')
queryset = Aggregate.objects.annotate(
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
)
filterset = filtersets.AggregateFilterSet
table = tables.AggregateTable

View File

@@ -89,9 +89,9 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
super().clean()
# An MPTT model cannot be its own parent
if self.pk and self.parent_id == self.pk:
if self.pk and self.parent and self.parent in self.get_descendants(include_self=True):
raise ValidationError({
"parent": "Cannot assign self as parent."
"parent": f"Cannot assign self or child {self._meta.verbose_name} as parent."
})

View File

@@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
# Environment setup
#
VERSION = '3.2.7'
VERSION = '3.2.9'
# Hostname
HOSTNAME = platform.node()

View File

@@ -7,12 +7,14 @@ from django.contrib.auth.models import AnonymousUser
from django.db.models import DateField, DateTimeField
from django.template import Context, Template
from django.urls import reverse
from django.utils.html import escape
from django.utils.formats import date_format
from django.utils.safestring import mark_safe
from django_tables2.columns import library
from django_tables2.utils import Accessor
from extras.choices import CustomFieldTypeChoices
from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import content_type_identifier, content_type_name, get_viewname
__all__ = (
@@ -426,10 +428,10 @@ class CustomFieldColumn(tables.Column):
super().__init__(*args, **kwargs)
@staticmethod
def _likify_item(item):
def _linkify_item(item):
if hasattr(item, 'get_absolute_url'):
return f'<a href="{item.get_absolute_url()}">{item}</a>'
return item
return f'<a href="{item.get_absolute_url()}">{escape(item)}</a>'
return escape(item)
def render(self, value):
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True:
@@ -437,16 +439,18 @@ class CustomFieldColumn(tables.Column):
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False:
return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
return mark_safe(f'<a href="{value}">{value}</a>')
return mark_safe(f'<a href="{escape(value)}">{escape(value)}</a>')
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
return ', '.join(v for v in value)
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
return mark_safe(', '.join([
self._likify_item(obj) for obj in self.customfield.deserialize(value)
]))
return mark_safe(', '.join(
self._linkify_item(obj) for obj in self.customfield.deserialize(value)
))
if self.customfield.type == CustomFieldTypeChoices.TYPE_LONGTEXT and value:
return render_markdown(value)
if value is not None:
obj = self.customfield.deserialize(value)
return mark_safe(self._likify_item(obj))
return mark_safe(self._linkify_item(obj))
return self.default
def value(self, value):

View File

@@ -633,7 +633,7 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
replace = form.cleaned_data['replace']
if form.cleaned_data['use_regex']:
try:
obj.new_name = re.sub(find, replace, obj.name)
obj.new_name = re.sub(find, replace, obj.name or '')
# Catch regex group reference errors
except re.error:
obj.new_name = obj.name
@@ -795,6 +795,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
model_form = None
filterset = None
table = None
patterned_fields = ('name', 'label')
def get_required_permission(self):
return f'dcim.add_{self.queryset.model._meta.model_name}'
@@ -830,16 +831,16 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
for obj in data['pk']:
names = data['name_pattern']
labels = data['label_pattern'] if 'label_pattern' in data else None
for i, name in enumerate(names):
label = labels[i] if labels else None
pattern_count = len(data[f'{self.patterned_fields[0]}_pattern'])
for i in range(pattern_count):
component_data = {
self.parent_field: obj.pk,
'name': name,
'label': label
self.parent_field: obj.pk
}
for field_name in self.patterned_fields:
if data.get(f'{field_name}_pattern'):
component_data[field_name] = data[f'{field_name}_pattern'][i]
component_data.update(data)
component_form = self.model_form(component_data)
if component_form.is_valid():

View File

@@ -386,10 +386,10 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
)
logger.info(f"{msg} {obj} (PK: {obj.pk})")
if hasattr(obj, 'get_absolute_url'):
msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
msg = mark_safe(f'{msg} <a href="{obj.get_absolute_url()}">{escape(obj)}</a>')
else:
msg = '{} {}'.format(msg, escape(obj))
messages.success(request, mark_safe(msg))
msg = f'{msg} {obj}'
messages.success(request, msg)
if '_addanother' in request.POST:
redirect_url = request.path

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -27,6 +27,23 @@ function handleSearchDropdownClick(event: Event, button: HTMLButtonElement): voi
}
}
/**
* Show/hide quicksearch clear button.
*
* @param event "keyup" or "search" event for the quicksearch input
*/
function quickSearchEventHandler(event: Event): void {
const quicksearch = event.currentTarget as HTMLInputElement;
const inputgroup = quicksearch.parentElement as HTMLDivElement;
if (isTruthy(inputgroup)) {
if (quicksearch.value === "") {
inputgroup.classList.add("hide-last-child");
} else {
inputgroup.classList.remove("hide-last-child");
}
}
}
/**
* Initialize Search Bar Elements.
*/
@@ -40,8 +57,35 @@ function initSearchBar(): void {
}
}
/**
* Initialize Quicksearch Event listener/handlers.
*/
function initQuickSearch(): void {
const quicksearch = document.getElementById("quicksearch") as HTMLInputElement;
const clearbtn = document.getElementById("quicksearch_clear") as HTMLButtonElement;
if (isTruthy(quicksearch)) {
quicksearch.addEventListener("keyup", quickSearchEventHandler, {
passive: true
})
quicksearch.addEventListener("search", quickSearchEventHandler, {
passive: true
})
if (isTruthy(clearbtn)) {
clearbtn.addEventListener("click", async () => {
const search = new Event('search');
quicksearch.value = '';
await new Promise(f => setTimeout(f, 100));
quicksearch.dispatchEvent(search);
}, {
passive: true
})
}
}
}
export function initSearch(): void {
for (const func of [initSearchBar]) {
func();
}
initQuickSearch();
}

View File

@@ -416,6 +416,27 @@ nav.search {
}
}
// Styles for the quicksearch and its clear button;
// Overrides input-group styles and adds transition effects
.quicksearch {
input[type="search"] {
border-radius: $border-radius !important;
}
button {
margin-left: -32px !important;
z-index: 100 !important;
outline: none !important;
border-radius: $border-radius !important;
transition: visibility 0s, opacity 0.2s linear;
}
button :hover {
opacity: 50%;
transition: visibility 0s, opacity 0.1s linear;
}
}
main.layout {
display: flex;
flex-wrap: nowrap;
@@ -714,11 +735,8 @@ textarea.form-control[rows='10'] {
height: 18rem;
}
textarea#id_local_context_data,
textarea.markdown,
textarea#id_public_key,
textarea.form-control[name='csv'],
textarea.form-control[name='data'] {
textarea.form-control[name='csv'] {
font-family: $font-family-monospace;
}

View File

@@ -34,3 +34,11 @@ a[type='button'] {
.badge {
font-size: $font-size-xs;
}
/* clears the 'X' in search inputs from webkit browsers */
input[type='search']::-webkit-search-decoration,
input[type='search']::-webkit-search-cancel-button,
input[type='search']::-webkit-search-results-button,
input[type='search']::-webkit-search-results-decoration {
-webkit-appearance: none !important;
}

View File

@@ -92,6 +92,10 @@ $input-focus-color: $input-color;
$input-placeholder-color: $gray-700;
$input-plaintext-color: $body-color;
input {
color-scheme: dark;
}
$form-check-input-active-filter: brightness(90%);
$form-check-input-bg: $input-bg;
$form-check-input-border: 1px solid rgba(255, 255, 255, 0.25);

View File

@@ -22,7 +22,6 @@ $theme-colors: (
'danger': $danger,
'light': $light,
'dark': $dark,
// General-purpose palette
'blue': $blue-500,
'indigo': $indigo-500,
@@ -36,7 +35,7 @@ $theme-colors: (
'cyan': $cyan-500,
'gray': $gray-500,
'black': $black,
'white': $white,
'white': $white
);
$light: $gray-200;

View File

@@ -42,3 +42,9 @@ table td {
visibility: visible !important;
}
}
// Hides the last child of an element
.hide-last-child :last-child {
visibility: hidden;
opacity: 0;
}

View File

@@ -16,31 +16,37 @@
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_consoleport %}
<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> Rename
</button>
<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> Edit
</button>
<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> Disconnect
</button>
{% endif %}
{% if perms.dcim.delete_consoleport %}
<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> Delete
</button>
{% endif %}
</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> Add Console Port
</a>
</div>
<div class="bulk-button-group">
{% if perms.dcim.change_consoleport %}
<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> Edit
</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> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if perms.dcim.delete_consoleport %}
<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> Delete
</button>
{% endif %}
{% if perms.dcim.change_consoleport %}
<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> 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> Add Console Port
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -16,31 +16,37 @@
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_consoleserverport %}
<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> Rename
</button>
<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> Edit
</button>
<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> Disconnect
</button>
{% endif %}
{% if perms.dcim.delete_consoleserverport %}
<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> Delete
</button>
{% endif %}
</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> Add Console Server Ports
</a>
</div>
<div class="bulk-button-group">
{% if perms.dcim.change_consoleserverport %}
<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> Edit
</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> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if perms.dcim.delete_consoleserverport %}
<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> Delete
</button>
{% endif %}
{% if perms.dcim.change_consoleserverport %}
<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> 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> Add Console Server Ports
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -16,28 +16,30 @@
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_devicebay %}
<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> Rename
</button>
<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> Edit
</button>
{% endif %}
{% if perms.dcim.delete_devicebay %}
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
</button>
{% endif %}
</div>
{% 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> Add Device Bays
</a>
</div>
<div class="bulk-button-group">
{% if perms.dcim.change_devicebay %}
<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> 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> Rename
</button>
</div>
{% endif %}
{% if perms.dcim.delete_devicebay %}
<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> Delete
</button>
{% endif %}
</div>
{% 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> Add Device Bays
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -16,31 +16,37 @@
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_frontport %}
<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> Rename
</button>
<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> Edit
</button>
<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> Disconnect
</button>
{% endif %}
{% if perms.dcim.delete_frontport %}
<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> Delete
</button>
{% endif %}
</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> Add front ports
</a>
</div>
<div class="bulk-button-group">
{% if perms.dcim.change_frontport %}
<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> Edit
</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> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if perms.dcim.delete_frontport %}
<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> Delete
</button>
{% endif %}
{% if perms.dcim.change_frontport %}
<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> 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> Add front ports
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends 'inc/table_controls_htmx.html' %}
{% block extra_table_controls %}
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-eye"></i>
</button>
<ul class="dropdown-menu">
<button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
<button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
</ul>
{% endblock extra_table_controls %}

View File

@@ -4,84 +4,63 @@
{% load static %}
{% block content %}
<div class="row mb-3 justify-content-between">
<div class="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls">
<div class="input-group input-group-sm">
<input
type="text"
name="q"
class="form-control"
placeholder="Quick search"
hx-get="{{ request.full_path }}"
hx-target="#object_list"
hx-trigger="keyup changed delay:500ms"
/>
</div>
</div>
<div class="col col-md-3 mb-0 d-flex noprint table-controls">
<div class="input-group input-group-sm justify-content-end">
{% if request.user.is_authenticated %}
<button
type="button"
class="btn btn-sm btn-outline-dark"
data-bs-toggle="modal"
data-bs-target="#DeviceInterfaceTable_config"
title="Configure Table">
<i class="mdi mdi-cog"></i> Configure Table
</button>
{% endif %}
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-eye"></i>
</button>
<ul class="dropdown-menu">
<button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
<button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
</ul>
</div>
{% 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" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_interface %}
<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> 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> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if perms.dcim.delete_interface %}
<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> Delete
</button>
{% endif %}
{% if perms.dcim.change_interface %}
<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> Disconnect
</button>
{% endif %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_interface %}
<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> Rename
</button>
<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> Edit
</button>
<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> Disconnect
</button>
{% endif %}
{% if perms.dcim.delete_interface %}
<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> Delete
</button>
{% endif %}
</div>
{% 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> Add Interfaces
</a>
</div>
{% endif %}
{% 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> Add Interfaces
</a>
</div>
</form>
{% endif %}
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}

View File

@@ -16,28 +16,30 @@
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_inventoryitem %}
<button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
<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> Edit
</button>
{% endif %}
{% if perms.dcim.delete_inventoryitem %}
<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> Delete
</button>
{% endif %}
</div>
{% 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> Add Inventory Item
</a>
</div>
<div class="bulk-button-group">
{% if perms.dcim.change_inventoryitem %}
<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> 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> Rename
</button>
</div>
{% endif %}
{% if perms.dcim.delete_inventoryitem %}
<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> Delete
</button>
{% endif %}
</div>
{% 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> Add Inventory Item
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -16,28 +16,30 @@
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_modulebay %}
<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> Rename
</button>
<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> Edit
</button>
{% endif %}
{% if perms.dcim.delete_modulebay %}
<button type="submit" formaction="{% url 'dcim:modulebay_bulk_delete' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
</button>
{% endif %}
</div>
{% 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> Add Module Bays
</a>
</div>
<div class="bulk-button-group">
{% if perms.dcim.change_modulebay %}
<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> 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> Rename
</button>
</div>
{% endif %}
{% if perms.dcim.delete_modulebay %}
<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> Delete
</button>
{% endif %}
</div>
{% 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> Add Module Bays
</a>
</div>
{% endif %}
</div>
</form>
{% table_config_form table %}

View File

@@ -16,31 +16,37 @@
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_powerport %}
<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> Rename
</button>
<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> Edit
</button>
<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> Disconnect
</button>
{% endif %}
{% if perms.dcim.delete_poweroutlet %}
<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> Delete
</button>
{% endif %}
</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> Add Power Outlets
</a>
</div>
<div class="bulk-button-group">
{% if perms.dcim.change_powerport %}
<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> Edit
</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> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if perms.dcim.delete_poweroutlet %}
<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> Delete
</button>
{% endif %}
{% if perms.dcim.change_powerport %}
<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> 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> Add Power Outlets
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -16,31 +16,37 @@
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_powerport %}
<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> Rename
</button>
<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> Edit
</button>
<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> Disconnect
</button>
{% endif %}
{% if perms.dcim.delete_powerport %}
<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> Delete
</button>
{% endif %}
</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> Add Power Port
</a>
</div>
<div class="bulk-button-group">
{% if perms.dcim.change_powerport %}
<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> Edit
</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> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if perms.dcim.delete_powerport %}
<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> Delete
</button>
{% endif %}
{% if perms.dcim.change_powerport %}
<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> 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> Add Power Port
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -16,31 +16,37 @@
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.dcim.change_rearport %}
<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> Rename
</button>
<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> Edit
</button>
<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> Disconnect
</button>
{% endif %}
{% if perms.dcim.delete_rearport %}
<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> Delete
</button>
{% endif %}
</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> Add rear ports
</a>
</div>
<div class="bulk-button-group">
{% if perms.dcim.change_rearport %}
<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> Edit
</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> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if perms.dcim.delete_rearport %}
<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> Delete
</button>
{% endif %}
{% if perms.dcim.change_rearport %}
<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> 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> Add rear ports
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends 'generic/object_list.html' %}
{% load buttons %}
{% block bulk_buttons %}
{% if perms.dcim.change_device %}
@@ -73,5 +74,15 @@
</ul>
</div>
{% endif %}
{{ block.super }}
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
{% bulk_edit_button model query_params=request.GET %}
<button type="submit" name="_rename" formaction="{% url 'dcim:device_bulk_rename' %}?return_url={% url 'dcim:device_list' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
</div>
{% endif %}
{% if 'bulk_delete' in actions %}
{% bulk_delete_button model query_params=request.GET %}
{% endif %}
{% endblock %}

View File

@@ -10,6 +10,8 @@
<th>Name</th>
<th>Role</th>
<th>Priority</th>
<th>Phone</th>
<th>Email</th>
<th></th>
</tr>
{% for contact in contacts %}
@@ -17,6 +19,20 @@
<td>{{ contact.contact|linkify }}</td>
<td>{{ contact.role|placeholder }}</td>
<td>{{ contact.get_priority_display|placeholder }}</td>
<td>
{% if contact.contact.phone %}
<a href="tel:{{ contact.contact.phone }}">{{ contact.contact.phone }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td>
{% if contact.contact.email %}
<a href="mailto:{{ contact.contact.email }}">{{ contact.contact.email }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
<td class="text-end noprint">
{% if perms.tenancy.change_contactassignment %}
<a href="{% url 'tenancy:contactassignment_edit' pk=contact.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning btn-sm lh-1" title="Edit">

View File

@@ -1,29 +1,19 @@
{% load helpers %}
<div class="row mb-3 justify-content-between">
<div class="table-controls noprint col col-12 col-md-8 col-lg-4">
<div class="input-group input-group-sm">
<input
type="text"
name="q"
class="form-control"
placeholder="Quick search"
hx-get="{{ request.full_path }}"
hx-target="#object_list"
hx-trigger="keyup changed delay:500ms"
/>
<div class="row mb-3">
<div class="col-auto table-controls noprint">
<div class="input-group input-group-sm me-2 quicksearch hide-last-child">
<input type="search" results=5 name="q" id="quicksearch" class="form-control" placeholder="Quick search"
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
<button class="btn bg-transparent" type="button" id="quicksearch_clear"><i class="mdi mdi-close-circle"></i></button>
</div>
{% block extra_table_controls %}{% endblock %}
</div>
<div class="table-controls noprint col col-md-3 mb-0">
<div class="col-auto ms-auto table-controls noprint">
{% if request.user.is_authenticated and table_modal %}
<div class="table-configure input-group input-group-sm">
<button
type="button"
data-bs-toggle="modal"
title="Configure Table"
data-bs-target="#{{ table_modal }}"
class="btn btn-sm btn-outline-dark"
>
<button type="button" data-bs-toggle="modal" title="Configure Table" data-bs-target="#{{ table_modal }}"
class="btn btn-sm btn-outline-dark">
<i class="mdi mdi-cog"></i> Configure Table
</button>
</div>

View File

@@ -144,6 +144,13 @@
</td>
</tr>
</table>
{% if object.prefix.version == 4 %}
<div class="float-end">
<a class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#prefix-modal">
<i class="mdi mdi-information-outline" aria-hidden="true"></i> Addressing Details
</a>
</div>
{% endif %}
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
@@ -161,3 +168,39 @@
</div>
</div>
{% endblock %}
{% block modals %}
{{ block.super }}
{% if object.prefix.version == 4 %}
<div class="modal fade" id="prefix-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Prefix Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<table class="table table-hover attr-table">
<tr>
<td>Network Address</td>
<td>{{ object.prefix.network }}</td>
</tr>
<tr>
<td>Network Mask</td>
<td>{{ object.prefix.netmask }}</td>
</tr>
<tr>
<td>Wildcard Mask</td>
<td>{{ object.prefix.hostmask }}</td>
</tr>
<tr>
<td>Broadcast Address</td>
<td>{{ object.prefix.broadcast }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -15,27 +15,28 @@
</div>
<div class="noprint">
{% if perms.virtualization.change_vminterface %}
<button type="submit" name="_rename" formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Rename
</button>
<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> Edit
</button>
{% 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> Delete
</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> Add interfaces
</a>
</div>
{% endif %}
<div class="clearfix"></div>
{% 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> 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> 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> Delete
</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> Add interfaces
</a>
</div>
{% endif %}
</div>
</form>
{% endblock content %}

View File

@@ -58,6 +58,8 @@ class TokenViewSet(NetBoxModelViewSet):
# Workaround for schema generation (drf_yasg)
if getattr(self, 'swagger_fake_view', False):
return queryset.none()
if not self.request.user.is_authenticated:
return queryset.none()
if self.request.user.is_superuser:
return queryset
return queryset.filter(user=self.request.user)
@@ -74,11 +76,11 @@ class TokenProvisionView(APIView):
serializer.is_valid()
# Authenticate the user account based on the provided credentials
user = authenticate(
request=request,
username=serializer.data['username'],
password=serializer.data['password']
)
username = serializer.data.get('username')
password = serializer.data.get('password')
if not username or not password:
raise AuthenticationFailed("Username and password must be provided to provision a token.")
user = authenticate(request=request, username=username, password=password)
if user is None:
raise AuthenticationFailed("Invalid username/password")

View File

@@ -10,6 +10,7 @@ from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import View
from social_core.backends.utils import load_backends
@@ -91,7 +92,7 @@ class LoginView(View):
data = request.POST if request.method == "POST" else request.GET
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
if redirect_url and redirect_url.startswith('/'):
if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
logger.debug(f"Redirecting user to {redirect_url}")
else:
if redirect_url:

View File

@@ -99,6 +99,7 @@ class JSONField(_JSONField):
if not self.help_text:
self.help_text = 'Enter context data in <a href="https://json.org/">JSON</a> format.'
self.widget.attrs['placeholder'] = ''
self.widget.attrs['class'] = 'font-monospace'
def prepare_value(self, value):
if isinstance(value, InvalidJSONInput):

View File

@@ -136,7 +136,7 @@ class ImportForm(BootstrapMixin, forms.Form):
Generic form for creating an object from JSON/YAML data
"""
data = forms.CharField(
widget=forms.Textarea,
widget=forms.Textarea(attrs={'class': 'font-monospace'}),
help_text="Enter object data in JSON or YAML format. Note: Only a single object/document is supported."
)
format = forms.ChoiceField(

View File

@@ -1,21 +1,15 @@
{% if utilization == 0 %}
<div class="progress align-items-center justify-content-center">
<span class="w-100 text-center">{{ utilization }}%</span>
<div class="progress">
<div
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="{{ utilization }}"
class="progress-bar {{ bar_class }}"
style="width: {{ utilization }}%;"
>
{% if utilization >= 35 %}{{ utilization|floatformat:1 }}%{% endif %}
</div>
{% else %}
<div class="progress">
<div
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="{{ utilization }}"
class="progress-bar {{ bar_class }}"
style="width: {{ utilization }}%;"
>
{% if utilization >= 25 %}{{ utilization|floatformat:0 }}%{% endif %}
</div>
{% if utilization < 25 %}
<span class="ps-1">{{ utilization|floatformat:0 }}%</span>
{% endif %}
</div>
{% endif %}
{% if utilization < 35 %}
<span class="ps-1">{{ utilization|floatformat:1 }}%</span>
{% endif %}
</div>

View File

@@ -86,8 +86,8 @@ def placeholder(value):
"""
if value not in ('', None):
return value
placeholder = '<span class="text-muted">&mdash;</span>'
return mark_safe(placeholder)
return mark_safe('<span class="text-muted">&mdash;</span>')
@register.filter()

View File

@@ -109,9 +109,7 @@ def annotated_date(date_value):
long_ts = date(date_value, 'DATETIME_FORMAT')
short_ts = date(date_value, 'SHORT_DATETIME_FORMAT')
span = f'<span title="{long_ts}">{short_ts}</span>'
return mark_safe(span)
return mark_safe(f'<span title="{long_ts}">{short_ts}</span>')
@register.simple_tag

View File

@@ -148,7 +148,7 @@ def serialize_object(obj, extra=None):
# Include any tags. Check for tags cached on the instance; fall back to using the manager.
if is_taggable(obj):
tags = getattr(obj, '_tags', None) or obj.tags.all()
data['tags'] = [tag.name for tag in tags]
data['tags'] = sorted([tag.name for tag in tags])
# Append any extra data
if extra is not None:

View File

@@ -89,7 +89,7 @@ class VirtualMachineFilterForm(
(None, ('q', 'tag')),
('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
('Attributes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)

View File

@@ -48,6 +48,9 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
order_by=('primary_ip4', 'primary_ip6'),
verbose_name='IP Address'
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='virtualization:virtualmachine_list'
)
@@ -55,8 +58,9 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = VirtualMachine
fields = (
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'tenant_group', 'platform', 'vcpus', 'memory', 'disk',
'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'tenant_group', 'platform', 'vcpus', 'memory',
'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'contacts', 'tags', 'created',
'last_updated',
)
default_columns = (
'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
@@ -77,9 +81,6 @@ class VMInterfaceTable(BaseInterfaceTable):
vrf = tables.Column(
linkify=True
)
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn(
url_name='virtualization:vminterface_list'
)
@@ -88,8 +89,7 @@ class VMInterfaceTable(BaseInterfaceTable):
model = VMInterface
fields = (
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'contacts', 'created',
'last_updated',
'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')

View File

@@ -1,5 +1,5 @@
bleach==5.0.1
Django==4.0.6
Django==4.0.7
django-cors-headers==3.13.0
django-debug-toolbar==3.5.0
django-filter==22.1
@@ -13,24 +13,27 @@ django-tables2==2.4.1
django-taggit==2.1.0
django-timezone-field==5.0
djangorestframework==3.13.1
drf-yasg[validation]==1.20.0
drf-yasg[validation]==1.21.3
graphene-django==2.15.0
gunicorn==20.1.0
Jinja2==3.1.2
Markdown==3.3.7
markdown-include==0.6.0
mkdocs-material==8.3.9
Markdown==3.4.1
markdown-include==0.7.0
mkdocs-material==8.4.0
mkdocstrings[python-legacy]==0.19.0
netaddr==0.8.0
Pillow==9.2.0
psycopg2-binary==2.9.3
PyYAML==6.0
sentry-sdk==1.7.0
sentry-sdk==1.9.5
social-auth-app-django==5.0.0
social-auth-core==4.3.0
svgwrite==1.4.2
svgwrite==1.4.3
tablib==3.2.1
tzdata==2022.1
# Workaround for #7401
jsonschema==3.2.0
# Workaround for #9986
pytz==2022.1