diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2943106..1a8996bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +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 + +--- + v2.5.7 (2019-02-21) ## Enhancements @@ -24,6 +54,8 @@ v2.5.7 (2019-02-21) * [#2914](https://github.com/digitalocean/netbox/issues/2914) - Fix empty connected circuit link on device interfaces list * [#2915](https://github.com/digitalocean/netbox/issues/2915) - Fix bulk editing of pass-through ports +--- + v2.5.6 (2019-02-13) ## Enhancements diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index d481deb54..4deee57c9 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -107,7 +107,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', - widget=APISelect( + widget=APISelectMultiple( api_url="/api/dcim/sites/", value_field="slug", ) 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/api/serializers.py b/netbox/dcim/api/serializers.py index c17400a35..4c65a3a19 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -507,7 +507,7 @@ class CableSerializer(ValidatedModelSerializer): termination_a = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True) status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) - length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False) + length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True) class Meta: model = Cable diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 4e14d8163..8fddc7129 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -496,11 +496,11 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet): class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): queryset = Interface.objects.select_related( - 'device', '_connected_interface', '_connected_circuittermination' + 'device', '_connected_interface__device' ).filter( # Avoid duplicate connections by only selecting the lower PK in a connected pair - Q(_connected_interface__isnull=False, pk__lt=F('_connected_interface')) | - Q(_connected_circuittermination__isnull=False) + _connected_interface__isnull=False, + pk__lt=F('_connected_interface') ) serializer_class = serializers.InterfaceConnectionSerializer filterset_class = filters.InterfaceConnectionFilter diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 96ecefafd..dda904f1c 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,6 +1,5 @@ import django_filters from django.contrib.auth.models import User -from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError @@ -8,7 +7,9 @@ from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES -from utilities.filters import NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter +from utilities.filters import ( + NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter +) from virtualization.models import Cluster from .constants import * from .models import ( @@ -49,14 +50,15 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): choices=SITE_STATUS_CHOICES, null_value=None ) - region_id = django_filters.NumberFilter( - method='filter_region', - field_name='pk', + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='region__in', label='Region (ID)', ) - region = django_filters.CharFilter( - method='filter_region', - field_name='slug', + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='region__in', + to_field_name='slug', label='Region (slug)', ) tenant_id = django_filters.ModelMultipleChoiceFilter( @@ -95,16 +97,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): pass return queryset.filter(qs_filter) - def filter_region(self, queryset, name, value): - try: - region = Region.objects.get(**{name: value}) - except ObjectDoesNotExist: - return queryset.none() - return queryset.filter( - Q(region=region) | - Q(region__in=region.get_descendants()) - ) - class RackGroupFilter(NameSlugSearchFilterSet): site_id = django_filters.ModelMultipleChoiceFilter( @@ -513,14 +505,15 @@ class DeviceFilter(CustomFieldFilterSet): ) name = NullableCharFieldFilter() asset_tag = NullableCharFieldFilter() - region_id = django_filters.NumberFilter( - method='filter_region', - field_name='pk', + region_id = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', label='Region (ID)', ) - region = django_filters.CharFilter( - method='filter_region', - field_name='slug', + region = TreeNodeMultipleChoiceFilter( + queryset=Region.objects.all(), + field_name='site__region__in', + to_field_name='slug', label='Region (slug)', ) site_id = django_filters.ModelMultipleChoiceFilter( @@ -619,16 +612,6 @@ class DeviceFilter(CustomFieldFilterSet): Q(comments__icontains=value) ).distinct() - def filter_region(self, queryset, name, value): - try: - region = Region.objects.get(**{name: value}) - except ObjectDoesNotExist: - return queryset.none() - return queryset.filter( - Q(site__region=region) | - Q(site__region__in=region.get_descendants()) - ) - def _mac_address(self, queryset, name, value): value = value.strip() if not value: diff --git a/netbox/dcim/managers.py b/netbox/dcim/managers.py index 52df1afe8..feaa09d74 100644 --- a/netbox/dcim/managers.py +++ b/netbox/dcim/managers.py @@ -27,7 +27,7 @@ class DeviceComponentManager(Manager): select={ 'name_padded': sql.format(table_name, table_name), } - ).order_by('name_padded') + ).order_by('name_padded', 'pk') class InterfaceQuerySet(QuerySet): diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 49879beb1..004d7b1aa 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -2423,7 +2423,7 @@ class InventoryItem(ComponentModel): def to_csv(self): return ( - self.device.name or '{' + self.device.pk + '}', + self.device.name or '{{{}}}'.format(self.device.pk), self.name, self.manufacturer.name if self.manufacturer else None, self.part_id, @@ -2557,16 +2557,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/middleware.py b/netbox/extras/middleware.py index 16461c32a..38dde6275 100644 --- a/netbox/extras/middleware.py +++ b/netbox/extras/middleware.py @@ -29,7 +29,11 @@ def cache_changed_object(instance, **kwargs): def _record_object_deleted(request, instance, **kwargs): - # Record that the object was deleted. + # Force resolution of request.user in case it's still a SimpleLazyObject. This seems to happen + # occasionally during tests, but haven't been able to determine why. + assert request.user.is_authenticated + + # Record that the object was deleted if hasattr(instance, 'log_change'): instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE) diff --git a/netbox/extras/migrations/0017_exporttemplate_mime_type_length.py b/netbox/extras/migrations/0017_exporttemplate_mime_type_length.py new file mode 100644 index 000000000..29283e0d1 --- /dev/null +++ b/netbox/extras/migrations/0017_exporttemplate_mime_type_length.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.7 on 2019-03-05 18:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0016_exporttemplate_add_cable'), + ] + + operations = [ + migrations.AlterField( + model_name='exporttemplate', + name='mime_type', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index d3b9f4eff..1b106a62a 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -357,7 +357,7 @@ class ExportTemplate(models.Model): ) template_code = models.TextField() mime_type = models.CharField( - max_length=15, + max_length=50, blank=True ) file_extension = models.CharField( diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index 5fab8910f..f6933bf48 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -68,7 +68,7 @@ class TagTable(BaseTable): ) actions = tables.TemplateColumn( template_code=TAG_ACTIONS, - attrs={'td': {'class': 'text-right'}}, + attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' ) 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 181852ad3..a2f7bbe07 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 +from extras.models import CustomFieldModel, ObjectChange 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 1d2c9d121..aabe6adbd 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ except ImportError: ) -VERSION = '2.5.7' +VERSION = '2.5.8' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -197,7 +197,7 @@ ROOT_URLCONF = 'netbox.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR + '/templates/'], + 'DIRS': [BASE_DIR + '/templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -223,7 +223,7 @@ USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) -STATIC_ROOT = BASE_DIR + '/static/' +STATIC_ROOT = BASE_DIR + '/static' STATIC_URL = '/{}static/'.format(BASE_PATH) STATICFILES_DIRS = ( os.path.join(BASE_DIR, "project-static"), @@ -315,6 +315,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/netbox/views.py b/netbox/netbox/views.py index ff11e3892..837d9473d 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -267,6 +267,7 @@ class SearchView(View): class APIRootView(APIView): _ignore_model_permissions = True exclude_from_schema = True + swagger_schema = None def get_view_name(self): return "API Root" 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' %}

