822 bulk import of device components (#3711)

Closes #822: CSV import for device components

* Implement CSV import for netbox-community#822

* Comment out default_return_url until there is a proper target

* Fix the default value of `enabled` when not included in the import

* rear_port is definitely required here

* Power Ports don't have a type (yet)

* Add import for console-ports and console-server-ports

* Add import for device-bays
This commit is contained in:
Sander Steffann 2019-12-05 21:36:11 +01:00 committed by Jeremy Stretch
parent f3a41df395
commit adb25fd7d7
23 changed files with 523 additions and 41 deletions

View File

@ -36,13 +36,6 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases) instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases)
and run `upgrade.sh`. and run `upgrade.sh`.
## Alternative Installations
* [Docker container](https://github.com/netbox-community/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
* [Kubernetes deployment](https://github.com/CENGN/netbox-kubernetes) (via [@CENGN](https://github.com/CENGN))
# Providing Feedback # Providing Feedback
Feature requests and bug reports must be submitted as GiHub issues. (Please be Feature requests and bug reports must be submitted as GiHub issues. (Please be

View File

@ -182,7 +182,7 @@ class NewBranchScript(Script):
class Meta: class Meta:
name = "New Branch" name = "New Branch"
description = "Provision a new branch site" description = "Provision a new branch site"
fields = ['site_name', 'switch_count', 'switch_model'] field_order = ['site_name', 'switch_count', 'switch_model']
site_name = StringVar( site_name = StringVar(
description="Name of the new site" description="Name of the new site"

View File

@ -32,7 +32,6 @@ server {
proxy_set_header X-Forwarded-Host $server_name; proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
} }
} }
``` ```

View File

@ -1,3 +1,18 @@
# v2.6.8 (FUTURE)
## Enhancements
* [#3139](https://github.com/netbox-community/netbox/issues/3139) - Disable password change form for LDAP-authenticated users
* [#3457](https://github.com/netbox-community/netbox/issues/3457) - Display cable colors on device view
* [#3329](https://github.com/netbox-community/netbox/issues/3329) - Remove obsolete P3P policy header
* [#3663](https://github.com/netbox-community/netbox/issues/3663) - Add query filters for `created` and `last_updated` fields
## Bug Fixes
* [#3669](https://github.com/netbox-community/netbox/issues/3669) - Include `weight` field in prefix/VLAN role form
* [#3674](https://github.com/netbox-community/netbox/issues/3674) - Include comments on PowerFeed view
* [#3679](https://github.com/netbox-community/netbox/issues/3679) - Fix link for assigned ipaddress in interface page
# v2.6.7 (2019-11-01) # v2.6.7 (2019-11-01)
## Enhancements ## Enhancements

View File

@ -2,14 +2,14 @@ import django_filters
from django.db.models import Q from django.db.models import Q
from dcim.models import Region, Site from dcim.models import Region, Site
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
from .constants import * from .constants import *
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
class ProviderFilter(CustomFieldFilterSet): class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet): class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

View File

@ -2,7 +2,7 @@ import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES from utilities.constants import COLOR_CHOICES
@ -39,7 +39,7 @@ class RegionFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class SiteFilter(TenancyFilterSet, CustomFieldFilterSet): class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -117,7 +117,7 @@ class RackRoleFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'color'] fields = ['id', 'name', 'slug', 'color']
class RackFilter(TenancyFilterSet, CustomFieldFilterSet): class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -252,7 +252,7 @@ class ManufacturerFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class DeviceTypeFilter(CustomFieldFilterSet): class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -424,7 +424,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver'] fields = ['id', 'name', 'slug', 'napalm_driver']
class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet): class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -1113,7 +1113,7 @@ class PowerPanelFilter(django_filters.FilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class PowerFeedFilter(CustomFieldFilterSet): class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

View File

@ -25,7 +25,7 @@ from utilities.forms import (
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField, ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
) )
from virtualization.models import Cluster, ClusterGroup from virtualization.models import Cluster, ClusterGroup, VirtualMachine
from .choices import * from .choices import *
from .constants import * from .constants import *
from .models import ( from .models import (
@ -2096,6 +2096,21 @@ class ConsolePortCreateForm(ComponentForm):
) )
class ConsolePortCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
class Meta:
model = ConsolePort
fields = ConsolePort.csv_headers
# #
# Console server ports # Console server ports
# #
@ -2168,6 +2183,21 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
) )
class ConsoleServerPortCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
class Meta:
model = ConsoleServerPort
fields = ConsoleServerPort.csv_headers
# #
# Power ports # Power ports
# #
@ -2215,6 +2245,21 @@ class PowerPortCreateForm(ComponentForm):
) )
class PowerPortCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
class Meta:
model = PowerPort
fields = PowerPort.csv_headers
# #
# Power outlets # Power outlets
# #
@ -2280,6 +2325,56 @@ class PowerOutletCreateForm(ComponentForm):
self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent) self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent)
class PowerOutletCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
power_port = FlexibleModelChoiceField(
queryset=PowerPort.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of Power Port',
error_messages={
'invalid_choice': 'Power Port not found.',
}
)
feed_leg = CSVChoiceField(
choices=POWERFEED_LEG_CHOICES,
required=False,
)
class Meta:
model = PowerOutlet
fields = PowerOutlet.csv_headers
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit PowerPort choices to those belonging to this device (or VC master)
if self.is_bound:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
device = None
else:
try:
device = self.instance.device
except Device.DoesNotExist:
device = None
if device:
self.fields['power_port'].queryset = PowerPort.objects.filter(
device__in=[device, device.get_vc_master()]
)
else:
self.fields['power_port'].queryset = PowerPort.objects.none()
class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=PowerOutlet.objects.all(), queryset=PowerOutlet.objects.all(),
@ -2531,6 +2626,73 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
self.fields['tagged_vlans'].choices = vlan_choices self.fields['tagged_vlans'].choices = vlan_choices
class InterfaceCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
virtual_machine = FlexibleModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of virtual machine',
error_messages={
'invalid_choice': 'Virtual machine not found.',
}
)
lag = FlexibleModelChoiceField(
queryset=Interface.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of LAG interface',
error_messages={
'invalid_choice': 'LAG interface not found.',
}
)
type = CSVChoiceField(
choices=IFACE_TYPE_CHOICES,
)
mode = CSVChoiceField(
choices=IFACE_MODE_CHOICES,
required=False,
)
class Meta:
model = Interface
fields = Interface.csv_headers
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device (or VC master)
if self.is_bound:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
device = None
else:
device = self.instance.device
if device:
self.fields['lag'].queryset = Interface.objects.filter(
device__in=[device, device.get_vc_master()], type=IFACE_TYPE_LAG
)
else:
self.fields['lag'].queryset = Interface.objects.none()
def clean_enabled(self):
# Make sure enabled is True when it's not included in the uploaded data
if 'enabled' not in self.data:
return True
else:
return self.cleaned_data['enabled']
class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
@ -2747,6 +2909,54 @@ class FrontPortCreateForm(ComponentForm):
} }
class FrontPortCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
rear_port = FlexibleModelChoiceField(
queryset=RearPort.objects.all(),
to_field_name='name',
help_text='Name or ID of Rear Port',
error_messages={
'invalid_choice': 'Rear Port not found.',
}
)
type = CSVChoiceField(
choices=PORT_TYPE_CHOICES,
)
class Meta:
model = FrontPort
fields = FrontPort.csv_headers
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit RearPort choices to those belonging to this device (or VC master)
if self.is_bound:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
device = None
else:
try:
device = self.instance.device
except Device.DoesNotExist:
device = None
if device:
self.fields['rear_port'].queryset = RearPort.objects.filter(
device__in=[device, device.get_vc_master()]
)
else:
self.fields['rear_port'].queryset = RearPort.objects.none()
class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=FrontPort.objects.all(), queryset=FrontPort.objects.all(),
@ -2821,6 +3031,24 @@ class RearPortCreateForm(ComponentForm):
) )
class RearPortCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
type = CSVChoiceField(
choices=PORT_TYPE_CHOICES,
)
class Meta:
model = RearPort
fields = RearPort.csv_headers
class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=RearPort.objects.all(), queryset=RearPort.objects.all(),
@ -3389,6 +3617,56 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
).exclude(pk=device_bay.device.pk) ).exclude(pk=device_bay.device.pk)
class DeviceBayCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
installed_device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Child device not found.',
}
)
class Meta:
model = DeviceBay
fields = DeviceBay.csv_headers
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit installed device choices to devices of the correct type and location
if self.is_bound:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
device = None
else:
try:
device = self.instance.device
except Device.DoesNotExist:
device = None
if device:
self.fields['installed_device'].queryset = Device.objects.filter(
site=device.site,
rack=device.rack,
parent_bay__isnull=True,
device_type__u_height=0,
device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
).exclude(pk=device.pk)
else:
self.fields['installed_device'].queryset = Interface.objects.none()
class DeviceBayBulkRenameForm(BulkRenameForm): class DeviceBayBulkRenameForm(BulkRenameForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=DeviceBay.objects.all(), queryset=DeviceBay.objects.all(),

View File

@ -426,6 +426,15 @@ class ConsolePortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class ConsolePortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = ConsolePort
fields = ('device', 'name', 'description')
empty_text = False
class ConsoleServerPortTemplateTable(BaseTable): class ConsoleServerPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@ -440,6 +449,15 @@ class ConsoleServerPortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class ConsoleServerPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = ConsoleServerPort
fields = ('device', 'name', 'description')
empty_text = False
class PowerPortTemplateTable(BaseTable): class PowerPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@ -454,6 +472,15 @@ class PowerPortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class PowerPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = PowerPort
fields = ('device', 'name', 'description', 'maximum_draw', 'allocated_draw')
empty_text = False
class PowerOutletTemplateTable(BaseTable): class PowerOutletTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@ -468,6 +495,15 @@ class PowerOutletTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class PowerOutletImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = PowerOutlet
fields = ('device', 'name', 'description', 'power_port', 'feed_leg')
empty_text = False
class InterfaceTemplateTable(BaseTable): class InterfaceTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}") mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
@ -483,6 +519,16 @@ class InterfaceTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class InterfaceImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
virtual_machine = tables.LinkColumn('virtualization:virtualmachine', args=[Accessor('virtual_machine.pk')], verbose_name='Virtual Machine')
class Meta(BaseTable.Meta):
model = Interface
fields = ('device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode')
empty_text = False
class FrontPortTemplateTable(BaseTable): class FrontPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
rear_port_position = tables.Column( rear_port_position = tables.Column(
@ -500,6 +546,15 @@ class FrontPortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class FrontPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = FrontPort
fields = ('device', 'name', 'description', 'type', 'rear_port', 'rear_port_position')
empty_text = False
class RearPortTemplateTable(BaseTable): class RearPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@ -514,6 +569,15 @@ class RearPortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class RearPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = RearPort
fields = ('device', 'name', 'description', 'type', 'position')
empty_text = False
class DeviceBayTemplateTable(BaseTable): class DeviceBayTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@ -701,6 +765,16 @@ class DeviceBayTable(BaseTable):
fields = ('name',) fields = ('name',)
class DeviceBayImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
installed_device = tables.LinkColumn('dcim:device', args=[Accessor('installed_device.pk')], verbose_name='Installed Device')
class Meta(BaseTable.Meta):
model = DeviceBay
fields = ('device', 'name', 'installed_device', 'description')
empty_text = False
# #
# Cables # Cables
# #

View File

@ -175,6 +175,7 @@ urlpatterns = [
path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
path(r'console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
# Console server ports # Console server ports
path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
@ -187,6 +188,7 @@ urlpatterns = [
path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'), path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
path(r'console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'),
# Power ports # Power ports
path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
@ -196,6 +198,7 @@ urlpatterns = [
path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
path(r'power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
# Power outlets # Power outlets
path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
@ -208,6 +211,7 @@ urlpatterns = [
path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'), path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
path(r'power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'),
# Interfaces # Interfaces
path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
@ -222,6 +226,7 @@ urlpatterns = [
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'), path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
path(r'interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'),
# Front ports # Front ports
# path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), # path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
@ -234,6 +239,7 @@ urlpatterns = [
path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'), path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'), path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
path(r'front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'),
# Rear ports # Rear ports
# path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), # path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
@ -246,6 +252,7 @@ urlpatterns = [
path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'), path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'), path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
path(r'rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'),
# Device bays # Device bays
path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
@ -256,6 +263,7 @@ urlpatterns = [
path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'), path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
path(r'device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
# Inventory items # Inventory items
path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'), path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'),

View File

@ -1218,6 +1218,14 @@ class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = ConsolePort model = ConsolePort
class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_consoleport'
model_form = forms.ConsolePortCSVForm
table = tables.ConsolePortImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:consoleport_list'
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport' permission_required = 'dcim.delete_consoleport'
queryset = ConsolePort.objects.all() queryset = ConsolePort.objects.all()
@ -1250,6 +1258,14 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = ConsoleServerPort model = ConsoleServerPort
class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_consoleserverport'
model_form = forms.ConsoleServerPortCSVForm
table = tables.ConsoleServerPortImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:consoleserverport_list'
class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView): class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_consoleserverport' permission_required = 'dcim.change_consoleserverport'
queryset = ConsoleServerPort.objects.all() queryset = ConsoleServerPort.objects.all()
@ -1302,6 +1318,14 @@ class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = PowerPort model = PowerPort
class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_powerport'
model_form = forms.PowerPortCSVForm
table = tables.PowerPortImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:powerport_list'
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport' permission_required = 'dcim.delete_powerport'
queryset = PowerPort.objects.all() queryset = PowerPort.objects.all()
@ -1334,6 +1358,14 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = PowerOutlet model = PowerOutlet
class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_poweroutlet'
model_form = forms.PowerOutletCSVForm
table = tables.PowerOutletImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:poweroutlet_list'
class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView): class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_poweroutlet' permission_required = 'dcim.change_poweroutlet'
queryset = PowerOutlet.objects.all() queryset = PowerOutlet.objects.all()
@ -1423,6 +1455,14 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = Interface model = Interface
class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_interface'
model_form = forms.InterfaceCSVForm
table = tables.InterfaceImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:interface_list'
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface' permission_required = 'dcim.change_interface'
queryset = Interface.objects.all() queryset = Interface.objects.all()
@ -1475,6 +1515,14 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = FrontPort model = FrontPort
class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_frontport'
model_form = forms.FrontPortCSVForm
table = tables.FrontPortImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:frontport_list'
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView): class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_frontport' permission_required = 'dcim.change_frontport'
queryset = FrontPort.objects.all() queryset = FrontPort.objects.all()
@ -1527,6 +1575,14 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = RearPort model = RearPort
class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_rearport'
model_form = forms.RearPortCSVForm
table = tables.RearPortImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:rearport_list'
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView): class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rearport' permission_required = 'dcim.change_rearport'
queryset = RearPort.objects.all() queryset = RearPort.objects.all()
@ -1648,6 +1704,14 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View):
}) })
class DeviceBayBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_devicebay'
model_form = forms.DeviceBayCSVForm
table = tables.DeviceBayImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:devicebay_list'
class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView): class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_devicebay' permission_required = 'dcim.change_devicebay'
queryset = DeviceBay.objects.all() queryset = DeviceBay.objects.all()

