mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Changelog and cleanup for #6560
This commit is contained in:
parent
ea0de4b01d
commit
8d9d3a9e7d
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
* [#6560](https://github.com/netbox-community/netbox/issues/6560) - Enable CSV import via uploaded file
|
||||||
|
* [#6771](https://github.com/netbox-community/netbox/issues/6771) - Add count of inventory items to manufacturer view
|
||||||
* [#6785](https://github.com/netbox-community/netbox/issues/6785) - Add "hardwired" type for power port types
|
* [#6785](https://github.com/netbox-community/netbox/issues/6785) - Add "hardwired" type for power port types
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
@ -22,7 +24,6 @@
|
|||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
* [#6771](https://github.com/netbox-community/netbox/issues/6771) - Add count of inventory items to manufacturer view
|
|
||||||
* [#6781](https://github.com/netbox-community/netbox/issues/6781) - Database query caching is now disabled by default
|
* [#6781](https://github.com/netbox-community/netbox/issues/6781) - Database query caching is now disabled by default
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -20,7 +20,8 @@ from extras.models import CustomField, ExportTemplate
|
|||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortTransaction, PermissionsViolation
|
from utilities.exceptions import AbortTransaction, PermissionsViolation
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, ImportForm, TableConfigForm, restrict_form_fields, CSVFileField
|
BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, CSVFileField, ImportForm, TableConfigForm,
|
||||||
|
restrict_form_fields,
|
||||||
)
|
)
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.tables import paginate_table
|
from utilities.tables import paginate_table
|
||||||
@ -673,8 +674,16 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def used_both_csv_fields(self):
|
def clean(self):
|
||||||
return self.cleaned_data['csv_file'][1] and self.cleaned_data['csv'][1]
|
csv_rows = self.cleaned_data['csv'][1]
|
||||||
|
csv_file = self.files.get('csv_file')
|
||||||
|
|
||||||
|
# Check that the user has not submitted both text data and a file
|
||||||
|
if csv_rows and csv_file:
|
||||||
|
raise ValidationError(
|
||||||
|
"Cannot process CSV text and file attachment simultaneously. Please choose only one import "
|
||||||
|
"method."
|
||||||
|
)
|
||||||
|
|
||||||
return ImportForm(*args, **kwargs)
|
return ImportForm(*args, **kwargs)
|
||||||
|
|
||||||
@ -705,9 +714,6 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
logger.debug("Form validation was successful")
|
logger.debug("Form validation was successful")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if form.used_both_csv_fields():
|
|
||||||
form.add_error('csv_file', "Choose one of two import methods")
|
|
||||||
raise ValidationError("")
|
|
||||||
# Iterate through CSV data and bind each row to a new model form instance.
|
# Iterate through CSV data and bind each row to a new model form instance.
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if request.FILES:
|
if request.FILES:
|
||||||
|
@ -16,103 +16,107 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul class="nav nav-tabs" role="tablist">
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
<li role="presentation" class="active"><a href="#csv" role="tab" data-toggle="tab">CSV</a></li>
|
<li role="presentation" class="active"><a href="#csv" role="tab" data-toggle="tab">CSV Data</a></li>
|
||||||
|
<li role="presentation"><a href="#csv-file" role="tab" data-toggle="tab">CSV File Upload</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<form action="" method="post" class="form" enctype="multipart/form-data">
|
||||||
<div role="tabpanel" class="tab-pane active" id="csv">
|
{% csrf_token %}
|
||||||
<form action="" method="post" class="form" enctype="multipart/form-data">
|
<div class="tab-content">
|
||||||
{% csrf_token %}
|
<div role="tabpanel" class="tab-pane active" id="csv">
|
||||||
{% render_form form %}
|
{% render_field form.csv %}
|
||||||
<div class="form-group">
|
</div>
|
||||||
<div class="col-md-12 text-right">
|
<div role="tabpanel" class="tab-pane" id="csv-file">
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
{% render_field form.csv_file %}
|
||||||
{% if return_url %}
|
</div>
|
||||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="clearfix"></div>
|
|
||||||
<p></p>
|
|
||||||
{% if fields %}
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<strong>CSV Field Options</strong>
|
|
||||||
</div>
|
|
||||||
<table class="table">
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Required</th>
|
|
||||||
<th>Accessor</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
{% for name, field in fields.items %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<code>{{ name }}</code>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if field.required %}
|
|
||||||
<i class="mdi mdi-check-bold text-success" title="Required"></i>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if field.to_field_name %}
|
|
||||||
<code>{{ field.to_field_name }}</code>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if field.STATIC_CHOICES %}
|
|
||||||
<button type="button" class="btn btn-link btn-xs pull-right" data-toggle="modal" data-target="#{{ name }}_choices">
|
|
||||||
<i class="mdi mdi-help-circle"></i>
|
|
||||||
</button>
|
|
||||||
<div class="modal fade" id="{{ name }}_choices" tabindex="-1" role="dialog">
|
|
||||||
<div class="modal-dialog" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
|
||||||
<h4 class="modal-title"><code>{{ name }}</code> Choices</h4>
|
|
||||||
</div>
|
|
||||||
<table class="table table-striped modal-body">
|
|
||||||
<tr><th>Import Value</th><th>Label</th></tr>
|
|
||||||
{% for value, label in field.choices %}
|
|
||||||
{% if value %}<tr><td><samp>{{ value }}</samp></td><td>{{ label }}</td></tr>{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if field.help_text %}
|
|
||||||
{{ field.help_text }}<br />
|
|
||||||
{% elif field.label %}
|
|
||||||
{{ field.label }}<br />
|
|
||||||
{% endif %}
|
|
||||||
{% if field|widget_type == 'dateinput' %}
|
|
||||||
<small class="text-muted">Format: YYYY-MM-DD</small>
|
|
||||||
{% elif field|widget_type == 'checkboxinput' %}
|
|
||||||
<small class="text-muted">Specify "true" or "false"</small>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<p class="small text-muted">
|
|
||||||
<i class="mdi mdi-check-bold"></i> Required fields <strong>must</strong> be specified for all
|
|
||||||
objects.
|
|
||||||
</p>
|
|
||||||
<p class="small text-muted">
|
|
||||||
<i class="mdi mdi-information-outline"></i> Related objects may be referenced by any unique attribute.
|
|
||||||
For example, <code>vrf.rd</code> would identify a VRF by its route distinguisher.
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<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 class="clearfix"></div>
|
||||||
|
<p></p>
|
||||||
|
{% if fields %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>CSV Field Options</strong>
|
||||||
|
</div>
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Required</th>
|
||||||
|
<th>Accessor</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
{% for name, field in fields.items %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>{{ name }}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if field.required %}
|
||||||
|
<i class="mdi mdi-check-bold text-success" title="Required"></i>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if field.to_field_name %}
|
||||||
|
<code>{{ field.to_field_name }}</code>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if field.STATIC_CHOICES %}
|
||||||
|
<button type="button" class="btn btn-link btn-xs pull-right" data-toggle="modal" data-target="#{{ name }}_choices">
|
||||||
|
<i class="mdi mdi-help-circle"></i>
|
||||||
|
</button>
|
||||||
|
<div class="modal fade" id="{{ name }}_choices" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
<h4 class="modal-title"><code>{{ name }}</code> Choices</h4>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped modal-body">
|
||||||
|
<tr><th>Import Value</th><th>Label</th></tr>
|
||||||
|
{% for value, label in field.choices %}
|
||||||
|
{% if value %}<tr><td><samp>{{ value }}</samp></td><td>{{ label }}</td></tr>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.help_text %}
|
||||||
|
{{ field.help_text }}<br />
|
||||||
|
{% elif field.label %}
|
||||||
|
{{ field.label }}<br />
|
||||||
|
{% endif %}
|
||||||
|
{% if field|widget_type == 'dateinput' %}
|
||||||
|
<small class="text-muted">Format: YYYY-MM-DD</small>
|
||||||
|
{% elif field|widget_type == 'checkboxinput' %}
|
||||||
|
<small class="text-muted">Specify "true" or "false"</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="small text-muted">
|
||||||
|
<i class="mdi mdi-check-bold"></i> Required fields <strong>must</strong> be specified for all
|
||||||
|
objects.
|
||||||
|
</p>
|
||||||
|
<p class="small text-muted">
|
||||||
|
<i class="mdi mdi-information-outline"></i> Related objects may be referenced by any unique attribute.
|
||||||
|
For example, <code>vrf.rd</code> would identify a VRF by its route distinguisher.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -208,22 +208,20 @@ class CSVFileField(forms.FileField):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def to_python(self, file):
|
def to_python(self, file):
|
||||||
if file:
|
if file is None:
|
||||||
csv_str = file.read().decode('utf-8')
|
return None
|
||||||
reader = csv.reader(csv_str.splitlines())
|
|
||||||
|
|
||||||
headers = {}
|
csv_str = file.read().decode('utf-8').strip()
|
||||||
records = []
|
reader = csv.reader(csv_str.splitlines())
|
||||||
if file:
|
headers, records = parse_csv(reader)
|
||||||
headers, records = parse_csv(reader)
|
|
||||||
|
|
||||||
return headers, records
|
return headers, records
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
headers, records = value
|
if value is None:
|
||||||
if not headers and not records:
|
return None
|
||||||
return value
|
|
||||||
|
|
||||||
|
headers, records = value
|
||||||
validate_csv(headers, self.fields, self.required_fields)
|
validate_csv(headers, self.fields, self.required_fields)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
@ -166,6 +166,7 @@ def parse_csv(reader):
|
|||||||
row = [col.strip() for col in row]
|
row = [col.strip() for col in row]
|
||||||
record = dict(zip(headers.keys(), row))
|
record = dict(zip(headers.keys(), row))
|
||||||
records.append(record)
|
records.append(record)
|
||||||
|
|
||||||
return headers, records
|
return headers, records
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user