|
|
@@ -405,10 +405,11 @@ class CSVDataField(forms.CharField):
|
|
|
"""
|
|
|
widget = forms.Textarea
|
|
|
|
|
|
- def __init__(self, fields, required_fields=[], *args, **kwargs):
|
|
|
+ def __init__(self, model, fields, required_fields=None, *args, **kwargs):
|
|
|
|
|
|
+ self.model = model
|
|
|
self.fields = fields
|
|
|
- self.required_fields = required_fields
|
|
|
+ self.required_fields = required_fields or list()
|
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
@@ -423,31 +424,49 @@ class CSVDataField(forms.CharField):
|
|
|
'in double quotes.'
|
|
|
|
|
|
def to_python(self, value):
|
|
|
-
|
|
|
records = []
|
|
|
reader = csv.reader(StringIO(value))
|
|
|
|
|
|
- # 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:
|
|
|
- raise forms.ValidationError('Required column header "{}" not found.'.format(f))
|
|
|
- for f in headers:
|
|
|
- if f not in self.fields:
|
|
|
- raise forms.ValidationError('Unexpected column header "{}" found.'.format(f))
|
|
|
+ # Consume the first line of CSV data as column headers. Create a dictionary mapping each header to an optional
|
|
|
+ # "to" field specifying how the related object is being referenced. For example, importing a Device might use a
|
|
|
+ # `site.slug` header, to indicate the related site is being referenced by its slug.
|
|
|
+ headers = {}
|
|
|
+ for header in next(reader):
|
|
|
+ if '.' in header:
|
|
|
+ field, to_field = header.split('.', 1)
|
|
|
+ headers[field] = to_field
|
|
|
+ else:
|
|
|
+ headers[header] = None
|
|
|
|
|
|
# Parse CSV data
|
|
|
for i, row in enumerate(reader, start=1):
|
|
|
if row:
|
|
|
if len(row) != len(headers):
|
|
|
- raise forms.ValidationError(
|
|
|
- "Row {}: Expected {} columns but found {}".format(i, len(headers), len(row))
|
|
|
- )
|
|
|
+ raise forms.ValidationError(f"Row {i}: Expected {len(headers)} columns but found {len(row)}")
|
|
|
row = [col.strip() for col in row]
|
|
|
- record = dict(zip(headers, row))
|
|
|
+ record = dict(zip(headers.keys(), row))
|
|
|
records.append(record)
|
|
|
|
|
|
- return records
|
|
|
+ return headers, records
|
|
|
+
|
|
|
+ def validate(self, value):
|
|
|
+ headers, records = value
|
|
|
+
|
|
|
+ # Validate provided column headers
|
|
|
+ for field, to_field in headers.items():
|
|
|
+ if field not in self.fields:
|
|
|
+ raise forms.ValidationError(f'Unexpected column header "{field}" found.')
|
|
|
+ if to_field and not hasattr(self.fields[field], 'to_field_name'):
|
|
|
+ raise forms.ValidationError(f'Column "{field}" is not a related object; cannot use dots')
|
|
|
+ if to_field and not hasattr(self.fields[field].queryset.model, to_field):
|
|
|
+ raise forms.ValidationError(f'Invalid related object attribute for column "{field}": {to_field}')
|
|
|
+
|
|
|
+ # Validate required fields
|
|
|
+ for f in self.required_fields:
|
|
|
+ if f not in headers:
|
|
|
+ raise forms.ValidationError(f'Required column header "{f}" not found.')
|
|
|
+
|
|
|
+ return value
|
|
|
|
|
|
|
|
|
class CSVChoiceField(forms.ChoiceField):
|