jeremystretch 4 лет назад
Родитель
Сommit
8d9d3a9e7d

+ 2 - 1
docs/release-notes/version-2.11.md

@@ -4,6 +4,8 @@
 
 ### 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
 
 ### Bug Fixes
@@ -22,7 +24,6 @@
 
 ### 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
 
 ---

+ 12 - 6
netbox/netbox/views/generic.py

@@ -20,7 +20,8 @@ from extras.models import CustomField, ExportTemplate
 from utilities.error_handlers import handle_protectederror
 from utilities.exceptions import AbortTransaction, PermissionsViolation
 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.tables import paginate_table
@@ -673,8 +674,16 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
                 required=False
             )
 
-            def used_both_csv_fields(self):
-                return self.cleaned_data['csv_file'][1] and self.cleaned_data['csv'][1]
+            def clean(self):
+                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)
 
@@ -705,9 +714,6 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
             logger.debug("Form validation was successful")
 
             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.
                 with transaction.atomic():
                     if request.FILES:

+ 97 - 93
netbox/templates/generic/object_bulk_import.html

@@ -16,103 +16,107 @@
                 </div>
             {% endif %}
             <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>
-            <div class="tab-content">
-                <div role="tabpanel" class="tab-pane active" id="csv">
-                    <form action="" method="post" class="form" enctype="multipart/form-data">
-                        {% 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 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">&mdash;</span>
-                                            {% endif %}
-                                        </td>
-                                        <td>
-                                            {% if field.to_field_name %}
-                                                <code>{{ field.to_field_name }}</code>
-                                            {% else %}
-                                                <span class="text-muted">&mdash;</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">&times;</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>
+            <form action="" method="post" class="form" enctype="multipart/form-data">
+                {% csrf_token %}
+                <div class="tab-content">
+                    <div role="tabpanel" class="tab-pane active" id="csv">
+                        {% render_field form.csv %}
+                    </div>
+                    <div role="tabpanel" class="tab-pane" id="csv-file">
+                        {% render_field form.csv_file %}
+                    </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">&mdash;</span>
+                                    {% endif %}
+                                </td>
+                                <td>
+                                    {% if field.to_field_name %}
+                                        <code>{{ field.to_field_name }}</code>
+                                    {% else %}
+                                        <span class="text-muted">&mdash;</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">&times;</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>
-                                            {% 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>
+                                    {% 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>
-            </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>
 {% endblock %}

+ 8 - 10
netbox/utilities/forms/fields.py

@@ -208,22 +208,20 @@ class CSVFileField(forms.FileField):
         super().__init__(*args, **kwargs)
 
     def to_python(self, file):
-        if file:
-            csv_str = file.read().decode('utf-8')
-            reader = csv.reader(csv_str.splitlines())
+        if file is None:
+            return None
 
-        headers = {}
-        records = []
-        if file:
-            headers, records = parse_csv(reader)
+        csv_str = file.read().decode('utf-8').strip()
+        reader = csv.reader(csv_str.splitlines())
+        headers, records = parse_csv(reader)
 
         return headers, records
 
     def validate(self, value):
-        headers, records = value
-        if not headers and not records:
-            return value
+        if value is None:
+            return None
 
+        headers, records = value
         validate_csv(headers, self.fields, self.required_fields)
 
         return value

+ 1 - 0
netbox/utilities/forms/utils.py

@@ -166,6 +166,7 @@ def parse_csv(reader):
         row = [col.strip() for col in row]
         record = dict(zip(headers.keys(), row))
         records.append(record)
+
     return headers, records