diff --git a/CHANGELOG.md b/CHANGELOG.md index bdcf78aa5..b821322e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -195,6 +195,28 @@ functionality provided by the front end UI. --- +2.5.11 (2019-04-29) + +## Notes + +This release upgrades the Django framework to version 2.2. + +## Enhancements + +* [#2986](https://github.com/digitalocean/netbox/issues/2986) - Improve natural ordering of device components +* [#3023](https://github.com/digitalocean/netbox/issues/3023) - Add support for filtering cables by connected device +* [#3070](https://github.com/digitalocean/netbox/issues/3070) - Add decommissioning status for devices + +## Bug Fixes + +* [#2621](https://github.com/digitalocean/netbox/issues/2621) - Upgrade Django requirement to 2.2 to fix object deletion issue in the changelog middleware +* [#3072](https://github.com/digitalocean/netbox/issues/3072) - Preserve multiselect filter values when updating per-page count for list views +* [#3112](https://github.com/digitalocean/netbox/issues/3112) - Fix ordering of interface connections list by termination B name/device +* [#3116](https://github.com/digitalocean/netbox/issues/3116) - Fix `tagged_items` count in tags API endpoint +* [#3118](https://github.com/digitalocean/netbox/issues/3118) - Disable `last_login` update on login when maintenance mode is enabled + +--- + v2.5.10 (2019-04-08) ## Enhancements diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 9f07abf18..d55c07660 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -318,6 +318,7 @@ DEVICE_STATUS_PLANNED = 2 DEVICE_STATUS_STAGED = 3 DEVICE_STATUS_FAILED = 4 DEVICE_STATUS_INVENTORY = 5 +DEVICE_STATUS_DECOMMISSIONING = 6 DEVICE_STATUS_CHOICES = [ [DEVICE_STATUS_ACTIVE, 'Active'], [DEVICE_STATUS_OFFLINE, 'Offline'], @@ -325,6 +326,7 @@ DEVICE_STATUS_CHOICES = [ [DEVICE_STATUS_STAGED, 'Staged'], [DEVICE_STATUS_FAILED, 'Failed'], [DEVICE_STATUS_INVENTORY, 'Inventory'], + [DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'], ] # Site statuses @@ -345,6 +347,7 @@ STATUS_CLASSES = { 3: 'primary', 4: 'danger', 5: 'default', + 6: 'warning', } # Console/power/interface connection statuses diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 63a2d3d1a..53163e7bd 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,5 +1,7 @@ import django_filters from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError @@ -969,6 +971,14 @@ class CableFilter(django_filters.FilterSet): color = django_filters.MultipleChoiceFilter( choices=COLOR_CHOICES ) + device = django_filters.CharFilter( + method='filter_connected_device', + field_name='name' + ) + device_id = django_filters.CharFilter( + method='filter_connected_device', + field_name='pk' + ) class Meta: model = Cable @@ -979,6 +989,16 @@ class CableFilter(django_filters.FilterSet): return queryset return queryset.filter(label__icontains=value) + def filter_connected_device(self, queryset, name, value): + if not value.strip(): + return queryset + try: + device = Device.objects.get(**{name: value}) + except ObjectDoesNotExist: + return queryset.none() + cable_pks = device.get_cables(pk_list=True) + return queryset.filter(pk__in=cable_pks) + class ConsoleConnectionFilter(django_filters.FilterSet): site = django_filters.CharFilter( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index f95b4b18b..99ecec2d6 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3003,6 +3003,10 @@ class CableFilterForm(BootstrapMixin, forms.Form): required=False, widget=ColorSelect() ) + device = forms.CharField( + required=False, + label='Device name' + ) # diff --git a/netbox/dcim/managers.py b/netbox/dcim/managers.py index 8b1000529..e1124b84e 100644 --- a/netbox/dcim/managers.py +++ b/netbox/dcim/managers.py @@ -14,22 +14,6 @@ CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$') VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)" -class DeviceComponentManager(Manager): - - def get_queryset(self): - - queryset = super().get_queryset() - table_name = self.model._meta.db_table - sql = r"CONCAT(REGEXP_REPLACE({}.name, '\d+$', ''), LPAD(SUBSTRING({}.name FROM '\d+$'), 8, '0'))" - - # Pad any trailing digits to effect natural sorting - return queryset.extra( - select={ - 'name_padded': sql.format(table_name, table_name), - } - ).order_by('name_padded', 'pk') - - class InterfaceQuerySet(QuerySet): def connectable(self): diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index ae76e369c..76db26c29 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -23,7 +23,7 @@ from utilities.utils import serialize_object, to_meters from .constants import * from .exceptions import LoopDetected from .fields import ASNField, MACAddressField -from .managers import DeviceComponentManager, InterfaceManager +from .managers import InterfaceManager class ComponentTemplateModel(models.Model): @@ -982,7 +982,7 @@ class ConsolePortTemplate(ComponentTemplateModel): max_length=50 ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1005,7 +1005,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel): max_length=50 ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1040,7 +1040,7 @@ class PowerPortTemplate(ComponentTemplateModel): help_text="Allocated current draw (watts)" ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1076,7 +1076,7 @@ class PowerOutletTemplate(ComponentTemplateModel): help_text="Phase (for three-phase feeds)" ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1166,7 +1166,7 @@ class FrontPortTemplate(ComponentTemplateModel): validators=[MinValueValidator(1), MaxValueValidator(64)] ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1215,7 +1215,7 @@ class RearPortTemplate(ComponentTemplateModel): validators=[MinValueValidator(1), MaxValueValidator(64)] ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1238,7 +1238,7 @@ class DeviceBayTemplate(ComponentTemplateModel): max_length=50 ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() class Meta: ordering = ['device_type', 'name'] @@ -1731,6 +1731,21 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False) return Interface.objects.filter(filter) + def get_cables(self, pk_list=False): + """ + Return a QuerySet or PK list matching all Cables connected to a component of this Device. + """ + cable_pks = [] + for component_model in [ + ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort + ]: + cable_pks += component_model.objects.filter( + device=self, cable__isnull=False + ).values_list('cable', flat=True) + if pk_list: + return cable_pks + return Cable.objects.filter(pk__in=cable_pks) + def get_children(self): """ Return the set of child Devices installed in DeviceBays within this Device. @@ -1769,7 +1784,7 @@ class ConsolePort(CableTermination, ComponentModel): blank=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'description'] @@ -1813,7 +1828,7 @@ class ConsoleServerPort(CableTermination, ComponentModel): blank=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'description'] @@ -1882,7 +1897,7 @@ class PowerPort(CableTermination, ComponentModel): blank=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'maximum_draw', 'allocated_draw', 'description'] @@ -1998,7 +2013,7 @@ class PowerOutlet(CableTermination, ComponentModel): blank=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'power_port', 'feed_leg', 'description'] @@ -2338,7 +2353,7 @@ class FrontPort(CableTermination, ComponentModel): validators=[MinValueValidator(1), MaxValueValidator(64)] ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] @@ -2400,7 +2415,7 @@ class RearPort(CableTermination, ComponentModel): validators=[MinValueValidator(1), MaxValueValidator(64)] ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'type', 'positions', 'description'] @@ -2447,7 +2462,7 @@ class DeviceBay(ComponentModel): null=True ) - objects = DeviceComponentManager() + objects = NaturalOrderingManager() tags = TaggableManager(through=TaggedItem) csv_headers = ['device', 'name', 'installed_device', 'description'] diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 942e10029..3682ab37a 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -743,18 +743,18 @@ class InterfaceConnectionTable(BaseTable): ) device_b = tables.LinkColumn( viewname='dcim:device', - accessor=Accessor('connected_endpoint.device'), - args=[Accessor('connected_endpoint.device.pk')], + accessor=Accessor('_connected_interface.device'), + args=[Accessor('_connected_interface.device.pk')], verbose_name='Device B' ) interface_b = tables.LinkColumn( viewname='dcim:interface', - accessor=Accessor('connected_endpoint.name'), - args=[Accessor('connected_endpoint.pk')], + accessor=Accessor('_connected_interface'), + args=[Accessor('_connected_interface.pk')], verbose_name='Interface B' ) description_b = tables.Column( - accessor=Accessor('connected_endpoint.description'), + accessor=Accessor('_connected_interface.description'), verbose_name='Description' ) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index c32aa92a9..58e3f7252 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -148,7 +148,9 @@ class TopologyMapViewSet(ModelViewSet): # class TagViewSet(ModelViewSet): - queryset = Tag.objects.annotate(tagged_items=Count('extras_taggeditem_items')) + queryset = Tag.objects.annotate( + tagged_items=Count('taggit_taggeditem_items', distinct=True) + ) serializer_class = serializers.TagSerializer filterset_class = filters.TagFilter diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 21ea4f1c9..a06d837d4 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -29,7 +29,7 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemT class TagListView(ObjectListView): queryset = Tag.objects.annotate( - items=Count('extras_taggeditem_items') + items=Count('taggit_taggeditem_items', distinct=True) ).order_by( 'name' ) diff --git a/netbox/templates/inc/paginator.html b/netbox/templates/inc/paginator.html index ac4648988..fe9177f87 100644 --- a/netbox/templates/inc/paginator.html +++ b/netbox/templates/inc/paginator.html @@ -20,9 +20,11 @@