Merge branch 'develop' into develop-2.3

This commit is contained in:
Jeremy Stretch
2018-02-06 14:58:11 -05:00
62 changed files with 571 additions and 515 deletions

View File

@@ -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:

View 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>

View 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 %}

View 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>

View 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,
}

View File

@@ -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.

View File

@@ -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())