mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into 568-csv-import-cf
This commit is contained in:
@@ -24,10 +24,12 @@ from django_tables2 import RequestConfig
|
||||
|
||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate
|
||||
from extras.querysets import CustomFieldQueryset
|
||||
from extras.utils import is_taggable
|
||||
from utilities.exceptions import AbortTransaction
|
||||
from utilities.forms import BootstrapMixin, CSVDataField
|
||||
from utilities.utils import csv_format
|
||||
from utilities.utils import csv_format, prepare_cloned_fields
|
||||
from .error_handlers import handle_protectederror
|
||||
from .forms import ConfirmationForm
|
||||
from .forms import ConfirmationForm, ImportForm
|
||||
from .paginator import EnhancedPaginator
|
||||
|
||||
|
||||
@@ -68,11 +70,19 @@ class ObjectListView(View):
|
||||
template_name: The name of the template
|
||||
"""
|
||||
queryset = None
|
||||
filter = None
|
||||
filter_form = None
|
||||
filterset = None
|
||||
filterset_form = None
|
||||
table = None
|
||||
template_name = None
|
||||
|
||||
def queryset_to_yaml(self):
|
||||
"""
|
||||
Export the queryset of objects as concatenated YAML documents.
|
||||
"""
|
||||
yaml_data = [obj.to_yaml() for obj in self.queryset]
|
||||
|
||||
return '---\n'.join(yaml_data)
|
||||
|
||||
def queryset_to_csv(self):
|
||||
"""
|
||||
Export the queryset of objects as comma-separated value (CSV), using the model's to_csv() method.
|
||||
@@ -101,15 +111,15 @@ class ObjectListView(View):
|
||||
|
||||
csv_data.append(csv_format(data))
|
||||
|
||||
return csv_data
|
||||
return '\n'.join(csv_data)
|
||||
|
||||
def get(self, request):
|
||||
|
||||
model = self.queryset.model
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
|
||||
if self.filter:
|
||||
self.queryset = self.filter(request.GET, self.queryset).qs
|
||||
if self.filterset:
|
||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||
|
||||
# If this type of object has one or more custom fields, prefetch any relevant custom field values
|
||||
custom_fields = CustomField.objects.filter(
|
||||
@@ -132,13 +142,16 @@ class ObjectListView(View):
|
||||
)
|
||||
)
|
||||
|
||||
# Check for YAML export support
|
||||
elif 'export' in request.GET and hasattr(model, 'to_yaml'):
|
||||
response = HttpResponse(self.queryset_to_yaml(), content_type='text/yaml')
|
||||
filename = 'netbox_{}.yaml'.format(self.queryset.model._meta.verbose_name_plural)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
return response
|
||||
|
||||
# Fall back to built-in CSV formatting if export requested but no template specified
|
||||
elif 'export' in request.GET and hasattr(model, 'to_csv'):
|
||||
data = self.queryset_to_csv()
|
||||
response = HttpResponse(
|
||||
'\n'.join(data),
|
||||
content_type='text/csv'
|
||||
)
|
||||
response = HttpResponse(self.queryset_to_csv(), content_type='text/csv')
|
||||
filename = 'netbox_{}.csv'.format(self.queryset.model._meta.verbose_name_plural)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
return response
|
||||
@@ -156,7 +169,7 @@ class ObjectListView(View):
|
||||
table.columns.show('pk')
|
||||
|
||||
# Construct queryset for tags list
|
||||
if hasattr(model, 'tags'):
|
||||
if is_taggable(model):
|
||||
tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name')
|
||||
else:
|
||||
tags = None
|
||||
@@ -172,7 +185,7 @@ class ObjectListView(View):
|
||||
'content_type': content_type,
|
||||
'table': table,
|
||||
'permissions': permissions,
|
||||
'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None,
|
||||
'filter_form': self.filterset_form(request.GET, label_suffix='') if self.filterset_form else None,
|
||||
'tags': tags,
|
||||
}
|
||||
context.update(self.extra_context())
|
||||
@@ -248,6 +261,12 @@ class ObjectEditView(GetReturnURLMixin, View):
|
||||
messages.success(request, mark_safe(msg))
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
|
||||
# If the object has clone_fields, pre-populate a new instance of the form
|
||||
if hasattr(obj, 'clone_fields'):
|
||||
url = '{}?{}'.format(request.path, prepare_cloned_fields(obj))
|
||||
return redirect(url)
|
||||
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
return_url = form.cleaned_data.get('return_url')
|
||||
@@ -407,6 +426,106 @@ class BulkCreateView(GetReturnURLMixin, View):
|
||||
})
|
||||
|
||||
|
||||
class ObjectImportView(GetReturnURLMixin, View):
|
||||
"""
|
||||
Import a single object (YAML or JSON format).
|
||||
"""
|
||||
model = None
|
||||
model_form = None
|
||||
related_object_forms = dict()
|
||||
template_name = 'utilities/obj_import.html'
|
||||
|
||||
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():
|
||||
|
||||
# Initialize model form
|
||||
data = form.cleaned_data['data']
|
||||
model_form = self.model_form(data)
|
||||
|
||||
# Assign default values for any fields which were not specified. We have to do this manually because passing
|
||||
# 'initial=' to the form on initialization merely sets default values for the widgets. Since widgets are not
|
||||
# used for YAML/JSON import, we first bind the imported data normally, then update the form's data with the
|
||||
# applicable field defaults as needed prior to form validation.
|
||||
for field_name, field in model_form.fields.items():
|
||||
if field_name not in data and hasattr(field, 'initial'):
|
||||
model_form.data[field_name] = field.initial
|
||||
|
||||
if model_form.is_valid():
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
|
||||
# Save the primary object
|
||||
obj = model_form.save()
|
||||
|
||||
# Iterate through the related object forms (if any), validating and saving each instance.
|
||||
for field_name, related_object_form in self.related_object_forms.items():
|
||||
|
||||
for i, rel_obj_data in enumerate(data.get(field_name, list())):
|
||||
|
||||
f = related_object_form(obj, rel_obj_data)
|
||||
|
||||
for subfield_name, field in f.fields.items():
|
||||
if subfield_name not in rel_obj_data and hasattr(field, 'initial'):
|
||||
f.data[subfield_name] = field.initial
|
||||
|
||||
if f.is_valid():
|
||||
f.save()
|
||||
else:
|
||||
# Replicate errors on the related object form to the primary form for display
|
||||
for subfield_name, errors in f.errors.items():
|
||||
for err in errors:
|
||||
err_msg = "{}[{}] {}: {}".format(field_name, i, subfield_name, err)
|
||||
model_form.add_error(None, err_msg)
|
||||
raise AbortTransaction()
|
||||
|
||||
except AbortTransaction:
|
||||
pass
|
||||
|
||||
if not model_form.errors:
|
||||
|
||||
messages.success(request, mark_safe('Imported object: <a href="{}">{}</a>'.format(
|
||||
obj.get_absolute_url(), obj
|
||||
)))
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
return_url = form.cleaned_data.get('return_url')
|
||||
if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
|
||||
return redirect(return_url)
|
||||
else:
|
||||
return redirect(self.get_return_url(request, obj))
|
||||
|
||||
else:
|
||||
|
||||
# Replicate model form errors for display
|
||||
for field, errors in model_form.errors.items():
|
||||
for err in errors:
|
||||
if field == '__all__':
|
||||
form.add_error(None, err)
|
||||
else:
|
||||
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):
|
||||
"""
|
||||
Import objects in bulk (CSV format).
|
||||
@@ -418,7 +537,7 @@ class BulkImportView(GetReturnURLMixin, View):
|
||||
"""
|
||||
model_form = None
|
||||
table = None
|
||||
template_name = 'utilities/obj_import.html'
|
||||
template_name = 'utilities/obj_bulk_import.html'
|
||||
widget_attrs = {}
|
||||
|
||||
def _import_form(self, *args, **kwargs):
|
||||
@@ -503,7 +622,7 @@ class BulkEditView(GetReturnURLMixin, View):
|
||||
"""
|
||||
queryset = None
|
||||
parent_model = None
|
||||
filter = None
|
||||
filterset = None
|
||||
table = None
|
||||
form = None
|
||||
template_name = 'utilities/obj_bulk_edit.html'
|
||||
@@ -522,8 +641,8 @@ class BulkEditView(GetReturnURLMixin, View):
|
||||
parent_obj = None
|
||||
|
||||
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all') and self.filter is not None:
|
||||
pk_list = [obj.pk for obj in self.filter(request.GET, model.objects.only('pk')).qs]
|
||||
if request.POST.get('_all') and self.filterset is not None:
|
||||
pk_list = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs]
|
||||
else:
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
@@ -624,7 +743,7 @@ class BulkDeleteView(GetReturnURLMixin, View):
|
||||
"""
|
||||
queryset = None
|
||||
parent_model = None
|
||||
filter = None
|
||||
filterset = None
|
||||
table = None
|
||||
form = None
|
||||
template_name = 'utilities/obj_bulk_delete.html'
|
||||
@@ -644,8 +763,8 @@ class BulkDeleteView(GetReturnURLMixin, View):
|
||||
|
||||
# Are we deleting *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all'):
|
||||
if self.filter is not None:
|
||||
pk_list = [obj.pk for obj in self.filter(request.GET, model.objects.only('pk')).qs]
|
||||
if self.filterset is not None:
|
||||
pk_list = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs]
|
||||
else:
|
||||
pk_list = model.objects.values_list('pk', flat=True)
|
||||
else:
|
||||
@@ -788,7 +907,7 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
|
||||
form = None
|
||||
model = None
|
||||
model_form = None
|
||||
filter = None
|
||||
filterset = None
|
||||
table = None
|
||||
template_name = 'utilities/obj_bulk_add_component.html'
|
||||
|
||||
@@ -798,8 +917,8 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
|
||||
model_name = self.model._meta.verbose_name_plural
|
||||
|
||||
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all') and self.filter is not None:
|
||||
pk_list = [obj.pk for obj in self.filter(request.GET, self.parent_model.objects.only('pk')).qs]
|
||||
if request.POST.get('_all') and self.filterset is not None:
|
||||
pk_list = [obj.pk for obj in self.filterset(request.GET, self.parent_model.objects.only('pk')).qs]
|
||||
else:
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
|
Reference in New Issue
Block a user