Просмотр исходного кода

Merge branch 'develop' into feature

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

+ 2 - 0
.github/workflows/ci.yml

@@ -1,5 +1,7 @@
 name: CI
 name: CI
 on: [push, pull_request]
 on: [push, pull_request]
+permissions:
+  contents: read
 jobs:
 jobs:
   build:
   build:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest

+ 5 - 1
.github/workflows/lock.yml

@@ -4,6 +4,11 @@ name: 'Lock threads'
 on:
 on:
   schedule:
   schedule:
     - cron: '0 3 * * *'
     - cron: '0 3 * * *'
+  workflow_dispatch:
+
+permissions:
+  issues: write
+  pull-requests: write
 
 
 jobs:
 jobs:
   lock:
   lock:
@@ -11,7 +16,6 @@ jobs:
     steps:
     steps:
       - uses: dessant/lock-threads@v3
       - uses: dessant/lock-threads@v3
         with:
         with:
-          github-token: ${{ github.token }}
           issue-inactive-days: 90
           issue-inactive-days: 90
           pr-inactive-days: 30
           pr-inactive-days: 30
           issue-lock-reason: 'resolved'
           issue-lock-reason: 'resolved'

+ 8 - 1
.github/workflows/stale.yml

@@ -1,14 +1,21 @@
 # close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
 # close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
 name: 'Close stale issues/PRs'
 name: 'Close stale issues/PRs'
+
 on:
 on:
   schedule:
   schedule:
     - cron: '0 4 * * *'
     - cron: '0 4 * * *'
+  workflow_dispatch:
+
+permissions:
+  issues: write
+  pull-requests: write
 
 
 jobs:
 jobs:
   stale:
   stale:
+
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/stale@v5
+      - uses: actions/stale@v6
         with:
         with:
           close-issue-message: >
           close-issue-message: >
             This issue has been automatically closed due to lack of activity. In an
             This issue has been automatically closed due to lack of activity. In an

+ 2 - 2
docs/_theme/main.html

@@ -2,8 +2,8 @@
 
 
 {% block site_meta %}
 {% block site_meta %}
   {{ super() }}
   {{ super() }}
