diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index a37c5dfb1..6919ff16f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,6 +1,6 @@ --- name: 🐛 Bug Report -about: Report a reproducible bug in the current release of NetBox +description: Report a reproducible bug in the current release of NetBox labels: ["type: bug"] body: - type: markdown diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/documentation_change.yaml index b480e629a..19d9696ad 100644 --- a/.github/ISSUE_TEMPLATE/documentation_change.yaml +++ b/.github/ISSUE_TEMPLATE/documentation_change.yaml @@ -1,6 +1,6 @@ --- name: 📖 Documentation Change -about: Suggest an addition or modification to the NetBox documentation +description: Suggest an addition or modification to the NetBox documentation labels: ["type: documentation"] body: - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 6282eedde..7d7bde225 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,6 +1,6 @@ --- name: ✨ Feature Request -about: Propose a new NetBox feature or enhancement +description: Propose a new NetBox feature or enhancement labels: ["type: feature"] body: - type: markdown diff --git a/.github/ISSUE_TEMPLATE/housekeeping.yaml b/.github/ISSUE_TEMPLATE/housekeeping.yaml index 778dca235..5e675583e 100644 --- a/.github/ISSUE_TEMPLATE/housekeeping.yaml +++ b/.github/ISSUE_TEMPLATE/housekeeping.yaml @@ -1,6 +1,6 @@ --- name: 🏡 Housekeeping -about: A change pertaining to the codebase itself (developers only) +description: A change pertaining to the codebase itself (developers only) labels: ["type: housekeeping"] body: - type: markdown diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 483cd7c9c..838022006 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -1,5 +1,24 @@ # NetBox v2.11 +## v2.11.1 (2021-04-21) + +### Enhancements + +* [#6161](https://github.com/netbox-community/netbox/issues/6161) - Enable ordering of device component tables +* [#6179](https://github.com/netbox-community/netbox/issues/6179) - Enable natural ordering for virtual machines +* [#6189](https://github.com/netbox-community/netbox/issues/6189) - Add ability to search for locations by name or description +* [#6190](https://github.com/netbox-community/netbox/issues/6190) - Allow filtering devices with no location assigned +* [#6210](https://github.com/netbox-community/netbox/issues/6210) - Include child locations on location view + +### Bug Fixes + +* [#6184](https://github.com/netbox-community/netbox/issues/6184) - Fix parent object table column in prefix IP addresses list +* [#6188](https://github.com/netbox-community/netbox/issues/6188) - Support custom field filtering for regions, site groups, and locations +* [#6196](https://github.com/netbox-community/netbox/issues/6196) - Fix object list display for users with read-only permissions +* [#6215](https://github.com/netbox-community/netbox/issues/6215) - Restore tenancy section in virtual machine form + +--- + ## v2.11.0 (2021-04-16) **Note:** NetBox v2.11 is the last major release that will support Python 3.6. Beginning with NetBox v2.12, Python 3.7 or later will be required. diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 7ae016dc4..2dc4faefb 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -209,6 +209,14 @@ class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet): model = Location fields = ['id', 'name', 'slug', 'description'] + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) + ) + class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index feb8c5e81..d6799b6c9 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -230,7 +230,7 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): nullable_fields = ['parent', 'description'] -class RegionFilterForm(BootstrapMixin, forms.Form): +class RegionFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Site q = forms.CharField( required=False, @@ -287,8 +287,8 @@ class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): nullable_fields = ['parent', 'description'] -class SiteGroupFilterForm(BootstrapMixin, forms.Form): - model = Site +class SiteGroupFilterForm(BootstrapMixin, CustomFieldFilterForm): + model = SiteGroup q = forms.CharField( required=False, label=_('Search') @@ -557,7 +557,12 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): nullable_fields = ['parent', 'description'] -class LocationFilterForm(BootstrapMixin, forms.Form): +class LocationFilterForm(BootstrapMixin, CustomFieldFilterForm): + model = Location + q = forms.CharField( + required=False, + label=_('Search') + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -2424,10 +2429,11 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt location_id = DynamicModelMultipleChoiceField( queryset=Location.objects.all(), required=False, - label=_('Location'), + null_option='None', query_params={ 'site_id': '$site_id' - } + }, + label=_('Location') ) rack_id = DynamicModelMultipleChoiceField( queryset=Rack.objects.all(), diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index cc1d78fa3..4f7c05c71 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -291,6 +291,7 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable): class DeviceConsolePortTable(ConsolePortTable): name = tables.TemplateColumn( template_code=' {{ value }}', + order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = ButtonsColumn( @@ -335,6 +336,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable): name = tables.TemplateColumn( template_code=' ' '{{ value }}', + order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = ButtonsColumn( @@ -379,6 +381,7 @@ class DevicePowerPortTable(PowerPortTable): name = tables.TemplateColumn( template_code=' ' '{{ value }}', + order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = ButtonsColumn( @@ -428,6 +431,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable): class DevicePowerOutletTable(PowerOutletTable): name = tables.TemplateColumn( template_code=' {{ value }}', + order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = ButtonsColumn( @@ -492,6 +496,7 @@ class DeviceInterfaceTable(InterfaceTable): template_code=' {{ value }}', + order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) parent = tables.Column( @@ -555,6 +560,7 @@ class DeviceFrontPortTable(FrontPortTable): name = tables.TemplateColumn( template_code=' ' '{{ value }}', + order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = ButtonsColumn( @@ -602,6 +608,7 @@ class DeviceRearPortTable(RearPortTable): name = tables.TemplateColumn( template_code=' ' '{{ value }}', + order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = ButtonsColumn( @@ -651,6 +658,7 @@ class DeviceDeviceBayTable(DeviceBayTable): name = tables.TemplateColumn( template_code=' {{ value }}', + order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = ButtonsColumn( @@ -698,6 +706,7 @@ class DeviceInventoryItemTable(InventoryItemTable): name = tables.TemplateColumn( template_code='' '{{ value }}', + order_by=Accessor('_name'), attrs={'td': {'class': 'text-nowrap'}} ) actions = ButtonsColumn( diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 5da50e0db..a9aee80f1 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -364,16 +364,30 @@ class LocationView(generic.ObjectView): queryset = Location.objects.all() def get_extra_context(self, request, instance): - devices = Device.objects.restrict(request.user, 'view').filter( - location=instance - ) + location_ids = instance.get_descendants(include_self=True).values_list('pk', flat=True) + rack_count = Rack.objects.filter(location__in=location_ids).count() + device_count = Device.objects.filter(location__in=location_ids).count() - devices_table = tables.DeviceTable(devices) - devices_table.columns.hide('location') - paginate_table(devices_table, request) + child_locations = Location.objects.add_related_count( + Location.objects.add_related_count( + Location.objects.all(), + Device, + 'location', + 'device_count', + cumulative=True + ), + Rack, + 'location', + 'rack_count', + cumulative=True + ).filter(pk__in=location_ids).exclude(pk=instance.pk) + child_locations_table = tables.LocationTable(child_locations) + paginate_table(child_locations_table, request) return { - 'devices_table': devices_table, + 'rack_count': rack_count, + 'device_count': device_count, + 'child_locations_table': child_locations_table, } @@ -1305,8 +1319,7 @@ class DeviceConsolePortsView(generic.ObjectView): ) consoleport_table = tables.DeviceConsolePortTable( data=consoleports, - user=request.user, - orderable=False + user=request.user ) if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'): consoleport_table.columns.show('pk') @@ -1330,8 +1343,7 @@ class DeviceConsoleServerPortsView(generic.ObjectView): ) consoleserverport_table = tables.DeviceConsoleServerPortTable( data=consoleserverports, - user=request.user, - orderable=False + user=request.user ) if request.user.has_perm('dcim.change_consoleserverport') or \ request.user.has_perm('dcim.delete_consoleserverport'): @@ -1354,8 +1366,7 @@ class DevicePowerPortsView(generic.ObjectView): ) powerport_table = tables.DevicePowerPortTable( data=powerports, - user=request.user, - orderable=False + user=request.user ) if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'): powerport_table.columns.show('pk') @@ -1377,8 +1388,7 @@ class DevicePowerOutletsView(generic.ObjectView): ) poweroutlet_table = tables.DevicePowerOutletTable( data=poweroutlets, - user=request.user, - orderable=False + user=request.user ) if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'): poweroutlet_table.columns.show('pk') @@ -1402,8 +1412,7 @@ class DeviceInterfacesView(generic.ObjectView): ) interface_table = tables.DeviceInterfaceTable( data=interfaces, - user=request.user, - orderable=False + user=request.user ) if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'): interface_table.columns.show('pk') @@ -1425,8 +1434,7 @@ class DeviceFrontPortsView(generic.ObjectView): ) frontport_table = tables.DeviceFrontPortTable( data=frontports, - user=request.user, - orderable=False + user=request.user ) if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'): frontport_table.columns.show('pk') @@ -1446,8 +1454,7 @@ class DeviceRearPortsView(generic.ObjectView): rearports = RearPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related('cable') rearport_table = tables.DeviceRearPortTable( data=rearports, - user=request.user, - orderable=False + user=request.user ) if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'): rearport_table.columns.show('pk') @@ -1469,8 +1476,7 @@ class DeviceDeviceBaysView(generic.ObjectView): ) devicebay_table = tables.DeviceDeviceBayTable( data=devicebays, - user=request.user, - orderable=False + user=request.user ) if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'): devicebay_table.columns.show('pk') @@ -1492,8 +1498,7 @@ class DeviceInventoryView(generic.ObjectView): ).prefetch_related('manufacturer') inventoryitem_table = tables.DeviceInventoryItemTable( data=inventoryitems, - user=request.user, - orderable=False + user=request.user ) if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'): inventoryitem_table.columns.show('pk') diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 6fe8365f5..ff28f2fc7 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -340,10 +340,10 @@ class IPAddressTable(BaseTable): verbose_name='Interface' ) assigned_object_parent = tables.Column( - accessor='assigned_object__parent', + accessor='assigned_object.parent_object', linkify=True, orderable=False, - verbose_name='Interface Parent' + verbose_name='Device/VM' ) class Meta(BaseTable.Meta): diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index fd78ae6ae..a7af4d8fd 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '2.11.0' +VERSION = '2.11.1' # Hostname HOSTNAME = platform.node() diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html index a5eeb4e71..e523465ec 100644 --- a/netbox/templates/dcim/location.html +++ b/netbox/templates/dcim/location.html @@ -43,13 +43,13 @@ Racks - {{ object.racks.count }} + {{ rack_count }} Devices - {{ devices_table.rows|length }} + {{ device_count }} @@ -79,18 +79,18 @@
- Devices + Locations
- {% include 'inc/table.html' with table=devices_table %} - {% if perms.dcim.add_device %} + {% include 'inc/table.html' with table=child_locations_table %} + {% if perms.dcim.add_location %} {% endif %}
- {% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %} + {% include 'inc/paginator.html' with paginator=child_locations_table.paginator page=child_locations_table.page %} {% plugin_full_width_page object %}
diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index 441589d27..e37676286 100644 --- a/netbox/templates/generic/object_list.html +++ b/netbox/templates/generic/object_list.html @@ -76,7 +76,9 @@ {% else %} - {% render_table table 'inc/table.html' %} +
+ {% render_table table 'inc/table.html' %} +
{% endif %} {% endwith %} {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index dc6f3e4c5..3da5c98d2 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -376,6 +376,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): fieldsets = ( ('Virtual Machine', ('name', 'role', 'status', 'tags')), ('Cluster', ('cluster_group', 'cluster')), + ('Tenancy', ('tenant_group', 'tenant')), ('Management', ('platform', 'primary_ip4', 'primary_ip6')), ('Resources', ('vcpus', 'memory', 'disk')), ('Config Context', ('local_context_data',)), diff --git a/netbox/virtualization/migrations/0023_virtualmachine_natural_ordering.py b/netbox/virtualization/migrations/0023_virtualmachine_natural_ordering.py new file mode 100644 index 000000000..2fc2a38e0 --- /dev/null +++ b/netbox/virtualization/migrations/0023_virtualmachine_natural_ordering.py @@ -0,0 +1,32 @@ +from django.db import migrations +import utilities.fields +import utilities.ordering + + +def naturalize_virtualmachines(apps, schema_editor): + VirtualMachine = apps.get_model('virtualization', 'VirtualMachine') + for name in VirtualMachine.objects.values_list('name', flat=True).order_by('name').distinct(): + VirtualMachine.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100)) + + +class Migration(migrations.Migration): + + dependencies = [ + ('virtualization', '0022_vminterface_parent'), + ] + + operations = [ + migrations.AlterModelOptions( + name='virtualmachine', + options={'ordering': ('_name', 'pk')}, + ), + migrations.AddField( + model_name='virtualmachine', + name='_name', + field=utilities.fields.NaturalOrderingField('name', max_length=100, blank=True, naturalize_function=utilities.ordering.naturalize), + ), + migrations.RunPython( + code=naturalize_virtualmachines, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index f2aa9e17f..76f7fe845 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -226,6 +226,11 @@ class VirtualMachine(PrimaryModel, ConfigContextModel): name = models.CharField( max_length=64 ) + _name = NaturalOrderingField( + target_field='name', + max_length=100, + blank=True + ) status = models.CharField( max_length=50, choices=VirtualMachineStatusChoices, @@ -296,7 +301,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel): ] class Meta: - ordering = ('name', 'pk') # Name may be non-unique + ordering = ('_name', 'pk') # Name may be non-unique unique_together = [ ['cluster', 'tenant', 'name'] ] diff --git a/requirements.txt b/requirements.txt index c81757f58..a9e000bf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ django-pglocks==1.0.4 django-prometheus==2.1.0 django-rq==2.4.1 django-tables2==2.3.4 -django-taggit==1.3.0 +django-taggit==1.4.0 django-timezone-field==4.1.2 djangorestframework==3.12.4 drf-yasg[validation]==1.20.0