Merge v3.1.9

This commit is contained in:
jeremystretch 2022-03-07 10:55:30 -05:00
commit cd29293dd6
19 changed files with 217 additions and 97 deletions

View File

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

View File

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

View File

@ -2,6 +2,8 @@
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" /> <img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
</div> </div>
:loudspeaker: The **[2022 NetBox community survey](https://forms.gle/KR8YbR8GiJ9EYXM28)** is now open! We collect this feedback and demographic data from NetBox users around the world to help shape the project's long-term development goals. Please take a few minutes to share your responses!
![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) ![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master)
NetBox is an infrastructure resource modeling (IRM) tool designed to empower NetBox is an infrastructure resource modeling (IRM) tool designed to empower

View File

@ -1,5 +1,7 @@
![NetBox](netbox_logo.svg "NetBox logo"){style="height: 100px; margin-bottom: 3em"} ![NetBox](netbox_logo.svg "NetBox logo"){style="height: 100px; margin-bottom: 3em"}
:loudspeaker: The **[2022 NetBox community survey](https://forms.gle/KR8YbR8GiJ9EYXM28)** is now open! We collect this feedback and demographic data from NetBox users around the world to help shape the project's long-term development goals. Please take a few minutes to share your responses!
# What is NetBox? # What is NetBox?
NetBox is an infrastructure resource modeling (IRM) application designed to empower network automation. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. NetBox is made available as open source under the Apache 2 license. It encompasses the following aspects of network management: NetBox is an infrastructure resource modeling (IRM) application designed to empower network automation. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. NetBox is made available as open source under the Apache 2 license. It encompasses the following aspects of network management:

View File

@ -1,11 +1,20 @@
# NetBox v3.1 # NetBox v3.1
## v3.1.9 (FUTURE) ## v3.1.10 (FUTURE)
---
## v3.1.9 (2022-03-07)
### Enhancements ### Enhancements
* [#8594](https://github.com/netbox-community/netbox/issues/8594) - Enable filtering by exact description match for all applicable models * [#8594](https://github.com/netbox-community/netbox/issues/8594) - Enable filtering by exact description match for all applicable models
* [#8629](https://github.com/netbox-community/netbox/issues/8629) - Add description to tag table search function * [#8629](https://github.com/netbox-community/netbox/issues/8629) - Add description to tag table search function
* [#8664](https://github.com/netbox-community/netbox/issues/8664) - Show assigned ASNs/sites under list views
* [#8736](https://github.com/netbox-community/netbox/issues/8736) - Add PC and UPC fiber end faces for LC/SC/LSH port types
* [#8758](https://github.com/netbox-community/netbox/issues/8758) - Allow empty string substitution when renaming objects in bulk
* [#8762](https://github.com/netbox-community/netbox/issues/8762) - Link to rack elevations list from site view
* [#8766](https://github.com/netbox-community/netbox/issues/8766) - Add SCTP to service protocols list
### Bug Fixes ### Bug Fixes
@ -14,7 +23,11 @@
* [#8674](https://github.com/netbox-community/netbox/issues/8674) - Fix rendering of tabbed content in documentation * [#8674](https://github.com/netbox-community/netbox/issues/8674) - Fix rendering of tabbed content in documentation
* [#8710](https://github.com/netbox-community/netbox/issues/8710) - Fix dynamic scope selection form fields when creating a VLAN group * [#8710](https://github.com/netbox-community/netbox/issues/8710) - Fix dynamic scope selection form fields when creating a VLAN group
* [#8713](https://github.com/netbox-community/netbox/issues/8713) - Restore missing "add" button on services list view * [#8713](https://github.com/netbox-community/netbox/issues/8713) - Restore missing "add" button on services list view
* [#8715](https://github.com/netbox-community/netbox/issues/8715) - Avoid returning multiple objects when restricting querysets using multiple tags in permissions
* [#8717](https://github.com/netbox-community/netbox/issues/8717) - Fix redirection after bulk edit/delete of prefixes from aggregate view * [#8717](https://github.com/netbox-community/netbox/issues/8717) - Fix redirection after bulk edit/delete of prefixes from aggregate view
* [#8724](https://github.com/netbox-community/netbox/issues/8724) - Fix exception during device import with invalid device type
* [#8807](https://github.com/netbox-community/netbox/issues/8807) - Correct REST API URL for FHRP group assignments
* [#8808](https://github.com/netbox-community/netbox/issues/8808) - Fix members count under FHRP group list
--- ---

View File

@ -8,11 +8,13 @@ theme:
icon: icon:
repo: fontawesome/brands/github repo: fontawesome/brands/github
palette: palette:
- scheme: default - media: "(prefers-color-scheme: light)"
scheme: default
toggle: toggle:
icon: material/lightbulb-outline icon: material/lightbulb-outline
name: Switch to Dark Mode name: Switch to Dark Mode
- scheme: slate - media: "(prefers-color-scheme: dark)"
scheme: slate
toggle: toggle:
icon: material/lightbulb icon: material/lightbulb
name: Switch to Light Mode name: Switch to Light Mode

View File

@ -1003,13 +1003,19 @@ class PortTypeChoices(ChoiceSet):
TYPE_MRJ21 = 'mrj21' TYPE_MRJ21 = 'mrj21'
TYPE_ST = 'st' TYPE_ST = 'st'
TYPE_SC = 'sc' TYPE_SC = 'sc'
TYPE_SC_PC = 'sc-pc'
TYPE_SC_UPC = 'sc-upc'
TYPE_SC_APC = 'sc-apc' TYPE_SC_APC = 'sc-apc'
TYPE_FC = 'fc' TYPE_FC = 'fc'
TYPE_LC = 'lc' TYPE_LC = 'lc'
TYPE_LC_PC = 'lc-pc'
TYPE_LC_UPC = 'lc-upc'
TYPE_LC_APC = 'lc-apc' TYPE_LC_APC = 'lc-apc'
TYPE_MTRJ = 'mtrj' TYPE_MTRJ = 'mtrj'
TYPE_MPO = 'mpo' TYPE_MPO = 'mpo'
TYPE_LSH = 'lsh' TYPE_LSH = 'lsh'
TYPE_LSH_PC = 'lsh-pc'
TYPE_LSH_UPC = 'lsh-upc'
TYPE_LSH_APC = 'lsh-apc' TYPE_LSH_APC = 'lsh-apc'
TYPE_SPLICE = 'splice' TYPE_SPLICE = 'splice'
TYPE_CS = 'cs' TYPE_CS = 'cs'
@ -1049,12 +1055,18 @@ class PortTypeChoices(ChoiceSet):
( (
(TYPE_FC, 'FC'), (TYPE_FC, 'FC'),
(TYPE_LC, 'LC'), (TYPE_LC, 'LC'),
(TYPE_LC_PC, 'LC/PC'),
(TYPE_LC_UPC, 'LC/UPC'),
(TYPE_LC_APC, 'LC/APC'), (TYPE_LC_APC, 'LC/APC'),
(TYPE_LSH, 'LSH'), (TYPE_LSH, 'LSH'),
(TYPE_LSH_PC, 'LSH/PC'),
(TYPE_LSH_UPC, 'LSH/UPC'),
(TYPE_LSH_APC, 'LSH/APC'), (TYPE_LSH_APC, 'LSH/APC'),
(TYPE_MPO, 'MPO'), (TYPE_MPO, 'MPO'),
(TYPE_MTRJ, 'MTRJ'), (TYPE_MTRJ, 'MTRJ'),
(TYPE_SC, 'SC'), (TYPE_SC, 'SC'),
(TYPE_SC_PC, 'SC/PC'),
(TYPE_SC_UPC, 'SC/UPC'),
(TYPE_SC_APC, 'SC/APC'), (TYPE_SC_APC, 'SC/APC'),
(TYPE_ST, 'ST'), (TYPE_ST, 'ST'),
(TYPE_CS, 'CS'), (TYPE_CS, 'CS'),

View File

@ -804,10 +804,11 @@ class Device(NetBoxModel, ConfigContextModel):
}) })
# Prevent 0U devices from being assigned to a specific position # Prevent 0U devices from being assigned to a specific position
if self.position and self.device_type.u_height == 0: if hasattr(self, 'device_type'):
raise ValidationError({ if self.position and self.device_type.u_height == 0:
'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position." raise ValidationError({
}) 'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."
})
if self.rack: if self.rack:

View File

@ -82,6 +82,10 @@ class SiteTable(NetBoxTable):
accessor=tables.A('asns__count'), accessor=tables.A('asns__count'),
viewname='ipam:asn_list', viewname='ipam:asn_list',
url_params={'site_id': 'pk'}, url_params={'site_id': 'pk'},
verbose_name='ASN Count'
)
asns = tables.ManyToManyColumn(
linkify_item=True,
verbose_name='ASNs' verbose_name='ASNs'
) )
tenant = TenantColumn() tenant = TenantColumn()
@ -93,9 +97,9 @@ class SiteTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Site model = Site
fields = ( fields = (
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone', 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
'created', 'last_updated', 'actions', 'tags', 'created', 'last_updated', 'actions',
) )
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description') default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')

View File

@ -126,7 +126,7 @@ class FHRPGroupSerializer(PrimaryModelSerializer):
class FHRPGroupAssignmentSerializer(PrimaryModelSerializer): class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
group = NestedFHRPGroupSerializer() group = NestedFHRPGroupSerializer()
interface_type = ContentTypeField( interface_type = ContentTypeField(
queryset=ContentType.objects.all() queryset=ContentType.objects.all()

View File

@ -155,8 +155,10 @@ class ServiceProtocolChoices(ChoiceSet):
PROTOCOL_TCP = 'tcp' PROTOCOL_TCP = 'tcp'
PROTOCOL_UDP = 'udp' PROTOCOL_UDP = 'udp'
PROTOCOL_SCTP = 'sctp'
CHOICES = ( CHOICES = (
(PROTOCOL_TCP, 'TCP'), (PROTOCOL_TCP, 'TCP'),
(PROTOCOL_UDP, 'UDP'), (PROTOCOL_UDP, 'UDP'),
(PROTOCOL_SCTP, 'SCTP'),
) )

View File

@ -26,8 +26,8 @@ class FHRPGroupTable(NetBoxTable):
orderable=False, orderable=False,
verbose_name='IP Addresses' verbose_name='IP Addresses'
) )
interface_count = tables.Column( member_count = tables.Column(
verbose_name='Interfaces' verbose_name='Members'
) )
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='ipam:fhrpgroup_list' url_name='ipam:fhrpgroup_list'
@ -36,10 +36,10 @@ class FHRPGroupTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = FHRPGroup model = FHRPGroup
fields = ( fields = (
'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'interface_count', 'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'member_count',
'tags', 'created', 'last_updated', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'interface_count') default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'member_count')
class FHRPGroupAssignmentTable(NetBoxTable): class FHRPGroupAssignmentTable(NetBoxTable):

View File

@ -112,6 +112,10 @@ class ASNTable(NetBoxTable):
site_count = columns.LinkedCountColumn( site_count = columns.LinkedCountColumn(
viewname='dcim:site_list', viewname='dcim:site_list',
url_params={'asn_id': 'pk'}, url_params={'asn_id': 'pk'},
verbose_name='Site Count'
)
sites = tables.ManyToManyColumn(
linkify_item=True,
verbose_name='Sites' verbose_name='Sites'
) )
tenant = TenantColumn() tenant = TenantColumn()
@ -122,8 +126,8 @@ class ASNTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ASN model = ASN
fields = ( fields = (
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'tenant', 'description', 'created', 'last_updated', 'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'tenant', 'description', 'sites', 'tags', 'created',
'actions', 'last_updated', 'actions',
) )
default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'description', 'tenant') default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'description', 'tenant')

View File

@ -116,52 +116,54 @@ Blocks:
{# Page footer #} {# Page footer #}
<footer class="footer container-fluid"> <footer class="footer container-fluid">
<div class="row align-items-center justify-content-between mx-0"> {% block footer %}
<div class="row align-items-center justify-content-between mx-0">
{# Docs & Community Links #} <div class="col-sm-12 col-md-auto fs-4 noprint">
<div class="col-sm-12 col-md-auto fs-4 noprint"> <nav class="nav justify-content-center justify-content-lg-start">
<nav class="nav justify-content-center justify-content-lg-start"> {% block footer_links %}
{# Documentation #} {# Documentation #}
<a type="button" class="nav-link" href="{% static 'docs/' %}" target="_blank"> <a type="button" class="nav-link" href="{% static 'docs/' %}" target="_blank">
<i title="Docs" class="mdi mdi-book-open-variant text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="Docs" class="mdi mdi-book-open-variant text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>
{# REST API #} {# REST API #}
<a type="button" class="nav-link" href="{% url 'api-root' %}" target="_blank"> <a type="button" class="nav-link" href="{% url 'api-root' %}" target="_blank">
<i title="REST API" class="mdi mdi-cloud-braces text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="REST API" class="mdi mdi-cloud-braces text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>
{# API docs #} {# API docs #}
<a type="button" class="nav-link" href="{% url 'api_docs' %}" target="_blank"> <a type="button" class="nav-link" href="{% url 'api_docs' %}" target="_blank">
<i title="REST API documentation" class="mdi mdi-book text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="REST API documentation" class="mdi mdi-book text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>
{# GraphQL API #} {# GraphQL API #}
{% if config.GRAPHQL_ENABLED %} {% if config.GRAPHQL_ENABLED %}
<a type="button" class="nav-link" href="{% url 'graphql' %}" target="_blank"> <a type="button" class="nav-link" href="{% url 'graphql' %}" target="_blank">
<i title="GraphQL API" class="mdi mdi-graphql text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="GraphQL API" class="mdi mdi-graphql text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>
{% endif %} {% endif %}
{# GitHub #} {# GitHub #}
<a type="button" class="nav-link" href="https://github.com/netbox-community/netbox" target="_blank"> <a type="button" class="nav-link" href="https://github.com/netbox-community/netbox" target="_blank">
<i title="Source Code" class="mdi mdi-github text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="Source Code" class="mdi mdi-github text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>
{# NetDev Slack #}
<a type="button" class="nav-link" href="https://netdev.chat/" target="_blank">
<i title="Community" class="mdi mdi-slack text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a>
{% endblock footer_links %}
</nav>
</div>
<div class="col-sm-12 col-md-auto text-center text-lg-end text-muted">
<span class="d-block d-md-inline">{% annotated_now %} {% now 'T' %}</span>
<span class="ms-md-3 d-block d-md-inline">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</span>
</div>
{# NetDev Slack #}
<a type="button" class="nav-link" href="https://netdev.chat/" target="_blank">
<i title="Community" class="mdi mdi-slack text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a>
</nav>
</div> </div>
{% endblock footer %}
{# System Info #}
<div class="col-sm-12 col-md-auto text-center text-lg-end text-muted">
<span class="d-block d-md-inline">{% annotated_now %} {% now 'T' %}</span>
<span class="ms-md-3 d-block d-md-inline">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</span>
</div>
</div>
</footer> </footer>
</div> </div>

View File

@ -131,42 +131,98 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Stats</h5> <h5 class="card-header">Related Objects</h5>
<div class="card-body"> <div class="card-body">
<div class="row"> <table class="table table-hover attr-table">
<div class="col col-md-4 text-center"> <tr>
<h2><a href="{% url 'dcim:location_list' %}?site_id={{ object.pk }}" class="btn {% if stats.location_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.location_count }}</a></h2> <th scope="row">Locations</th>
<p>Locations</p> <td class="text-end">
</div> {% if stats.location_count %}
<div class="col col-md-4 text-center"> <a href="{% url 'dcim:location_list' %}?site_id={{ object.pk }}">{{ stats.location_count }}</a>
<h2><a href="{% url 'dcim:rack_list' %}?site_id={{ object.pk }}" class="btn {% if stats.rack_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.rack_count }}</a></h2> {% else %}
<p>Racks</p> {{ ''|placeholder }}
</div> {% endif %}
<div class="col col-md-4 text-center"> </td>
<h2><a href="{% url 'dcim:device_list' %}?site_id={{ object.pk }}" class="btn {% if stats.device_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.device_count }}</a></h2> </tr>
<p>Devices</p> <tr>
</div> <th scope="row">Racks</th>
<div class="col col-md-4 text-center"> <td class="text-end">
<h2><a href="{% url 'ipam:prefix_list' %}?site_id={{ object.pk }}" class="btn {% if stats.prefix_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2> {% if stats.rack_count %}
<p>Prefixes</p> <div class="dropdown">
</div> <button class="btn btn-sm btn-light dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<div class="col col-md-4 text-center"> {{ stats.rack_count }}
<h2><a href="{% url 'ipam:vlan_list' %}?site_id={{ object.pk }}" class="btn {% if stats.vlan_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vlan_count }}</a></h2> </button>
<p>VLANs</p> <ul class="dropdown-menu">
</div> <li><a class="dropdown-item" href="{% url 'dcim:rack_list' %}?site_id={{ object.pk }}">View Racks</a></li>
<div class="col col-md-4 text-center"> <li><a class="dropdown-item" href="{% url 'dcim:rack_elevation_list' %}?site_id={{ object.pk }}">View Elevations</a></li>
<h2><a href="{% url 'circuits:circuit_list' %}?site_id={{ object.pk }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2> </ul>
<p>Circuits</p> </div>
</div> {% else %}
<div class="col col-md-4 text-center"> {{ ''|placeholder }}
<h2><a href="{% url 'virtualization:virtualmachine_list' %}?site_id={{ object.pk }}" class="btn {% if stats.vm_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vm_count }}</a></h2> {% endif %}
<p>Virtual Machines</p> </td>
</div> </tr>
<div class="col col-md-4 text-center"> <tr>
<h2><a href="{% url 'ipam:asn_list' %}?site_id={{ object.pk }}" class="btn {% if stats.asn_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.asn_count }}</a></h2> <th scope="row">Devices</th>
<p>ASNs</p> <td class="text-end">
</div> {% if stats.device_count %}
</div> <a href="{% url 'dcim:device_list' %}?site_id={{ object.pk }}">{{ stats.device_count }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Virtual Machines</th>
<td class="text-end">
{% if stats.vm_count %}
<a href="{% url 'virtualization:virtualmachine_list' %}?site_id={{ object.pk }}">{{ stats.vm_count }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Prefixes</th>
<td class="text-end">
{% if stats.prefix_count %}
<a href="{% url 'ipam:prefix_list' %}?site_id={{ object.pk }}">{{ stats.prefix_count }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">VLANs</th>
<td class="text-end">
{% if stats.vlan_count %}
<a href="{% url 'ipam:vlan_list' %}?site_id={{ object.pk }}">{{ stats.vlan_count }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">ASNs</th>
<td class="text-end">
{% if stats.asn_count %}
<a href="{% url 'ipam:asn_list' %}?site_id={{ object.pk }}">{{ stats.asn_count }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Circuits</th>
<td class="text-end">
{% if stats.circuit_count %}
<a href="{% url 'circuits:circuit_list' %}?site_id={{ object.pk }}">{{ stats.circuit_count }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
</table>
</div> </div>
</div> </div>
{% include 'inc/panels/contacts.html' %} {% include 'inc/panels/contacts.html' %}

View File

@ -5,7 +5,18 @@
{% block title %}Search{% endblock %} {% block title %}Search{% endblock %}
{% block content %} {% block tabs %}
<ul class="nav nav-tabs px-3">
<li class="nav-item" role="presentation">
<button class="nav-link active" type="button" role="tab">
Results
</button>
</li>
</ul>
{% endblock tabs %}
{% block content-wrapper %}
<div class="tab-content">
{% if request.GET.q %} {% if request.GET.q %}
{% if results %} {% if results %}
<div class="row"> <div class="row">
@ -73,4 +84,5 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endblock content %} </div>
{% endblock content-wrapper %}

View File

@ -94,7 +94,9 @@ class BulkRenameForm(BootstrapMixin, forms.Form):
An extendable form to be used for renaming objects in bulk. An extendable form to be used for renaming objects in bulk.
""" """
find = forms.CharField() find = forms.CharField()
replace = forms.CharField() replace = forms.CharField(
required=False
)
use_regex = forms.BooleanField( use_regex = forms.BooleanField(
required=False, required=False,
initial=True, initial=True,

View File

@ -39,6 +39,12 @@ class RestrictedQuerySet(QuerySet):
# Any permission with null constraints grants access to _all_ instances # Any permission with null constraints grants access to _all_ instances
attrs = Q() attrs = Q()
break break
else:
# for else, when no break
# avoid duplicates when JOIN on many-to-many fields without using DISTINCT.
# DISTINCT acts globally on the entire request, which may not be desirable.
allowed_objects = self.model.objects.filter(attrs)
attrs = Q(pk__in=allowed_objects)
qs = self.filter(attrs) qs = self.filter(attrs)
return qs return qs

View File

@ -20,7 +20,7 @@ gunicorn==20.1.0
Jinja2==3.0.3 Jinja2==3.0.3
Markdown==3.3.6 Markdown==3.3.6
markdown-include==0.6.0 markdown-include==0.6.0
mkdocs-material==8.1.11 mkdocs-material==8.2.5
mkdocstrings==0.17.0 mkdocstrings==0.17.0
netaddr==0.8.0 netaddr==0.8.0
Pillow==9.0.1 Pillow==9.0.1