mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-21 12:52:21 -06:00
Merge branch 'develop' into develop-2.3
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import csv
|
||||
import itertools
|
||||
from io import StringIO
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
@@ -245,14 +245,10 @@ class CSVDataField(forms.CharField):
|
||||
|
||||
def to_python(self, value):
|
||||
|
||||
# Python 2's csv module has problems with Unicode
|
||||
if not isinstance(value, str):
|
||||
value = value.encode('utf-8')
|
||||
|
||||
records = []
|
||||
reader = csv.reader(value.splitlines())
|
||||
reader = csv.reader(StringIO(value))
|
||||
|
||||
# Consume and valdiate the first line of CSV data as column headers
|
||||
# Consume and validate the first line of CSV data as column headers
|
||||
headers = next(reader)
|
||||
for f in self.required_fields:
|
||||
if f not in headers:
|
||||
|
||||
3
netbox/utilities/templates/buttons/add.html
Normal file
3
netbox/utilities/templates/buttons/add.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<a href="{% url add_url %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span> Add
|
||||
</a>
|
||||
19
netbox/utilities/templates/buttons/export.html
Normal file
19
netbox/utilities/templates/buttons/export.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% if export_templates %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="fa fa-upload" aria-hidden="true"></span>
|
||||
Export <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export">CSV (default)</a></li>
|
||||
<li class="divider"></li>
|
||||
{% for et in export_templates %}
|
||||
<li><a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export={{ et.name }}"{% if et.description %} title="{{ et.description }}"{% endif %}>{{ et.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="?{% if url_params %}{{ url_params.urlencode }}&{% endif %}export" class="btn btn-success">
|
||||
<span class="fa fa-upload" aria-hidden="true"></span> Export
|
||||
</a>
|
||||
{% endif %}
|
||||
3
netbox/utilities/templates/buttons/import.html
Normal file
3
netbox/utilities/templates/buttons/import.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<a href="{% url import_url %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span> Import
|
||||
</a>
|
||||
26
netbox/utilities/templatetags/buttons.py
Normal file
26
netbox/utilities/templatetags/buttons.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import template
|
||||
|
||||
from extras.models import ExportTemplate
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag('buttons/add.html')
|
||||
def add_button(url):
|
||||
return {'add_url': url}
|
||||
|
||||
|
||||
@register.inclusion_tag('buttons/import.html')
|
||||
def import_button(url):
|
||||
return {'import_url': url}
|
||||
|
||||
|
||||
@register.inclusion_tag('buttons/export.html', takes_context=True)
|
||||
def export_button(context, content_type=None):
|
||||
export_templates = ExportTemplate.objects.filter(content_type=content_type)
|
||||
return {
|
||||
'url_params': context['request'].GET,
|
||||
'export_templates': export_templates,
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import six
|
||||
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
def csv_format(data):
|
||||
"""
|
||||
@@ -15,12 +18,16 @@ def csv_format(data):
|
||||
csv.append('')
|
||||
continue
|
||||
|
||||
# Convert dates to ISO format
|
||||
if isinstance(value, (datetime.date, datetime.datetime)):
|
||||
value = value.isoformat()
|
||||
|
||||
# Force conversion to string first so we can check for any commas
|
||||
if not isinstance(value, six.string_types):
|
||||
value = '{}'.format(value)
|
||||
|
||||
# Double-quote the value if it contains a comma
|
||||
if ',' in value:
|
||||
if ',' in value or '\n' in value:
|
||||
csv.append('"{}"'.format(value))
|
||||
else:
|
||||
csv.append('{}'.format(value))
|
||||
@@ -28,6 +35,32 @@ def csv_format(data):
|
||||
return ','.join(csv)
|
||||
|
||||
|
||||
def queryset_to_csv(queryset):
|
||||
"""
|
||||
Export a queryset of objects as CSV, using the model's to_csv() method.
|
||||
"""
|
||||
output = []
|
||||
|
||||
# Start with the column headers
|
||||
headers = ','.join(queryset.model.csv_headers)
|
||||
output.append(headers)
|
||||
|
||||
# Iterate through the queryset
|
||||
for obj in queryset:
|
||||
data = csv_format(obj.to_csv())
|
||||
output.append(data)
|
||||
|
||||
# Build the HTTP response
|
||||
response = HttpResponse(
|
||||
'\n'.join(output),
|
||||
content_type='text/csv'
|
||||
)
|
||||
filename = 'netbox_{}.csv'.format(queryset.model._meta.verbose_name_plural)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def foreground_color(bg_color):
|
||||
"""
|
||||
Return the ideal foreground color (black or white) for a given background color in hexadecimal RGB format.
|
||||
|
||||
@@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import transaction, IntegrityError
|
||||
from django.db.models import ProtectedError
|
||||
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.urls import reverse
|
||||
@@ -21,6 +20,7 @@ from django.views.generic import View
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
|
||||
from utilities.utils import queryset_to_csv
|
||||
from utilities.forms import BootstrapMixin, CSVDataField
|
||||
from .constants import M2M_FIELD_TYPES
|
||||
from .error_handlers import handle_protectederror
|
||||
@@ -80,7 +80,7 @@ class ObjectListView(View):
|
||||
def get(self, request):
|
||||
|
||||
model = self.queryset.model
|
||||
object_ct = ContentType.objects.get_for_model(model)
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
|
||||
if self.filter:
|
||||
self.queryset = self.filter(request.GET, self.queryset).qs
|
||||
@@ -93,27 +93,18 @@ class ObjectListView(View):
|
||||
|
||||
# Check for export template rendering
|
||||
if request.GET.get('export'):
|
||||
et = get_object_or_404(ExportTemplate, content_type=object_ct, name=request.GET.get('export'))
|
||||
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET.get('export'))
|
||||
queryset = CustomFieldQueryset(self.queryset, custom_fields) if custom_fields else self.queryset
|
||||
try:
|
||||
response = et.to_response(context_dict={'queryset': queryset},
|
||||
filename='netbox_{}'.format(model._meta.verbose_name_plural))
|
||||
return response
|
||||
return et.render_to_response(queryset)
|
||||
except TemplateSyntaxError:
|
||||
messages.error(request, "There was an error rendering the selected export template ({})."
|
||||
.format(et.name))
|
||||
# Fall back to built-in CSV export
|
||||
messages.error(
|
||||
request,
|
||||
"There was an error rendering the selected export template ({}).".format(et.name)
|
||||
)
|
||||
# Fall back to built-in CSV export if no template was specified
|
||||
elif 'export' in request.GET and hasattr(model, 'to_csv'):
|
||||
headers = getattr(model, 'csv_headers', None)
|
||||
output = ','.join(headers) + '\n' if headers else ''
|
||||
output += '\n'.join([obj.to_csv() for obj in self.queryset])
|
||||
response = HttpResponse(
|
||||
output,
|
||||
content_type='text/csv'
|
||||
)
|
||||
response['Content-Disposition'] = 'attachment; filename="netbox_{}.csv"'\
|
||||
.format(self.queryset.model._meta.verbose_name_plural)
|
||||
return response
|
||||
return queryset_to_csv(self.queryset)
|
||||
|
||||
# Provide a hook to tweak the queryset based on the request immediately prior to rendering the object list
|
||||
self.queryset = self.alter_queryset(request)
|
||||
@@ -135,10 +126,10 @@ class ObjectListView(View):
|
||||
RequestConfig(request, paginate).configure(table)
|
||||
|
||||
context = {
|
||||
'content_type': content_type,
|
||||
'table': table,
|
||||
'permissions': permissions,
|
||||
'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None,
|
||||
'export_templates': ExportTemplate.objects.filter(content_type=object_ct),
|
||||
}
|
||||
context.update(self.extra_context())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user