1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into develop-2.8

This commit is contained in:
Jeremy Stretch
2020-04-06 13:51:05 -04:00
15 changed files with 150 additions and 88 deletions

View File

@@ -169,7 +169,8 @@ class RackViewSet(CustomFieldModelViewSet):
unit_width=data['unit_width'],
unit_height=data['unit_height'],
legend_width=data['legend_width'],
include_images=data['include_images']
include_images=data['include_images'],
base_url=request.build_absolute_uri('/')
)
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')

View File

@@ -92,5 +92,5 @@ COMPATIBLE_TERMINATION_TYPES = {
'interface': ['interface', 'circuittermination', 'frontport', 'rearport'],
'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
'circuittermination': ['interface', 'frontport', 'rearport'],
'circuittermination': ['interface', 'frontport', 'rearport', 'circuittermination'],
}

View File

@@ -15,10 +15,15 @@ class RackElevationSVG:
:param rack: A NetBox Rack instance
:param include_images: If true, the SVG document will embed front/rear device face images, where available
:param base_url: Base URL for links within the SVG document. If none, links will be relative.
"""
def __init__(self, rack, include_images=True):
def __init__(self, rack, include_images=True, base_url=None):
self.rack = rack
self.include_images = include_images
if base_url is not None:
self.base_url = base_url.rstrip('/')
else:
self.base_url = ''
def _get_device_description(self, device):
return '{} ({}) — {} ({}U) {} {}'.format(
@@ -69,7 +74,7 @@ class RackElevationSVG:
color = device.device_role.color
link = drawing.add(
drawing.a(
href=reverse('dcim:device', kwargs={'pk': device.pk}),
href='{}{}'.format(self.base_url, reverse('dcim:device', kwargs={'pk': device.pk})),
target='_top',
fill='black'
)
@@ -81,7 +86,7 @@ class RackElevationSVG:
# Embed front device type image if one exists
if self.include_images and device.device_type.front_image:
url = device.device_type.front_image.url
url = '{}{}'.format(self.base_url, device.device_type.front_image.url)
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
image.fit(scale='slice')
link.add(image)

View File

@@ -719,7 +719,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT,
legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT,
include_images=True
include_images=True,
base_url=None
):
"""
Return an SVG of the rack elevation
@@ -730,8 +731,9 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
height of the elevation
:param legend_width: Width of the unit legend, in pixels
:param include_images: Embed front/rear device images where available
:param base_url: Base URL for links and images. If none, URLs will be relative.
"""
elevation = RackElevationSVG(self, include_images=include_images)
elevation = RackElevationSVG(self, include_images=include_images, base_url=base_url)
return elevation.render(face, unit_width, unit_height, legend_width)
@@ -1952,6 +1954,10 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
super().save(*args, **kwargs)
@property
def parent(self):
return self.power_panel
def get_type_class(self):
return self.TYPE_CLASS_MAP.get(self.type)

View File

@@ -335,9 +335,9 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class PrefixCSVForm(CustomFieldModelCSVForm):
vrf = FlexibleModelChoiceField(
queryset=VRF.objects.all(),
to_field_name='rd',
to_field_name='name',
required=False,
help_text='Route distinguisher of parent VRF (or {ID})',
help_text='Name of parent VRF (or {ID})',
error_messages={
'invalid_choice': 'VRF not found.',
}
@@ -739,9 +739,9 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class IPAddressCSVForm(CustomFieldModelCSVForm):
vrf = FlexibleModelChoiceField(
queryset=VRF.objects.all(),
to_field_name='rd',
to_field_name='name',
required=False,
help_text='Route distinguisher of parent VRF (or {ID})',
help_text='Name of parent VRF (or {ID})',
error_messages={
'invalid_choice': 'VRF not found.',
}

View File

@@ -180,10 +180,10 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
"prefix,status",
"10.4.0.0/16,Active",
"10.5.0.0/16,Active",
"10.6.0.0/16,Active",
"vrf,prefix,status",
"VRF 1,10.4.0.0/16,Active",
"VRF 1,10.5.0.0/16,Active",
"VRF 1,10.6.0.0/16,Active",
)
cls.bulk_edit_data = {
@@ -207,6 +207,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
VRF(name='VRF 1', rd='65000:1'),
VRF(name='VRF 2', rd='65000:2'),
)
VRF.objects.bulk_create(vrfs)
IPAddress.objects.bulk_create([
IPAddress(address=IPNetwork('192.0.2.1/24'), vrf=vrfs[0]),
@@ -228,10 +229,10 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
"address,status",
"192.0.2.4/24,Active",
"192.0.2.5/24,Active",
"192.0.2.6/24,Active",
"vrf,address,status",
"VRF 1,192.0.2.4/24,Active",
"VRF 1,192.0.2.5/24,Active",
"VRF 1,192.0.2.6/24,Active",
)
cls.bulk_edit_data = {

View File

@@ -66,6 +66,7 @@
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='interface' %}?return_url={{ device.get_absolute_url }}">Interface</a></li>
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='front-port' %}?return_url={{ device.get_absolute_url }}">Front Port</a></li>
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='rear-port' %}?return_url={{ device.get_absolute_url }}">Rear Port</a></li>
<li><a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='circuit-termination' %}?return_url={{ device.get_absolute_url }}">Circuit Termination</a></li>
</ul>
</span>
</div>

View File

@@ -289,12 +289,12 @@
</td>
<td class="text-right noprint">
{% if perms.dcim.change_rackreservation %}
<a href="{% url 'dcim:rackreservation_edit' pk=resv.pk %}&return_url={{ rack.get_absolute_url }}" class="btn btn-warning btn-xs" title="Edit reservation">
<a href="{% url 'dcim:rackreservation_edit' pk=resv.pk %}?return_url={{ rack.get_absolute_url }}" class="btn btn-warning btn-xs" title="Edit reservation">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.dcim.delete_rackreservation %}
<a href="{% url 'dcim:rackreservation_delete' pk=resv.pk %}&return_url={{ rack.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete reservation">
<a href="{% url 'dcim:rackreservation_delete' pk=resv.pk %}?return_url={{ rack.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete reservation">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
{% endif %}

View File

@@ -15,7 +15,7 @@
<i class="glyphicon glyphicon-remove text-danger" title="False"></i>
{% elif field.type == 'url' and value %}
<a href="{{ value }}">{{ value|truncatechars:70 }}</a>
{% elif field.type == 'integer' or value %}
{% elif value is not None %}
{{ value }}
{% elif field.required %}
<span class="text-warning">Not defined</span>

View File

@@ -698,7 +698,7 @@ class ImportForm(BootstrapMixin, forms.Form):
"""
data = forms.CharField(
widget=forms.Textarea,
help_text="Enter object data in JSON or YAML format."
help_text="Enter object data in JSON or YAML format. Note: Only a single object/document is supported."
)
format = forms.ChoiceField(
choices=(
@@ -717,14 +717,24 @@ class ImportForm(BootstrapMixin, forms.Form):
if format == 'json':
try:
self.cleaned_data['data'] = json.loads(data)
# Check for multiple JSON objects
if type(self.cleaned_data['data']) is not dict:
raise forms.ValidationError({
'data': "Import is limited to one object at a time."
})
except json.decoder.JSONDecodeError as err:
raise forms.ValidationError({
'data': "Invalid JSON data: {}".format(err)
})
else:
# Check for multiple YAML documents
if '\n---' in data:
raise forms.ValidationError({
'data': "Import is limited to one object at a time."
})
try:
self.cleaned_data['data'] = yaml.load(data, Loader=yaml.SafeLoader)
except yaml.scanner.ScannerError as err:
except yaml.error.YAMLError as err:
raise forms.ValidationError({
'data': "Invalid YAML data: {}".format(err)
})