View File

@ -18,7 +18,7 @@ router.APIRootView = ExtrasRootView
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice') router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
# Custom field choices # Custom field choices
router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, base_name='custom-field-choice') router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')
# Graphs # Graphs
router.register(r'graphs', views.GraphViewSet) router.register(r'graphs', views.GraphViewSet)

View File

@ -223,3 +223,24 @@ class ObjectChangeFilter(django_filters.FilterSet):
Q(user_name__icontains=value) | Q(user_name__icontains=value) |
Q(object_repr__icontains=value) Q(object_repr__icontains=value)
) )
class CreatedUpdatedFilterSet(django_filters.FilterSet):
created = django_filters.DateFilter()
created__gte = django_filters.DateFilter(
field_name='created',
lookup_expr='gte'
)
created__lte = django_filters.DateFilter(
field_name='created',
lookup_expr='lte'
)
last_updated = django_filters.DateTimeFilter()
last_updated__gte = django_filters.DateTimeFilter(
field_name='last_updated',
lookup_expr='gte'
)
last_updated__lte = django_filters.DateTimeFilter(
field_name='last_updated',
lookup_expr='lte'
)

View File

@ -5,7 +5,7 @@ from django.db.models import Q
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
@ -13,7 +13,7 @@ from .constants import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
class VRFFilter(TenancyFilterSet, CustomFieldFilterSet): class VRFFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -49,7 +49,7 @@ class RIRFilter(NameSlugSearchFilterSet):
fields = ['name', 'slug', 'is_private'] fields = ['name', 'slug', 'is_private']
class AggregateFilter(CustomFieldFilterSet): class AggregateFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -110,7 +110,7 @@ class RoleFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet): class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -247,7 +247,7 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet):
return queryset.filter(prefix__net_mask_length=value) return queryset.filter(prefix__net_mask_length=value)
class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet): class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -384,7 +384,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class VLANFilter(TenancyFilterSet, CustomFieldFilterSet): class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -444,7 +444,7 @@ class VLANFilter(TenancyFilterSet, CustomFieldFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class ServiceFilter(django_filters.FilterSet): class ServiceFilter(CreatedUpdatedFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',

View File

@ -240,7 +240,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = Role model = Role
fields = [ fields = [
'name', 'slug', 'name', 'slug', 'weight',
] ]

View File

@ -85,7 +85,7 @@ IPADDRESS_LINK = """
""" """
IPADDRESS_ASSIGN_LINK = """ IPADDRESS_ASSIGN_LINK = """
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a> <a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ record.interface.pk }}&return_url={{ request.path }}">{{ record }}</a>
""" """
IPADDRESS_PARENT = """ IPADDRESS_PARENT = """
@ -292,7 +292,7 @@ class RoleTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Role model = Role
fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'actions') fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'weight', 'actions')
# #

View File

@ -457,6 +457,14 @@ table.report th a {
width: 80px; width: 80px;
border: 1px solid grey; border: 1px solid grey;
} }
.inline-color-block {
display: inline-block;
width: 1.5em;
height: 1.5em;
border: 1px solid grey;
border-radius: .25em;
vertical-align: middle;
}
.text-nowrap { .text-nowrap {
white-space: nowrap; white-space: nowrap;
} }

View File

@ -2,7 +2,7 @@ import django_filters
from django.db.models import Q from django.db.models import Q
from dcim.models import Device from dcim.models import Device
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .models import Secret, SecretRole from .models import Secret, SecretRole
@ -14,7 +14,7 @@ class SecretRoleFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class SecretFilter(CustomFieldFilterSet): class SecretFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

View File

@ -48,6 +48,9 @@
<td class="text-nowrap"> <td class="text-nowrap">
{% if iface.cable %} {% if iface.cable %}
<a href="{{ iface.cable.get_absolute_url }}">{{ iface.cable }}</a> <a href="{{ iface.cable.get_absolute_url }}">{{ iface.cable }}</a>
{% if iface.cable.color %}
<span class="inline-color-block" style="background-color: #{{ iface.cable.color }}">&nbsp;</span>
{% endif %}
<a href="{% url 'dcim:interface_trace' pk=iface.pk %}" class="btn btn-primary btn-xs" title="Trace"> <a href="{% url 'dcim:interface_trace' pk=iface.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i> <i class="fa fa-share-alt" aria-hidden="true"></i>
</a> </a>

View File

@ -121,6 +121,18 @@
</tr> </tr>
</table> </table>
</div> </div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body rendered-markdown">
{% if powerfeed.comments %}
{{ powerfeed.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -12,9 +12,11 @@
<li{% ifequal active_tab "profile" %} class="active"{% endifequal %}> <li{% ifequal active_tab "profile" %} class="active"{% endifequal %}>
<a href="{% url 'user:profile' %}">Profile</a> <a href="{% url 'user:profile' %}">Profile</a>
</li> </li>
{% if not request.user.ldap_username %}
<li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}> <li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}>
<a href="{% url 'user:change_password' %}">Change Password</a> <a href="{% url 'user:change_password' %}">Change Password</a>
</li> </li>
{% endif %}
<li{% ifequal active_tab "api_tokens" %} class="active"{% endifequal %}> <li{% ifequal active_tab "api_tokens" %} class="active"{% endifequal %}>
<a href="{% url 'user:token_list' %}">API Tokens</a> <a href="{% url 'user:token_list' %}">API Tokens</a>
</li> </li>

View File

@ -1,7 +1,7 @@
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .models import Tenant, TenantGroup from .models import Tenant, TenantGroup
@ -13,7 +13,7 @@ class TenantGroupFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class TenantFilter(CustomFieldFilterSet): class TenantFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

View File

@ -95,6 +95,11 @@ class ChangePasswordView(LoginRequiredMixin, View):
template_name = 'users/change_password.html' template_name = 'users/change_password.html'
def get(self, request): def get(self, request):
# LDAP users cannot change their password here
if getattr(request.user, 'ldap_username'):
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
return redirect('user:profile')
form = PasswordChangeForm(user=request.user) form = PasswordChangeForm(user=request.user)
return render(request, self.template_name, { return render(request, self.template_name, {

View File

@ -4,9 +4,9 @@ from netaddr import EUI
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from dcim.models import DeviceRole, Interface, Platform, Region, Site from dcim.models import DeviceRole, Interface, Platform, Region, Site
from tenancy.models import Tenant from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from extras.filters import CustomFieldFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant
from utilities.filters import ( from utilities.filters import (
MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter, MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
) )
@ -28,7 +28,7 @@ class ClusterGroupFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class ClusterFilter(CustomFieldFilterSet): class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -86,7 +86,7 @@ class ClusterFilter(CustomFieldFilterSet):
) )
class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet): class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'