diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index b5308e46d..6d6bd321b 100644 --- a/docs/release-notes/version-2.11.md +++ b/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 --- diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index a312441e5..bd3d6300a 100644 --- a/netbox/netbox/views/generic.py +++ b/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: diff --git a/netbox/templates/generic/object_bulk_import.html b/netbox/templates/generic/object_bulk_import.html index e9972e919..ec6dddf14 100644 --- a/netbox/templates/generic/object_bulk_import.html +++ b/netbox/templates/generic/object_bulk_import.html @@ -16,103 +16,107 @@ {% endif %} -
-
-
- {% csrf_token %} - {% render_form form %} -
-
- - {% if return_url %} - Cancel - {% endif %} -
-
-
-
-

- {% if fields %} -
-
- CSV Field Options -
- - - - - - - - {% for name, field in fields.items %} - - - - - - - {% endfor %} -
FieldRequiredAccessorDescription
- {{ name }} - - {% if field.required %} - - {% else %} - - {% endif %} - - {% if field.to_field_name %} - {{ field.to_field_name }} - {% else %} - - {% endif %} - - {% if field.STATIC_CHOICES %} - - - {% endif %} - {% if field.help_text %} - {{ field.help_text }}
- {% elif field.label %} - {{ field.label }}
- {% endif %} - {% if field|widget_type == 'dateinput' %} - Format: YYYY-MM-DD - {% elif field|widget_type == 'checkboxinput' %} - Specify "true" or "false" - {% endif %} -
-
-

- Required fields must be specified for all - objects. -

-

- Related objects may be referenced by any unique attribute. - For example, vrf.rd would identify a VRF by its route distinguisher. -

- {% endif %} +
+ {% csrf_token %} +
+
+ {% render_field form.csv %} +
+
+ {% render_field form.csv_file %} +
-
+
+
+ + {% if return_url %} + Cancel + {% endif %} +
+
+ +
+

+ {% if fields %} +
+
+ CSV Field Options +
+ + + + + + + + {% for name, field in fields.items %} + + + + + + + {% endfor %} +
FieldRequiredAccessorDescription
+ {{ name }} + + {% if field.required %} + + {% else %} + + {% endif %} + + {% if field.to_field_name %} + {{ field.to_field_name }} + {% else %} + + {% endif %} + + {% if field.STATIC_CHOICES %} + + + {% endif %} + {% if field.help_text %} + {{ field.help_text }}
+ {% elif field.label %} + {{ field.label }}
+ {% endif %} + {% if field|widget_type == 'dateinput' %} + Format: YYYY-MM-DD + {% elif field|widget_type == 'checkboxinput' %} + Specify "true" or "false" + {% endif %} +
+
+

+ Required fields must be specified for all + objects. +

+

+ Related objects may be referenced by any unique attribute. + For example, vrf.rd would identify a VRF by its route distinguisher. +

+ {% endif %}
{% endblock %} diff --git a/netbox/utilities/forms/fields.py b/netbox/utilities/forms/fields.py index 2b9b57ea3..7cb2cd705 100644 --- a/netbox/utilities/forms/fields.py +++ b/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 diff --git a/netbox/utilities/forms/utils.py b/netbox/utilities/forms/utils.py index 50a6a416e..503a2e8a0 100644 --- a/netbox/utilities/forms/utils.py +++ b/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