mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge v2.4.8
This commit is contained in:
commit
874acab90f
10
CHANGELOG.md
10
CHANGELOG.md
@ -80,13 +80,21 @@ NetBox now supports modeling physical cables for console, power, and interface c
|
||||
|
||||
---
|
||||
|
||||
v2.4.8 (FUTURE)
|
||||
v2.4.8 (2018-11-20)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2490](https://github.com/digitalocean/netbox/issues/2490) - Added bulk editing for config contexts
|
||||
* [#2557](https://github.com/digitalocean/netbox/issues/2557) - Added object view for tags
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2473](https://github.com/digitalocean/netbox/issues/2473) - Fix encoding of long (>127 character) secrets
|
||||
* [#2558](https://github.com/digitalocean/netbox/issues/2558) - Filter on all tags when multiple are passed
|
||||
* [#2565](https://github.com/digitalocean/netbox/issues/2565) - Improved rendering of Markdown tables
|
||||
* [#2575](https://github.com/digitalocean/netbox/issues/2575) - Correct model specified for rack roles table
|
||||
* [#2588](https://github.com/digitalocean/netbox/issues/2588) - Catch all exceptions from failed NAPALM API Calls
|
||||
* [#2589](https://github.com/digitalocean/netbox/issues/2589) - Virtual machine API serializer should require cluster assignment
|
||||
|
||||
---
|
||||
|
||||
|
@ -309,9 +309,9 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
# Check that NAPALM is installed
|
||||
try:
|
||||
import napalm
|
||||
from napalm.base.exceptions import ModuleImportError
|
||||
except ImportError:
|
||||
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
||||
from napalm.base.exceptions import ModuleImportError
|
||||
|
||||
# Validate the configured driver
|
||||
try:
|
||||
@ -355,7 +355,9 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
try:
|
||||
response[method] = getattr(d, method)()
|
||||
except NotImplementedError:
|
||||
response[method] = {'error': 'Method not implemented for NAPALM driver {}'.format(driver)}
|
||||
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
|
||||
except Exception as e:
|
||||
response[method] = {'error': 'Method {} failed: {}'.format(method, e)}
|
||||
d.close()
|
||||
|
||||
return Response(response)
|
||||
|
@ -900,7 +900,7 @@ class DeviceView(View):
|
||||
interfaces = device.vc_interfaces.select_related(
|
||||
'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable'
|
||||
).prefetch_related(
|
||||
'cable__termination_a', 'cable__termination_b', 'ip_addresses'
|
||||
'cable__termination_a', 'cable__termination_b', 'ip_addresses', 'tags'
|
||||
)
|
||||
|
||||
# Front ports
|
||||
|
@ -11,8 +11,8 @@ from taggit.models import Tag
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField,
|
||||
JSONField, SlugField,
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, FilterChoiceField,
|
||||
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
|
||||
)
|
||||
from .constants import (
|
||||
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||
@ -206,6 +206,11 @@ class AddRemoveTagsForm(forms.Form):
|
||||
self.fields['remove_tags'] = TagField(required=False)
|
||||
|
||||
|
||||
class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
model = Tag
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
|
||||
|
||||
#
|
||||
# Config contexts
|
||||
#
|
||||
@ -225,6 +230,28 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
]
|
||||
|
||||
|
||||
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=ConfigContext.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
weight = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=0
|
||||
)
|
||||
is_active = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False,
|
||||
max_length=100
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
|
||||
|
||||
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import django_tables2 as tables
|
||||
from taggit.models import Tag
|
||||
from django_tables2.utils import Accessor
|
||||
from taggit.models import Tag, TaggedItem
|
||||
|
||||
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
|
||||
from .models import ConfigContext, ObjectChange
|
||||
@ -13,6 +14,14 @@ TAG_ACTIONS = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
TAGGED_ITEM = """
|
||||
{% if value.get_absolute_url %}
|
||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
CONFIGCONTEXT_ACTIONS = """
|
||||
{% if perms.extras.change_configcontext %}
|
||||
<a href="{% url 'extras:configcontext_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@ -53,6 +62,10 @@ OBJECTCHANGE_REQUEST_ID = """
|
||||
|
||||
class TagTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(
|
||||
viewname='extras:tag',
|
||||
args=[Accessor('slug')]
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=TAG_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
@ -64,6 +77,21 @@ class TagTable(BaseTable):
|
||||
fields = ('pk', 'name', 'items', 'slug', 'actions')
|
||||
|
||||
|
||||
class TaggedItemTable(BaseTable):
|
||||
content_object = tables.TemplateColumn(
|
||||
template_code=TAGGED_ITEM,
|
||||
orderable=False,
|
||||
verbose_name='Object'
|
||||
)
|
||||
content_type = tables.Column(
|
||||
verbose_name='Type'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = TaggedItem
|
||||
fields = ('content_object', 'content_type')
|
||||
|
||||
|
||||
class ConfigContextTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
|
@ -7,6 +7,7 @@ urlpatterns = [
|
||||
|
||||
# Tags
|
||||
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/$', views.TagView.as_view(), name='tag'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
@ -14,6 +15,7 @@ urlpatterns = [
|
||||
# Config contexts
|
||||
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
|
||||
url(r'^config-contexts/edit/$', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||
|
@ -1,4 +1,5 @@
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@ -7,15 +8,20 @@ from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
from taggit.models import Tag
|
||||
from django_tables2 import RequestConfig
|
||||
from taggit.models import Tag, TaggedItem
|
||||
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||
from . import filters
|
||||
from .forms import ConfigContextForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
||||
from .forms import (
|
||||
ConfigContextForm, ConfigContextBulkEditForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm,
|
||||
TagFilterForm, TagForm,
|
||||
)
|
||||
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
||||
from .reports import get_report, get_reports
|
||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable
|
||||
|
||||
|
||||
#
|
||||
@ -23,11 +29,45 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
||||
#
|
||||
|
||||
class TagListView(ObjectListView):
|
||||
queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('taggit_taggeditem_items')
|
||||
).order_by(
|
||||
'name'
|
||||
)
|
||||
filter = filters.TagFilter
|
||||
filter_form = TagFilterForm
|
||||
table = TagTable
|
||||
template_name = 'extras/tag_list.html'
|
||||
|
||||
|
||||
class TagView(View):
|
||||
|
||||
def get(self, request, slug):
|
||||
|
||||
tag = get_object_or_404(Tag, slug=slug)
|
||||
tagged_items = TaggedItem.objects.filter(
|
||||
tag=tag
|
||||
).select_related(
|
||||
'content_type'
|
||||
).prefetch_related(
|
||||
'content_object'
|
||||
)
|
||||
|
||||
# Generate a table of all items tagged with this Tag
|
||||
items_table = TaggedItemTable(tagged_items)
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(items_table)
|
||||
|
||||
return render(request, 'extras/tag.html', {
|
||||
'tag': tag,
|
||||
'items_count': tagged_items.count(),
|
||||
'items_table': items_table,
|
||||
})
|
||||
|
||||
|
||||
class TagEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'taggit.change_tag'
|
||||
model = Tag
|
||||
@ -43,7 +83,11 @@ class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuittype'
|
||||
queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name')
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('taggit_taggeditem_items')
|
||||
).order_by(
|
||||
'name'
|
||||
)
|
||||
table = TagTable
|
||||
default_return_url = 'extras:tag_list'
|
||||
|
||||
@ -83,6 +127,15 @@ class ConfigContextEditView(ConfigContextCreateView):
|
||||
permission_required = 'extras.change_configcontext'
|
||||
|
||||
|
||||
class ConfigContextBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'extras.change_configcontext'
|
||||
queryset = ConfigContext.objects.all()
|
||||
filter = filters.ConfigContextFilter
|
||||
table = ConfigContextTable
|
||||
form = ConfigContextBulkEditForm
|
||||
default_return_url = 'extras:configcontext_list'
|
||||
|
||||
|
||||
class ConfigContextDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'extras.delete_configcontext'
|
||||
model = ConfigContext
|
||||
|
@ -390,6 +390,19 @@ table.report th a {
|
||||
top: -51px;
|
||||
}
|
||||
|
||||
/* Rendered Markdown */
|
||||
.rendered-markdown table {
|
||||
width: 100%;
|
||||
}
|
||||
.rendered-markdown th {
|
||||
border-bottom: 2px solid #dddddd;
|
||||
padding: 8px;
|
||||
}
|
||||
.rendered-markdown td {
|
||||
border-top: 1px solid #dddddd;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* AJAX loader */
|
||||
.loading {
|
||||
position: fixed;
|
||||
|
@ -113,7 +113,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if circuit.comments %}
|
||||
{{ circuit.comments|gfm }}
|
||||
{% else %}
|
||||
|
@ -105,7 +105,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if provider.comments %}
|
||||
{{ provider.comments|gfm }}
|
||||
{% else %}
|
||||
|
@ -293,7 +293,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if device.comments %}
|
||||
{{ device.comments|gfm }}
|
||||
{% else %}
|
||||
@ -508,6 +508,7 @@
|
||||
<th>Name</th>
|
||||
<th>LAG</th>
|
||||
<th>Description</th>
|
||||
<th>MTU</th>
|
||||
<th>Mode</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
|
@ -116,7 +116,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if devicetype.comments %}
|
||||
{{ devicetype.comments|gfm }}
|
||||
{% else %}
|
||||
|
@ -1,3 +1,4 @@
|
||||
{% load helpers %}
|
||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable.status %} success{% elif iface.cable %} info{% elif iface.is_virtual %} warning{% endif %}" id="iface_{{ iface.name }}">
|
||||
|
||||
{# Checkbox #}
|
||||
@ -13,17 +14,32 @@
|
||||
<i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}align-justify{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
|
||||
<a href="{{ iface.get_absolute_url }}">{{ iface }}</a>
|
||||
</span>
|
||||
{% if iface.mac_address %}
|
||||
<br/><small class="text-muted">{{ iface.mac_address }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# LAG #}
|
||||
<td>
|
||||
{% if iface.lag %}
|
||||
<a href="#iface_{{ iface.lag }}" class="label label-default" title="{{ iface.lag.description }}">{{ iface.lag }}</a>
|
||||
<a href="#interface_{{ iface.lag }}" class="label label-primary" title="{{ iface.lag.description }}">{{ iface.lag }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Description #}
|
||||
<td>{{ iface.description|default:"—" }}</td>
|
||||
{# Description/tags #}
|
||||
<td>
|
||||
{% if iface.description %}
|
||||
{{ iface.description }}<br/>
|
||||
{% endif %}
|
||||
{% for tag in iface.tags.all %}
|
||||
{% tag tag %}
|
||||
{% empty %}
|
||||
{% if not iface.description %}—{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
|
||||
{# MTU #}
|
||||
<td>{{ iface.mtu|default:"—" }}</td>
|
||||
|
||||
{# 802.1Q mode #}
|
||||
<td>{{ iface.get_mode_display|default:"—" }}</td>
|
||||
@ -44,7 +60,13 @@
|
||||
{% if iface.is_lag %}
|
||||
<td colspan="2" class="text-muted">
|
||||
LAG interface<br />
|
||||
<small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
|
||||
<small class="text-muted">
|
||||
{% for member in iface.member_interfaces.all %}
|
||||
<a href="#interface_{{ member.name }}">{{ member }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% empty %}
|
||||
No members
|
||||
{% endfor %}
|
||||
</small>
|
||||
</td>
|
||||
{% elif iface.is_virtual %}
|
||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||
@ -138,7 +160,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# IP addresses table #}
|
||||
<td colspan="8" style="padding: 0">
|
||||
<td colspan="9" style="padding: 0">
|
||||
<table class="table table-condensed interface-ips">
|
||||
<thead>
|
||||
<tr class="text-muted">
|
||||
|
@ -182,7 +182,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if rack.comments %}
|
||||
{{ rack.comments|gfm }}
|
||||
{% else %}
|
||||
|
@ -200,7 +200,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if site.comments %}
|
||||
{{ site.comments|gfm }}
|
||||
{% else %}
|
||||
|
@ -10,7 +10,7 @@
|
||||
<h1>{% block title %}Config Contexts{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:configcontext_bulk_delete' %}
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='extras:configcontext_bulk_edit' bulk_delete_url='extras:configcontext_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
|
69
netbox/templates/extras/tag.html
Normal file
69
netbox/templates/extras/tag.html
Normal file
@ -0,0 +1,69 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'extras:tag_list' %}">Tags</a></li>
|
||||
<li>{{ tag }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'extras:tag_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if perms.taggit.change_tag %}
|
||||
<a href="{% url 'extras:tag_edit' slug=tag.slug %}?return_url={% url 'extras:tag' slug=tag.slug %}" class="btn btn-warning">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||
Edit this tag
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}Tag: {{ tag }}{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Tag</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
{{ tag.name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slug</td>
|
||||
<td>
|
||||
{{ tag.slug }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tagged Items</td>
|
||||
<td>
|
||||
{{ items_count }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% include 'panel_table.html' with table=items_table heading='Tagged Objects' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -4,8 +4,11 @@
|
||||
{% block content %}
|
||||
<h1>{% block title %}Tags{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:tag_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -81,7 +81,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if tenant.comments %}
|
||||
{{ tenant.comments|gfm }}
|
||||
{% else %}
|
||||
|
@ -99,7 +99,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if cluster.comments %}
|
||||
{{ cluster.comments|gfm }}
|
||||
{% else %}
|
||||
|
@ -143,7 +143,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body rendered-markdown">
|
||||
{% if virtualmachine.comments %}
|
||||
{{ virtualmachine.comments|gfm }}
|
||||
{% else %}
|
||||
|
@ -52,7 +52,7 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
status = ChoiceField(choices=VM_STATUS_CHOICES, required=False)
|
||||
site = NestedSiteSerializer(read_only=True)
|
||||
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||
cluster = NestedClusterSerializer()
|
||||
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||
|
@ -378,6 +378,18 @@ class VirtualMachineTest(APITestCase):
|
||||
self.assertEqual(virtualmachine4.name, data['name'])
|
||||
self.assertEqual(virtualmachine4.cluster.pk, data['cluster'])
|
||||
|
||||
def test_create_virtualmachine_without_cluster(self):
|
||||
|
||||
data = {
|
||||
'name': 'Test Virtual Machine 4',
|
||||
}
|
||||
|
||||
url = reverse('virtualization-api:virtualmachine-list')
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(VirtualMachine.objects.count(), 3)
|
||||
|
||||
def test_create_virtualmachine_bulk(self):
|
||||
|
||||
data = [
|
||||
|
Loading…
Reference in New Issue
Block a user