manager: report all errors at once in CSV import (#70523)
gitea/chrono/pipeline/head This commit looks good Details

This commit is contained in:
Valentin Deniaud 2023-08-10 11:11:07 +02:00
parent b9c02c20bd
commit 8127fbff66
2 changed files with 63 additions and 38 deletions

View File

@ -1206,13 +1206,6 @@ class ImportEventsForm(forms.Form):
super().__init__(**kwargs)
def clean_events_csv_file(self):
class ValidationErrorWithOrdinal(ValidationError):
line_offset = 1
def __init__(self, message, event_no):
super().__init__(message)
self.message = format_html(message, event_no=mark_safe(ordinal(event_no + self.line_offset)))
self.exclude_from_validation = ['desk', 'meeting_type', 'primary_event']
content = self.cleaned_data['events_csv_file'].read()
@ -1234,6 +1227,7 @@ class ImportEventsForm(forms.Form):
except csv.Error:
dialect = None
errors = []
self.events = []
self.warnings = {}
self.events_by_slug = {x.slug: x for x in Event.objects.filter(agenda=self.agenda.pk)}
@ -1243,20 +1237,36 @@ class ImportEventsForm(forms.Form):
).values_list('event_id', flat=True)
)
self.seen_slugs = set(self.events_by_slug.keys())
line_offset = 1
for i, csvline in enumerate(csv.reader(StringIO(content), dialect=dialect)):
if not csvline:
continue
if i == 0 and csvline[0].strip('#') in ('date', 'Date', _('date'), _('Date')):
ValidationErrorWithOrdinal.line_offset = 0
line_offset = 0
continue
event = self.parse_csvline(csvline, i, ValidationErrorWithOrdinal)
self.events.append(event)
try:
event = self.parse_csvline(csvline)
except ValidationError as e:
for error in getattr(e, 'error_list', [e]):
errors.append(
format_html(
'{message} ({event_no} event)',
message=error.message,
event_no=mark_safe(ordinal(i + line_offset)),
)
)
else:
self.events.append(event)
def parse_csvline(self, csvline, i, ValidationErrorWithOrdinal):
if errors:
errors = [_('Invalid file format:')] + errors
raise ValidationError(errors)
def parse_csvline(self, csvline):
if len(csvline) < 3:
raise ValidationErrorWithOrdinal(_('Invalid file format. ({event_no} event)'), i)
raise ValidationError(_('Not enough columns.'))
# label needed to generate a slug
label = None
@ -1308,20 +1318,16 @@ class ImportEventsForm(forms.Form):
event.start_datetime = event_datetime
break
else:
raise ValidationErrorWithOrdinal(
_('Invalid file format. (date/time format, {event_no} event)'), i
)
raise ValidationError(_('Wrong start date/time format.'))
try:
event.places = int(csvline[2])
except ValueError:
raise ValidationError(_('Invalid file format. (number of places, {event_no} event)'), i)
raise ValidationError(_('Number of places must be an integer.'))
if len(csvline) >= 4:
try:
event.waiting_list_places = int(csvline[3])
except ValueError:
raise ValidationError(
_('Invalid file format. (number of places in waiting list, {event_no} event)'), i
)
raise ValidationError(_('Number of places in waiting list must be an integer.'))
column_index = 7
for more_attr in ('description', 'pricing', 'url'):
@ -1347,11 +1353,11 @@ class ImportEventsForm(forms.Form):
except ValueError:
continue
else:
raise ValidationError(_('Invalid file format. (date/time format, {event_no} event)'), i)
raise ValidationError(_('Wrong publication date/time format.'))
if self.agenda.partial_bookings:
if len(csvline) < 11 or not csvline[10]:
raise ValidationError(_('Invalid file format. (missing end_time, {event_no} event)'), i)
raise ValidationError(_('Missing end_time.'))
event.end_time = csvline[10]
else:
self.exclude_from_validation.append('end_time')
@ -1359,19 +1365,16 @@ class ImportEventsForm(forms.Form):
try:
event.duration = int(csvline[10])
except ValueError:
raise ValidationError(_('Invalid file format. (duration, {event_no} event)'), i)
raise ValidationError(_('Duration must be an integer.'))
try:
event.full_clean(exclude=self.exclude_from_validation)
except ValidationError as e:
errors = [_('Invalid file format:\n')]
errors = []
for label, field_errors in e.message_dict.items():
label_name = self.get_verbose_name(label)
msg = _('%s: ') % label_name if label_name else ''
msg += _('%(errors)s (line %(line)d)') % {
'errors': ', '.join(field_errors),
'line': i + 1,
}
msg += ', '.join(field_errors)
errors.append(msg)
raise ValidationError(errors)

View File

