Skip to content

Commit 37c2c4b

Browse files
committed
Merge v2.5.11
2 parents 2c730b0 + d5dcb77 commit 37c2c4b

13 files changed

+103
-43
lines changed

CHANGELOG.md

+22
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,28 @@ functionality provided by the front end UI.
195195

196196
---
197197

198+
2.5.11 (2019-04-29)
199+
200+
## Notes
201+
202+
This release upgrades the Django framework to version 2.2.
203+
204+
## Enhancements
205+
206+
* [#2986](https://github.com/digitalocean/netbox/issues/2986) - Improve natural ordering of device components
207+
* [#3023](https://github.com/digitalocean/netbox/issues/3023) - Add support for filtering cables by connected device
208+
* [#3070](https://github.com/digitalocean/netbox/issues/3070) - Add decommissioning status for devices
209+
210+
## Bug Fixes
211+
212+
* [#2621](https://github.com/digitalocean/netbox/issues/2621) - Upgrade Django requirement to 2.2 to fix object deletion issue in the changelog middleware
213+
* [#3072](https://github.com/digitalocean/netbox/issues/3072) - Preserve multiselect filter values when updating per-page count for list views
214+
* [#3112](https://github.com/digitalocean/netbox/issues/3112) - Fix ordering of interface connections list by termination B name/device
215+
* [#3116](https://github.com/digitalocean/netbox/issues/3116) - Fix `tagged_items` count in tags API endpoint
216+
* [#3118](https://github.com/digitalocean/netbox/issues/3118) - Disable `last_login` update on login when maintenance mode is enabled
217+
218+
---
219+
198220
v2.5.10 (2019-04-08)
199221

200222
## Enhancements

netbox/dcim/constants.py

+3
Original file line numberDiff line numberDiff line change
@@ -318,13 +318,15 @@
318318
DEVICE_STATUS_STAGED = 3
319319
DEVICE_STATUS_FAILED = 4
320320
DEVICE_STATUS_INVENTORY = 5
321+
DEVICE_STATUS_DECOMMISSIONING = 6
321322
DEVICE_STATUS_CHOICES = [
322323
[DEVICE_STATUS_ACTIVE, 'Active'],
323324
[DEVICE_STATUS_OFFLINE, 'Offline'],
324325
[DEVICE_STATUS_PLANNED, 'Planned'],
325326
[DEVICE_STATUS_STAGED, 'Staged'],
326327
[DEVICE_STATUS_FAILED, 'Failed'],
327328
[DEVICE_STATUS_INVENTORY, 'Inventory'],
329+
[DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'],
328330
]
329331

330332
# Site statuses
@@ -345,6 +347,7 @@
345347
3: 'primary',
346348
4: 'danger',
347349
5: 'default',
350+
6: 'warning',
348351
}
349352

350353
# Console/power/interface connection statuses

netbox/dcim/filters.py

+20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import django_filters
22
from django.contrib.auth.models import User
3+
from django.contrib.contenttypes.models import ContentType
4+
from django.core.exceptions import ObjectDoesNotExist
35
from django.db.models import Q
46
from netaddr import EUI
57
from netaddr.core import AddrFormatError
@@ -969,6 +971,14 @@ class CableFilter(django_filters.FilterSet):
969971
color = django_filters.MultipleChoiceFilter(
970972
choices=COLOR_CHOICES
971973
)
974+
device = django_filters.CharFilter(
975+
method='filter_connected_device',
976+
field_name='name'
977+
)
978+
device_id = django_filters.CharFilter(
979+
method='filter_connected_device',
980+
field_name='pk'
981+
)
972982

973983
class Meta:
974984
model = Cable
@@ -979,6 +989,16 @@ def search(self, queryset, name, value):
979989
return queryset
980990
return queryset.filter(label__icontains=value)
981991

992+
def filter_connected_device(self, queryset, name, value):
993+
if not value.strip():
994+
return queryset
995+
try:
996+
device = Device.objects.get(**{name: value})
997+
except ObjectDoesNotExist:
998+
return queryset.none()
999+
cable_pks = device.get_cables(pk_list=True)
1000+
return queryset.filter(pk__in=cable_pks)
1001+
9821002

9831003
class ConsoleConnectionFilter(django_filters.FilterSet):
9841004
site = django_filters.CharFilter(

netbox/dcim/forms.py

+4
Original file line numberDiff line numberDiff line change
@@ -3003,6 +3003,10 @@ class CableFilterForm(BootstrapMixin, forms.Form):
30033003
required=False,
30043004
widget=ColorSelect()
30053005
)
3006+
device = forms.CharField(
3007+
required=False,
3008+
label='Device name'
3009+
)
30063010

30073011

30083012
#

netbox/dcim/managers.py

-16
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,6 @@
1414
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)"
1515

1616

17-
class DeviceComponentManager(Manager):
18-
19-
def get_queryset(self):
20-
21-
queryset = super().get_queryset()
22-
table_name = self.model._meta.db_table
23-
sql = r"CONCAT(REGEXP_REPLACE({}.name, '\d+$', ''), LPAD(SUBSTRING({}.name FROM '\d+$'), 8, '0'))"
24-
25-
# Pad any trailing digits to effect natural sorting
26-
return queryset.extra(
27-
select={
28-
'name_padded': sql.format(table_name, table_name),
29-
}
30-
).order_by('name_padded', 'pk')
31-
32-
3317
class InterfaceQuerySet(QuerySet):
3418

3519
def connectable(self):

netbox/dcim/models.py

+30-15
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from .constants import *
2424
from .exceptions import LoopDetected
2525
from .fields import ASNField, MACAddressField
26-
from .managers import DeviceComponentManager, InterfaceManager
26+
from .managers import InterfaceManager
2727

2828

2929
class ComponentTemplateModel(models.Model):
@@ -982,7 +982,7 @@ class ConsolePortTemplate(ComponentTemplateModel):
982982
max_length=50
983983
)
984984

985-
objects = DeviceComponentManager()
985+
objects = NaturalOrderingManager()
986986

987987
class Meta:
988988
ordering = ['device_type', 'name']
@@ -1005,7 +1005,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
10051005
max_length=50
10061006
)
10071007

1008-
objects = DeviceComponentManager()
1008+
objects = NaturalOrderingManager()
10091009

10101010
class Meta:
10111011
ordering = ['device_type', 'name']
@@ -1040,7 +1040,7 @@ class PowerPortTemplate(ComponentTemplateModel):
10401040
help_text="Allocated current draw (watts)"
10411041
)
10421042

1043-
objects = DeviceComponentManager()
1043+
objects = NaturalOrderingManager()
10441044

10451045
class Meta:
10461046
ordering = ['device_type', 'name']
@@ -1076,7 +1076,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
10761076
help_text="Phase (for three-phase feeds)"
10771077
)
10781078

1079-
objects = DeviceComponentManager()
1079+
objects = NaturalOrderingManager()
10801080

10811081
class Meta:
10821082
ordering = ['device_type', 'name']
@@ -1166,7 +1166,7 @@ class FrontPortTemplate(ComponentTemplateModel):
11661166
validators=[MinValueValidator(1), MaxValueValidator(64)]
11671167
)
11681168

1169-
objects = DeviceComponentManager()
1169+
objects = NaturalOrderingManager()
11701170

11711171
class Meta:
11721172
ordering = ['device_type', 'name']
@@ -1215,7 +1215,7 @@ class RearPortTemplate(ComponentTemplateModel):
12151215
validators=[MinValueValidator(1), MaxValueValidator(64)]
12161216
)
12171217

1218-
objects = DeviceComponentManager()
1218+
objects = NaturalOrderingManager()
12191219

12201220
class Meta:
12211221
ordering = ['device_type', 'name']
@@ -1238,7 +1238,7 @@ class DeviceBayTemplate(ComponentTemplateModel):
12381238
max_length=50
12391239
)
12401240

