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

Merge branch 'develop' into 3834-filter-tests

Jeremy Stretch 6 лет назад
Родитель
Сommit
38c16d71b4

+ 16 - 0
docs/release-notes/version-2.6.md

@@ -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)
 
 ## Bug Fixes

+ 2 - 1
netbox/dcim/filters.py

@@ -646,7 +646,8 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
         queryset=Device.objects.all(),
         label='Device (ID)',
     )
-    device = django_filters.ModelChoiceFilter(
+    device = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__name',
         queryset=Device.objects.all(),
         to_field_name='name',
         label='Device (name)',

+ 39 - 90
netbox/dcim/forms.py

@@ -74,6 +74,17 @@ class InterfaceCommonForm:
         elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
             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):
     """
@@ -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
 #
@@ -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
             )
 
-        # 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):
     name_pattern = ExpandableNameField(
@@ -2360,36 +2369,6 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
         else:
             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):
     pk = forms.ModelMultipleChoiceField(
@@ -2472,36 +2451,6 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
         else:
             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):
     pk = forms.ModelMultipleChoiceField(

+ 1 - 1
netbox/dcim/views.py

@@ -388,7 +388,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
             'page': page,
             'total_count': total_count,
             'face_id': face_id,
-            'filter_form': forms.RackFilterForm(request.GET),
+            'filter_form': forms.RackElevationFilterForm(request.GET),
         })
 
 

+ 39 - 0
netbox/project-static/js/forms.js

@@ -398,4 +398,43 @@ $(document).ready(function() {
     // Account for the header height when hash-scrolling
     window.addEventListener('load', 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')
+    });
 });

+ 1 - 1
netbox/templates/extras/configcontext.html

@@ -112,7 +112,7 @@
                             {% if configcontext.roles.all %}
                                 <ul>
                                     {% 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 %}
                                 </ul>
                             {% else %}

+ 1 - 1
netbox/templates/inc/image_attachments.html

@@ -10,7 +10,7 @@
             <tr{% if not attachment.size %} class="danger"{% endif %}>
                 <td>
                     <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>{{ attachment.size|filesizeformat }}</td>
                 <td>{{ attachment.created }}</td>

+ 1 - 12
netbox/virtualization/filters.py

@@ -208,8 +208,7 @@ class InterfaceFilter(django_filters.FilterSet):
         to_field_name='name',
         label='Virtual machine',
     )
-    mac_address = django_filters.CharFilter(
-        method='_mac_address',
+    mac_address = MultiValueMACAddressFilter(
         label='MAC address',
     )
 
@@ -217,16 +216,6 @@ class InterfaceFilter(django_filters.FilterSet):
         model = Interface
         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):
         if not value.strip():
             return queryset