Merge remote-tracking branch 'origin/develop' into 10356-backplane-connections

This commit is contained in:
Patrick Hurrelmann 2022-09-28 08:40:52 +02:00
commit 9c4bd28c1f
15 changed files with 78 additions and 43 deletions

View File

@ -58,7 +58,7 @@ Email is sent from NetBox only for critical events or if configured for [logging
Default: None Default: None
A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://2.python-requests.org/en/master/user/advanced/). For example: A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://requests.readthedocs.io/en/latest/user/advanced/#proxies). For example:
```python ```python
HTTP_PROXIES = { HTTP_PROXIES = {

View File

@ -2,13 +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
* [#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) ## v3.3.4 (2022-09-16)
### Bug Fixes ### Bug Fixes
* [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view
* [#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

View File

@ -373,6 +373,7 @@ class DeviceTypeForm(NetBoxModelForm):
'front_image', 'rear_image', 'comments', 'tags', 'front_image', 'rear_image', 'comments', 'tags',
] ]
widgets = { widgets = {
'airflow': StaticSelect(),
'subdevice_role': StaticSelect(), 'subdevice_role': StaticSelect(),
'front_image': ClearableFileInput(attrs={ 'front_image': ClearableFileInput(attrs={
'accept': DEVICETYPE_IMAGE_FORMATS 'accept': DEVICETYPE_IMAGE_FORMATS

View File

@ -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

View File

@ -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 ''
) )

View File

@ -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 }}'
)
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = DeviceType model = DeviceType

View File

@ -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:

View File

@ -297,12 +297,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
@ -398,6 +399,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):

View File

@ -81,30 +81,34 @@ class VLANQuerySet(RestrictedQuerySet):
# Find all relevant VLANGroups # Find all relevant VLANGroups
q = Q() q = Q()
if vm.cluster.site: site = vm.site or vm.cluster.site
if vm.cluster.site.region: if vm.cluster:
# Add VLANGroups scoped to the assigned cluster (or its group)
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'),
scope_id=vm.cluster_id
)
if vm.cluster.group:
q |= Q( q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'), scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'),
scope_id__in=vm.cluster.site.region.get_ancestors(include_self=True) scope_id=vm.cluster.group_id
)
if vm.cluster.site.group:
q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
scope_id__in=vm.cluster.site.group.get_ancestors(include_self=True)
) )
if site:
# Add VLANGroups scoped to the assigned site (or its group or region)
q |= Q( q |= Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'), scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'),
scope_id=vm.cluster.site_id scope_id=site.pk
) )
if vm.cluster.group: if site.region:
q |= Q( q |= Q(
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'), scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'),
scope_id=vm.cluster.group_id scope_id__in=site.region.get_ancestors(include_self=True)
) )
q |= Q( if site.group:
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'), q |= Q(
scope_id=vm.cluster_id scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
) scope_id__in=site.group.get_ancestors(include_self=True)
)
vlan_groups = VLANGroup.objects.filter(q) vlan_groups = VLANGroup.objects.filter(q)
# Return all applicable VLANs # Return all applicable VLANs
@ -113,7 +117,7 @@ class VLANQuerySet(RestrictedQuerySet):
Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
Q(group__isnull=True, site__isnull=True) # Global VLANs Q(group__isnull=True, site__isnull=True) # Global VLANs
) )
if vm.cluster.site: if site:
q |= Q(site=vm.cluster.site) q |= Q(site=site)
return self.filter(q) return self.filter(q)

View File

@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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(),
} }

View File

@ -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'