jeremystretch 3 лет назад
Родитель
Сommit
cd29293dd6

+ 1 - 1
.github/ISSUE_TEMPLATE/bug_report.yaml

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

+ 1 - 1
.github/ISSUE_TEMPLATE/feature_request.yaml

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

+ 2 - 0
README.md

@@ -2,6 +2,8 @@
   <img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
 </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)
 
 NetBox is an infrastructure resource modeling (IRM) tool designed to empower

+ 2 - 0
docs/index.md

@@ -1,5 +1,7 @@
 ![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?
 
 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:

+ 14 - 1
docs/release-notes/version-3.1.md

@@ -1,11 +1,20 @@
 # NetBox v3.1
 
-## v3.1.9 (FUTURE)
+## v3.1.10 (FUTURE)
+
+---
+
+## v3.1.9 (2022-03-07)
 
 ### Enhancements
 
 * [#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
+* [#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
 
@@ -14,7 +23,11 @@
 * [#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
 * [#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
+* [#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
 
 ---
 

+ 4 - 2
mkdocs.yml

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

+ 12 - 0
netbox/dcim/choices.py

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

+ 5 - 4
netbox/dcim/models/devices.py

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

+ 7 - 3
netbox/dcim/tables/sites.py

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

+ 1 - 1
netbox/ipam/api/serializers.py

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

+ 2 - 0
netbox/ipam/choices.py

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

+ 4 - 4
netbox/ipam/tables/fhrp.py

@@ -26,8 +26,8 @@ class FHRPGroupTable(NetBoxTable):
         orderable=False,
         verbose_name='IP Addresses'
     )
-    interface_count = tables.Column(
-        verbose_name='Interfaces'
+    member_count = tables.Column(
+        verbose_name='Members'
     )
     tags = columns.TagColumn(
         url_name='ipam:fhrpgroup_list'
@@ -36,10 +36,10 @@ class FHRPGroupTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = FHRPGroup
         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',
         )
-        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):

+ 6 - 2
netbox/ipam/tables/ip.py

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

+ 45 - 43
netbox/templates/base/layout.html

@@ -116,52 +116,54 @@ Blocks:
 
         {# Page footer #}
         <footer class="footer container-fluid">
-          <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">
-              <nav class="nav justify-content-center justify-content-lg-start">
-                {# Documentation #}
-                <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>
-                </a>
-
-                {# REST API #}
-                <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>
-                </a>
-
-                {# API docs #}
-                <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>
-                </a>
-
-                {# GraphQL API #}
-                {% if config.GRAPHQL_ENABLED %}
-                  <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>
-                  </a>
-                {% endif %}
-
-                {# GitHub #}
-                <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>
-                </a>
+          {% block footer %}
+            <div class="row align-items-center justify-content-between mx-0">
+
+              <div class="col-sm-12 col-md-auto fs-4 noprint">
+                <nav class="nav justify-content-center justify-content-lg-start">
+                  {% block footer_links %}
+                    {# Documentation #}
+                    <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>
+                    </a>
+
+                    {# REST API #}
+                    <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>
+                    </a>
+
+                    {# API docs #}
+                    <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>
+                    </a>
+
+                    {# GraphQL API #}
+                    {% if config.GRAPHQL_ENABLED %}
+                    <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>
+                    </a>
+                    {% endif %}
+
+                    {# GitHub #}
+                    <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>
+                    </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>
 
-                {# 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 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>
 
-            {# 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>
+          {% endblock footer %}
         </footer>
 
       </div>

+ 91 - 35
netbox/templates/dcim/site.html

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

+ 14 - 2
netbox/templates/search.html

@@ -5,7 +5,18 @@
 
 {% 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 results %}
             <div class="row">
@@ -73,4 +84,5 @@
             </div>
         </div>
     {% endif %}
-{% endblock content %}
+  </div>
+{% endblock content-wrapper %}

+ 3 - 1
netbox/utilities/forms/forms.py

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

+ 6 - 0
netbox/utilities/querysets.py

@@ -39,6 +39,12 @@ class RestrictedQuerySet(QuerySet):
                     # Any permission with null constraints grants access to _all_ instances
                     attrs = Q()
                     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)
 
         return qs

+ 1 - 1
requirements.txt

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