diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 67f5028cd..d75f98fbc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,5 +1,7 @@
name: CI
on: [push, pull_request]
+permissions:
+ contents: read
jobs:
build:
runs-on: ubuntu-latest
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
index 9df4bc441..6019cef5d 100644
--- a/.github/workflows/lock.yml
+++ b/.github/workflows/lock.yml
@@ -4,6 +4,11 @@ name: 'Lock threads'
on:
schedule:
- cron: '0 3 * * *'
+ workflow_dispatch:
+
+permissions:
+ issues: write
+ pull-requests: write
jobs:
lock:
@@ -11,7 +16,6 @@ jobs:
steps:
- uses: dessant/lock-threads@v3
with:
- github-token: ${{ github.token }}
issue-inactive-days: 90
pr-inactive-days: 30
issue-lock-reason: 'resolved'
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 57666417a..ab259af2a 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -1,14 +1,21 @@
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
name: 'Close stale issues/PRs'
+
on:
schedule:
- cron: '0 4 * * *'
+ workflow_dispatch:
+
+permissions:
+ issues: write
+ pull-requests: write
jobs:
stale:
+
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v5
+ - uses: actions/stale@v6
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an
diff --git a/docs/_theme/main.html b/docs/_theme/main.html
index 4dfc4e14e..3ff44b9cb 100644
--- a/docs/_theme/main.html
+++ b/docs/_theme/main.html
@@ -2,8 +2,8 @@
{% block site_meta %}
{{ 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/' %}
{% endif %}
{% endblock %}
diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md
index 2955e17d5..e91e825f5 100644
--- a/docs/release-notes/version-3.3.md
+++ b/docs/release-notes/version-3.3.md
@@ -2,17 +2,27 @@
## v3.3.5 (FUTURE)
+### Enhancements
+
+* [#10465](https://github.com/netbox-community/netbox/issues/10465) - Improve formatting of device heights and rack positions
+
### Bug Fixes
* [#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
* [#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)
### Bug Fixes
+
* [#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
diff --git a/mkdocs.yml b/mkdocs.yml
index 8f6e2930a..4e2cb73dd 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -38,7 +38,6 @@ plugins:
show_root_toc_entry: false
show_source: false
extra:
- readthedocs: !ENV READTHEDOCS
social:
- icon: fontawesome/brands/github
link: https://github.com/netbox-community/netbox
diff --git a/netbox/dcim/svg/cables.py b/netbox/dcim/svg/cables.py
index 3872bc4fe..9a847acc9 100644
--- a/netbox/dcim/svg/cables.py
+++ b/netbox/dcim/svg/cables.py
@@ -35,7 +35,7 @@ class Node(Hyperlink):
"""
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
diff --git a/netbox/dcim/svg/racks.py b/netbox/dcim/svg/racks.py
index 573fc966c..6c57e6023 100644
--- a/netbox/dcim/svg/racks.py
+++ b/netbox/dcim/svg/racks.py
@@ -9,6 +9,7 @@ from svgwrite.text import Text
from django.conf import settings
from django.core.exceptions import FieldError
from django.db.models import Q
+from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils.http import urlencode
@@ -41,7 +42,7 @@ def get_device_description(device):
device.device_role,
device.device_type.manufacturer.name,
device.device_type.model,
- device.device_type.u_height,
+ floatformat(device.device_type.u_height),
device.asset_tag or '',
device.serial or ''
)
diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py
index 8f371ef1a..bc596a297 100644
--- a/netbox/dcim/tables/devicetypes.py
+++ b/netbox/dcim/tables/devicetypes.py
@@ -85,6 +85,9 @@ class DeviceTypeTable(NetBoxTable):
tags = columns.TagColumn(
url_name='dcim:devicetype_list'
)
+ u_height = columns.TemplateColumn(
+ template_code='{{ value|floatformat }}'
+ )
weight = columns.TemplateColumn(
template_code=DEVICE_WEIGHT,
order_by=('_abs_weight', 'weight_unit')
diff --git a/netbox/extras/forms/customfields.py b/netbox/extras/forms/customfields.py
index 7574f4f2b..40d068450 100644
--- a/netbox/extras/forms/customfields.py
+++ b/netbox/extras/forms/customfields.py
@@ -34,7 +34,9 @@ class CustomFieldsMixin:
return ContentType.objects.get_for_model(self.model)
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):
return customfield.to_form_field()
@@ -50,13 +52,6 @@ class CustomFieldsMixin:
field_name = f'cf_{customfield.name}'
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 += '
'
- self.fields[field_name].help_text += ' ' \
- 'Field is set to read-only.'
-
# Annotate the field in the list of CustomField form fields
self.custom_fields[field_name] = customfield
if customfield.group_name not in self.custom_field_groups:
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index 3cb5b506c..c3c298a44 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -295,12 +295,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
return model.objects.filter(pk__in=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.
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_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.
"""
initial = self.default if set_initial else None
@@ -407,6 +408,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
if 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 = '
' if field.help_text else ''
+ field.help_text += f'{prepend} Field is set to read-only.'
+
return field
def to_filter(self, lookup_expr=None):
diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py
index 2676e4cde..2cbc67971 100644
--- a/netbox/netbox/forms/base.py
+++ b/netbox/netbox/forms/base.py
@@ -2,7 +2,7 @@ from django import forms
from django.contrib.contenttypes.models import ContentType
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.models import CustomField, Tag
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)
+ 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):
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):
- return CustomField.objects.filter(content_types=content_type).exclude(
+ return super()._get_custom_fields(content_type).exclude(
Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) |
Q(type=CustomFieldTypeChoices.TYPE_JSON)
)
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)
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html
index 6cc859749..253d905f2 100644
--- a/netbox/templates/dcim/device.html
+++ b/netbox/templates/dcim/device.html
@@ -66,7 +66,7 @@
{% with object.parent_bay.device as parent %}
{{ parent|linkify }} / {{ object.parent_bay }}
{% if parent.position %}
- (U{{ parent.position }} / {{ parent.get_face_display }})
+ (U{{ parent.position|floatformat }} / {{ parent.get_face_display }})
{% endif %}
{% endwith %}
{% elif object.rack and object.position %}
@@ -90,7 +90,7 @@