Merge branch 'feature' into docs-refresh

This commit is contained in:
jeremystretch 2022-08-12 10:19:38 -04:00
commit 150c3d3a97
61 changed files with 381 additions and 258 deletions

View File

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

9
docs/_theme/main.html vendored Normal file
View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block site_meta %}
{{ super() }}
{# Disable search indexing unless we're building for ReadTheDocs #}
{% if not config.extra.readthedocs %}
<meta name="robots" content="noindex">
{% endif %}
{% endblock %}

View File

@ -1,3 +1,3 @@
## Front Ports ## 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,6 +1,20 @@
# NetBox v3.2 # NetBox v3.2
## v3.2.8 (FUTURE) ## v3.2.9 (FUTURE)
### Enhancements
* [#9161](https://github.com/netbox-community/netbox/issues/9161) - Pretty print JSON custom field data when editing
* [#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
### Bug Fixes
* [#9986](https://github.com/netbox-community/netbox/issues/9986) - Workaround for upstream timezone data bug
---
## v3.2.8 (2022-08-08)
### Enhancements ### Enhancements
@ -11,13 +25,20 @@
* [#9881](https://github.com/netbox-community/netbox/issues/9881) - Increase granularity in utilization graph values * [#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 * [#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 * [#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 ### 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 * [#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 * [#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 * [#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 * [#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
--- ---

View File

@ -97,22 +97,11 @@ Custom field UI visibility has no impact on API operation.
* [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times * [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times
* [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location * [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location
### Bug Fixes (from Beta1) ### Bug Fixes (from Beta2)
* [#9728](https://github.com/netbox-community/netbox/issues/9728) - Fix validation when assigning a virtual machine to a device * [#9900](https://github.com/netbox-community/netbox/issues/9900) - Pre-populate site & rack fields for cable connection form
* [#9729](https://github.com/netbox-community/netbox/issues/9729) - Fix ordering of content type creation to ensure compatability with demo data * [#9938](https://github.com/netbox-community/netbox/issues/9938) - Exclude virtual interfaces from terminations list when connecting a cable
* [#9730](https://github.com/netbox-community/netbox/issues/9730) - Fix validation error when creating a new cable via UI form * [#9939](https://github.com/netbox-community/netbox/issues/9939) - Fix list of next nodes for split paths under trace view
* [#9733](https://github.com/netbox-community/netbox/issues/9733) - Handle split paths during trace when fanning out to front ports with differing cables
* [#9765](https://github.com/netbox-community/netbox/issues/9765) - Report correct segment count under cable trace UI view
* [#9778](https://github.com/netbox-community/netbox/issues/9778) - Fix exception during cable deletion after deleting a connected termination
* [#9788](https://github.com/netbox-community/netbox/issues/9788) - Ensure denormalized fields on CableTermination are kept in sync with related objects
* [#9789](https://github.com/netbox-community/netbox/issues/9789) - Fix rendering of cable traces ending at provider networks
* [#9794](https://github.com/netbox-community/netbox/issues/9794) - Fix link to connect a rear port to a circuit termination
* [#9818](https://github.com/netbox-community/netbox/issues/9818) - Fix circuit side selection when connecting a cable to a circuit termination
* [#9829](https://github.com/netbox-community/netbox/issues/9829) - Arrange custom fields by group when editing objects
* [#9843](https://github.com/netbox-community/netbox/issues/9843) - Fix rendering of custom field values (regression from #9647)
* [#9844](https://github.com/netbox-community/netbox/issues/9844) - Fix interface api request when creating/editing L2VPN termination
* [#9847](https://github.com/netbox-community/netbox/issues/9847) - Respect `desc_units` when ordering rack units
### Plugins API ### Plugins API

View File

@ -5,6 +5,7 @@ repo_name: netbox-community/netbox
repo_url: https://github.com/netbox-community/netbox repo_url: https://github.com/netbox-community/netbox
theme: theme:
name: material name: material
custom_dir: docs/_theme/
icon: icon:
repo: fontawesome/brands/github repo: fontawesome/brands/github
palette: palette:
@ -37,6 +38,7 @@ plugins:
show_root_toc_entry: false show_root_toc_entry: false
show_source: false show_source: false
extra: extra:
readthedocs: !ENV READTHEDOCS
social: social:
- icon: fontawesome/brands/github - icon: fontawesome/brands/github
link: https://github.com/netbox-community/netbox link: https://github.com/netbox-community/netbox

View File

@ -125,9 +125,9 @@ class Circuit(NetBoxModel):
null=True null=True
) )
clone_fields = [ clone_fields = (
'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description',
] )
class Meta: class Meta:
ordering = ['provider', 'cid'] ordering = ['provider', 'cid']

View File

@ -61,9 +61,9 @@ class Provider(NetBoxModel):
to='tenancy.ContactAssignment' to='tenancy.ContactAssignment'
) )
clone_fields = [ clone_fields = (
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
] )
class Meta: class Meta:
ordering = ['name'] ordering = ['name']

View File

@ -84,6 +84,7 @@ def get_cable_form(a_type, b_type):
disabled_indicator='_occupied', disabled_indicator='_occupied',
query_params={ query_params={
'device_id': f'$termination_{cable_end}_device', 'device_id': f'$termination_{cable_end}_device',
'kind': 'physical', # Exclude virtual interfaces
} }
) )

View File

@ -156,7 +156,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = [ 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',
] ]
@ -168,7 +168,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = [ fields = [
'device_type', 'module_type', 'name', 'type', 'positions', 'label', 'description', 'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
] ]

View File

@ -677,6 +677,6 @@ class CablePath(models.Model):
""" """
Return all available next segments in a split cable path. Return all available next segments in a split cable path.
""" """
rearport = path_node_to_object(self._nodes[-1]) rearports = self.path_objects[-1]
return FrontPort.objects.filter(rear_port=rearport) return FrontPort.objects.filter(rear_port__in=rearports)

View File

@ -478,6 +478,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
return { return {
'name': self.name, 'name': self.name,
'type': self.type, 'type': self.type,
'color': self.color,
'rear_port': self.rear_port.name, 'rear_port': self.rear_port.name,
'rear_port_position': self.rear_port_position, 'rear_port_position': self.rear_port_position,
'label': self.label, 'label': self.label,
@ -527,6 +528,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
return { return {
'name': self.name, 'name': self.name,
'type': self.type, 'type': self.type,
'color': self.color,
'positions': self.positions, 'positions': self.positions,
'label': self.label, 'label': self.label,
'description': self.description, 'description': self.description,

View File

@ -263,7 +263,7 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint):
help_text='Port speed in bits per second' help_text='Port speed in bits per second'
) )
clone_fields = ['device', 'type', 'speed'] clone_fields = ('device', 'module', 'type', 'speed')
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@ -290,7 +290,7 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
help_text='Port speed in bits per second' help_text='Port speed in bits per second'
) )
clone_fields = ['device', 'type', 'speed'] clone_fields = ('device', 'module', 'type', 'speed')
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@ -327,7 +327,7 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
help_text="Allocated power draw (watts)" help_text="Allocated power draw (watts)"
) )
clone_fields = ['device', 'maximum_draw', 'allocated_draw'] clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw')
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@ -441,7 +441,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint):
help_text="Phase (for three-phase feeds)" help_text="Phase (for three-phase feeds)"
) )
clone_fields = ['device', 'type', 'power_port', 'feed_leg'] clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@ -672,7 +672,10 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
related_query_name='interface', related_query_name='interface',
) )
clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'poe_mode', 'poe_type'] clone_fields = (
'device', 'module', 'parent', 'bridge', 'lag', 'type', 'mgmt_only', 'mtu', 'mode', 'speed', 'duplex', 'rf_role',
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'poe_mode', 'poe_type', 'vrf',
)
class Meta: class Meta:
ordering = ('device', CollateAsChar('_name')) ordering = ('device', CollateAsChar('_name'))
@ -890,7 +893,7 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
] ]
) )
clone_fields = ['device', 'type'] clone_fields = ('device', 'type', 'color')
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@ -937,7 +940,7 @@ class RearPort(ModularComponentModel, CabledObjectModel):
MaxValueValidator(REARPORT_POSITIONS_MAX) MaxValueValidator(REARPORT_POSITIONS_MAX)
] ]
) )
clone_fields = ['device', 'type', 'positions'] clone_fields = ('device', 'type', 'color', 'positions')
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@ -972,7 +975,7 @@ class ModuleBay(ComponentModel):
help_text='Identifier to reference when renaming installed components' help_text='Identifier to reference when renaming installed components'
) )
clone_fields = ['device'] clone_fields = ('device',)
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@ -994,7 +997,7 @@ class DeviceBay(ComponentModel):
null=True null=True
) )
clone_fields = ['device'] clone_fields = ('device',)
class Meta: class Meta:
ordering = ('device', '_name') ordering = ('device', '_name')
@ -1131,7 +1134,7 @@ class InventoryItem(MPTTModel, ComponentModel):
objects = TreeManager() objects = TreeManager()
clone_fields = ['device', 'parent', 'role', 'manufacturer', 'part_id'] clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',)
class Meta: class Meta:
ordering = ('device__id', 'parent__id', '_name') ordering = ('device__id', 'parent__id', '_name')