1241-
objects = DeviceComponentManager()
1241+
objects = NaturalOrderingManager()
12421242

12431243
class Meta:
12441244
ordering = ['device_type', 'name']
@@ -1731,6 +1731,21 @@ def vc_interfaces(self):
17311731
filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
17321732
return Interface.objects.filter(filter)
17331733

1734+
def get_cables(self, pk_list=False):
1735+
"""
1736+
Return a QuerySet or PK list matching all Cables connected to a component of this Device.
1737+
"""
1738+
cable_pks = []
1739+
for component_model in [
1740+
ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort
1741+
]:
1742+
cable_pks += component_model.objects.filter(
1743+
device=self, cable__isnull=False
1744+
).values_list('cable', flat=True)
1745+
if pk_list:
1746+
return cable_pks
1747+
return Cable.objects.filter(pk__in=cable_pks)
1748+
17341749
def get_children(self):
17351750
"""
17361751
Return the set of child Devices installed in DeviceBays within this Device.
@@ -1769,7 +1784,7 @@ class ConsolePort(CableTermination, ComponentModel):
17691784
blank=True
17701785
)
17711786

1772-
objects = DeviceComponentManager()
1787+
objects = NaturalOrderingManager()
17731788
tags = TaggableManager(through=TaggedItem)
17741789

17751790
csv_headers = ['device', 'name', 'description']
@@ -1813,7 +1828,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
18131828
blank=True
18141829
)
18151830

1816-
objects = DeviceComponentManager()
1831+
objects = NaturalOrderingManager()
18171832
tags = TaggableManager(through=TaggedItem)
18181833

18191834
csv_headers = ['device', 'name', 'description']
@@ -1882,7 +1897,7 @@ class PowerPort(CableTermination, ComponentModel):
18821897
blank=True
18831898
)
18841899

1885-
objects = DeviceComponentManager()
1900+
objects = NaturalOrderingManager()
18861901
tags = TaggableManager(through=TaggedItem)
18871902

18881903
csv_headers = ['device', 'name', 'maximum_draw', 'allocated_draw', 'description']
@@ -1998,7 +2013,7 @@ class PowerOutlet(CableTermination, ComponentModel):
19982013
blank=True
19992014
)
20002015

2001-
objects = DeviceComponentManager()
2016+
objects = NaturalOrderingManager()
20022017
tags = TaggableManager(through=TaggedItem)
20032018

20042019
csv_headers = ['device', 'name', 'power_port', 'feed_leg', 'description']
@@ -2338,7 +2353,7 @@ class FrontPort(CableTermination, ComponentModel):
23382353
validators=[MinValueValidator(1), MaxValueValidator(64)]
23392354
)
23402355

2341-
objects = DeviceComponentManager()
2356+
objects = NaturalOrderingManager()
23422357
tags = TaggableManager(through=TaggedItem)
23432358

23442359
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
@@ -2400,7 +2415,7 @@ class RearPort(CableTermination, ComponentModel):
24002415
validators=[MinValueValidator(1), MaxValueValidator(64)]
24012416
)
24022417

2403-
objects = DeviceComponentManager()
2418+
objects = NaturalOrderingManager()
24042419
tags = TaggableManager(through=TaggedItem)
24052420

24062421
csv_headers = ['device', 'name', 'type', 'positions', 'description']
@@ -2447,7 +2462,7 @@ class DeviceBay(ComponentModel):
24472462
null=True
24482463
)
24492464

2450-
objects = DeviceComponentManager()
2465+
objects = NaturalOrderingManager()
24512466
tags = TaggableManager(through=TaggedItem)
24522467

24532468
csv_headers = ['device', 'name', 'installed_device', 'description']

netbox/dcim/tables.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -743,18 +743,18 @@ class InterfaceConnectionTable(BaseTable):
743743
)
744744
device_b = tables.LinkColumn(
745745
viewname='dcim:device',
746-
accessor=Accessor('connected_endpoint.device'),
747-
args=[Accessor('connected_endpoint.device.pk')],
746+
accessor=Accessor('_connected_interface.device'),
747+
args=[Accessor('_connected_interface.device.pk')],
748748
verbose_name='Device B'
749749
)
750750
interface_b = tables.LinkColumn(
751751
viewname='dcim:interface',
752-
accessor=Accessor('connected_endpoint.name'),
753-
args=[Accessor('connected_endpoint.pk')],
752+
accessor=Accessor('_connected_interface'),
753+
args=[Accessor('_connected_interface.pk')],
754754
verbose_name='Interface B'
755755
)
756756
description_b = tables.Column(
757-
accessor=Accessor('connected_endpoint.description'),
757+
accessor=Accessor('_connected_interface.description'),
758758
verbose_name='Description'
759759
)
760760

netbox/extras/api/views.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ def render(self, request, pk):
148148
#
149149

150150
class TagViewSet(ModelViewSet):
151-
queryset = Tag.objects.annotate(tagged_items=Count('extras_taggeditem_items'))
151+
queryset = Tag.objects.annotate(
152+
tagged_items=Count('taggit_taggeditem_items', distinct=True)
153+
)
152154
serializer_class = serializers.TagSerializer
153155
filterset_class = filters.TagFilter
154156

netbox/extras/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
class TagListView(ObjectListView):
3131
queryset = Tag.objects.annotate(
32-
items=Count('extras_taggeditem_items')
32+
items=Count('taggit_taggeditem_items', distinct=True)
3333
).order_by(
3434
'name'
3535
)

netbox/templates/inc/paginator.html

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
</ul>
2121
</nav>
2222
<form method="get">
23-
{% for k, v in request.GET.items %}
23+
{% for k, v_list in request.GET.lists %}
2424
{% if k != 'per_page' %}
25-
<input type="hidden" name="{{ k }}" value="{{ v }}" />
25+
{% for v in v_list %}
26+
<input type="hidden" name="{{ k }}" value="{{ v }}" />
27+
{% endfor %}
2628
{% endif %}
2729
{% endfor %}
2830
<select name="per_page" id="per_page">

netbox/templates/ipam/vlan.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ <h1>{% block title %}VLAN {{ vlan.display_name }}{% endblock %}</h1>
6565

6666
{% block content %}
6767
<div class="row">
68-
<div class="col-md-6">
68+
<div class="col-md-4">
6969
<div class="panel panel-default">
7070
<div class="panel-heading">
7171
<strong>VLAN</strong>
@@ -142,7 +142,7 @@ <h1>{% block title %}VLAN {{ vlan.display_name }}{% endblock %}</h1>
142142
{% include 'inc/custom_fields_panel.html' with obj=vlan %}
143143
{% include 'extras/inc/tags_panel.html' with tags=vlan.tags.all url='ipam:vlan_list' %}
144144
</div>
145-
<div class="col-md-6">
145+
<div class="col-md-8">
146146
<div class="panel panel-default">
147147
<div class="panel-heading">
148148
<strong>Prefixes</strong>

netbox/users/views.py

+8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from django.conf import settings
12
from django.contrib import messages
23
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
34
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
5+
from django.contrib.auth.models import update_last_login
6+
from django.contrib.auth.signals import user_logged_in
47
from django.http import HttpResponseForbidden, HttpResponseRedirect
58
from django.shortcuts import get_object_or_404, redirect, render
69
from django.urls import reverse
@@ -43,6 +46,11 @@ def post(self, request):
4346
if not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
4447
redirect_to = reverse('home')
4548

49+
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
50+
# last_login time upon authentication.
51+
if settings.MAINTENANCE_MODE:
52+
user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
53+
4654
# Authenticate user
4755
auth_login(request, form.get_user())
4856
messages.info(request, "Logged in as {}.".format(request.user))

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Django>=2.2,<2.3
22
django-cacheops==4.1
3-
django-cors-headers==2.5.2
3+
django-cors-headers==2.4.0
44
django-debug-toolbar==1.11
55
django-filter==2.1.0
66
django-mptt==0.9.1

0 commit comments

Comments
 (0)