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 @@ Device Type - {{ 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) diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 6a37a8d06..458c74ac1 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -29,7 +29,7 @@ Height (U) - {{ object.u_height }} + {{ object.u_height|floatformat }} Full Depth diff --git a/netbox/templates/tenancy/contactassignment_edit.html b/netbox/templates/tenancy/contactassignment_edit.html index 4d1747e72..d904deead 100644 --- a/netbox/templates/tenancy/contactassignment_edit.html +++ b/netbox/templates/tenancy/contactassignment_edit.html @@ -3,6 +3,9 @@ {% load form_helpers %} {% block form %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %}
Contact Assignment
diff --git a/netbox/tenancy/forms/models.py b/netbox/tenancy/forms/models.py index 021e36a5b..eabcb1d0f 100644 --- a/netbox/tenancy/forms/models.py +++ b/netbox/tenancy/forms/models.py @@ -119,8 +119,10 @@ class ContactAssignmentForm(BootstrapMixin, forms.ModelForm): class Meta: model = ContactAssignment fields = ( - 'group', 'contact', 'role', 'priority', + 'content_type', 'object_id', 'group', 'contact', 'role', 'priority', ) widgets = { + 'content_type': forms.HiddenInput(), + 'object_id': forms.HiddenInput(), 'priority': StaticSelect(), } diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 67ed553b2..462b37feb 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -73,9 +73,9 @@ def humanize_megabytes(mb): """ if not mb: return '' - if mb >= 1048576: + if not mb % 1048576: # 1024^2 return f'{int(mb / 1048576)} TB' - if mb >= 1024: + if not mb % 1024: return f'{int(mb / 1024)} GB' return f'{mb} MB'