View File

@ -135,9 +135,9 @@ class DeviceType(NetBoxModel):
blank=True blank=True
) )
clone_fields = [ clone_fields = (
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
] )
class Meta: class Meta:
ordering = ['manufacturer', 'model'] ordering = ['manufacturer', 'model']
@ -630,9 +630,10 @@ class Device(NetBoxModel, ConfigContextModel):
objects = ConfigContextModelQuerySet.as_manager() objects = ConfigContextModelQuerySet.as_manager()
clone_fields = [ clone_fields = (
'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'status', 'airflow', 'cluster', 'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'face', 'status', 'airflow',
] 'cluster', 'virtual_chassis',
)
class Meta: class Meta:
ordering = ('_name', 'pk') # Name may be null ordering = ('_name', 'pk') # Name may be null

View File

@ -126,10 +126,10 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
blank=True blank=True
) )
clone_fields = [ clone_fields = (
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization', 'available_power', 'max_utilization',
] )
class Meta: class Meta:
ordering = ['power_panel', 'name'] ordering = ['power_panel', 'name']

View File

@ -183,10 +183,10 @@ class Rack(NetBoxModel):
to='extras.ImageAttachment' to='extras.ImageAttachment'
) )
clone_fields = [ clone_fields = (
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'outer_depth', 'outer_unit',
] )
class Meta: class Meta:
ordering = ('site', 'location', '_name', 'pk') # (site, location, name) may be non-unique ordering = ('site', 'location', '_name', 'pk') # (site, location, name) may be non-unique

