diff --git a/CHANGELOG.md b/CHANGELOG.md index fbbba1907..0d8ea0123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,20 +30,33 @@ to now use "Extras | Tag." --- -v2.5.8 (FUTURE) +v2.5.8 (2019-03-11) + +## Enhancements + +* [#2435](https://github.com/digitalocean/netbox/issues/2435) - Printer friendly CSS ## Bug Fixes +* [#2065](https://github.com/digitalocean/netbox/issues/2065) - Correct documentation for VM interface serializer * [#2705](https://github.com/digitalocean/netbox/issues/2705) - Fix endpoint grouping in API docs * [#2781](https://github.com/digitalocean/netbox/issues/2781) - Fix filtering of sites/devices/VMs by multiple regions * [#2923](https://github.com/digitalocean/netbox/issues/2923) - Provider filter form's site field should be blank by default * [#2938](https://github.com/digitalocean/netbox/issues/2938) - Enforce deterministic ordering of device components returned by API * [#2939](https://github.com/digitalocean/netbox/issues/2939) - Exclude circuit terminations from API interface connections endpoint +* [#2940](https://github.com/digitalocean/netbox/issues/2940) - Allow CSV import of prefixes/IPs to VRF without an RD assigned +* [#2944](https://github.com/digitalocean/netbox/issues/2944) - Record the deletion of an IP address in the changelog of its parent interface (if any) * [#2952](https://github.com/digitalocean/netbox/issues/2952) - Added the `slug` field to the Tenant filter for use in the API and search function * [#2954](https://github.com/digitalocean/netbox/issues/2954) - Remove trailing slashes to fix root/template paths on Windows * [#2961](https://github.com/digitalocean/netbox/issues/2961) - Prevent exception when exporting inventory items belonging to unnamed devices * [#2962](https://github.com/digitalocean/netbox/issues/2962) - Increase ExportTemplate `mime_type` field length * [#2966](https://github.com/digitalocean/netbox/issues/2966) - Accept `null` cable length_unit via API +* [#2972](https://github.com/digitalocean/netbox/issues/2972) - Improve ContentTypeField serializer to elegantly handle invalid data +* [#2976](https://github.com/digitalocean/netbox/issues/2976) - Add delete button to tag view +* [#2980](https://github.com/digitalocean/netbox/issues/2980) - Improve rendering time for API docs +* [#2982](https://github.com/digitalocean/netbox/issues/2982) - Correct CSS class assignment on color picker +* [#2984](https://github.com/digitalocean/netbox/issues/2984) - Fix logging of unlabeled cable ID on cable deletion +* [#2985](https://github.com/digitalocean/netbox/issues/2985) - Fix pagination page length for rack elevations --- diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index c6a215db8..1cddeffb2 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -59,7 +59,7 @@ class CircuitTypeTable(BaseTable): name = tables.LinkColumn() circuit_count = tables.Column(verbose_name='Circuits') actions = tables.TemplateColumn( - template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) class Meta(BaseTable.Meta): diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 9d5bd2dfc..cbfce0b91 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2550,16 +2550,15 @@ class Cable(ChangeLoggedModel): ('termination_b_type', 'termination_b_id'), ) - def __init__(self, *args, **kwargs): - - super().__init__(*args, **kwargs) - - # Create an ID string for use by __str__(). We have to save a copy of pk since it's nullified after .delete() - # is called. - self.id_string = '#{}'.format(self.pk) - def __str__(self): - return self.label or self.id_string + if self.label: + return self.label + + # Save a copy of the PK on the instance since it's nullified if .delete() is called + if not hasattr(self, 'id_string'): + self.id_string = '#{}'.format(self.pk) + + return self.id_string def get_absolute_url(self): return reverse('dcim:cable', args=[self.pk]) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 5649c10ef..436b9053d 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -196,7 +196,7 @@ class RegionTable(BaseTable): slug = tables.Column(verbose_name='Slug') actions = tables.TemplateColumn( template_code=REGION_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) @@ -239,7 +239,7 @@ class RackGroupTable(BaseTable): slug = tables.Column() actions = tables.TemplateColumn( template_code=RACKGROUP_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) @@ -258,7 +258,7 @@ class RackRoleTable(BaseTable): rack_count = tables.Column(verbose_name='Racks') color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color') slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, + actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') class Meta(BaseTable.Meta): @@ -309,7 +309,7 @@ class RackReservationTable(BaseTable): rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) unit_list = tables.Column(orderable=False, verbose_name='Units') actions = tables.TemplateColumn( - template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) class Meta(BaseTable.Meta): @@ -327,7 +327,7 @@ class ManufacturerTable(BaseTable): devicetype_count = tables.Column(verbose_name='Device Types') platform_count = tables.Column(verbose_name='Platforms') slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}}, + actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') class Meta(BaseTable.Meta): @@ -463,7 +463,7 @@ class DeviceRoleTable(BaseTable): slug = tables.Column(verbose_name='Slug') actions = tables.TemplateColumn( template_code=DEVICEROLE_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) @@ -492,7 +492,7 @@ class PlatformTable(BaseTable): ) actions = tables.TemplateColumn( template_code=PLATFORM_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) @@ -779,7 +779,7 @@ class VirtualChassisTable(BaseTable): member_count = tables.Column(verbose_name='Members') actions = tables.TemplateColumn( template_code=VIRTUALCHASSIS_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index dfe94625e..27f90a3a2 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,5 +1,6 @@ import re +from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.paginator import EmptyPage, PageNotAnInteger @@ -353,8 +354,9 @@ class RackElevationListView(View): total_count = racks.count() # Pagination - paginator = EnhancedPaginator(racks, 25) + per_page = request.GET.get('per_page', settings.PAGINATE_COUNT) page_number = request.GET.get('page', 1) + paginator = EnhancedPaginator(racks, per_page) try: page = paginator.page(page_number) except PageNotAnInteger: diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index d1454db4f..a5545693e 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -70,7 +70,7 @@ class TagTable(BaseTable): ) actions = tables.TemplateColumn( template_code=TAG_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) color = ColorColumn() diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index d0e25f580..1274164ca 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -349,11 +349,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): class PrefixCSVForm(forms.ModelForm): - vrf = forms.ModelChoiceField( + vrf = FlexibleModelChoiceField( queryset=VRF.objects.all(), - required=False, to_field_name='rd', - help_text='Route distinguisher of parent VRF', + required=False, + help_text='Route distinguisher of parent VRF (or {ID})', error_messages={ 'invalid_choice': 'VRF not found.', } @@ -764,11 +764,11 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm): class IPAddressCSVForm(forms.ModelForm): - vrf = forms.ModelChoiceField( + vrf = FlexibleModelChoiceField( queryset=VRF.objects.all(), - required=False, to_field_name='rd', - help_text='Route distinguisher of the assigned VRF', + required=False, + help_text='Route distinguisher of parent VRF (or {ID})', error_messages={ 'invalid_choice': 'VRF not found.', } diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index a8a6185fb..8ffb6f51b 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -1,7 +1,7 @@ import netaddr from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation -from django.core.exceptions import ValidationError +from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import Q @@ -10,8 +10,9 @@ from django.urls import reverse from taggit.managers import TaggableManager from dcim.models import Interface -from extras.models import CustomFieldModel, TaggedItem +from extras.models import CustomFieldModel, ObjectChange, TaggedItem from utilities.models import ChangeLoggedModel +from utilities.utils import serialize_object from .constants import * from .fields import IPNetworkField, IPAddressField from .querysets import PrefixQuerySet @@ -629,6 +630,27 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): self.family = self.address.version super().save(*args, **kwargs) + def log_change(self, user, request_id, action): + """ + Include the connected Interface (if any). + """ + + # It's possible that an IPAddress can be deleted _after_ its parent Interface, in which case trying to resolve + # the interface will raise DoesNotExist. + try: + parent_obj = self.interface + except ObjectDoesNotExist: + parent_obj = None + + ObjectChange( + user=user, + request_id=request_id, + changed_object=self, + related_object=parent_obj, + action=action, + object_data=serialize_object(self) + ).save() + def to_csv(self): # Determine if this IP is primary for a Device diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 026cbc980..3d46452b2 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -203,7 +203,7 @@ class RIRTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') is_private = BooleanColumn(verbose_name='Private') aggregate_count = tables.Column(verbose_name='Aggregates') - actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='') + actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') class Meta(BaseTable.Meta): model = RIR @@ -288,7 +288,7 @@ class RoleTable(BaseTable): orderable=False, verbose_name='VLANs' ) - actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='') + actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') class Meta(BaseTable.Meta): model = Role @@ -392,7 +392,7 @@ class VLANGroupTable(BaseTable): site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') vlan_count = tables.Column(verbose_name='VLANs') slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}}, + actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='') class Meta(BaseTable.Meta): @@ -437,7 +437,7 @@ class VLANMemberTable(BaseTable): ) actions = tables.TemplateColumn( template_code=VLAN_MEMBER_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 08bd63588..6a5e33f11 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -316,6 +316,7 @@ SWAGGER_SETTINGS = { 'utilities.custom_inspectors.IdInFilterInspector', 'drf_yasg.inspectors.CoreAPICompatInspector', ], + 'DEFAULT_MODEL_DEPTH': 1, 'DEFAULT_PAGINATOR_INSPECTORS': [ 'utilities.custom_inspectors.NullablePaginatorInspector', 'drf_yasg.inspectors.DjangoRestResponsePagination', diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index ad618b5d1..26ca50220 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -49,6 +49,19 @@ footer p { } } +/* Printer friendly CSS class and various fixes for printing. */ +@media print { + body { + padding-top: 0px; + } + a[href]:after { + content: none !important; + } + .noprint { + display: none !important; + } +} + /* Collapse the nav menu on displays less than 960px wide */ @media (max-width: 959px) { .navbar-header { @@ -575,4 +588,4 @@ td .progress { } textarea { font-family: Consolas, Lucida Console, monospace; -} \ No newline at end of file +} diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 438882805..2a6bf92ff 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -90,6 +90,10 @@ $(document).ready(function() { // Assign color picker selection classes function colorPickerClassCopy(data, container) { if (data.element) { + // Remove any existing color-selection classes + $(container).attr('class', function(i, c) { + return c.replace(/(^|\s)color-selection-\S+/g, ''); + }); $(container).addClass($(data.element).attr("class")); } return data.text; diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py index 39d260a6d..a547ef4f8 100644 --- a/netbox/secrets/tables.py +++ b/netbox/secrets/tables.py @@ -23,7 +23,7 @@ class SecretRoleTable(BaseTable): secret_count = tables.Column(verbose_name='Secrets') slug = tables.Column(verbose_name='Slug') actions = tables.TemplateColumn( - template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) class Meta(BaseTable.Meta): diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 9101e08f7..02b6bb32c 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -54,7 +54,7 @@
{% now 'Y-m-d H:i:s T' %}