@ -798,7 +798,8 @@ def test_import_events(app, admin_user):
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', b'xx', 'text/csv')
resp = resp.form.submit(status=200)
assert 'Invalid file format.' in resp.text
assert 'Invalid file format:' in resp.text
assert 'Wrong start date/time format. (1st event)' in resp.text
resp.form['events_csv_file'] = Upload('t.csv', b'xxxx\0\0xxxx', 'text/csv')
resp = resp.form.submit(status=200)
@ -806,15 +807,16 @@ def test_import_events(app, admin_user):
resp.form['events_csv_file'] = Upload('t.csv', b'2016-14-16,18:00', 'text/csv')
resp = resp.form.submit(status=200)
assert 'Invalid file format.' in resp.text
assert 'Invalid file format:' in resp.text
assert 'Not enough columns. (1st event)' in resp.text
resp.form['events_csv_file'] = Upload('t.csv', b'date,time,etc.\n2016-14-16,18:00,10', 'text/csv')
resp = resp.form.submit(status=200)
assert 'Invalid file format. (date/time format, 1st event)' in resp.text
assert 'Wrong start date/time format. (1st event)' in resp.text
resp.form['events_csv_file'] = Upload('t.csv', b'2016-14-16,18:00,10', 'text/csv')
resp = resp.form.submit(status=200)
assert 'Invalid file format. (date/time format, 1st event)' in resp.text
assert 'Wrong start date/time format. (1st event)' in resp.text
with override_settings(LANGUAGE_CODE='fr-fr'):
resp.form['events_csv_file'] = Upload('t.csv', b'2016-14-16,18:00,10', 'text/csv')
@ -824,11 +826,11 @@ def test_import_events(app, admin_user):
resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,blah', 'text/csv')
resp = resp.form.submit(status=200)
assert 'Invalid file format. (number of places,' in resp.text
assert 'Number of places must be an integer. (1st event)' in resp.text
resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,blah', 'text/csv')
resp = resp.form.submit(status=200)
assert 'Invalid file format. (number of places in waiting list,' in resp.text
assert 'Number of places in waiting list must be an integer. (1st event)' in resp.text
resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,' + b'x' * 151, 'text/csv')
resp = resp.form.submit(status=200)
@ -988,7 +990,7 @@ def test_import_events(app, admin_user):
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,foobar', 'text/csv'
)
resp = resp.form.submit(status=200)
assert 'Invalid file format. (date/time format' in resp.text
assert 'Wrong publication date/time format. (1st event)' in resp.text
# duration bad format
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
@ -996,7 +998,7 @@ def test_import_events(app, admin_user):
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,2016-09-16,foobar', 'text/csv'
)
resp = resp.form.submit(status=200)
assert 'Invalid file format. (duration' in resp.text
assert 'Duration must be an integer. (1st event)' in resp.text
# import events with empty slugs
Event.objects.all().delete()
@ -1027,8 +1029,7 @@ def test_import_events(app, admin_user):
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,label,1234', 'text/csv')
resp = resp.form.submit(status=200)
assert 'value cannot be a number' in resp.text
assert 'Identifier:' in resp.text # verbose_name is shown, not field name ('slug:')
assert 'Identifier: This value cannot be a number. (1st event)' in resp.text
def test_import_event_nested_quotes(app, admin_user):
@ -1192,7 +1193,7 @@ def test_import_events_partial_bookings(app, admin_user):
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,2016-09-16', 'text/csv'
)
resp = resp.form.submit(status=200)
assert 'Invalid file format. (missing end_time' in resp.text
assert 'Missing end_time.' in resp.text
# invalid end time
resp = app.get('/manage/agendas/%s/import-events' % agenda.pk)
@ -1203,6 +1204,27 @@ def test_import_events_partial_bookings(app, admin_user):
assert '“xxx” value has an invalid format' in resp.text
def test_import_events_multiple_errors(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
app = login(app)
resp = app.get('/manage/agendas/%s/import-events' % agenda.pk)
resp.form['events_csv_file'] = Upload(
't.csv',
b'2016-09-17,18:00,10,5,label,slug\n' # valid event
b'2016-09-17,19:00,xxx,5,label2,slug2\n' # invalid places
b'2016-09-17,20:00,10,5,,1234\n', # invalid slug
'text/csv',
)
resp = resp.form.submit(status=200)
assert [x.text for x in resp.pyquery('.errorlist li')] == [
'Invalid file format:',
'Number of places must be an integer. (2nd event)',
'Identifier: This value cannot be a number. (3rd event)',
]
@pytest.mark.freeze_time('2022-05-24')
def test_event_detail(app, admin_user):
agenda = Agenda.objects.create(label='Events', kind='events')