mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-17 12:42:52 -06:00
Merge branch 'develop' into 3834-filter-tests
This commit is contained in:
commit
38c16d71b4
@ -1,3 +1,19 @@
|
|||||||
|
# v2.6.12 (FUTURE)
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
|
||||||
|
* [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link
|
||||||
|
* [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
* [#3589](https://github.com/netbox-community/netbox/issues/3589) - Fix validation on tagged VLANs of an interface
|
||||||
|
* [#3853](https://github.com/netbox-community/netbox/issues/3853) - Fix device role link on config context view
|
||||||
|
* [#3856](https://github.com/netbox-community/netbox/issues/3856) - Allow filtering VM interfaces by multiple MAC addresses
|
||||||
|
* [#3862](https://github.com/netbox-community/netbox/issues/3862) - Allow filtering device components by multiple device names
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# v2.6.11 (2020-01-03)
|
# v2.6.11 (2020-01-03)
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
@ -646,7 +646,8 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
|||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
)
|
)
|
||||||
device = django_filters.ModelChoiceFilter(
|
device = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device__name',
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Device (name)',
|
label='Device (name)',
|
||||||
|
@ -74,6 +74,17 @@ class InterfaceCommonForm:
|
|||||||
elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
|
elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
|
||||||
self.cleaned_data['tagged_vlans'] = []
|
self.cleaned_data['tagged_vlans'] = []
|
||||||
|
|
||||||
|
# Validate tagged VLANs; must be a global VLAN or in the same site
|
||||||
|
elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED:
|
||||||
|
valid_sites = [None, self.cleaned_data['device'].site]
|
||||||
|
invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]
|
||||||
|
|
||||||
|
if invalid_vlans:
|
||||||
|
raise forms.ValidationError({
|
||||||
|
'tagged_vlans': "The tagged VLANs ({}) must belong to the same site as the interface's parent "
|
||||||
|
"device/VM, or they must be global".format(', '.join(invalid_vlans))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class BulkRenameForm(forms.Form):
|
class BulkRenameForm(forms.Form):
|
||||||
"""
|
"""
|
||||||
@ -703,6 +714,34 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack elevations
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackElevationFilterForm(RackFilterForm):
|
||||||
|
field_order = ['q', 'region', 'site', 'group_id', 'id', 'status', 'role', 'tenant_group', 'tenant']
|
||||||
|
id = ChainedModelChoiceField(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
label='Rack',
|
||||||
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
('group_id', 'group_id'),
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url='/api/dcim/racks/',
|
||||||
|
display_field='display_name',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Filter the rack field based on the site and group
|
||||||
|
self.fields['site'].widget.add_filter_for('id', 'site')
|
||||||
|
self.fields['group_id'].widget.add_filter_for('id', 'group_id')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack reservations
|
# Rack reservations
|
||||||
#
|
#
|
||||||
@ -2250,36 +2289,6 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
|
|||||||
device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG
|
device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG
|
||||||
)
|
)
|
||||||
|
|
||||||
# Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
|
|
||||||
vlan_choices = []
|
|
||||||
global_vlans = VLAN.objects.filter(site=None, group=None)
|
|
||||||
vlan_choices.append(
|
|
||||||
('Global', [(vlan.pk, vlan) for vlan in global_vlans])
|
|
||||||
)
|
|
||||||
for group in VLANGroup.objects.filter(site=None):
|
|
||||||
global_group_vlans = VLAN.objects.filter(group=group)
|
|
||||||
vlan_choices.append(
|
|
||||||
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
|
|
||||||
)
|
|
||||||
|
|
||||||
site = getattr(self.instance.parent, 'site', None)
|
|
||||||
if site is not None:
|
|
||||||
|
|
||||||
# Add non-grouped site VLANs
|
|
||||||
site_vlans = VLAN.objects.filter(site=site, group=None)
|
|
||||||
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
|
|
||||||
|
|
||||||
# Add grouped site VLANs
|
|
||||||
for group in VLANGroup.objects.filter(site=site):
|
|
||||||
site_group_vlans = VLAN.objects.filter(group=group)
|
|
||||||
vlan_choices.append((
|
|
||||||
'{} / {}'.format(group.site.name, group.name),
|
|
||||||
[(vlan.pk, vlan) for vlan in site_group_vlans]
|
|
||||||
))
|
|
||||||
|
|
||||||
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
|
|
||||||
self.fields['tagged_vlans'].choices = vlan_choices
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
|
class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
@ -2360,36 +2369,6 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
|
|||||||
else:
|
else:
|
||||||
self.fields['lag'].queryset = Interface.objects.none()
|
self.fields['lag'].queryset = Interface.objects.none()
|
||||||
|
|
||||||
# Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
|
|
||||||
vlan_choices = []
|
|
||||||
global_vlans = VLAN.objects.filter(site=None, group=None)
|
|
||||||
vlan_choices.append(
|
|
||||||
('Global', [(vlan.pk, vlan) for vlan in global_vlans])
|
|
||||||
)
|
|
||||||
for group in VLANGroup.objects.filter(site=None):
|
|
||||||
global_group_vlans = VLAN.objects.filter(group=group)
|
|
||||||
vlan_choices.append(
|
|
||||||
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
|
|
||||||
)
|
|
||||||
|
|
||||||
site = getattr(self.parent, 'site', None)
|
|
||||||
if site is not None:
|
|
||||||
|
|
||||||
# Add non-grouped site VLANs
|
|
||||||
site_vlans = VLAN.objects.filter(site=site, group=None)
|
|
||||||
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
|
|
||||||
|
|
||||||
# Add grouped site VLANs
|
|
||||||
for group in VLANGroup.objects.filter(site=site):
|
|
||||||
site_group_vlans = VLAN.objects.filter(group=group)
|
|
||||||
vlan_choices.append((
|
|
||||||
'{} / {}'.format(group.site.name, group.name),
|
|
||||||
[(vlan.pk, vlan) for vlan in site_group_vlans]
|
|
||||||
))
|
|
||||||
|
|
||||||
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
|
|
||||||
self.fields['tagged_vlans'].choices = vlan_choices
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
@ -2472,36 +2451,6 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
|
|||||||
else:
|
else:
|
||||||
self.fields['lag'].choices = []
|
self.fields['lag'].choices = []
|
||||||
|
|
||||||
# Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
|
|
||||||
vlan_choices = []
|
|
||||||
global_vlans = VLAN.objects.filter(site=None, group=None)
|
|
||||||
vlan_choices.append(
|
|
||||||
('Global', [(vlan.pk, vlan) for vlan in global_vlans])
|
|
||||||
)
|
|
||||||
for group in VLANGroup.objects.filter(site=None):
|
|
||||||
global_group_vlans = VLAN.objects.filter(group=group)
|
|
||||||
vlan_choices.append(
|
|
||||||
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
|
|
||||||
)
|
|
||||||
if self.parent_obj is not None:
|
|
||||||
site = getattr(self.parent_obj, 'site', None)
|
|
||||||
if site is not None:
|
|
||||||
|
|
||||||
# Add non-grouped site VLANs
|
|
||||||
site_vlans = VLAN.objects.filter(site=site, group=None)
|
|
||||||
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
|
|
||||||
|
|
||||||
# Add grouped site VLANs
|
|
||||||
for group in VLANGroup.objects.filter(site=site):
|
|
||||||
site_group_vlans = VLAN.objects.filter(group=group)
|
|
||||||
vlan_choices.append((
|
|
||||||
'{} / {}'.format(group.site.name, group.name),
|
|
||||||
[(vlan.pk, vlan) for vlan in site_group_vlans]
|
|
||||||
))
|
|
||||||
|
|
||||||
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
|
|
||||||
self.fields['tagged_vlans'].choices = vlan_choices
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkRenameForm(BulkRenameForm):
|
class InterfaceBulkRenameForm(BulkRenameForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
@ -388,7 +388,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
|
|||||||
'page': page,
|
'page': page,
|
||||||
'total_count': total_count,
|
'total_count': total_count,
|
||||||
'face_id': face_id,
|
'face_id': face_id,
|
||||||
'filter_form': forms.RackFilterForm(request.GET),
|
'filter_form': forms.RackElevationFilterForm(request.GET),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -398,4 +398,43 @@ $(document).ready(function() {
|
|||||||
// Account for the header height when hash-scrolling
|
// Account for the header height when hash-scrolling
|
||||||
window.addEventListener('load', headerOffsetScroll);
|
window.addEventListener('load', headerOffsetScroll);
|
||||||
window.addEventListener('hashchange', headerOffsetScroll);
|
window.addEventListener('hashchange', headerOffsetScroll);
|
||||||
|
|
||||||
|
// Offset between the preview window and the window edges
|
||||||
|
const IMAGE_PREVIEW_OFFSET_X = 20
|
||||||
|
const IMAGE_PREVIEW_OFFSET_Y = 10
|
||||||
|
|
||||||
|
// Preview an image attachment when the link is hovered over
|
||||||
|
$('a.image-preview').on('mouseover', function(e) {
|
||||||
|
// Twice the offset to account for all sides of the picture
|
||||||
|
var maxWidth = window.innerWidth - (e.clientX + (IMAGE_PREVIEW_OFFSET_X * 2));
|
||||||
|
var maxHeight = window.innerHeight - (e.clientY + (IMAGE_PREVIEW_OFFSET_Y * 2));
|
||||||
|
var img = $('<img>').attr('id', 'image-preview-window').css({
|
||||||
|
display: 'none',
|
||||||
|
position: 'absolute',
|
||||||
|
maxWidth: maxWidth + 'px',
|
||||||
|
maxHeight: maxHeight + 'px',
|
||||||
|
left: e.pageX + IMAGE_PREVIEW_OFFSET_X + 'px',
|
||||||
|
top: e.pageY + IMAGE_PREVIEW_OFFSET_Y + 'px',
|
||||||
|
boxShadow: '0 0px 12px 3px rgba(0, 0, 0, 0.4)',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove any existing preview windows and add the current one
|
||||||
|
$('#image-preview-window').remove();
|
||||||
|
$('body').append(img);
|
||||||
|
|
||||||
|
// Once loaded, show the preview if the image is indeed an image
|
||||||
|
img.on('load', function(e) {
|
||||||
|
if (e.target.complete && e.target.naturalWidth) {
|
||||||
|
$('#image-preview-window').fadeIn('fast');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Begin loading
|
||||||
|
img.attr('src', e.target.href);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fade the image out; it will be deleted when another one is previewed
|
||||||
|
$('a.image-preview').on('mouseout', function() {
|
||||||
|
$('#image-preview-window').fadeOut('fast')
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -112,7 +112,7 @@
|
|||||||
{% if configcontext.roles.all %}
|
{% if configcontext.roles.all %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for role in configcontext.roles.all %}
|
{% for role in configcontext.roles.all %}
|
||||||
<li><a href="{{ role.get_absolute_url }}">{{ role }}</a></li>
|
<li><a href="{% url 'dcim:device_list' %}?role={{ role.slug }}">{{ role }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<tr{% if not attachment.size %} class="danger"{% endif %}>
|
<tr{% if not attachment.size %} class="danger"{% endif %}>
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-image"></i>
|
<i class="fa fa-image"></i>
|
||||||
<a href="{{ attachment.image.url }}" target="_blank">{{ attachment }}</a>
|
<a class="image-preview" href="{{ attachment.image.url }}" target="_blank">{{ attachment }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ attachment.size|filesizeformat }}</td>
|
<td>{{ attachment.size|filesizeformat }}</td>
|
||||||
<td>{{ attachment.created }}</td>
|
<td>{{ attachment.created }}</td>
|
||||||
|
@ -208,8 +208,7 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Virtual machine',
|
label='Virtual machine',
|
||||||
)
|
)
|
||||||
mac_address = django_filters.CharFilter(
|
mac_address = MultiValueMACAddressFilter(
|
||||||
method='_mac_address',
|
|
||||||
label='MAC address',
|
label='MAC address',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -217,16 +216,6 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = ['id', 'name', 'enabled', 'mtu']
|
fields = ['id', 'name', 'enabled', 'mtu']
|
||||||
|
|
||||||
def _mac_address(self, queryset, name, value):
|
|
||||||
value = value.strip()
|
|
||||||
if not value:
|
|
||||||
return queryset
|
|
||||||
try:
|
|
||||||
mac = EUI(value.strip())
|
|
||||||
return queryset.filter(mac_address=mac)
|
|
||||||
except AddrFormatError:
|
|
||||||
return queryset.none()
|
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
|
Loading…
Reference in New Issue
Block a user