diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index fd7603c0e..b4dff6676 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -1,11 +1,20 @@ # NetBox v3.0 -## v2.11.12 (2021-08-23) +## v3.0.1 (FUTURE) ### Bug Fixes * [#7070](https://github.com/netbox-community/netbox/issues/7070) - Fix exception when filtering by prefix max length in UI * [#7071](https://github.com/netbox-community/netbox/issues/7071) - Fix exception when removing a primary IP from a device/VM +* [#7072](https://github.com/netbox-community/netbox/issues/7072) - Fix table configuration under prefix child object views +* [#7075](https://github.com/netbox-community/netbox/issues/7075) - Fix UI bug when a custom field has a space in the name +* [#7082](https://github.com/netbox-community/netbox/issues/7082) - Avoid exception when referencing invalid content type in table +* [#7083](https://github.com/netbox-community/netbox/issues/7083) - Correct labeling for VM memory attribute +* [#7084](https://github.com/netbox-community/netbox/issues/7084) - Fix KeyError exception when editing access VLAN on an interface +* [#7089](https://github.com/netbox-community/netbox/issues/7089) - Fix ContentTypeFilterSet not filtering on q filter +* [#7093](https://github.com/netbox-community/netbox/issues/7093) - Multi-select custom field filters should employ exact match +* [#7096](https://github.com/netbox-community/netbox/issues/7096) - Home links should honor `BASE_PATH` configuration +* [#7101](https://github.com/netbox-community/netbox/issues/7101) - Enforce `MAX_PAGE_SIZE` for table and REST API pagination --- diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 7fcd2a6b2..367980ac4 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -129,7 +129,7 @@ class InterfaceCommonForm(forms.Form): super().clean() parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine' - tagged_vlans = self.cleaned_data['tagged_vlans'] + tagged_vlans = self.cleaned_data.get('tagged_vlans') # Untagged interfaces cannot be assigned tagged VLANs if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans: @@ -142,7 +142,7 @@ class InterfaceCommonForm(forms.Form): self.cleaned_data['tagged_vlans'] = [] # Validate tagged VLANs; must be a global VLAN or in the same site - elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED: + elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans: valid_sites = [None, self.cleaned_data[parent_field].site] invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites] diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index aef2046fd..b37aaf40e 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -14,6 +14,7 @@ EXACT_FILTER_TYPES = ( CustomFieldTypeChoices.TYPE_DATE, CustomFieldTypeChoices.TYPE_INTEGER, CustomFieldTypeChoices.TYPE_SELECT, + CustomFieldTypeChoices.TYPE_MULTISELECT, ) @@ -35,7 +36,9 @@ class CustomFieldFilter(django_filters.Filter): self.field_name = f'custom_field_data__{self.field_name}' - if custom_field.type not in EXACT_FILTER_TYPES: + if custom_field.type == CustomFieldTypeChoices.TYPE_MULTISELECT: + self.lookup_expr = 'has_key' + elif custom_field.type not in EXACT_FILTER_TYPES: if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_LOOSE: self.lookup_expr = 'icontains' diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 2a6ae088c..25fd32f0d 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -367,7 +367,19 @@ class JobResultFilterSet(BaseFilterSet): # class ContentTypeFilterSet(django_filters.FilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) class Meta: model = ContentType fields = ['id', 'app_label', 'model'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(app_label__icontains=value) | + Q(model__icontains=value) + ) diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index b1bf10be6..c2a2da3dc 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -681,7 +681,12 @@ class CustomFieldFilterTest(TestCase): cf.content_types.set([obj_type]) # Selection filtering - cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_URL, choices=['Foo', 'Bar', 'Baz']) + cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_SELECT, choices=['Foo', 'Bar', 'Baz']) + cf.save() + cf.content_types.set([obj_type]) + + # Multiselect filtering + cf = CustomField(name='cf9', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=['A', 'AA', 'B', 'C']) cf.save() cf.content_types.set([obj_type]) @@ -695,6 +700,7 @@ class CustomFieldFilterTest(TestCase): 'cf6': 'http://foo.example.com/', 'cf7': 'http://foo.example.com/', 'cf8': 'Foo', + 'cf9': ['A', 'B'], }), Site(name='Site 2', slug='site-2', custom_field_data={ 'cf1': 200, @@ -705,9 +711,9 @@ class CustomFieldFilterTest(TestCase): 'cf6': 'http://bar.example.com/', 'cf7': 'http://bar.example.com/', 'cf8': 'Bar', + 'cf9': ['AA', 'B'], }), - Site(name='Site 3', slug='site-3', custom_field_data={ - }), + Site(name='Site 3', slug='site-3'), ]) def test_filter_integer(self): @@ -730,3 +736,10 @@ class CustomFieldFilterTest(TestCase): def test_filter_select(self): self.assertEqual(self.filterset({'cf_cf8': 'Foo'}, self.queryset).qs.count(), 1) + self.assertEqual(self.filterset({'cf_cf8': 'Bar'}, self.queryset).qs.count(), 1) + self.assertEqual(self.filterset({'cf_cf8': 'Baz'}, self.queryset).qs.count(), 0) + + def test_filter_multiselect(self): + self.assertEqual(self.filterset({'cf_cf9': 'A'}, self.queryset).qs.count(), 1) + self.assertEqual(self.filterset({'cf_cf9': 'B'}, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset({'cf_cf9': 'C'}, self.queryset).qs.count(), 0) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index eed03885e..5151fdd20 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -404,12 +404,11 @@ class PrefixPrefixesView(generic.ObjectView): bulk_querystring = 'vrf_id={}&within={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix) return { - 'first_available_prefix': instance.get_first_available_prefix(), 'table': table, 'bulk_querystring': bulk_querystring, 'active_tab': 'prefixes', + 'first_available_prefix': instance.get_first_available_prefix(), 'show_available': request.GET.get('show_available', 'true') == 'true', - 'table_config_form': TableConfigForm(table=table), } @@ -421,7 +420,7 @@ class PrefixIPRangesView(generic.ObjectView): # Find all IPRanges belonging to this Prefix ip_ranges = instance.get_child_ranges().restrict(request.user, 'view').prefetch_related('vrf') - table = tables.IPRangeTable(ip_ranges) + table = tables.IPRangeTable(ip_ranges, user=request.user) if request.user.has_perm('ipam.change_iprange') or request.user.has_perm('ipam.delete_iprange'): table.columns.show('pk') paginate_table(table, request) @@ -449,7 +448,7 @@ class PrefixIPAddressesView(generic.ObjectView): if request.GET.get('show_available', 'true') == 'true': ipaddresses = add_available_ipaddresses(instance.prefix, ipaddresses, instance.is_pool) - table = tables.IPAddressTable(ipaddresses) + table = tables.IPAddressTable(ipaddresses, user=request.user) if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'): table.columns.show('pk') paginate_table(table, request) @@ -457,10 +456,10 @@ class PrefixIPAddressesView(generic.ObjectView): bulk_querystring = 'vrf_id={}&parent={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix) return { - 'first_available_ip': instance.get_first_available_ip(), 'table': table, 'bulk_querystring': bulk_querystring, 'active_tab': 'ip-addresses', + 'first_available_ip': instance.get_first_available_ip(), 'show_available': request.GET.get('show_available', 'true') == 'true', } diff --git a/netbox/netbox/api/pagination.py b/netbox/netbox/api/pagination.py index d489ce951..77af755ce 100644 --- a/netbox/netbox/api/pagination.py +++ b/netbox/netbox/api/pagination.py @@ -34,23 +34,13 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination): return list(queryset[self.offset:]) def get_limit(self, request): + limit = super().get_limit(request) - if self.limit_query_param: - try: - limit = int(request.query_params[self.limit_query_param]) - if limit < 0: - raise ValueError() - # Enforce maximum page size, if defined - if settings.MAX_PAGE_SIZE: - if limit == 0: - return settings.MAX_PAGE_SIZE - else: - return min(limit, settings.MAX_PAGE_SIZE) - return limit - except (KeyError, ValueError): - pass + # Enforce maximum page size + if settings.MAX_PAGE_SIZE: + limit = min(limit, settings.MAX_PAGE_SIZE) - return self.default_limit + return limit def get_next_link(self): diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index b05097f3b..4f0e8956e 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -560,6 +560,10 @@ RQ_QUEUES = { # # Pagination +if MAX_PAGE_SIZE and PAGINATE_COUNT > MAX_PAGE_SIZE: + raise ImproperlyConfigured( + f"PAGINATE_COUNT ({PAGINATE_COUNT}) must be less than or equal to MAX_PAGE_SIZE ({MAX_PAGE_SIZE}), if set." + ) PER_PAGE_DEFAULTS = [ 25, 50, 100, 250, 500, 1000 ] diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index b00999b15..aafb2f3d8 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -181,7 +181,6 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): 'table': table, 'permissions': permissions, 'action_buttons': self.action_buttons, - 'table_config_form': TableConfigForm(table=table), 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None, } context.update(self.extra_context()) diff --git a/netbox/templates/500.html b/netbox/templates/500.html index 427345e56..097699ffc 100644 --- a/netbox/templates/500.html +++ b/netbox/templates/500.html @@ -4,7 +4,7 @@
- If further assistance is required, please post to the NetBox mailing list. + If further assistance is required, please post to the NetBox discussion forum on GitHub.
{{ filename }}
exists in the static root directory and is readable by the HTTP process.
- Click here to attempt loading NetBox again.
+Click here to attempt loading NetBox again.