Merge branch 'develop' of github.com:netbox-community/netbox into 7090-fix-cablebulkedit-length-field

This commit is contained in:
Stefan de Kooter 2021-08-31 21:18:35 +02:00
commit 4618cc2b22
No known key found for this signature in database
GPG Key ID: 340B95E679E5217B
22 changed files with 108 additions and 58 deletions

View File

@ -1,11 +1,20 @@
# NetBox v3.0 # NetBox v3.0
## v2.11.12 (2021-08-23) ## v3.0.1 (FUTURE)
### Bug Fixes ### Bug Fixes
* [#7070](https://github.com/netbox-community/netbox/issues/7070) - Fix exception when filtering by prefix max length in UI * [#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 * [#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
--- ---

View File

@ -129,7 +129,7 @@ class InterfaceCommonForm(forms.Form):
super().clean() super().clean()
parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine' 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 # Untagged interfaces cannot be assigned tagged VLANs
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and 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'] = [] self.cleaned_data['tagged_vlans'] = []
# Validate tagged VLANs; must be a global VLAN or in the same site # 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] 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] invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]

View File

@ -14,6 +14,7 @@ EXACT_FILTER_TYPES = (
CustomFieldTypeChoices.TYPE_DATE, CustomFieldTypeChoices.TYPE_DATE,
CustomFieldTypeChoices.TYPE_INTEGER, CustomFieldTypeChoices.TYPE_INTEGER,
CustomFieldTypeChoices.TYPE_SELECT, 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}' 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: if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_LOOSE:
self.lookup_expr = 'icontains' self.lookup_expr = 'icontains'

View File

@ -367,7 +367,19 @@ class JobResultFilterSet(BaseFilterSet):
# #
class ContentTypeFilterSet(django_filters.FilterSet): class ContentTypeFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
class Meta: class Meta:
model = ContentType model = ContentType
fields = ['id', 'app_label', 'model'] 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)
)

View File