View File

@ -295,10 +295,10 @@ class Site(NetBoxModel):
to='extras.ImageAttachment' to='extras.ImageAttachment'
) )
clone_fields = [ clone_fields = (
'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description', 'physical_address', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'physical_address', 'shipping_address',
'shipping_address', 'latitude', 'longitude', 'latitude', 'longitude', 'description',
] )
class Meta: class Meta:
ordering = ('_name',) ordering = ('_name',)
@ -372,7 +372,7 @@ class Location(NestedGroupModel):
to='extras.ImageAttachment' to='extras.ImageAttachment'
) )
clone_fields = ['site', 'parent', 'status', 'tenant', 'description'] clone_fields = ('site', 'parent', 'status', 'tenant', 'description')
class Meta: class Meta:
ordering = ['site', 'name'] ordering = ['site', 'name']

View File

@ -121,9 +121,9 @@ CONSOLEPORT_BUTTONS = """
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Console Server Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Console Server Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Front Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Front Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Rear Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Rear Port</a></li>
</ul> </ul>
</span> </span>
{% else %} {% else %}
@ -153,9 +153,9 @@ CONSOLESERVERPORT_BUTTONS = """
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Front Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Front Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Rear Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Rear Port</a></li>
</ul> </ul>
</span> </span>
{% else %} {% else %}
@ -185,8 +185,8 @@ POWERPORT_BUTTONS = """
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ record.pk }}&b_terminations_type=dcim.poweroutlet&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Outlet</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ record.pk }}&b_terminations_type=dcim.poweroutlet&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Outlet</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ record.pk }}&b_terminations_type=dcim.powerfeed&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Feed</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ record.pk }}&b_terminations_type=dcim.powerfeed&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Feed</a></li>
</ul> </ul>
</span> </span>
{% else %} {% else %}
@ -212,7 +212,7 @@ POWEROUTLET_BUTTONS = """
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a> <a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a> <a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
{% if not record.mark_connected %} {% if not record.mark_connected %}
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ record.pk }}&b_terminations_type=dcim.powerport&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Connect" class="btn btn-success btn-sm"> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ record.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Connect" class="btn btn-success btn-sm">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> <i class="mdi mdi-ethernet-cable" aria-hidden="true"></i>
</a> </a>
{% else %} {% else %}
@ -262,10 +262,10 @@ INTERFACE_BUTTONS = """
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Interface</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Interface</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Front Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Front Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Rear Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Rear Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Circuit Termination</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.site.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Circuit Termination</a></li>
</ul> </ul>
</span> </span>
{% else %} {% else %}
@ -301,12 +301,12 @@ FRONTPORT_BUTTONS = """
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Interface</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Interface</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Server Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Server Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Front Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Front Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Rear Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Rear Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Circuit Termination</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.site.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Circuit Termination</a></li>
</ul> </ul>
</span> </span>
{% else %} {% else %}
@ -338,12 +338,12 @@ REARPORT_BUTTONS = """
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Interface</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Interface</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Server Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Server Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Front Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Front Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.site.pk }}&termination_b_rack={{ object.rack.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Port</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.site.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li>
</ul> </ul>
</span> </span>
{% else %} {% else %}

View File

@ -2721,6 +2721,7 @@ class DeviceBulkAddModuleBayView(generic.BulkComponentCreateView):
filterset = filtersets.DeviceFilterSet filterset = filtersets.DeviceFilterSet
table = tables.DeviceTable table = tables.DeviceTable
default_return_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
patterned_fields = ('name', 'label', 'position')
class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView): class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
@ -3066,7 +3067,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
if membership_form.is_valid(): if membership_form.is_valid():
membership_form.save() 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)) messages.success(request, mark_safe(msg))
if '_addanother' in request.POST: if '_addanother' in request.POST:
@ -3111,8 +3112,7 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
# Protect master device from being removed # Protect master device from being removed
virtual_chassis = VirtualChassis.objects.filter(master=device).first() virtual_chassis = VirtualChassis.objects.filter(master=device).first()
if virtual_chassis is not None: if virtual_chassis is not None:
msg = 'Unable to remove master device {} from the virtual chassis.'.format(escape(device)) messages.error(request, f'Unable to remove master device {device} from the virtual chassis.')
messages.error(request, mark_safe(msg))
return redirect(device.get_absolute_url()) return redirect(device.get_absolute_url())
if form.is_valid(): if form.is_valid():

View File

@ -18,7 +18,7 @@ from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
from utilities import filters from utilities import filters
from utilities.forms import ( from utilities.forms import (
CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, 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.querysets import RestrictedQuerySet
from utilities.validators import validate_regex from utilities.validators import validate_regex
@ -355,7 +355,7 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
# JSON # JSON
elif self.type == CustomFieldTypeChoices.TYPE_JSON: elif self.type == CustomFieldTypeChoices.TYPE_JSON:
field = forms.JSONField(required=required, initial=initial) field = JSONField(required=required, initial=initial)
# Object # Object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:

View File

@ -48,7 +48,7 @@ class FHRPGroup(NetBoxModel):
related_query_name='fhrpgroup' related_query_name='fhrpgroup'
) )
clone_fields = ('protocol', 'auth_type', 'auth_key') clone_fields = ('protocol', 'auth_type', 'auth_key', 'description')
class Meta: class Meta:
ordering = ['protocol', 'group_id', 'pk'] ordering = ['protocol', 'group_id', 'pk']

View File

@ -175,9 +175,9 @@ class Aggregate(GetAvailablePrefixesMixin, NetBoxModel):
blank=True blank=True
) )
clone_fields = [ clone_fields = (
'rir', 'tenant', 'date_added', 'description', 'rir', 'tenant', 'date_added', 'description',
] )
class Meta: class Meta:
ordering = ('prefix', 'pk') # prefix may be non-unique ordering = ('prefix', 'pk') # prefix may be non-unique
@ -360,9 +360,9 @@ class Prefix(GetAvailablePrefixesMixin, NetBoxModel):
objects = PrefixQuerySet.as_manager() objects = PrefixQuerySet.as_manager()
clone_fields = [ clone_fields = (
'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
] )
class Meta: class Meta:
ordering = (F('vrf').asc(nulls_first=True), 'prefix', 'pk') # (vrf, prefix) may be non-unique ordering = (F('vrf').asc(nulls_first=True), 'prefix', 'pk') # (vrf, prefix) may be non-unique
@ -608,9 +608,9 @@ class IPRange(NetBoxModel):
blank=True blank=True
) )
clone_fields = [ clone_fields = (
'vrf', 'tenant', 'status', 'role', 'description', 'vrf', 'tenant', 'status', 'role', 'description',
] )
class Meta: class Meta:
ordering = (F('vrf').asc(nulls_first=True), 'start_address', 'pk') # (vrf, start_address) may be non-unique ordering = (F('vrf').asc(nulls_first=True), 'start_address', 'pk') # (vrf, start_address) may be non-unique
@ -836,9 +836,9 @@ class IPAddress(NetBoxModel):
objects = IPAddressManager() objects = IPAddressManager()
clone_fields = [ clone_fields = (
'vrf', 'tenant', 'status', 'role', 'description', 'vrf', 'tenant', 'status', 'role', 'dns_name', 'description',
] )
class Meta: class Meta:
ordering = ('address', 'pk') # address may be non-unique ordering = ('address', 'pk') # address may be non-unique

View File

@ -55,9 +55,9 @@ class VRF(NetBoxModel):
blank=True blank=True
) )
clone_fields = [ clone_fields = (
'tenant', 'enforce_unique', 'description', 'tenant', 'enforce_unique', 'description',
] )
class Meta: class Meta:
ordering = ('name', 'rd', 'pk') # (name, rd) may be non-unique ordering = ('name', 'rd', 'pk') # (name, rd) may be non-unique

View File

@ -109,9 +109,9 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
super().clean() super().clean()
# An MPTT model cannot be its own parent # 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({ raise ValidationError({
"parent": "Cannot assign self as parent." "parent": f"Cannot assign self or child {self._meta.verbose_name} as parent."
}) })

View File

@ -7,6 +7,7 @@ from django.contrib.auth.models import AnonymousUser
from django.db.models import DateField, DateTimeField from django.db.models import DateField, DateTimeField
from django.template import Context, Template from django.template import Context, Template
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django_tables2.columns import library from django_tables2.columns import library
@ -428,8 +429,8 @@ class CustomFieldColumn(tables.Column):
@staticmethod @staticmethod
def _likify_item(item): def _likify_item(item):
if hasattr(item, 'get_absolute_url'): if hasattr(item, 'get_absolute_url'):
return f'<a href="{item.get_absolute_url()}">{item}</a>' return f'<a href="{item.get_absolute_url()}">{escape(item)}</a>'
return item return escape(item)
def render(self, value): def render(self, value):
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True: if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is True:
@ -437,13 +438,13 @@ class CustomFieldColumn(tables.Column):
if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False: if self.customfield.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value is False:
return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>') return mark_safe('<i class="mdi mdi-close-thick text-danger"></i>')
if self.customfield.type == CustomFieldTypeChoices.TYPE_URL: 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: if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
return ', '.join(v for v in value) return ', '.join(v for v in value)
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
return mark_safe(', '.join([ return mark_safe(', '.join(
self._likify_item(obj) for obj in self.customfield.deserialize(value) self._likify_item(obj) for obj in self.customfield.deserialize(value)
])) ))
if value is not None: if value is not None:
obj = self.customfield.deserialize(value) obj = self.customfield.deserialize(value)
return mark_safe(self._likify_item(obj)) return mark_safe(self._likify_item(obj))

View File

@ -770,6 +770,7 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
model_form = None model_form = None
filterset = None filterset = None
table = None table = None
patterned_fields = ('name', 'label')
def get_required_permission(self): def get_required_permission(self):
return f'dcim.add_{self.queryset.model._meta.model_name}' return f'dcim.add_{self.queryset.model._meta.model_name}'
@ -805,16 +806,16 @@ class BulkComponentCreateView(GetReturnURLMixin, BaseMultiObjectView):
for obj in data['pk']: for obj in data['pk']:
names = data['name_pattern'] pattern_count = len(data[f'{self.patterned_fields[0]}_pattern'])
labels = data['label_pattern'] if 'label_pattern' in data else None for i in range(pattern_count):
for i, name in enumerate(names):
label = labels[i] if labels else None
component_data = { component_data = {
self.parent_field: obj.pk, self.parent_field: obj.pk
'name': name,
'label': label
} }
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_data.update(data)
component_form = self.model_form(component_data) component_form = self.model_form(component_data)
if component_form.is_valid(): if component_form.is_valid():

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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. * 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 { export function initSearch(): void {
for (const func of [initSearchBar]) { for (const func of [initSearchBar]) {
func(); 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 { main.layout {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -714,11 +735,8 @@ textarea.form-control[rows='10'] {
height: 18rem; height: 18rem;
} }
textarea#id_local_context_data,
textarea.markdown, textarea.markdown,
textarea#id_public_key, textarea.form-control[name='csv'] {
textarea.form-control[name='csv'],
textarea.form-control[name='data'] {
font-family: $font-family-monospace; font-family: $font-family-monospace;
} }

View File

@ -34,3 +34,11 @@ a[type='button'] {
.badge { .badge {
font-size: $font-size-xs; 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-placeholder-color: $gray-700;
$input-plaintext-color: $body-color; $input-plaintext-color: $body-color;
input {
color-scheme: dark;
}
$form-check-input-active-filter: brightness(90%); $form-check-input-active-filter: brightness(90%);
$form-check-input-bg: $input-bg; $form-check-input-bg: $input-bg;
$form-check-input-border: 1px solid rgba(255, 255, 255, 0.25); $form-check-input-border: 1px solid rgba(255, 255, 255, 0.25);

View File

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

View File

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

View File

@ -111,13 +111,13 @@
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Server Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Server Port</a>
</li> </li>
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
</li> </li>
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -113,13 +113,13 @@
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Port</a>
</li> </li>
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
</li> </li>
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -4,81 +4,82 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="row mb-3 justify-content-between"> <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="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm quicksearch hide-last-child">
<input <input type="search" results=5 name="q" id="quicksearch" class="form-control" placeholder="Quick search"
type="text" hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
name="q" <button class="btn bg-transparent" type="button" id="quicksearch_clear"><i
class="form-control" class="mdi mdi-close-circle"></i></button>
placeholder="Quick search"
hx-get="{{ request.full_path }}"
hx-target="#object_list"
hx-trigger="keyup changed delay:500ms"
/>
</div>
</div> </div>
<div class="col col-md-3 mb-0 d-flex noprint table-controls"> </div>
<div class="input-group input-group-sm justify-content-end"> <div class="col col-md-3 mb-0 d-flex noprint table-controls">
{% if request.user.is_authenticated %} <div class="input-group input-group-sm justify-content-end">
<button {% if request.user.is_authenticated %}
type="button" <button type="button" class="btn btn-sm btn-outline-dark" data-bs-toggle="modal"
class="btn btn-sm btn-outline-dark" data-bs-target="#DeviceInterfaceTable_config" title="Configure Table">
data-bs-toggle="modal" <i class="mdi mdi-cog"></i> Configure Table
data-bs-target="#DeviceInterfaceTable_config" </button>
title="Configure Table"> {% endif %}
<i class="mdi mdi-cog"></i> Configure Table <button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown"
</button> aria-expanded="false">
{% endif %} <i class="mdi mdi-eye"></i>
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> </button>
<i class="mdi mdi-eye"></i> <ul class="dropdown-menu">
</button> <button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
<ul class="dropdown-menu"> <button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
<button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button> </ul>
<button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button> </div>
</ul> </div>
</div> </div>
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div> </div>
</div> </div>
<form method="post"> <div class="noprint bulk-buttons">
{% csrf_token %} <div class="bulk-button-group">
{% if perms.dcim.change_interface %}
<button type="submit" name="_rename"
<div class="card"> formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
<div class="card-body" id="object_list"> class="btn btn-outline-warning btn-sm">
{% include 'htmx/table.html' %} <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> </div>
</div> {% endif %}
</div>
<div class="noprint bulk-buttons"> </form>
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<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 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> 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 %}
</div>
</form>
{% endblock %} {% endblock %}
{% block modals %} {% block modals %}

View File

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

View File

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

View File

@ -158,7 +158,7 @@
{% if not object.mark_connected and not object.cable %} {% if not object.mark_connected and not object.cable %}
<div class="card-footer"> <div class="card-footer">
{% if perms.dcim.add_cable %} {% if perms.dcim.add_cable %}
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm float-end"> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm float-end">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect <i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
</a> </a>
{% endif %} {% endif %}

View File

@ -111,7 +111,7 @@
<div class="text-muted"> <div class="text-muted">
Not Connected Not Connected
{% if perms.dcim.add_cable %} {% if perms.dcim.add_cable %}
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&return_url={{ object.get_absolute_url }}" title="Connect" class="btn btn-primary btn-sm float-end"> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" title="Connect" class="btn btn-primary btn-sm float-end">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect <i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
</a> </a>
{% endif %} {% endif %}

View File

@ -117,10 +117,10 @@
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Outlet</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Outlet</a>
</li> </li>
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Feed</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Feed</a>
</li> </li>
</ul> </ul>
</span> </span>

View File

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

View File

@ -10,6 +10,8 @@
<th>Name</th> <th>Name</th>
<th>Role</th> <th>Role</th>
<th>Priority</th> <th>Priority</th>
<th>Phone</th>
<th>Email</th>
<th></th> <th></th>
</tr> </tr>
{% for contact in contacts %} {% for contact in contacts %}
@ -17,6 +19,20 @@
<td>{{ contact.contact|linkify }}</td> <td>{{ contact.contact|linkify }}</td>
<td>{{ contact.role|placeholder }}</td> <td>{{ contact.role|placeholder }}</td>
<td>{{ contact.get_priority_display|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"> <td class="text-end noprint">
{% if perms.tenancy.change_contactassignment %} {% 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"> <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

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

View File

@ -112,9 +112,9 @@ class Contact(NetBoxModel):
blank=True blank=True
) )
clone_fields = [ clone_fields = (
'group', 'group', 'name', 'title', 'phone', 'email', 'address', 'link',
] )
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -155,7 +155,7 @@ class ContactAssignment(WebhooksMixin, ChangeLoggedModel):
blank=True blank=True
) )
clone_fields = ('content_type', 'object_id') clone_fields = ('content_type', 'object_id', 'role', 'priority')
class Meta: class Meta:
ordering = ('priority', 'contact') ordering = ('priority', 'contact')

View File

@ -76,9 +76,9 @@ class Tenant(NetBoxModel):
to='tenancy.ContactAssignment' to='tenancy.ContactAssignment'
) )
clone_fields = [ clone_fields = (
'group', 'description', 'group', 'description',
] )
class Meta: class Meta:
ordering = ['name'] ordering = ['name']

View File

@ -58,6 +58,8 @@ class TokenViewSet(NetBoxModelViewSet):
# Workaround for schema generation (drf_yasg) # Workaround for schema generation (drf_yasg)
if getattr(self, 'swagger_fake_view', False): if getattr(self, 'swagger_fake_view', False):
return queryset.none() return queryset.none()
if not self.request.user.is_authenticated:
return queryset.none()
if self.request.user.is_superuser: if self.request.user.is_superuser:
return queryset return queryset
return queryset.filter(user=self.request.user) return queryset.filter(user=self.request.user)
@ -74,11 +76,11 @@ class TokenProvisionView(APIView):
serializer.is_valid() serializer.is_valid()
# Authenticate the user account based on the provided credentials # Authenticate the user account based on the provided credentials
user = authenticate( username = serializer.data.get('username')
request=request, password = serializer.data.get('password')
username=serializer.data['username'], if not username or not password:
password=serializer.data['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: if user is None:
raise AuthenticationFailed("Invalid username/password") 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.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator 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.decorators.debug import sensitive_post_parameters
from django.views.generic import View from django.views.generic import View
from social_core.backends.utils import load_backends from social_core.backends.utils import load_backends
@ -92,7 +93,7 @@ class LoginView(View):
data = request.POST if request.method == "POST" else request.GET data = request.POST if request.method == "POST" else request.GET
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL) 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}") logger.debug(f"Redirecting user to {redirect_url}")
else: else:
if redirect_url: if redirect_url:

View File

@ -99,6 +99,7 @@ class JSONField(_JSONField):
if not self.help_text: if not self.help_text:
self.help_text = 'Enter context data in <a href="https://json.org/">JSON</a> format.' self.help_text = 'Enter context data in <a href="https://json.org/">JSON</a> format.'
self.widget.attrs['placeholder'] = '' self.widget.attrs['placeholder'] = ''
self.widget.attrs['class'] = 'font-monospace'
def prepare_value(self, value): def prepare_value(self, value):
if isinstance(value, InvalidJSONInput): 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 Generic form for creating an object from JSON/YAML data
""" """
data = forms.CharField( 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." help_text="Enter object data in JSON or YAML format. Note: Only a single object/document is supported."
) )
format = forms.ChoiceField( format = forms.ChoiceField(

View File

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

View File

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

View File

@ -93,7 +93,7 @@ class VirtualMachineFilterForm(
(None, ('q', 'tag')), (None, ('q', 'tag')),
('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')), ('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
('Location', ('region_id', 'site_group_id', 'site_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')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), ('Contacts', ('contact', 'contact_role', 'contact_group')),
) )

View File

@ -153,9 +153,9 @@ class Cluster(NetBoxModel):
to='tenancy.ContactAssignment' to='tenancy.ContactAssignment'
) )
clone_fields = [ clone_fields = (
'type', 'group', 'tenant', 'site', 'type', 'group', 'status', 'tenant', 'site',
] )
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -299,9 +299,9 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
objects = ConfigContextModelQuerySet.as_manager() objects = ConfigContextModelQuerySet.as_manager()
clone_fields = [ clone_fields = (
'site', 'cluster', 'device', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk', 'site', 'cluster', 'device', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
] )
class Meta: class Meta:
ordering = ('_name', 'pk') # Name may be non-unique ordering = ('_name', 'pk') # Name may be non-unique

View File

@ -113,6 +113,8 @@ class WirelessLAN(WirelessAuthenticationBase, NetBoxModel):
blank=True blank=True
) )
clone_fields = ('ssid', 'group', 'tenant', 'description')
class Meta: class Meta:
ordering = ('ssid', 'pk') ordering = ('ssid', 'pk')
verbose_name = 'Wireless LAN' verbose_name = 'Wireless LAN'

View File

@ -25,7 +25,7 @@ netaddr==0.8.0
Pillow==9.2.0 Pillow==9.2.0
psycopg2-binary==2.9.3 psycopg2-binary==2.9.3
PyYAML==6.0 PyYAML==6.0
sentry-sdk==1.9.0 sentry-sdk==1.9.2
social-auth-app-django==5.0.0 social-auth-app-django==5.0.0
social-auth-core==4.3.0 social-auth-core==4.3.0
svgwrite==1.4.3 svgwrite==1.4.3
@ -34,3 +34,6 @@ tzdata==2022.1
# Workaround for #7401 # Workaround for #7401
jsonschema==3.2.0 jsonschema==3.2.0
# Workaround for #9986
pytz==2022.1