From b5455ed882c47273064933f0120b0e368af93a1f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Dec 2019 15:04:56 -0500 Subject: [PATCH 01/26] Closes #3461: Fail gracefully on custom link rendering exception --- docs/release-notes/version-2.6.md | 1 + netbox/extras/templatetags/custom_links.py | 36 +++++++++++++--------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 548492438..33b59b5d9 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -2,6 +2,7 @@ ## Enhancements +* [#3461](https://github.com/netbox-community/netbox/issues/3461) - Fail gracefully on custom link rendering exception * [#3705](https://github.com/netbox-community/netbox/issues/3705) - Provide request context when executing custom scripts * [#3762](https://github.com/netbox-community/netbox/issues/3762) - Add date/time picker widgets * [#3788](https://github.com/netbox-community/netbox/issues/3788) - Enable partial search for inventory items diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py index 0d2239ecc..8c927a0ae 100644 --- a/netbox/extras/templatetags/custom_links.py +++ b/netbox/extras/templatetags/custom_links.py @@ -46,12 +46,17 @@ def custom_links(obj): # Add non-grouped links else: - text_rendered = render_jinja2(cl.text, context) - if text_rendered: - link_target = ' target="_blank"' if cl.new_window else '' - template_code += LINK_BUTTON.format( - cl.url, link_target, cl.button_class, text_rendered - ) + try: + text_rendered = render_jinja2(cl.text, context) + if text_rendered: + link_rendered = render_jinja2(cl.url, context) + link_target = ' target="_blank"' if cl.new_window else '' + template_code += LINK_BUTTON.format( + link_rendered, link_target, cl.button_class, text_rendered + ) + except Exception as e: + template_code += '' \ + ' {}\n'.format(e, cl.name) # Add grouped links to template for group, links in group_names.items(): @@ -59,11 +64,17 @@ def custom_links(obj): links_rendered = [] for cl in links: - text_rendered = render_jinja2(cl.text, context) - if text_rendered: - link_target = ' target="_blank"' if cl.new_window else '' + try: + text_rendered = render_jinja2(cl.text, context) + if text_rendered: + link_target = ' target="_blank"' if cl.new_window else '' + links_rendered.append( + GROUP_LINK.format(cl.url, link_target, cl.text) + ) + except Exception as e: links_rendered.append( - GROUP_LINK.format(cl.url, link_target, cl.text) + '
  • ' + ' {}
  • '.format(e, cl.name) ) if links_rendered: @@ -71,7 +82,4 @@ def custom_links(obj): links[0].button_class, group, ''.join(links_rendered) ) - # Render template - rendered = render_jinja2(template_code, context) - - return mark_safe(rendered) + return mark_safe(template_code) From 0cf94cff162aed205a40e53201d0455b3c250fb8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Dec 2019 15:24:00 -0500 Subject: [PATCH 02/26] Closes #3062: Add assigned_to_interface filter for IP addresses --- docs/release-notes/version-2.6.md | 1 + netbox/ipam/filters.py | 7 +++++++ netbox/ipam/forms.py | 10 +++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 33b59b5d9..38e3de632 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -2,6 +2,7 @@ ## Enhancements +* [#3062](https://github.com/netbox-community/netbox/issues/3062) - Add `assigned_to_interface` filter for IP addresses * [#3461](https://github.com/netbox-community/netbox/issues/3461) - Fail gracefully on custom link rendering exception * [#3705](https://github.com/netbox-community/netbox/issues/3705) - Provide request context when executing custom scripts * [#3762](https://github.com/netbox-community/netbox/issues/3762) - Add date/time picker widgets diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index c54ba2f62..1f8fd2caf 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -309,6 +309,10 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt queryset=Interface.objects.all(), label='Interface (ID)', ) + assigned_to_interface = django_filters.BooleanFilter( + method='_assigned_to_interface', + label='Is assigned to an interface', + ) status = django_filters.MultipleChoiceFilter( choices=IPADDRESS_STATUS_CHOICES, null_value=None @@ -366,6 +370,9 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt except Device.DoesNotExist: return queryset.none() + def _assigned_to_interface(self, queryset, name, value): + return queryset.exclude(interface__isnull=value) + class VLANGroupFilter(NameSlugSearchFilterSet): site_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index a95967b20..44056653b 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -938,7 +938,8 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = IPAddress field_order = [ - 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'tenant_group', 'tenant', + 'q', 'parent', 'family', 'mask_length', 'vrf_id', 'status', 'role', 'assigned_to_interface', 'tenant_group', + 'tenant', ] q = forms.CharField( required=False, @@ -984,6 +985,13 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo required=False, widget=StaticSelect2Multiple() ) + assigned_to_interface = forms.NullBooleanField( + required=False, + label='Assigned to an interface', + widget=StaticSelect2( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) # From 8c7b0cf6703b2719067a313a017b75e20ee4499d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Dec 2019 16:11:47 -0500 Subject: [PATCH 03/26] Close #2892: Extend admin UI to allow deleting old report results --- docs/release-notes/version-2.6.md | 1 + netbox/extras/admin.py | 35 ++++++++++++++++++++++++++++++- netbox/extras/models.py | 7 +++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 38e3de632..36afbc3e2 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -2,6 +2,7 @@ ## Enhancements +* [#2892](https://github.com/netbox-community/netbox/issues/2892) - Extend admin UI to allow deleting old report results * [#3062](https://github.com/netbox-community/netbox/issues/3062) - Add `assigned_to_interface` filter for IP addresses * [#3461](https://github.com/netbox-community/netbox/issues/3461) - Fail gracefully on custom link rendering exception * [#3705](https://github.com/netbox-community/netbox/issues/3705) - Provide request context when executing custom scripts diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index f99848b1b..ee21b4f5d 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -3,7 +3,10 @@ from django.contrib import admin from netbox.admin import admin_site from utilities.forms import LaxURLField -from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, TopologyMap, Webhook +from .models import ( + CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, TopologyMap, Webhook, +) +from .reports import get_report def order_content_types(field): @@ -166,6 +169,36 @@ class ExportTemplateAdmin(admin.ModelAdmin): form = ExportTemplateForm +# +# Reports +# + +@admin.register(ReportResult, site=admin_site) +class ReportResultAdmin(admin.ModelAdmin): + list_display = [ + 'report', 'active', 'created', 'user', 'passing', + ] + fields = [ + 'report', 'user', 'passing', 'data', + ] + list_filter = [ + 'failed', + ] + readonly_fields = fields + + def has_add_permission(self, request): + return False + + def active(self, obj): + module, report_name = obj.report.split('.') + return True if get_report(module, report_name) else False + active.boolean = True + + def passing(self, obj): + return not obj.failed + passing.boolean = True + + # # Topology maps # diff --git a/netbox/extras/models.py b/netbox/extras/models.py index e9ca396cc..038576b63 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -915,6 +915,13 @@ class ReportResult(models.Model): class Meta: ordering = ['report'] + def __str__(self): + return "{} {} at {}".format( + self.report, + "passed" if not self.failed else "failed", + self.created + ) + # # Change logging From 53625e0deabd807e324d8478d9e535c54eec19f3 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Wed, 1 Jan 2020 15:54:00 +0000 Subject: [PATCH 04/26] Fixes #3812: Only preload selected options for API-based select --- docs/release-notes/version-2.6.md | 1 + netbox/utilities/forms.py | 2 ++ netbox/utilities/templates/widgets/select_api.html | 5 +++++ 3 files changed, 8 insertions(+) create mode 100644 netbox/utilities/templates/widgets/select_api.html diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 548492438..a3bd9336a 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -5,6 +5,7 @@ * [#3705](https://github.com/netbox-community/netbox/issues/3705) - Provide request context when executing custom scripts * [#3762](https://github.com/netbox-community/netbox/issues/3762) - Add date/time picker widgets * [#3788](https://github.com/netbox-community/netbox/issues/3788) - Enable partial search for inventory items +* [#3812](https://github.com/netbox-community/netbox/issues/3812) - Optimize size of pages containing a dynamic selection field ## Bug Fixes diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 44d76186d..baac01e4b 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -285,6 +285,8 @@ class APISelect(SelectWithDisabled): name of the query param and the value if the query param's value. :param null_option: If true, include the static null option in the selection list. """ + # Only preload the selected option; new options are dynamically displayed and added via the API + template_name = 'widgets/select_api.html' def __init__( self, diff --git a/netbox/utilities/templates/widgets/select_api.html b/netbox/utilities/templates/widgets/select_api.html new file mode 100644 index 000000000..0146a31ff --- /dev/null +++ b/netbox/utilities/templates/widgets/select_api.html @@ -0,0 +1,5 @@ + From 242ae9eb91de3756fe20fa0e68f1c8c0442f08dc Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Wed, 1 Jan 2020 16:04:08 +0000 Subject: [PATCH 05/26] Comment clarification --- netbox/utilities/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index baac01e4b..eeee719ae 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -285,7 +285,7 @@ class APISelect(SelectWithDisabled): name of the query param and the value if the query param's value. :param null_option: If true, include the static null option in the selection list. """ - # Only preload the selected option; new options are dynamically displayed and added via the API + # Only preload the selected option(s); new options are dynamically displayed and added via the API template_name = 'widgets/select_api.html' def __init__( From 57ea73db469b64543ee4691c85cfb98bb051493b Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Wed, 1 Jan 2020 21:01:08 +0000 Subject: [PATCH 06/26] Turn off Select2 static width calculation --- netbox/project-static/js/forms.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 9303e3013..8eff1e00a 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -103,14 +103,16 @@ $(document).ready(function() { placeholder: "---------", theme: "bootstrap", templateResult: colorPickerClassCopy, - templateSelection: colorPickerClassCopy + templateSelection: colorPickerClassCopy, + width: "off" }); // Static choice selection $('.netbox-select2-static').select2({ allowClear: true, placeholder: "---------", - theme: "bootstrap" + theme: "bootstrap", + width: "off" }); // API backed selection @@ -120,6 +122,7 @@ $(document).ready(function() { allowClear: true, placeholder: "---------", theme: "bootstrap", + width: "off", ajax: { delay: 500, @@ -299,7 +302,7 @@ $(document).ready(function() { multiple: true, allowClear: true, placeholder: "Tags", - + width: "off", ajax: { delay: 250, url: netbox_api_path + "extras/tags/", From cfa078c9291a06830025f984a79e96430cbee0fd Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Wed, 1 Jan 2020 21:14:38 +0000 Subject: [PATCH 07/26] Added theme to select2 of tags --- netbox/project-static/js/forms.js | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 8eff1e00a..55f9afbd5 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -302,6 +302,7 @@ $(document).ready(function() { multiple: true, allowClear: true, placeholder: "Tags", + theme: "bootstrap", width: "off", ajax: { delay: 250, From 7dc0591e3e6febfaaabe9fb0c52a0534eb80da5d Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Wed, 1 Jan 2020 22:38:02 +0000 Subject: [PATCH 08/26] Note about the group existance --- docs/installation/4-ldap.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/installation/4-ldap.md b/docs/installation/4-ldap.md index 32623439a..a41400808 100644 --- a/docs/installation/4-ldap.md +++ b/docs/installation/4-ldap.md @@ -80,6 +80,7 @@ AUTH_LDAP_USER_ATTR_MAP = { ``` # User Groups for Permissions + !!! info When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. You will also need to modify the import line to use `NestedGroupOfNamesType` instead of `GroupOfNamesType` . @@ -117,6 +118,9 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600 * `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions. * `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions. +!!! warning + Authentication will fail if the groups (the distinguished names) do not exist in the LDAP directory. + # Troubleshooting LDAP `supervisorctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/supervisor/`. From c75795ceda9a09d797db94af31503bfe03d2aa66 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Wed, 1 Jan 2020 23:28:20 +0000 Subject: [PATCH 09/26] Ability to move inventory items between devices --- docs/release-notes/version-2.6.md | 1 + netbox/dcim/forms.py | 24 ++++++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 548492438..91a795be5 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -3,6 +3,7 @@ ## Enhancements * [#3705](https://github.com/netbox-community/netbox/issues/3705) - Provide request context when executing custom scripts +* [#2288](https://github.com/netbox-community/netbox/issues/2288) - Ability to move inventory items between devices * [#3762](https://github.com/netbox-community/netbox/issues/3762) - Add date/time picker widgets * [#3788](https://github.com/netbox-community/netbox/issues/3788) - Enable partial search for inventory items diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 1774fc986..6e6cb8af8 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3285,9 +3285,12 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm): class Meta: model = InventoryItem fields = [ - 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags', + 'name', 'device', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags', ] widgets = { + 'device': APISelect( + api_url="/api/dcim/devices/" + ), 'manufacturer': APISelect( api_url="/api/dcim/manufacturers/" ) @@ -3323,9 +3326,19 @@ class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm): queryset=InventoryItem.objects.all(), widget=forms.MultipleHiddenInput() ) + device = forms.ModelChoiceField( + queryset=Device.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/devices/" + ) + ) manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), - required=False + required=False, + widget=APISelect( + api_url="/api/dcim/manufacturers/" + ) ) part_id = forms.CharField( max_length=50, @@ -3356,11 +3369,14 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): manufacturer = FilterChoiceField( queryset=Manufacturer.objects.all(), to_field_name='slug', - null_label='-- None --' + widget=APISelect( + api_url="/api/dcim/manufacturers/", + value_field="slug", + ) ) discovered = forms.NullBooleanField( required=False, - widget=forms.Select( + widget=StaticSelect2( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) From ca131e5b2a6ca362511079a95aa4e01f85fce4ad Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Wed, 1 Jan 2020 23:46:51 +0000 Subject: [PATCH 10/26] Select2 for custom fields --- netbox/extras/forms.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 5223c3c05..4f7f57fff 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -52,7 +52,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F else: initial = None field = forms.NullBooleanField( - required=cf.required, initial=initial, widget=forms.Select(choices=choices) + required=cf.required, initial=initial, widget=StaticSelect2(choices=choices) ) # Date @@ -71,7 +71,9 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F default_choice = cf.choices.get(value=initial).pk except ObjectDoesNotExist: pass - field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice) + field = forms.TypedChoiceField( + choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2() + ) # URL elif cf.type == CF_TYPE_URL: From dd854484512744fef42b75714598250e023460b4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Jan 2020 09:26:38 -0500 Subject: [PATCH 11/26] Fixes #3822: Fix exception when editing a device bay (regression from #3596) --- docs/release-notes/version-2.6.md | 1 + netbox/dcim/models.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 36afbc3e2..4e26ffab4 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -17,6 +17,7 @@ * [#3780](https://github.com/netbox-community/netbox/issues/3780) - Fix AttributeError exception in API docs * [#3809](https://github.com/netbox-community/netbox/issues/3809) - Filter platform by manufacturer when editing devices * [#3811](https://github.com/netbox-community/netbox/issues/3811) - Fix filtering of racks by group on device list +* [#3822](https://github.com/netbox-community/netbox/issues/3822) - Fix exception when editing a device bay (regression from #3596) --- diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index db88901b6..6d3de7c0b 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2597,7 +2597,7 @@ class DeviceBay(ComponentModel): # Check that the installed device is not already installed elsewhere if self.installed_device: current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first() - if current_bay: + if current_bay != self: raise ValidationError({ 'installed_device': "Cannot install the specified device; device is already installed in {}".format( current_bay From 395f23e1d3f50015b0dfe62481ed324e3b1e4025 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Jan 2020 09:47:02 -0500 Subject: [PATCH 12/26] Fix for #3822 --- netbox/dcim/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 6d3de7c0b..8f95fa19a 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2597,7 +2597,7 @@ class DeviceBay(ComponentModel): # Check that the installed device is not already installed elsewhere if self.installed_device: current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first() - if current_bay != self: + if current_bay and current_bay != self: raise ValidationError({ 'installed_device': "Cannot install the specified device; device is already installed in {}".format( current_bay From 523a1388db81f9d151fc939de780e5c795a4b74e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Jan 2020 11:56:29 -0500 Subject: [PATCH 13/26] Correct release notes --- docs/release-notes/version-2.6.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 2d16a72c0..c175a2321 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -2,11 +2,11 @@ ## Enhancements +* [#2233](https://github.com/netbox-community/netbox/issues/2233) - Add ability to move inventory items between devices * [#2892](https://github.com/netbox-community/netbox/issues/2892) - Extend admin UI to allow deleting old report results * [#3062](https://github.com/netbox-community/netbox/issues/3062) - Add `assigned_to_interface` filter for IP addresses * [#3461](https://github.com/netbox-community/netbox/issues/3461) - Fail gracefully on custom link rendering exception * [#3705](https://github.com/netbox-community/netbox/issues/3705) - Provide request context when executing custom scripts -* [#2288](https://github.com/netbox-community/netbox/issues/2288) - Ability to move inventory items between devices * [#3762](https://github.com/netbox-community/netbox/issues/3762) - Add date/time picker widgets * [#3788](https://github.com/netbox-community/netbox/issues/3788) - Enable partial search for inventory items * [#3812](https://github.com/netbox-community/netbox/issues/3812) - Optimize size of pages containing a dynamic selection field From a707204f98c98759b5e129bad44fce30b52a888f Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 2 Jan 2020 17:46:37 +0000 Subject: [PATCH 14/26] Select2 for device field --- netbox/dcim/forms.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 1774fc986..325ed1b73 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3171,9 +3171,12 @@ class CableFilterForm(BootstrapMixin, forms.Form): required=False, widget=ColorSelect() ) - device = forms.CharField( + device = forms.ChoiceField( required=False, - label='Device name' + widget=APISelect( + api_url='/api/dcim/devices/', + value_field='text', + ) ) @@ -3243,9 +3246,12 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): required=False, to_field_name='slug' ) - device = forms.CharField( + device = forms.ChoiceField( required=False, - label='Device name' + widget=APISelect( + api_url='/api/dcim/devices/', + value_field='text', + ) ) @@ -3255,9 +3261,12 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form): required=False, to_field_name='slug' ) - device = forms.CharField( + device = forms.ChoiceField( required=False, - label='Device name' + widget=APISelect( + api_url='/api/dcim/devices/', + value_field='text', + ) ) @@ -3267,9 +3276,12 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): required=False, to_field_name='slug' ) - device = forms.CharField( + device = forms.ChoiceField( required=False, - label='Device name' + widget=APISelect( + api_url='/api/dcim/devices/', + value_field='text', + ) ) @@ -3349,9 +3361,12 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): required=False, label='Search' ) - device = forms.CharField( + device = forms.ChoiceField( required=False, - label='Device name' + widget=APISelect( + api_url='/api/dcim/devices/', + value_field='text', + ) ) manufacturer = FilterChoiceField( queryset=Manufacturer.objects.all(), From 80d1f80b61e7887bc6c2a88244b0bf2f16f84e17 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Jan 2020 13:44:18 -0500 Subject: [PATCH 15/26] Fixes #3827: Allow filtering console/power/interface connections by device ID --- docs/release-notes/version-2.6.md | 1 + netbox/dcim/filters.py | 39 +++++++++++++++++++------------ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index c175a2321..af82fa024 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -10,6 +10,7 @@ * [#3762](https://github.com/netbox-community/netbox/issues/3762) - Add date/time picker widgets * [#3788](https://github.com/netbox-community/netbox/issues/3788) - Enable partial search for inventory items * [#3812](https://github.com/netbox-community/netbox/issues/3812) - Optimize size of pages containing a dynamic selection field +* [#3827](https://github.com/netbox-community/netbox/issues/3827) - Allow filtering console/power/interface connections by device ID ## Bug Fixes diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 111469d98..cf9c04e5c 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -978,9 +978,12 @@ class ConsoleConnectionFilter(django_filters.FilterSet): method='filter_site', label='Site (slug)', ) - device = django_filters.CharFilter( + device_id = MultiValueNumberFilter( + method='filter_device' + ) + device = MultiValueCharFilter( method='filter_device', - label='Device', + field_name='device__name' ) class Meta: @@ -993,11 +996,11 @@ class ConsoleConnectionFilter(django_filters.FilterSet): return queryset.filter(connected_endpoint__device__site__slug=value) def filter_device(self, queryset, name, value): - if not value.strip(): + if not value: return queryset return queryset.filter( - Q(device__name__icontains=value) | - Q(connected_endpoint__device__name__icontains=value) + Q(**{'{}__in'.format(name): value}) | + Q(**{'connected_endpoint__{}__in'.format(name): value}) ) @@ -1006,9 +1009,12 @@ class PowerConnectionFilter(django_filters.FilterSet): method='filter_site', label='Site (slug)', ) - device = django_filters.CharFilter( + device_id = MultiValueNumberFilter( + method='filter_device' + ) + device = MultiValueCharFilter( method='filter_device', - label='Device', + field_name='device__name' ) class Meta: @@ -1021,11 +1027,11 @@ class PowerConnectionFilter(django_filters.FilterSet): return queryset.filter(_connected_poweroutlet__device__site__slug=value) def filter_device(self, queryset, name, value): - if not value.strip(): + if not value: return queryset return queryset.filter( - Q(device__name__icontains=value) | - Q(_connected_poweroutlet__device__name__icontains=value) + Q(**{'{}__in'.format(name): value}) | + Q(**{'_connected_poweroutlet__{}__in'.format(name): value}) ) @@ -1034,9 +1040,12 @@ class InterfaceConnectionFilter(django_filters.FilterSet): method='filter_site', label='Site (slug)', ) - device = django_filters.CharFilter( + device_id = MultiValueNumberFilter( + method='filter_device' + ) + device = MultiValueCharFilter( method='filter_device', - label='Device', + field_name='device__name' ) class Meta: @@ -1052,11 +1061,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet): ) def filter_device(self, queryset, name, value): - if not value.strip(): + if not value: return queryset return queryset.filter( - Q(device__name__icontains=value) | - Q(_connected_interface__device__name__icontains=value) + Q(**{'{}__in'.format(name): value}) | + Q(**{'_connected_interface__{}__in'.format(name): value}) ) From a3861ed492c280f5ba965fd8126558822155ea63 Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 2 Jan 2020 21:21:52 +0000 Subject: [PATCH 16/26] Update device filters to use IDs --- netbox/dcim/forms.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6952bba65..2aab48a1b 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3171,11 +3171,12 @@ class CableFilterForm(BootstrapMixin, forms.Form): required=False, widget=ColorSelect() ) - device = forms.ChoiceField( + device_id = FilterChoiceField( + queryset=Device.objects.all(), required=False, + label='Device', widget=APISelect( api_url='/api/dcim/devices/', - value_field='text', ) ) @@ -3246,11 +3247,12 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): required=False, to_field_name='slug' ) - device = forms.ChoiceField( + device_id = FilterChoiceField( + queryset=Device.objects.all(), required=False, + label='Device', widget=APISelect( api_url='/api/dcim/devices/', - value_field='text', ) ) @@ -3261,11 +3263,12 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form): required=False, to_field_name='slug' ) - device = forms.ChoiceField( + device_id = FilterChoiceField( + queryset=Device.objects.all(), required=False, + label='Device', widget=APISelect( api_url='/api/dcim/devices/', - value_field='text', ) ) @@ -3276,11 +3279,12 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): required=False, to_field_name='slug' ) - device = forms.ChoiceField( + device_id = FilterChoiceField( + queryset=Device.objects.all(), required=False, + label='Device', widget=APISelect( api_url='/api/dcim/devices/', - value_field='text', ) ) @@ -3374,11 +3378,12 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): required=False, label='Search' ) - device = forms.ChoiceField( + device_id = FilterChoiceField( + queryset=Device.objects.all(), required=False, + label='Device', widget=APISelect( api_url='/api/dcim/devices/', - value_field='text', ) ) manufacturer = FilterChoiceField( From 240bbc2944a6f934f03dce6f8e672d707a1a26cf Mon Sep 17 00:00:00 2001 From: Saria Hajjar Date: Thu, 2 Jan 2020 21:28:06 +0000 Subject: [PATCH 17/26] Select2 site widget --- netbox/dcim/forms.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 2aab48a1b..5d62201c4 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3242,10 +3242,13 @@ class DeviceBayBulkRenameForm(BulkRenameForm): # class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): - site = forms.ModelChoiceField( + site = FilterChoiceField( queryset=Site.objects.all(), - required=False, - to_field_name='slug' + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + ) ) device_id = FilterChoiceField( queryset=Device.objects.all(), @@ -3258,10 +3261,13 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): class PowerConnectionFilterForm(BootstrapMixin, forms.Form): - site = forms.ModelChoiceField( + site = FilterChoiceField( queryset=Site.objects.all(), - required=False, - to_field_name='slug' + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + ) ) device_id = FilterChoiceField( queryset=Device.objects.all(), @@ -3274,10 +3280,13 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form): class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): - site = forms.ModelChoiceField( + site = FilterChoiceField( queryset=Site.objects.all(), - required=False, - to_field_name='slug' + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + ) ) device_id = FilterChoiceField( queryset=Device.objects.all(), From 53db5090c1e39e59f4a6f2247c6c6e69f170847c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Jan 2020 16:53:14 -0500 Subject: [PATCH 18/26] #3827: Fix erroneous filter class --- netbox/dcim/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index cf9c04e5c..433e08960 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -935,7 +935,7 @@ class CableFilter(django_filters.FilterSet): device_id = MultiValueNumberFilter( method='filter_device' ) - device = MultiValueNumberFilter( + device = MultiValueCharFilter( method='filter_device', field_name='device__name' ) From 435d24864561e4cfb374c27bd95d0311406a50b0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Jan 2020 16:56:14 -0500 Subject: [PATCH 19/26] #3122: Allow multiple selections --- netbox/dcim/forms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 5d62201c4..c34c750c5 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3175,7 +3175,7 @@ class CableFilterForm(BootstrapMixin, forms.Form): queryset=Device.objects.all(), required=False, label='Device', - widget=APISelect( + widget=APISelectMultiple( api_url='/api/dcim/devices/', ) ) @@ -3254,7 +3254,7 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form): queryset=Device.objects.all(), required=False, label='Device', - widget=APISelect( + widget=APISelectMultiple( api_url='/api/dcim/devices/', ) ) @@ -3273,7 +3273,7 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form): queryset=Device.objects.all(), required=False, label='Device', - widget=APISelect( + widget=APISelectMultiple( api_url='/api/dcim/devices/', ) ) @@ -3292,7 +3292,7 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): queryset=Device.objects.all(), required=False, label='Device', - widget=APISelect( + widget=APISelectMultiple( api_url='/api/dcim/devices/', ) ) From 9ef3e68479a21e3686f5c863ddaf48cf9f8db5aa Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Jan 2020 17:01:05 -0500 Subject: [PATCH 20/26] Release v2.6.10 --- docs/release-notes/version-2.6.md | 2 +- netbox/netbox/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index af82fa024..a5338698c 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,4 +1,4 @@ -# v2.6.10 (FUTURE) +# v2.6.10 (2019-01-02) ## Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index b60a6691c..c2945a79c 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured # Environment setup # -VERSION = '2.6.10-dev' +VERSION = '2.6.10' # Hostname HOSTNAME = platform.node() From 156fbae7d356af6017c06e639e971d58724d9d67 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Jan 2020 17:04:20 -0500 Subject: [PATCH 21/26] Fix v2.6.10 release date --- docs/release-notes/version-2.6.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index a5338698c..c6e3934e7 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,4 +1,4 @@ -# v2.6.10 (2019-01-02) +# v2.6.10 (2020-01-02) ## Enhancements From c2dc243c7c5f8874cd28e3d9732f5e9adac9ae38 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Jan 2020 17:08:04 -0500 Subject: [PATCH 22/26] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index c2945a79c..4c339cfbc 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured # Environment setup # -VERSION = '2.6.10' +VERSION = '2.6.11-dev' # Hostname HOSTNAME = platform.node() From b38bb64c81951694bec75690cef27029cf57df08 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 3 Jan 2020 11:25:22 -0500 Subject: [PATCH 23/26] Fixes #3831: Fix API-driven filter field rendering (#3812 regression) --- docs/release-notes/version-2.6.md | 8 ++++++++ netbox/circuits/forms.py | 3 +++ netbox/dcim/forms.py | 13 ++++++------- netbox/utilities/templates/widgets/select_api.html | 12 ++++++++---- netbox/virtualization/forms.py | 4 +++- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index c6e3934e7..9f9702f62 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,3 +1,11 @@ +# v2.6.11 (FUTURE) + +## Bug Fixes + +* [#3831](https://github.com/netbox-community/netbox/issues/3831) - Fix API-driven filter field rendering (#3812 regression) + +--- + # v2.6.10 (2020-01-02) ## Enhancements diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 8ff6a0718..49c023299 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -302,6 +302,9 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm widget=APISelectMultiple( api_url="/api/dcim/regions/", value_field="slug", + filter_for={ + 'site': 'region' + } ) ) site = FilterChoiceField( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c34c750c5..429d26c60 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -651,16 +651,15 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): } ) ) - group_id = ChainedModelChoiceField( - label='Rack group', - queryset=RackGroup.objects.prefetch_related('site'), - chains=( - ('site', 'site'), + group_id = FilterChoiceField( + queryset=RackGroup.objects.prefetch_related( + 'site' ), - required=False, + label='Rack group', + null_label='-- None --', widget=APISelectMultiple( api_url="/api/dcim/rack-groups/", - null_option=True, + null_option=True ) ) status = forms.MultipleChoiceField( diff --git a/netbox/utilities/templates/widgets/select_api.html b/netbox/utilities/templates/widgets/select_api.html index 0146a31ff..d9516086b 100644 --- a/netbox/utilities/templates/widgets/select_api.html +++ b/netbox/utilities/templates/widgets/select_api.html @@ -1,5 +1,9 @@ - +{% for group_name, group_choices, group_index in widget.optgroups %} + {% if group_name %}{% endif %} + {% for option in group_choices %} + {% if option.attrs.selected or option.value == "null" %}{% include option.template_name with widget=option %}{% endif %} + {% endfor %} + {% if group_name %}{% endif %} +{% endfor %} diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 2cf55fcde..48b370dc1 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -563,7 +563,9 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil widget=APISelectMultiple( api_url='/api/dcim/regions/', value_field="slug", - null_option=True, + filter_for={ + 'site': 'region' + } ) ) site = FilterChoiceField( From 57f199f89944a3cdd5b70f2535d00e3cde1d81ff Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 3 Jan 2020 13:52:50 -0500 Subject: [PATCH 24/26] Fixes #3833: Add region and region_id filters where missing (#3836) --- docs/release-notes/version-2.6.md | 1 + netbox/circuits/filters.py | 11 ++++ netbox/circuits/forms.py | 12 +++++ netbox/dcim/filters.py | 77 ++++++++++++++++++++++++++++ netbox/dcim/forms.py | 85 ++++++++++++++++++++++++++++++- netbox/ipam/filters.py | 37 +++++++++++++- netbox/ipam/forms.py | 45 ++++++++++++++-- netbox/virtualization/filters.py | 31 +++++++---- netbox/virtualization/forms.py | 34 +++++++++---- 9 files changed, 305 insertions(+), 28 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 9f9702f62..99950c883 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -3,6 +3,7 @@ ## Bug Fixes * [#3831](https://github.com/netbox-community/netbox/issues/3831) - Fix API-driven filter field rendering (#3812 regression) +* [#3833](https://github.com/netbox-community/netbox/issues/3833) - Add missing region filters for multiple objects --- diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 502d2d103..0ac5ec170 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -18,6 +18,17 @@ class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='circuits__terminations__site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='circuits__terminations__site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( field_name='circuits__terminations__site', queryset=Site.objects.all(), diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 49c023299..4a5c06a6e 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -104,6 +104,18 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 433e08960..638313507 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -93,6 +93,17 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet class RackGroupFilter(NameSlugSearchFilterSet): + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -125,6 +136,17 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -831,6 +853,28 @@ class InventoryItemFilter(DeviceComponentFilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='device__site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='device__site__region__in', + to_field_name='slug', + label='Region (slug)', + ) + site_id = django_filters.ModelMultipleChoiceFilter( + field_name='device__site', + queryset=Site.objects.all(), + label='Site (ID)', + ) + site = django_filters.ModelMultipleChoiceFilter( + field_name='device__site__slug', + queryset=Site.objects.all(), + to_field_name='slug', + label='Site name (slug)', + ) device_id = django_filters.ModelChoiceFilter( queryset=Device.objects.all(), label='Device (ID)', @@ -880,6 +924,17 @@ class VirtualChassisFilter(django_filters.FilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='master__site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='master__site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( field_name='master__site', queryset=Site.objects.all(), @@ -1078,6 +1133,17 @@ class PowerPanelFilter(django_filters.FilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -1116,6 +1182,17 @@ class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='power_panel__site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='power_panel__site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( field_name='power_panel__site', queryset=Site.objects.all(), diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 429d26c60..a5ce2811c 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -364,6 +364,18 @@ class RackGroupCSVForm(forms.ModelForm): class RackGroupFilterForm(BootstrapMixin, forms.Form): + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -635,11 +647,23 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Rack - field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] + field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -3386,6 +3410,29 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form): required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) + site = FilterChoiceField( + queryset=Site.objects.all(), + to_field_name='slug', + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field="slug", + filter_for={ + 'device_id': 'site' + } + ) + ) device_id = FilterChoiceField( queryset=Device.objects.all(), required=False, @@ -3551,6 +3598,18 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -3656,6 +3715,18 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -3876,6 +3947,18 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 1f8fd2caf..bf14b80d6 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -4,10 +4,10 @@ from django.core.exceptions import ValidationError from django.db.models import Q from netaddr.core import AddrFormatError -from dcim.models import Site, Device, Interface +from dcim.models import Device, Interface, Region, Site from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from tenancy.filtersets import TenancyFilterSet -from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter +from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from virtualization.models import VirtualMachine from .constants import * from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -149,6 +149,17 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS to_field_name='rd', label='VRF (RD)', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -375,6 +386,17 @@ class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt class VLANGroupFilter(NameSlugSearchFilterSet): + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -400,6 +422,17 @@ class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 44056653b..413e72eaf 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -3,7 +3,7 @@ from django.core.exceptions import MultipleObjectsReturned from django.core.validators import MaxValueValidator, MinValueValidator from taggit.forms import TagField -from dcim.models import Site, Rack, Device, Interface +from dcim.models import Device, Interface, Rack, Region, Site from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant @@ -492,8 +492,8 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = Prefix field_order = [ - 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'site', 'role', 'tenant_group', 'tenant', - 'is_pool', 'expand', + 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'status', 'region', 'site', 'role', 'tenant_group', + 'tenant', 'is_pool', 'expand', ] q = forms.CharField( required=False, @@ -534,6 +534,18 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) required=False, widget=StaticSelect2Multiple() ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -1034,6 +1046,18 @@ class VLANGroupCSVForm(forms.ModelForm): class VLANGroupFilterForm(BootstrapMixin, forms.Form): + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region', + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', @@ -1215,11 +1239,24 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): model = VLAN - field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] + field_order = ['q', 'region', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant'] q = forms.CharField( required=False, label='Search' ) + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region', + 'group_id': 'region' + } + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 4f4ae03ae..6c75c78fc 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -36,6 +36,27 @@ class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): method='search', label='Search', ) + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + label='Region (ID)', + ) + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', + label='Region (slug)', + ) + site_id = django_filters.ModelMultipleChoiceFilter( + queryset=Site.objects.all(), + label='Site (ID)', + ) + site = django_filters.ModelMultipleChoiceFilter( + field_name='site__slug', + queryset=Site.objects.all(), + to_field_name='slug', + label='Site (slug)', + ) group_id = django_filters.ModelMultipleChoiceFilter( queryset=ClusterGroup.objects.all(), label='Parent group (ID)', @@ -56,16 +77,6 @@ class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): to_field_name='slug', label='Cluster type (slug)', ) - site_id = django_filters.ModelMultipleChoiceFilter( - queryset=Site.objects.all(), - label='Site (ID)', - ) - site = django_filters.ModelMultipleChoiceFilter( - field_name='site__slug', - queryset=Site.objects.all(), - to_field_name='slug', - label='Site (slug)', - ) tag = TagFilter() class Meta: diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 48b370dc1..427e676f6 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -173,6 +173,29 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Cluster q = forms.CharField(required=False, label='Search') + region = FilterChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/regions/", + value_field="slug", + filter_for={ + 'site': 'region' + } + ) + ) + site = FilterChoiceField( + queryset=Site.objects.all(), + to_field_name='slug', + null_label='-- None --', + required=False, + widget=APISelectMultiple( + api_url="/api/dcim/sites/", + value_field='slug', + null_option=True, + ) + ) type = FilterChoiceField( queryset=ClusterType.objects.all(), to_field_name='slug', @@ -193,17 +216,6 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) - site = FilterChoiceField( - queryset=Site.objects.all(), - to_field_name='slug', - null_label='-- None --', - required=False, - widget=APISelectMultiple( - api_url="/api/dcim/sites/", - value_field='slug', - null_option=True, - ) - ) class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): From e3b448b7ad40312d1fe7bd3d5f1b796eb97e9394 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 3 Jan 2020 13:55:43 -0500 Subject: [PATCH 25/26] Release v2.6.11 --- docs/release-notes/version-2.6.md | 2 +- netbox/netbox/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index 99950c883..2bf55d857 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,4 +1,4 @@ -# v2.6.11 (FUTURE) +# v2.6.11 (2020-01-03) ## Bug Fixes diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4c339cfbc..819ca7c83 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured # Environment setup # -VERSION = '2.6.11-dev' +VERSION = '2.6.11' # Hostname HOSTNAME = platform.node() From feb04f0401edacc687bc5a956df1708853d8e49c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 3 Jan 2020 14:01:10 -0500 Subject: [PATCH 26/26] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 819ca7c83..75f67e2a6 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured # Environment setup # -VERSION = '2.6.11' +VERSION = '2.6.12-dev' # Hostname HOSTNAME = platform.node()