mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Initial work on JSON/YAML-based DeviceType import
This commit is contained in:
@ -22,7 +22,8 @@ from utilities.forms import (
|
|||||||
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||||
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
|
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
|
||||||
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, MultiObjectField,
|
||||||
|
BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup
|
from virtualization.models import Cluster, ClusterGroup
|
||||||
from .constants import *
|
from .constants import *
|
||||||
@ -831,6 +832,36 @@ class DeviceTypeCSVForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceTemplateImportForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
name_pattern = ExpandableNameField(
|
||||||
|
label='Name'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InterfaceTemplate
|
||||||
|
fields = [
|
||||||
|
'type', 'mgmt_only',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypeImportForm(forms.ModelForm):
|
||||||
|
manufacturer = forms.ModelChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
to_field_name='name'
|
||||||
|
)
|
||||||
|
interfaces = MultiObjectField(
|
||||||
|
form=InterfaceTemplateImportForm,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DeviceType
|
||||||
|
fields = [
|
||||||
|
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||||
|
'interfaces',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
|
@ -82,7 +82,8 @@ urlpatterns = [
|
|||||||
# Device types
|
# Device types
|
||||||
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||||
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||||
path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
|
# path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
|
||||||
|
path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'),
|
||||||
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||||
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||||
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||||
|
@ -13,6 +13,7 @@ from django.urls import reverse
|
|||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.http import is_safe_url
|
from django.utils.http import is_safe_url
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.text import slugify
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
@ -25,7 +26,7 @@ from utilities.paginator import EnhancedPaginator
|
|||||||
from utilities.utils import csv_format
|
from utilities.utils import csv_format
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
|
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
|
||||||
ObjectDeleteView, ObjectEditView, ObjectListView,
|
ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
@ -654,6 +655,13 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
default_return_url = 'dcim:devicetype_list'
|
default_return_url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView):
|
||||||
|
permission_required = 'dcim.add_devicetype'
|
||||||
|
model = DeviceType
|
||||||
|
model_form = forms.DeviceTypeImportForm
|
||||||
|
default_return_url = 'dcim:devicetype_import'
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.add_devicetype'
|
permission_required = 'dcim.add_devicetype'
|
||||||
model_form = forms.DeviceTypeCSVForm
|
model_form = forms.DeviceTypeCSVForm
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
{% extends 'utilities/obj_bulk_import.html' %}
|
||||||
|
|
||||||
{% block tabs %}
|
{% block tabs %}
|
||||||
{% include 'dcim/inc/device_import_header.html' %}
|
{% include 'dcim/inc/device_import_header.html' %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
{% extends 'utilities/obj_bulk_import.html' %}
|
||||||
|
|
||||||
{% block tabs %}
|
{% block tabs %}
|
||||||
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
{% extends 'utilities/obj_bulk_import.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
60
netbox/templates/utilities/obj_bulk_import.html
Normal file
60
netbox/templates/utilities/obj_bulk_import.html
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% block title %}{{ obj_type|bettertitle }} Bulk Import{% endblock %}</h1>
|
||||||
|
{% block tabs %}{% endblock %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-7">
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="panel panel-danger">
|
||||||
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<form action="" method="post" class="form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% render_form form %}
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-12 text-right">
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
{% if return_url %}
|
||||||
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
{% if fields %}
|
||||||
|
<h4 class="text-center">CSV Format</h4>
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Required</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
{% for name, field in fields.items %}
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ name }}</code></td>
|
||||||
|
<td>{% if field.required %}<i class="glyphicon glyphicon-ok" title="Required"></i>{% endif %}</td>
|
||||||
|
<td>
|
||||||
|
{{ field.help_text|default:field.label }}
|
||||||
|
{% if field.choices %}
|
||||||
|
<br /><small class="text-muted">Choices: {{ field|example_choices }}</small>
|
||||||
|
{% elif field|widget_type == 'dateinput' %}
|
||||||
|
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
||||||
|
{% elif field|widget_type == 'checkboxinput' %}
|
||||||
|
<br /><small class="text-muted">Specify "true" or "false"</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -6,7 +6,7 @@
|
|||||||
<h1>{% block title %}{{ obj_type|bettertitle }} Import{% endblock %}</h1>
|
<h1>{% block title %}{{ obj_type|bettertitle }} Import{% endblock %}</h1>
|
||||||
{% block tabs %}{% endblock %}
|
{% block tabs %}{% endblock %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-7">
|
<div class="col-md-8 col-md-offset-2">
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
<div class="panel panel-danger">
|
<div class="panel panel-danger">
|
||||||
<div class="panel-heading"><strong>Errors</strong></div>
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
@ -28,33 +28,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
|
||||||
{% if fields %}
|
|
||||||
<h4 class="text-center">CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Required</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
{% for name, field in fields.items %}
|
|
||||||
<tr>
|
|
||||||
<td><code>{{ name }}</code></td>
|
|
||||||
<td>{% if field.required %}<i class="glyphicon glyphicon-ok" title="Required"></i>{% endif %}</td>
|
|
||||||
<td>
|
|
||||||
{{ field.help_text|default:field.label }}
|
|
||||||
{% if field.choices %}
|
|
||||||
<br /><small class="text-muted">Choices: {{ field|example_choices }}</small>
|
|
||||||
{% elif field|widget_type == 'dateinput' %}
|
|
||||||
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
|
||||||
{% elif field|widget_type == 'checkboxinput' %}
|
|
||||||
<br /><small class="text-muted">Specify "true" or "false"</small>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -6,8 +6,7 @@ from io import StringIO
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
|
from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput
|
||||||
from django.db.models import Count
|
from django.utils.html import mark_safe
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from mptt.forms import TreeNodeMultipleChoiceField
|
from mptt.forms import TreeNodeMultipleChoiceField
|
||||||
|
|
||||||
from .constants import *
|
from .constants import *
|
||||||
@ -554,6 +553,24 @@ class SlugField(forms.SlugField):
|
|||||||
self.widget.attrs['slug-source'] = slug_source
|
self.widget.attrs['slug-source'] = slug_source
|
||||||
|
|
||||||
|
|
||||||
|
class MultiObjectField(forms.Field):
|
||||||
|
"""
|
||||||
|
Use this field to relay data to another form for validation. Useful when importing data via JSON/YAML.
|
||||||
|
"""
|
||||||
|
def __init__(self, form, *args, **kwargs):
|
||||||
|
self.form = form
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
|
||||||
|
for obj in value:
|
||||||
|
subform = self.form(obj)
|
||||||
|
if not subform.is_valid():
|
||||||
|
raise forms.ValidationError(mark_safe(subform.errors.items()))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class FilterChoiceIterator(forms.models.ModelChoiceIterator):
|
class FilterChoiceIterator(forms.models.ModelChoiceIterator):
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
@ -721,3 +738,19 @@ class BulkEditForm(forms.Form):
|
|||||||
# Copy any nullable fields defined in Meta
|
# Copy any nullable fields defined in Meta
|
||||||
if hasattr(self.Meta, 'nullable_fields'):
|
if hasattr(self.Meta, 'nullable_fields'):
|
||||||
self.nullable_fields = self.Meta.nullable_fields
|
self.nullable_fields = self.Meta.nullable_fields
|
||||||
|
|
||||||
|
|
||||||
|
class ImportForm(BootstrapMixin, forms.Form):
|
||||||
|
"""
|
||||||
|
Generic form for creating an object from JSON/YAML data
|
||||||
|
"""
|
||||||
|
data = forms.CharField(
|
||||||
|
widget=forms.Textarea
|
||||||
|
)
|
||||||
|
format = forms.ChoiceField(
|
||||||
|
choices=(
|
||||||
|
('json', 'JSON'),
|
||||||
|
('yaml', 'YAML')
|
||||||
|
),
|
||||||
|
initial='yaml'
|
||||||
|
)
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import yaml
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -26,7 +28,7 @@ from extras.querysets import CustomFieldQueryset
|
|||||||
from utilities.forms import BootstrapMixin, CSVDataField
|
from utilities.forms import BootstrapMixin, CSVDataField
|
||||||
from utilities.utils import csv_format
|
from utilities.utils import csv_format
|
||||||
from .error_handlers import handle_protectederror
|
from .error_handlers import handle_protectederror
|
||||||
from .forms import ConfirmationForm
|
from .forms import ConfirmationForm, ImportForm
|
||||||
from .paginator import EnhancedPaginator
|
from .paginator import EnhancedPaginator
|
||||||
|
|
||||||
|
|
||||||
@ -393,6 +395,62 @@ class BulkCreateView(GetReturnURLMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectImportView(GetReturnURLMixin, View):
|
||||||
|
"""
|
||||||
|
Import a single object (YAML or JSON format).
|
||||||
|
"""
|
||||||
|
model = None
|
||||||
|
model_form = None
|
||||||
|
template_name = 'utilities/obj_import.html'
|
||||||
|
|
||||||
|
def create_object(self, data):
|
||||||
|
raise NotImplementedError("View must implement object creation logic")
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
|
||||||
|
form = ImportForm()
|
||||||
|
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
'form': form,
|
||||||
|
'obj_type': self.model._meta.verbose_name,
|
||||||
|
'return_url': self.get_return_url(request),
|
||||||
|
})
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
|
||||||
|
form = ImportForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
# Process object data
|
||||||
|
if form.cleaned_data['format'] == 'json':
|
||||||
|
data = json.loads(form.cleaned_data['data'])
|
||||||
|
else:
|
||||||
|
data = yaml.load(form.cleaned_data['data'])
|
||||||
|
|
||||||
|
# Initialize model form
|
||||||
|
model_form = self.model_form(data)
|
||||||
|
|
||||||
|
if model_form.is_valid():
|
||||||
|
|
||||||
|
obj = model_form.save(commit=False)
|
||||||
|
# assert False, model_form.cleaned_data['interfaces']
|
||||||
|
|
||||||
|
messages.success(request, "Imported object: {}".format(obj))
|
||||||
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Replicate model form errors for display
|
||||||
|
for field, err in model_form.errors.items():
|
||||||
|
form.add_error(None, "{}: {}".format(field, err))
|
||||||
|
|
||||||
|
return render(request, self.template_name, {
|
||||||
|
'form': form,
|
||||||
|
'obj_type': self.model._meta.verbose_name,
|
||||||
|
'return_url': self.get_return_url(request),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class BulkImportView(GetReturnURLMixin, View):
|
class BulkImportView(GetReturnURLMixin, View):
|
||||||
"""
|
"""
|
||||||
Import objects in bulk (CSV format).
|
Import objects in bulk (CSV format).
|
||||||
@ -404,7 +462,7 @@ class BulkImportView(GetReturnURLMixin, View):
|
|||||||
"""
|
"""
|
||||||
model_form = None
|
model_form = None
|
||||||
table = None
|
table = None
|
||||||
template_name = 'utilities/obj_import.html'
|
template_name = 'utilities/obj_bulk_import.html'
|
||||||
widget_attrs = {}
|
widget_attrs = {}
|
||||||
|
|
||||||
def _import_form(self, *args, **kwargs):
|
def _import_form(self, *args, **kwargs):
|
||||||
|
Reference in New Issue
Block a user