mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 09:28:38 -06:00
commit
d0ac4332ab
@ -25,7 +25,7 @@ Begin by installing all system packages required by NetBox and its dependencies.
|
|||||||
Before continuing with either platform, update pip (Python's package management tool) to its latest release:
|
Before continuing with either platform, update pip (Python's package management tool) to its latest release:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# pip install --upgrade pip
|
# pip3 install --upgrade pip
|
||||||
```
|
```
|
||||||
|
|
||||||
## Download NetBox
|
## Download NetBox
|
||||||
|
@ -328,6 +328,9 @@ A `PluginMenuButton` has the following attributes:
|
|||||||
* `color` - One of the choices provided by `ButtonColorChoices` (optional)
|
* `color` - One of the choices provided by `ButtonColorChoices` (optional)
|
||||||
* `permissions` - A list of permissions required to display this button (optional)
|
* `permissions` - A list of permissions required to display this button (optional)
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons.
|
||||||
|
|
||||||
## Extending Core Templates
|
## Extending Core Templates
|
||||||
|
|
||||||
Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
|
Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
|
||||||
|
@ -1,5 +1,30 @@
|
|||||||
# NetBox v2.9
|
# NetBox v2.9
|
||||||
|
|
||||||
|
## v2.9.3 (2020-09-04)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#4977](https://github.com/netbox-community/netbox/issues/4977) - Redirect authenticated users from login view
|
||||||
|
* [#5048](https://github.com/netbox-community/netbox/issues/5048) - Show the device/VM name when editing a component
|
||||||
|
* [#5072](https://github.com/netbox-community/netbox/issues/5072) - Add REST API filters for image attachments
|
||||||
|
* [#5080](https://github.com/netbox-community/netbox/issues/5080) - Add 8P6C, 8P4C, 8P2C port types
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#5046](https://github.com/netbox-community/netbox/issues/5046) - Disabled plugin menu items are no longer clickable
|
||||||
|
* [#5063](https://github.com/netbox-community/netbox/issues/5063) - Fix "add device" link in rack elevations for opposite side of half-depth devices
|
||||||
|
* [#5074](https://github.com/netbox-community/netbox/issues/5074) - Fix inclusion of VC member interfaces when viewing VC master
|
||||||
|
* [#5078](https://github.com/netbox-community/netbox/issues/5078) - Fix assignment of existing IP addresses to interfaces via web UI
|
||||||
|
* [#5081](https://github.com/netbox-community/netbox/issues/5081) - Fix exception during webhook processing with custom select field
|
||||||
|
* [#5085](https://github.com/netbox-community/netbox/issues/5085) - Fix ordering by assignment in IP addresses table
|
||||||
|
* [#5087](https://github.com/netbox-community/netbox/issues/5087) - Restore label field when editing console server ports, power ports, and power outlets
|
||||||
|
* [#5089](https://github.com/netbox-community/netbox/issues/5089) - Redirect to device view after editing component
|
||||||
|
* [#5090](https://github.com/netbox-community/netbox/issues/5090) - Fix status display for console/power/interface connections
|
||||||
|
* [#5091](https://github.com/netbox-community/netbox/issues/5091) - Avoid KeyError when handling invalid table preferences
|
||||||
|
* [#5095](https://github.com/netbox-community/netbox/issues/5095) - Show assigned prefixes in VLANs list
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.9.2 (2020-08-27)
|
## v2.9.2 (2020-08-27)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -814,6 +814,9 @@ class InterfaceModeChoices(ChoiceSet):
|
|||||||
class PortTypeChoices(ChoiceSet):
|
class PortTypeChoices(ChoiceSet):
|
||||||
|
|
||||||
TYPE_8P8C = '8p8c'
|
TYPE_8P8C = '8p8c'
|
||||||
|
TYPE_8P6C = '8p6c'
|
||||||
|
TYPE_8P4C = '8p4c'
|
||||||
|
TYPE_8P2C = '8p2c'
|
||||||
TYPE_110_PUNCH = '110-punch'
|
TYPE_110_PUNCH = '110-punch'
|
||||||
TYPE_BNC = 'bnc'
|
TYPE_BNC = 'bnc'
|
||||||
TYPE_MRJ21 = 'mrj21'
|
TYPE_MRJ21 = 'mrj21'
|
||||||
@ -833,6 +836,9 @@ class PortTypeChoices(ChoiceSet):
|
|||||||
'Copper',
|
'Copper',
|
||||||
(
|
(
|
||||||
(TYPE_8P8C, '8P8C'),
|
(TYPE_8P8C, '8P8C'),
|
||||||
|
(TYPE_8P6C, '8P6C'),
|
||||||
|
(TYPE_8P4C, '8P4C'),
|
||||||
|
(TYPE_8P2C, '8P2C'),
|
||||||
(TYPE_110_PUNCH, '110 Punch'),
|
(TYPE_110_PUNCH, '110 Punch'),
|
||||||
(TYPE_BNC, 'BNC'),
|
(TYPE_BNC, 'BNC'),
|
||||||
(TYPE_MRJ21, 'MRJ21'),
|
(TYPE_MRJ21, 'MRJ21'),
|
||||||
|
@ -149,7 +149,7 @@ class RackElevationSVG:
|
|||||||
unit_cursor = 0
|
unit_cursor = 0
|
||||||
for u in elevation:
|
for u in elevation:
|
||||||
o = other[unit_cursor]
|
o = other[unit_cursor]
|
||||||
if not u['device'] and o['device']:
|
if not u['device'] and o['device'] and o['device'].device_type.is_full_depth:
|
||||||
u['device'] = o['device']
|
u['device'] = o['device']
|
||||||
u['height'] = 1
|
u['height'] = 1
|
||||||
unit_cursor += u.get('height', 1)
|
unit_cursor += u.get('height', 1)
|
||||||
|
@ -2317,7 +2317,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'type', 'description', 'tags',
|
'device', 'name', 'label', 'type', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
@ -2390,7 +2390,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags',
|
'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
@ -2479,7 +2479,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'tags',
|
'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
|
@ -152,6 +152,10 @@ INTERFACE_TAGGED_VLANS = """
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
CONNECTION_STATUS = """
|
||||||
|
<span class="label label-{% if record.connection_status %}success{% else %}danger{% endif %}">{{ record.get_connection_status_display }}</span>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Regions
|
# Regions
|
||||||
@ -908,15 +912,20 @@ class ConsoleConnectionTable(BaseTable):
|
|||||||
verbose_name='Console Server'
|
verbose_name='Console Server'
|
||||||
)
|
)
|
||||||
connected_endpoint = tables.Column(
|
connected_endpoint = tables.Column(
|
||||||
|
linkify=True,
|
||||||
verbose_name='Port'
|
verbose_name='Port'
|
||||||
)
|
)
|
||||||
device = tables.Column(
|
device = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
|
linkify=True,
|
||||||
verbose_name='Console Port'
|
verbose_name='Console Port'
|
||||||
)
|
)
|
||||||
connection_status = BooleanColumn()
|
connection_status = tables.TemplateColumn(
|
||||||
|
template_code=CONNECTION_STATUS,
|
||||||
|
verbose_name='Status'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
@ -933,14 +942,20 @@ class PowerConnectionTable(BaseTable):
|
|||||||
)
|
)
|
||||||
outlet = tables.Column(
|
outlet = tables.Column(
|
||||||
accessor=Accessor('_connected_poweroutlet'),
|
accessor=Accessor('_connected_poweroutlet'),
|
||||||
|
linkify=True,
|
||||||
verbose_name='Outlet'
|
verbose_name='Outlet'
|
||||||
)
|
)
|
||||||
device = tables.Column(
|
device = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
|
linkify=True,
|
||||||
verbose_name='Power Port'
|
verbose_name='Power Port'
|
||||||
)
|
)
|
||||||
|
connection_status = tables.TemplateColumn(
|
||||||
|
template_code=CONNECTION_STATUS,
|
||||||
|
verbose_name='Status'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
@ -972,6 +987,10 @@ class InterfaceConnectionTable(BaseTable):
|
|||||||
args=[Accessor('_connected_interface__pk')],
|
args=[Accessor('_connected_interface__pk')],
|
||||||
verbose_name='Interface B'
|
verbose_name='Interface B'
|
||||||
)
|
)
|
||||||
|
connection_status = tables.TemplateColumn(
|
||||||
|
template_code=CONNECTION_STATUS,
|
||||||
|
verbose_name='Status'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
|
@ -1033,7 +1033,7 @@ class DeviceView(ObjectView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Interfaces
|
# Interfaces
|
||||||
interfaces = device.vc_interfaces.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related(
|
||||||
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
||||||
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
||||||
'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable',
|
'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable',
|
||||||
@ -1233,6 +1233,7 @@ class ConsolePortCreateView(ComponentCreateView):
|
|||||||
class ConsolePortEditView(ObjectEditView):
|
class ConsolePortEditView(ObjectEditView):
|
||||||
queryset = ConsolePort.objects.all()
|
queryset = ConsolePort.objects.all()
|
||||||
model_form = forms.ConsolePortForm
|
model_form = forms.ConsolePortForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortDeleteView(ObjectDeleteView):
|
class ConsolePortDeleteView(ObjectDeleteView):
|
||||||
@ -1292,6 +1293,7 @@ class ConsoleServerPortCreateView(ComponentCreateView):
|
|||||||
class ConsoleServerPortEditView(ObjectEditView):
|
class ConsoleServerPortEditView(ObjectEditView):
|
||||||
queryset = ConsoleServerPort.objects.all()
|
queryset = ConsoleServerPort.objects.all()
|
||||||
model_form = forms.ConsoleServerPortForm
|
model_form = forms.ConsoleServerPortForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortDeleteView(ObjectDeleteView):
|
class ConsoleServerPortDeleteView(ObjectDeleteView):
|
||||||
@ -1351,6 +1353,7 @@ class PowerPortCreateView(ComponentCreateView):
|
|||||||
class PowerPortEditView(ObjectEditView):
|
class PowerPortEditView(ObjectEditView):
|
||||||
queryset = PowerPort.objects.all()
|
queryset = PowerPort.objects.all()
|
||||||
model_form = forms.PowerPortForm
|
model_form = forms.PowerPortForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class PowerPortDeleteView(ObjectDeleteView):
|
class PowerPortDeleteView(ObjectDeleteView):
|
||||||
@ -1410,6 +1413,7 @@ class PowerOutletCreateView(ComponentCreateView):
|
|||||||
class PowerOutletEditView(ObjectEditView):
|
class PowerOutletEditView(ObjectEditView):
|
||||||
queryset = PowerOutlet.objects.all()
|
queryset = PowerOutlet.objects.all()
|
||||||
model_form = forms.PowerOutletForm
|
model_form = forms.PowerOutletForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletDeleteView(ObjectDeleteView):
|
class PowerOutletDeleteView(ObjectDeleteView):
|
||||||
@ -1561,6 +1565,7 @@ class FrontPortCreateView(ComponentCreateView):
|
|||||||
class FrontPortEditView(ObjectEditView):
|
class FrontPortEditView(ObjectEditView):
|
||||||
queryset = FrontPort.objects.all()
|
queryset = FrontPort.objects.all()
|
||||||
model_form = forms.FrontPortForm
|
model_form = forms.FrontPortForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class FrontPortDeleteView(ObjectDeleteView):
|
class FrontPortDeleteView(ObjectDeleteView):
|
||||||
@ -1620,6 +1625,7 @@ class RearPortCreateView(ComponentCreateView):
|
|||||||
class RearPortEditView(ObjectEditView):
|
class RearPortEditView(ObjectEditView):
|
||||||
queryset = RearPort.objects.all()
|
queryset = RearPort.objects.all()
|
||||||
model_form = forms.RearPortForm
|
model_form = forms.RearPortForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class RearPortDeleteView(ObjectDeleteView):
|
class RearPortDeleteView(ObjectDeleteView):
|
||||||
@ -1679,6 +1685,7 @@ class DeviceBayCreateView(ComponentCreateView):
|
|||||||
class DeviceBayEditView(ObjectEditView):
|
class DeviceBayEditView(ObjectEditView):
|
||||||
queryset = DeviceBay.objects.all()
|
queryset = DeviceBay.objects.all()
|
||||||
model_form = forms.DeviceBayForm
|
model_form = forms.DeviceBayForm
|
||||||
|
template_name = 'dcim/device_component_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayDeleteView(ObjectDeleteView):
|
class DeviceBayDeleteView(ObjectDeleteView):
|
||||||
|
@ -158,7 +158,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
|||||||
instance.custom_fields = {}
|
instance.custom_fields = {}
|
||||||
for field in custom_fields:
|
for field in custom_fields:
|
||||||
value = instance.cf.get(field.name)
|
value = instance.cf.get(field.name)
|
||||||
if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None:
|
if field.type == CustomFieldTypeChoices.TYPE_SELECT and value:
|
||||||
instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
|
instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
|
||||||
else:
|
else:
|
||||||
instance.custom_fields[field.name] = value
|
instance.custom_fields[field.name] = value
|
||||||
|
@ -140,6 +140,7 @@ class ImageAttachmentViewSet(ModelViewSet):
|
|||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = ImageAttachment.objects.all()
|
queryset = ImageAttachment.objects.all()
|
||||||
serializer_class = serializers.ImageAttachmentSerializer
|
serializer_class = serializers.ImageAttachmentSerializer
|
||||||
|
filterset_class = filters.ImageAttachmentFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -7,7 +7,7 @@ from tenancy.models import Tenant, TenantGroup
|
|||||||
from utilities.filters import BaseFilterSet
|
from utilities.filters import BaseFilterSet
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, JobResult, Tag
|
from .models import ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, JobResult, ObjectChange, Tag
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -17,6 +17,7 @@ __all__ = (
|
|||||||
'CustomFieldFilterSet',
|
'CustomFieldFilterSet',
|
||||||
'ExportTemplateFilterSet',
|
'ExportTemplateFilterSet',
|
||||||
'GraphFilterSet',
|
'GraphFilterSet',
|
||||||
|
'ImageAttachmentFilterSet',
|
||||||
'LocalConfigContextFilterSet',
|
'LocalConfigContextFilterSet',
|
||||||
'ObjectChangeFilterSet',
|
'ObjectChangeFilterSet',
|
||||||
'TagFilterSet',
|
'TagFilterSet',
|
||||||
@ -104,6 +105,13 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
|||||||
fields = ['id', 'content_type', 'name', 'template_language']
|
fields = ['id', 'content_type', 'name', 'template_language']
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ImageAttachment
|
||||||
|
fields = ['id', 'content_type', 'object_id', 'name']
|
||||||
|
|
||||||
|
|
||||||
class TagFilterSet(BaseFilterSet):
|
class TagFilterSet(BaseFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Rack, Region, Site
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.filters import *
|
from extras.filters import *
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
from extras.models import ConfigContext, ExportTemplate, Graph, Tag
|
from extras.models import ConfigContext, ExportTemplate, Graph, ImageAttachment, Tag
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
@ -78,6 +78,84 @@ class ExportTemplateTestCase(TestCase):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAttachmentTestCase(TestCase):
|
||||||
|
queryset = ImageAttachment.objects.all()
|
||||||
|
filterset = ImageAttachmentFilterSet
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
site_ct = ContentType.objects.get(app_label='dcim', model='site')
|
||||||
|
rack_ct = ContentType.objects.get(app_label='dcim', model='rack')
|
||||||
|
|
||||||
|
sites = (
|
||||||
|
Site(name='Site 1', slug='site-1'),
|
||||||
|
Site(name='Site 2', slug='site-2'),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
|
image_attachments = (
|
||||||
|
ImageAttachment(
|
||||||
|
content_type=site_ct,
|
||||||
|
object_id=sites[0].pk,
|
||||||
|
name='Image Attachment 1',
|
||||||
|
image='http://example.com/image1.png',
|
||||||
|
image_height=100,
|
||||||
|
image_width=100
|
||||||
|
),
|
||||||
|
ImageAttachment(
|
||||||
|
content_type=site_ct,
|
||||||
|
object_id=sites[1].pk,
|
||||||
|
name='Image Attachment 2',
|
||||||
|
image='http://example.com/image2.png',
|
||||||
|
image_height=100,
|
||||||
|
image_width=100
|
||||||
|
),
|
||||||
|
ImageAttachment(
|
||||||
|
content_type=rack_ct,
|
||||||
|
object_id=racks[0].pk,
|
||||||
|
name='Image Attachment 3',
|
||||||
|
image='http://example.com/image3.png',
|
||||||
|
image_height=100,
|
||||||
|
image_width=100
|
||||||
|
),
|
||||||
|
ImageAttachment(
|
||||||
|
content_type=rack_ct,
|
||||||
|
object_id=racks[1].pk,
|
||||||
|
name='Image Attachment 4',
|
||||||
|
image='http://example.com/image4.png',
|
||||||
|
image_height=100,
|
||||||
|
image_width=100
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ImageAttachment.objects.bulk_create(image_attachments)
|
||||||
|
|
||||||
|
def test_id(self):
|
||||||
|
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_name(self):
|
||||||
|
params = {'name': ['Image Attachment 1', 'Image Attachment 2']}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_content_type(self):
|
||||||
|
params = {'content_type': ContentType.objects.get(app_label='dcim', model='site').pk}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_content_type_and_object_id(self):
|
||||||
|
params = {
|
||||||
|
'content_type': ContentType.objects.get(app_label='dcim', model='site').pk,
|
||||||
|
'object_id': [Site.objects.first().pk],
|
||||||
|
}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class ConfigContextTestCase(TestCase):
|
class ConfigContextTestCase(TestCase):
|
||||||
queryset = ConfigContext.objects.all()
|
queryset = ConfigContext.objects.all()
|
||||||
filterset = ConfigContextFilterSet
|
filterset = ConfigContextFilterSet
|
||||||
|
@ -67,11 +67,7 @@ IPADDRESS_LINK = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
IPADDRESS_ASSIGN_LINK = """
|
IPADDRESS_ASSIGN_LINK = """
|
||||||
{% if request.GET %}
|
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?{% if request.GET.interface %}interface={{ request.GET.interface }}{% elif request.GET.vminterface %}vminterface={{ request.GET.vminterface }}{% endif %}&return_url={{ request.GET.return_url }}">{{ record }}</a>
|
||||||
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ record.interface.pk }}&return_url={{ request.path }}">{{ record }}</a>
|
|
||||||
{% endif %}
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VRF_LINK = """
|
VRF_LINK = """
|
||||||
@ -103,7 +99,7 @@ VLAN_LINK = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
VLAN_PREFIXES = """
|
VLAN_PREFIXES = """
|
||||||
{% for prefix in record.prefixes.unrestricted %}
|
{% for prefix in record.prefixes.all %}
|
||||||
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
|
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
—
|
—
|
||||||
@ -419,6 +415,10 @@ class IPAddressDetailTable(IPAddressTable):
|
|||||||
tenant = tables.TemplateColumn(
|
tenant = tables.TemplateColumn(
|
||||||
template_code=COL_TENANT
|
template_code=COL_TENANT
|
||||||
)
|
)
|
||||||
|
assigned = tables.BooleanColumn(
|
||||||
|
accessor='assigned_object_id',
|
||||||
|
verbose_name='Assigned'
|
||||||
|
)
|
||||||
tags = TagColumn(
|
tags = TagColumn(
|
||||||
url_name='ipam:ipaddress_list'
|
url_name='ipam:ipaddress_list'
|
||||||
)
|
)
|
||||||
|
@ -582,7 +582,7 @@ class IPAddressAssignView(ObjectView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
|
||||||
# Redirect user if an interface has not been provided
|
# Redirect user if an interface has not been provided
|
||||||
if 'interface' not in request.GET:
|
if 'interface' not in request.GET and 'vminterface' not in request.GET:
|
||||||
return redirect('ipam:ipaddress_add')
|
return redirect('ipam:ipaddress_add')
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
@ -609,7 +609,7 @@ class IPAddressAssignView(ObjectView):
|
|||||||
return render(request, 'ipam/ipaddress_assign.html', {
|
return render(request, 'ipam/ipaddress_assign.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
'table': table,
|
'table': table,
|
||||||
'return_url': request.GET.get('return_url', ''),
|
'return_url': request.GET.get('return_url'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.9.2'
|
VERSION = '2.9.3'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
16
netbox/templates/dcim/device_component_edit.html
Normal file
16
netbox/templates/dcim/device_component_edit.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends 'utilities/obj_edit.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block form_fields %}
|
||||||
|
{% if form.instance.device %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label required" for="id_device">Device</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">
|
||||||
|
<a href="{{ form.instance.device.get_absolute_url }}">{{ form.instance.device }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% render_form form %}
|
||||||
|
{% endblock %}
|
@ -66,7 +66,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.change_consoleport %}
|
{% if perms.dcim.change_consoleport %}
|
||||||
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -68,7 +68,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.change_consoleserverport %}
|
{% if perms.dcim.change_consoleserverport %}
|
||||||
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Install device"></i>
|
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Install device"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:devicebay_edit' pk=devicebay.pk %}" class="btn btn-info btn-xs">
|
<a href="{% url 'dcim:devicebay_edit' pk=devicebay.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit device bay"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit device bay"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.change_poweroutlet %}
|
{% if perms.dcim.change_poweroutlet %}
|
||||||
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" title="Edit outlet" class="btn btn-info btn-xs">
|
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}?return_url={{ device.get_absolute_url }}" title="Edit outlet" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -78,7 +78,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.change_powerport %}
|
{% if perms.dcim.change_powerport %}
|
||||||
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -5,6 +5,16 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Interface</strong></div>
|
<div class="panel-heading"><strong>Interface</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
{% if form.instance.device %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label required" for="id_device">Device</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">
|
||||||
|
<a href="{{ form.instance.device.get_absolute_url }}">{{ form.instance.device }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.label %}
|
{% render_field form.label %}
|
||||||
{% render_field form.type %}
|
{% render_field form.type %}
|
||||||
@ -14,6 +24,11 @@
|
|||||||
{% render_field form.mtu %}
|
{% render_field form.mtu %}
|
||||||
{% render_field form.mgmt_only %}
|
{% render_field form.mgmt_only %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>802.1Q Switching</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
{% render_field form.mode %}
|
{% render_field form.mode %}
|
||||||
{% render_field form.untagged_vlan %}
|
{% render_field form.untagged_vlan %}
|
||||||
{% render_field form.tagged_vlans %}
|
{% render_field form.tagged_vlans %}
|
||||||
|
@ -5,18 +5,22 @@
|
|||||||
{% for section_name, menu_items in registry.plugin_menu_items.items %}
|
{% for section_name, menu_items in registry.plugin_menu_items.items %}
|
||||||
<li class="dropdown-header">{{ section_name }}</li>
|
<li class="dropdown-header">{{ section_name }}</li>
|
||||||
{% for menu_item in menu_items %}
|
{% for menu_item in menu_items %}
|
||||||
<li{% if menu_item.permissions and not request.user|has_perms:menu_item.permissions %} class="disabled"{% endif %}>
|
{% if not menu_item.permissions or request.user|has_perms:menu_item.permissions %}
|
||||||
{% if menu_item.buttons %}
|
<li>
|
||||||
<div class="buttons pull-right">
|
{% if menu_item.buttons %}
|
||||||
{% for button in menu_item.buttons %}
|
<div class="buttons pull-right">
|
||||||
{% if not button.permissions or request.user|has_perms:button.permissions %}
|
{% for button in menu_item.buttons %}
|
||||||
<a href="{% url button.link %}" class="btn btn-xs btn-{{ button.color }}" title="{{ button.title }}"><i class="{{ button.icon_class }}"></i></a>
|
{% if not button.permissions or request.user|has_perms:button.permissions %}
|
||||||
{% endif %}
|
<a href="{% url button.link %}" class="btn btn-xs btn-{{ button.color }}" title="{{ button.title }}"><i class="{{ button.icon_class }}"></i></a>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endif %}
|
</div>
|
||||||
<a href="{% url menu_item.link %}">{{ menu_item.link_text }}</a>
|
{% endif %}
|
||||||
</li>
|
<a href="{% url menu_item.link %}">{{ menu_item.link_text }}</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><a href="#">{{ menu_item.link_text }}</a></li>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if not forloop.last %}
|
{% if not forloop.last %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
|
@ -31,7 +31,9 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>{{ obj_type|capfirst }}</strong></div>
|
<div class="panel-heading"><strong>{{ obj_type|capfirst }}</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_form form %}
|
{% block form_fields %}
|
||||||
|
{% render_form form %}
|
||||||
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
|
{% block title %}Create {{ component_type }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form action="" method="post" class="form form-horizontal">
|
<form action="" method="post" class="form form-horizontal">
|
||||||
|
@ -5,14 +5,34 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Interface</strong></div>
|
<div class="panel-heading"><strong>Interface</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
{% if form.instance.virtual_machine %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-3 control-label required" for="id_device">Virtual Machine</label>
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p class="form-control-static">
|
||||||
|
<a href="{{ form.instance.virtual_machine.get_absolute_url }}">{{ form.instance.virtual_machine }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.enabled %}
|
{% render_field form.enabled %}
|
||||||
{% render_field form.mac_address %}
|
{% render_field form.mac_address %}
|
||||||
{% render_field form.mtu %}
|
{% render_field form.mtu %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>802.1Q Switching</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
{% render_field form.mode %}
|
{% render_field form.mode %}
|
||||||
{% render_field form.untagged_vlan %}
|
{% render_field form.untagged_vlan %}
|
||||||
{% render_field form.tagged_vlans %}
|
{% render_field form.tagged_vlans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
{% render_field form.tags %}
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,6 +38,10 @@ class LoginView(View):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
form = LoginForm(request)
|
form = LoginForm(request)
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
logger = logging.getLogger('netbox.auth.login')
|
||||||
|
return self.redirect_to_next(request, logger)
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
@ -49,12 +53,6 @@ class LoginView(View):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
logger.debug("Login form validation was successful")
|
logger.debug("Login form validation was successful")
|
||||||
|
|
||||||
# Determine where to direct user after successful login
|
|
||||||
redirect_to = request.POST.get('next', reverse('home'))
|
|
||||||
if redirect_to and not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
|
|
||||||
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_to}")
|
|
||||||
redirect_to = reverse('home')
|
|
||||||
|
|
||||||
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
|
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
|
||||||
# last_login time upon authentication.
|
# last_login time upon authentication.
|
||||||
if settings.MAINTENANCE_MODE:
|
if settings.MAINTENANCE_MODE:
|
||||||
@ -66,8 +64,7 @@ class LoginView(View):
|
|||||||
logger.info(f"User {request.user} successfully authenticated")
|
logger.info(f"User {request.user} successfully authenticated")
|
||||||
messages.info(request, "Logged in as {}.".format(request.user))
|
messages.info(request, "Logged in as {}.".format(request.user))
|
||||||
|
|
||||||
logger.debug(f"Redirecting user to {redirect_to}")
|
return self.redirect_to_next(request, logger)
|
||||||
return HttpResponseRedirect(redirect_to)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug("Login form validation failed")
|
logger.debug("Login form validation failed")
|
||||||
@ -76,6 +73,19 @@ class LoginView(View):
|
|||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def redirect_to_next(self, request, logger):
|
||||||
|
if request.method == "POST":
|
||||||
|
redirect_to = request.POST.get('next', reverse('home'))
|
||||||
|
else:
|
||||||
|
redirect_to = request.GET.get('next', reverse('home'))
|
||||||
|
|
||||||
|
if redirect_to and not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
|
||||||
|
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_to}")
|
||||||
|
redirect_to = reverse('home')
|
||||||
|
|
||||||
|
logger.debug(f"Redirecting user to {redirect_to}")
|
||||||
|
return HttpResponseRedirect(redirect_to)
|
||||||
|
|
||||||
|
|
||||||
class LogoutView(View):
|
class LogoutView(View):
|
||||||
"""
|
"""
|
||||||
|
@ -44,7 +44,7 @@ class BaseTable(tables.Table):
|
|||||||
self.columns.show(name)
|
self.columns.show(name)
|
||||||
else:
|
else:
|
||||||
self.columns.hide(name)
|
self.columns.hide(name)
|
||||||
self.sequence = columns
|
self.sequence = [c for c in columns if c in self.base_columns]
|
||||||
|
|
||||||
# Always include PK and actions column, if defined on the table
|
# Always include PK and actions column, if defined on the table
|
||||||
if pk:
|
if pk:
|
||||||
|
Loading…
Reference in New Issue
Block a user