-  {# Disable search indexing unless we're building for ReadTheDocs #}
-  {% if not config.extra.readthedocs %}
+  {# Disable search indexing unless we're building for ReadTheDocs (see #10496) #}
+  {% if page.canonical_url != 'https://docs.netbox.dev/' %}
     <meta name="robots" content="noindex">
     <meta name="robots" content="noindex">
   {% endif %}
   {% endif %}
 {% endblock %}
 {% endblock %}

+ 10 - 0
docs/release-notes/version-3.3.md

@@ -2,17 +2,27 @@
 
 
 ## v3.3.5 (FUTURE)
 ## v3.3.5 (FUTURE)
 
 
+### Enhancements
+
+* [#10465](https://github.com/netbox-community/netbox/issues/10465) - Improve formatting of device heights and rack positions
+
 ### Bug Fixes
 ### Bug Fixes
 
 
 * [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view
 * [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view
+* [#10408](https://github.com/netbox-community/netbox/issues/10408) - Fix validation when attempting to add redundant contact assignments
 * [#10435](https://github.com/netbox-community/netbox/issues/10435) - Fix exception when filtering VLANs by virtual machine with no cluster assigned
 * [#10435](https://github.com/netbox-community/netbox/issues/10435) - Fix exception when filtering VLANs by virtual machine with no cluster assigned
 * [#10439](https://github.com/netbox-community/netbox/issues/10439) - Fix form widget styling for DeviceType airflow field
 * [#10439](https://github.com/netbox-community/netbox/issues/10439) - Fix form widget styling for DeviceType airflow field
+* [#10445](https://github.com/netbox-community/netbox/issues/10445) - Avoid rounding virtual machine memory values
+* [#10461](https://github.com/netbox-community/netbox/issues/10461) - Enable filtering by read-only custom fields in the UI
+* [#10470](https://github.com/netbox-community/netbox/issues/10470) - Omit read-only custom fields from CSV import forms
+* [#10480](https://github.com/netbox-community/netbox/issues/10480) - Cable trace SVG links should not force a new window
 
 
 ---
 ---
 
 
 ## v3.3.4 (2022-09-16)
 ## v3.3.4 (2022-09-16)
 
 
 ### Bug Fixes
 ### Bug Fixes
+
 * [#10383](https://github.com/netbox-community/netbox/issues/10383) - Fix assignment of component templates to module types via web UI
 * [#10383](https://github.com/netbox-community/netbox/issues/10383) - Fix assignment of component templates to module types via web UI
 * [#10387](https://github.com/netbox-community/netbox/issues/10387) - Fix `MultiValueDictKeyError` exception when editing a device interface
 * [#10387](https://github.com/netbox-community/netbox/issues/10387) - Fix `MultiValueDictKeyError` exception when editing a device interface
 
 

+ 0 - 1
mkdocs.yml

@@ -38,7 +38,6 @@ 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

+ 1 - 1
netbox/dcim/svg/cables.py

@@ -35,7 +35,7 @@ class Node(Hyperlink):
     """
     """
 
 
     def __init__(self, position, width, url, color, labels, radius=10, **extra):
     def __init__(self, position, width, url, color, labels, radius=10, **extra):
-        super(Node, self).__init__(href=url, target='_blank', **extra)
+        super(Node, self).__init__(href=url, target='_parent', **extra)
 
 
         x, y = position
         x, y = position
 
 

+ 2 - 1
netbox/dcim/svg/racks.py

@@ -9,6 +9,7 @@ from svgwrite.text import Text
 from django.conf import settings
 from django.conf import settings
 from django.core.exceptions import FieldError
 from django.core.exceptions import FieldError
 from django.db.models import Q
 from django.db.models import Q
+from django.template.defaultfilters import floatformat
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.http import urlencode
 from django.utils.http import urlencode
 
 
@@ -41,7 +42,7 @@ def get_device_description(device):
         device.device_role,
         device.device_role,
         device.device_type.manufacturer.name,
         device.device_type.manufacturer.name,
         device.device_type.model,
         device.device_type.model,
-        device.device_type.u_height,
+        floatformat(device.device_type.u_height),
         device.asset_tag or '',
         device.asset_tag or '',
         device.serial or ''
         device.serial or ''
     )
     )

+ 3 - 0
netbox/dcim/tables/devicetypes.py

@@ -85,6 +85,9 @@ class DeviceTypeTable(NetBoxTable):
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:devicetype_list'
         url_name='dcim:devicetype_list'
     )
     )
+    u_height = columns.TemplateColumn(
+        template_code='{{ value|floatformat }}'
+    )
     weight = columns.TemplateColumn(
     weight = columns.TemplateColumn(
         template_code=DEVICE_WEIGHT,
         template_code=DEVICE_WEIGHT,
         order_by=('_abs_weight', 'weight_unit')
         order_by=('_abs_weight', 'weight_unit')

+ 3 - 8
netbox/extras/forms/customfields.py

@@ -34,7 +34,9 @@ class CustomFieldsMixin:
         return ContentType.objects.get_for_model(self.model)
         return ContentType.objects.get_for_model(self.model)
 
 
     def _get_custom_fields(self, content_type):
     def _get_custom_fields(self, content_type):
-        return CustomField.objects.filter(content_types=content_type)
+        return CustomField.objects.filter(content_types=content_type).exclude(
+            ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN
+        )
 
 
     def _get_form_field(self, customfield):
     def _get_form_field(self, customfield):
         return customfield.to_form_field()
         return customfield.to_form_field()
@@ -50,13 +52,6 @@ class CustomFieldsMixin:
             field_name = f'cf_{customfield.name}'
             field_name = f'cf_{customfield.name}'
             self.fields[field_name] = self._get_form_field(customfield)
             self.fields[field_name] = self._get_form_field(customfield)
 
 
-            if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
-                self.fields[field_name].disabled = True
-                if self.fields[field_name].help_text:
-                    self.fields[field_name].help_text += '<br />'
-                self.fields[field_name].help_text += '<i class="mdi mdi-alert-circle-outline"></i> ' \
-                                                     'Field is set to read-only.'
-
             # Annotate the field in the list of CustomField form fields
             # Annotate the field in the list of CustomField form fields
             self.custom_fields[field_name] = customfield
             self.custom_fields[field_name] = customfield
             if customfield.group_name not in self.custom_field_groups:
             if customfield.group_name not in self.custom_field_groups:

+ 8 - 1
netbox/extras/models/customfields.py

@@ -295,12 +295,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
             return model.objects.filter(pk__in=value)
             return model.objects.filter(pk__in=value)
         return value
         return value
 
 
-    def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
+    def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibility=True, for_csv_import=False):
         """
         """
         Return a form field suitable for setting a CustomField's value for an object.
         Return a form field suitable for setting a CustomField's value for an object.
 
 
         set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
         set_initial: Set initial data for the field. This should be False when generating a field for bulk editing.
         enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
         enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
+        enforce_visibility: Honor the value of CustomField.ui_visibility. Set to False for filtering.
         for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
         for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
         """
         """
         initial = self.default if set_initial else None
         initial = self.default if set_initial else None
@@ -407,6 +408,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
         if self.description:
         if self.description:
             field.help_text = escape(self.description)
             field.help_text = escape(self.description)
 
 
+        # Annotate read-only fields
+        if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
+            field.disabled = True
+            prepend = '<br />' if field.help_text else ''
+            field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> Field is set to read-only.'
+
         return field
         return field
 
 
     def to_filter(self, lookup_expr=None):
     def to_filter(self, lookup_expr=None):

+ 8 - 3
netbox/netbox/forms/base.py

@@ -2,7 +2,7 @@ from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
 from django.db.models import Q
 
 
-from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices
+from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
 from extras.forms.customfields import CustomFieldsMixin
 from extras.forms.customfields import CustomFieldsMixin
 from extras.models import CustomField, Tag
 from extras.models import CustomField, Tag
 from utilities.forms import BootstrapMixin, CSVModelForm
 from utilities.forms import BootstrapMixin, CSVModelForm
@@ -63,6 +63,11 @@ class NetBoxModelCSVForm(CSVModelForm, NetBoxModelForm):
     """
     """
     tags = None  # Temporary fix in lieu of tag import support (see #9158)
     tags = None  # Temporary fix in lieu of tag import support (see #9158)
 
 
+    def _get_custom_fields(self, content_type):
+        return CustomField.objects.filter(content_types=content_type).filter(
+            ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE
+        )
+
     def _get_form_field(self, customfield):
     def _get_form_field(self, customfield):
         return customfield.to_form_field(for_csv_import=True)
         return customfield.to_form_field(for_csv_import=True)
 
 
@@ -125,10 +130,10 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
     )
     )
 
 
     def _get_custom_fields(self, content_type):
     def _get_custom_fields(self, content_type):
-        return CustomField.objects.filter(content_types=content_type).exclude(
+        return super()._get_custom_fields(content_type).exclude(
             Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
             Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
             Q(type=CustomFieldTypeChoices.TYPE_JSON)
             Q(type=CustomFieldTypeChoices.TYPE_JSON)
         )
         )
 
 
     def _get_form_field(self, customfield):
     def _get_form_field(self, customfield):
-        return customfield.to_form_field(set_initial=False, enforce_required=False)
+        return customfield.to_form_field(set_initial=False, enforce_required=False, enforce_visibility=False)

+ 2 - 2
netbox/templates/dcim/device.html

@@ -66,7 +66,7 @@
                                     {% with object.parent_bay.device as parent %}
                                     {% with object.parent_bay.device as parent %}
                                         {{ parent|linkify }} / {{ object.parent_bay }}
                                         {{ parent|linkify }} / {{ object.parent_bay }}
                                         {% if parent.position %}
                                         {% if parent.position %}
-                                            (U{{ parent.position }} / {{ parent.get_face_display }})
+                                            (U{{ parent.position|floatformat }} / {{ parent.get_face_display }})
                                         {% endif %}
                                         {% endif %}
                                     {% endwith %}
                                     {% endwith %}
                                 {% elif object.rack and object.position %}
                                 {% elif object.rack and object.position %}
@@ -90,7 +90,7 @@
                         <tr>
                         <tr>
                             <th scope="row">Device Type</th>
                             <th scope="row">Device Type</th>
                             <td>
                             <td>
-                                {{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height }}U)
+                                {{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height|floatformat }}U)
                             </td>
                             </td>
                         </tr>
                         </tr>
                         <tr>
                         <tr>

+ 1 - 1
netbox/templates/dcim/devicetype.html

@@ -29,7 +29,7 @@
                         </tr>
                         </tr>
                         <tr>
                         <tr>
                             <td>Height (U)</td>
                             <td>Height (U)</td>
-                            <td>{{ object.u_height }}</td>
+                            <td>{{ object.u_height|floatformat }}</td>
                         </tr>
                         </tr>
                         <tr>
                         <tr>
                             <td>Full Depth</td>
                             <td>Full Depth</td>

+ 3 - 0
netbox/templates/tenancy/contactassignment_edit.html

@@ -3,6 +3,9 @@
 {% load form_helpers %}
 {% load form_helpers %}
 
 
 {% block form %}
 {% block form %}
+  {% for field in form.hidden_fields %}
+    {{ field }}
+  {% endfor %}
   <div class="field-group my-5">
   <div class="field-group my-5">
     <div class="row mb-2">
     <div class="row mb-2">
       <h5 class="offset-sm-3">Contact Assignment</h5>
       <h5 class="offset-sm-3">Contact Assignment</h5>

+ 3 - 1
netbox/tenancy/forms/models.py

@@ -119,8 +119,10 @@ class ContactAssignmentForm(BootstrapMixin, forms.ModelForm):
     class Meta:
     class Meta:
         model = ContactAssignment
         model = ContactAssignment
         fields = (
         fields = (
-            'group', 'contact', 'role', 'priority',
+            'content_type', 'object_id', 'group', 'contact', 'role', 'priority',
         )
         )
         widgets = {
         widgets = {
+            'content_type': forms.HiddenInput(),
+            'object_id': forms.HiddenInput(),
             'priority': StaticSelect(),
             'priority': StaticSelect(),
         }
         }

+ 2 - 2
netbox/utilities/templatetags/helpers.py

@@ -73,9 +73,9 @@ def humanize_megabytes(mb):
     """
     """
     if not mb:
     if not mb:
         return ''
         return ''
-    if mb >= 1048576:
+    if not mb % 1048576:  # 1024^2
         return f'{int(mb / 1048576)} TB'
         return f'{int(mb / 1048576)} TB'
-    if mb >= 1024:
+    if not mb % 1024:
         return f'{int(mb / 1024)} GB'
         return f'{int(mb / 1024)} GB'
     return f'{mb} MB'
     return f'{mb} MB'