@ -681,7 +681,12 @@ class CustomFieldFilterTest(TestCase):
cf.content_types.set([obj_type]) cf.content_types.set([obj_type])
# Selection filtering # 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.save()
cf.content_types.set([obj_type]) cf.content_types.set([obj_type])
@ -695,6 +700,7 @@ class CustomFieldFilterTest(TestCase):
'cf6': 'http://foo.example.com/', 'cf6': 'http://foo.example.com/',
'cf7': 'http://foo.example.com/', 'cf7': 'http://foo.example.com/',
'cf8': 'Foo', 'cf8': 'Foo',
'cf9': ['A', 'B'],
}), }),
Site(name='Site 2', slug='site-2', custom_field_data={ Site(name='Site 2', slug='site-2', custom_field_data={
'cf1': 200, 'cf1': 200,
@ -705,9 +711,9 @@ class CustomFieldFilterTest(TestCase):
'cf6': 'http://bar.example.com/', 'cf6': 'http://bar.example.com/',
'cf7': 'http://bar.example.com/', 'cf7': 'http://bar.example.com/',
'cf8': 'Bar', '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): def test_filter_integer(self):
@ -730,3 +736,10 @@ class CustomFieldFilterTest(TestCase):
def test_filter_select(self): def test_filter_select(self):
self.assertEqual(self.filterset({'cf_cf8': 'Foo'}, self.queryset).qs.count(), 1) 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)

View File

@ -404,12 +404,11 @@ class PrefixPrefixesView(generic.ObjectView):
bulk_querystring = 'vrf_id={}&within={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix) bulk_querystring = 'vrf_id={}&within={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
return { return {
'first_available_prefix': instance.get_first_available_prefix(),
'table': table, 'table': table,
'bulk_querystring': bulk_querystring, 'bulk_querystring': bulk_querystring,
'active_tab': 'prefixes', 'active_tab': 'prefixes',
'first_available_prefix': instance.get_first_available_prefix(),
'show_available': request.GET.get('show_available', 'true') == 'true', '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 # Find all IPRanges belonging to this Prefix
ip_ranges = instance.get_child_ranges().restrict(request.user, 'view').prefetch_related('vrf') 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'): if request.user.has_perm('ipam.change_iprange') or request.user.has_perm('ipam.delete_iprange'):
table.columns.show('pk') table.columns.show('pk')
paginate_table(table, request) paginate_table(table, request)
@ -449,7 +448,7 @@ class PrefixIPAddressesView(generic.ObjectView):
if request.GET.get('show_available', 'true') == 'true': if request.GET.get('show_available', 'true') == 'true':
ipaddresses = add_available_ipaddresses(instance.prefix, ipaddresses, instance.is_pool) 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'): if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
table.columns.show('pk') table.columns.show('pk')
paginate_table(table, request) 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) bulk_querystring = 'vrf_id={}&parent={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
return { return {
'first_available_ip': instance.get_first_available_ip(),
'table': table, 'table': table,
'bulk_querystring': bulk_querystring, 'bulk_querystring': bulk_querystring,
'active_tab': 'ip-addresses', 'active_tab': 'ip-addresses',
'first_available_ip': instance.get_first_available_ip(),
'show_available': request.GET.get('show_available', 'true') == 'true', 'show_available': request.GET.get('show_available', 'true') == 'true',
} }

View File

@ -34,23 +34,13 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
return list(queryset[self.offset:]) return list(queryset[self.offset:])
def get_limit(self, request): def get_limit(self, request):
limit = super().get_limit(request)
if self.limit_query_param: # Enforce maximum page size
try: if settings.MAX_PAGE_SIZE:
limit = int(request.query_params[self.limit_query_param]) limit = min(limit, settings.MAX_PAGE_SIZE)
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
return self.default_limit return limit
def get_next_link(self): def get_next_link(self):

View File

@ -560,6 +560,10 @@ RQ_QUEUES = {
# #
# Pagination # 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 = [ PER_PAGE_DEFAULTS = [
25, 50, 100, 250, 500, 1000 25, 50, 100, 250, 500, 1000
] ]

View File

@ -181,7 +181,6 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
'table': table, 'table': table,
'permissions': permissions, 'permissions': permissions,
'action_buttons': self.action_buttons, '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, 'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
} }
context.update(self.extra_context()) context.update(self.extra_context())

View File

@ -4,7 +4,7 @@
<head> <head>
<title>Server Error</title> <title>Server Error</title>
<link rel="stylesheet" href="{% static 'netbox.css'%}" /> <link rel="stylesheet" href="{% static 'netbox-light.css'%}" />
<meta charset="UTF-8"> <meta charset="UTF-8">
</head> </head>
@ -12,7 +12,7 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col col-md-6 offset-md-3"> <div class="col col-md-6 offset-md-3">
<div class="card bg-danger mt-5"> <div class="card border-danger mt-5">
<h5 class="card-header"> <h5 class="card-header">
<i class="mdi mdi-alert"></i> Server Error <i class="mdi mdi-alert"></i> Server Error
</h5> </h5>
@ -32,7 +32,7 @@
Python version: {{ python_version }} Python version: {{ python_version }}
NetBox version: {{ netbox_version }}</pre> NetBox version: {{ netbox_version }}</pre>
<p> <p>
If further assistance is required, please post to the <a href="https://groups.google.com/g/netbox-discuss">NetBox mailing list</a>. If further assistance is required, please post to the <a href="https://github.com/netbox-community/netbox/discussions">NetBox discussion forum</a> on GitHub.
</p> </p>
<div class="text-end"> <div class="text-end">
<a href="{% url 'home' %}" class="btn btn-primary">Home Page</a> <a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>

View File

@ -7,12 +7,12 @@
{# Brand #} {# Brand #}
{# Full Logo #} {# Full Logo #}
<a class="sidenav-brand" href="/"> <a class="sidenav-brand" href="{% url 'home' %}">
<img src="{% static 'netbox_logo.svg' %}" height="48" class="sidenav-brand-img" alt="NetBox Logo"> <img src="{% static 'netbox_logo.svg' %}" height="48" class="sidenav-brand-img" alt="NetBox Logo">
</a> </a>
{# Icon Logo #} {# Icon Logo #}
<a class="sidenav-brand-icon" href="/"> <a class="sidenav-brand-icon" href="{% url 'home' %}">
<img src="{% static 'netbox_icon.svg' %}" height="32" class="sidenav-brand-img" alt="NetBox Logo"> <img src="{% static 'netbox_icon.svg' %}" height="32" class="sidenav-brand-img" alt="NetBox Logo">
</a> </a>

View File

@ -1,4 +1,6 @@
{% extends 'ipam/prefix/base.html' %} {% extends 'ipam/prefix/base.html' %}
{% load helpers %}
{% load static %}
{% block extra_controls %} {% block extra_controls %}
{% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %} {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %}
@ -11,7 +13,13 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
{% include 'inc/table_controls.html' with table_modal="IPAddressTable_config" %}
{% include 'utilities/obj_table.html' with heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %} {% include 'utilities/obj_table.html' with heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
</div> </div>
</div> </div>
{% table_config_form table table_name="IPAddressTable" %}
{% endblock %}
{% block javascript %}
<script src="{% static 'js/tableconfig.js' %}"></script>
{% endblock %} {% endblock %}

View File

@ -1,10 +1,17 @@
{% extends 'ipam/prefix/base.html' %} {% extends 'ipam/prefix/base.html' %}
{% load helpers %}
{% load static %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
{% include 'inc/table_controls.html' with table_modal="IPRangeTable_config" %}
{% include 'utilities/obj_table.html' with heading='Child IP Ranges' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %} {% include 'utilities/obj_table.html' with heading='Child IP Ranges' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %}
</div> </div>
</div> </div>
{% table_config_form table table_name="IPRangeTable" %}
{% endblock %}
{% block javascript %}
<script src="{% static 'js/tableconfig.js' %}"></script>
{% endblock %} {% endblock %}

View File

@ -2,20 +2,17 @@
{% load helpers %} {% load helpers %}
{% load static %} {% load static %}
{% block buttons %} {% block extra_controls %}
{% include 'ipam/inc/toggle_available.html' %} {% include 'ipam/inc/toggle_available.html' %}
{% if request.user.is_authenticated and table_config_form %}
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#PrefixDetailTable_config" title="Configure table"><i class="mdi mdi-cog"></i> Configure</button>
{% endif %}
{% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %} {% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %}
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ object.vrf.pk }}&site={{ object.site.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-success"> <a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ object.vrf.pk }}&site={{ object.site.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-sm btn-success">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Child Prefix <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Child Prefix
</a> </a>
{% endif %} {% endif %}
{% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %} {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-success"> <a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-sm btn-success">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> <span class="mdi mdi-plus-thick" aria-hidden="true"></span>
Add an IP Address Add Child IP Address
</a> </a>
{% endif %} {% endif %}
{{ block.super }} {{ block.super }}
@ -24,12 +21,13 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
{% include 'inc/table_controls.html' with table_modal="PrefixDetailTable_config" %}
{% include 'utilities/obj_table.html' with heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %} {% include 'utilities/obj_table.html' with heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %}
</div> </div>
</div> </div>
{% table_config_form prefix_table table_name="PrefixDetailTable" %} {% table_config_form table table_name="PrefixDetailTable" %}
{% endblock %} {% endblock %}
{% block javascript %} {% block javascript %}
<script src="{% static 'js/tableconfig.js' %}"></script> <script src="{% static 'js/tableconfig.js' %}"></script>
{% endblock %} {% endblock %}

View File

@ -42,7 +42,7 @@
The file <code>{{ filename }}</code> exists in the static root directory and is readable by the HTTP process. The file <code>{{ filename }}</code> exists in the static root directory and is readable by the HTTP process.
</li> </li>
</ul> </ul>
<p>Click <a href="/">here</a> to attempt loading NetBox again.</p> <p>Click <a href="{% url 'home' %}">here</a> to attempt loading NetBox again.</p>
</div> </div>
</body> </body>
</html> </html>

View File

@ -9,5 +9,5 @@
{% block title %}{% if name %}{{ name }} | {% endif %}NetBox REST API{% endblock %} {% block title %}{% if name %}{{ name }} | {% endif %}NetBox REST API{% endblock %}
{% block branding %} {% block branding %}
<a class="navbar-brand" href="/{{ settings.BASE_PATH }}">NetBox</a> <a class="navbar-brand" href="{% url 'home' %}">NetBox</a>
{% endblock branding %} {% endblock branding %}

View File

@ -7,11 +7,11 @@
<h5 class="modal-title">Table Configuration</h5> <h5 class="modal-title">Table Configuration</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form class="form-horizontal userconfigform" data-config-root="tables.{{ table_config_form.table_name }}"> <form class="form-horizontal userconfigform" data-config-root="tables.{{ form.table_name }}">
<div class="modal-body row"> <div class="modal-body row">
<div class="col-5 text-center"> <div class="col-5 text-center">
{{ table_config_form.available_columns.label }} {{ form.available_columns.label }}
{{ table_config_form.available_columns }} {{ form.available_columns }}
</div> </div>
<div class="col-2 d-flex align-items-center"> <div class="col-2 d-flex align-items-center">
<div> <div>
@ -24,8 +24,8 @@
</div> </div>
</div> </div>
<div class="col-5 text-center"> <div class="col-5 text-center">
{{ table_config_form.columns.label }} {{ form.columns.label }}
{{ table_config_form.columns }} {{ form.columns }}
<a class="btn btn-primary btn-sm mt-2" id="move-option-up" data-target="id_columns"> <a class="btn btn-primary btn-sm mt-2" id="move-option-up" data-target="id_columns">
<i class="mdi mdi-arrow-up-bold"></i> Move Up <i class="mdi mdi-arrow-up-bold"></i> Move Up
</a> </a>

View File

@ -131,7 +131,7 @@
<th scope="row"><i class="mdi mdi-chip"></i> Memory</th> <th scope="row"><i class="mdi mdi-chip"></i> Memory</th>
<td> <td>
{% if object.memory %} {% if object.memory %}
{{ object.memory|humanize_megabytes }} MB {{ object.memory|humanize_megabytes }}
{% else %} {% else %}
<span class="text-muted">&mdash;</span> <span class="text-muted">&mdash;</span>
{% endif %} {% endif %}

View File

@ -21,8 +21,8 @@ class Command(_Command):
raise CommandError( raise CommandError(
"This command is available for development purposes only. It will\n" "This command is available for development purposes only. It will\n"
"NOT resolve any issues with missing or unapplied migrations. For assistance,\n" "NOT resolve any issues with missing or unapplied migrations. For assistance,\n"
"please post to the NetBox mailing list:\n" "please post to the NetBox discussion forum on GitHub:\n"
" https://groups.google.com/g/netbox-discuss" " https://github.com/netbox-community/netbox/discussions"
) )
super().handle(*args, **kwargs) super().handle(*args, **kwargs)

View File

@ -49,21 +49,25 @@ class EnhancedPage(Page):
def get_paginate_count(request): def get_paginate_count(request):
""" """
Determine the length of a page, using the following in order: Determine the desired length of a page, using the following in order:
1. per_page URL query parameter 1. per_page URL query parameter
2. Saved user preference 2. Saved user preference
3. PAGINATE_COUNT global setting. 3. PAGINATE_COUNT global setting.
Return the lesser of the calculated value and MAX_PAGE_SIZE.
""" """
if 'per_page' in request.GET: if 'per_page' in request.GET:
try: try:
per_page = int(request.GET.get('per_page')) per_page = int(request.GET.get('per_page'))
if request.user.is_authenticated: if request.user.is_authenticated:
request.user.config.set('pagination.per_page', per_page, commit=True) request.user.config.set('pagination.per_page', per_page, commit=True)
return per_page return min(per_page, settings.MAX_PAGE_SIZE)
except ValueError: except ValueError:
pass pass
if request.user.is_authenticated: if request.user.is_authenticated:
return request.user.config.get('pagination.per_page', settings.PAGINATE_COUNT) per_page = request.user.config.get('pagination.per_page', settings.PAGINATE_COUNT)
return settings.PAGINATE_COUNT return min(per_page, settings.MAX_PAGE_SIZE)
return min(settings.PAGINATE_COUNT, settings.MAX_PAGE_SIZE)

View File

@ -237,9 +237,13 @@ class ContentTypeColumn(tables.Column):
Display a ContentType instance. Display a ContentType instance.
""" """
def render(self, value): def render(self, value):
if value is None:
return None
return content_type_name(value) return content_type_name(value)
def value(self, value): def value(self, value):
if value is None:
return None
return f"{value.app_label}.{value.model}" return f"{value.app_label}.{value.model}"

View File

@ -401,7 +401,7 @@ def badge(value, bg_class='secondary', show_empty=False):
def table_config_form(table, table_name=None): def table_config_form(table, table_name=None):
return { return {
'table_name': table_name or table.__class__.__name__, 'table_name': table_name or table.__class__.__name__,
'table_config_form': TableConfigForm(table=table), 'form': TableConfigForm(table=table),
} }