-
+

Docs · API · diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index edbab3ed4..890b2a880 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -4,7 +4,7 @@ {% block title %}{{ circuit }}{% endblock %} {% block header %} -

+
-
+
{% if perms.circuits.change_circuit %} diff --git a/netbox/templates/circuits/circuit_list.html b/netbox/templates/circuits/circuit_list.html index 81e09c32b..d686bdf7a 100644 --- a/netbox/templates/circuits/circuit_list.html +++ b/netbox/templates/circuits/circuit_list.html @@ -2,7 +2,7 @@ {% load buttons %} {% block content %} -
+
{% if perms.circuits.add_circuit %} {% add_button 'circuits:circuit_add' %} {% import_button 'circuits:circuit_import' %} @@ -14,7 +14,7 @@
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
-
+
{% include 'inc/search_panel.html' %} {% include 'inc/tags_panel.html' %}
diff --git a/netbox/templates/circuits/circuittype_list.html b/netbox/templates/circuits/circuittype_list.html index 2b9469042..654d4ab09 100644 --- a/netbox/templates/circuits/circuittype_list.html +++ b/netbox/templates/circuits/circuittype_list.html @@ -2,7 +2,7 @@ {% load buttons %} {% block content %} -
+
{% if perms.circuits.add_circuittype %} {% add_button 'circuits:circuittype_add' %} {% import_button 'circuits:circuittype_import' %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index a31f093c9..3dd5d973f 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -5,7 +5,7 @@ {% block title %}{{ provider }}{% endblock %} {% block header %} -
+ -
+
{% if show_graphs %} @@ -521,7 +521,7 @@ {% endfor %} -