forms: add exception string to technical error message (#63776) #815

Merged
fpeters merged 1 commits from wip/63776-technical-error-details into main 2023-11-10 08:58:10 +01:00
13 changed files with 73 additions and 39 deletions

View File

@ -175,7 +175,8 @@ def test_form_item_data_source_error(pub, monkeypatch, fail_after_count_page, fa
NamedDataSource, 'get_structured_value', failing_get_structured_value(fail_after_count_page).method
)
resp = resp.forms[0].submit('submit')
assert 'Technical error, please try again' in resp.text
assert resp.pyquery('.global-errors summary').text() == 'Technical error, please try again.'
assert resp.pyquery('.global-errors p').text() == 'datasource is unavailable (field id: 1)'
# fix transient failure
monkeypatch.setattr(NamedDataSource, 'get_structured_value', normal_get_structured_value)
@ -189,7 +190,8 @@ def test_form_item_data_source_error(pub, monkeypatch, fail_after_count_page, fa
failing_get_structured_value(fail_after_count_validation).method,
)
resp = resp.forms[0].submit('submit')
assert 'Technical error, please try again' in resp.text
assert resp.pyquery('.global-errors summary').text() == 'Technical error, please try again.'
assert resp.pyquery('.global-errors p').text() == 'datasource is unavailable (field id: 1)'
# fix transient failure
monkeypatch.setattr(NamedDataSource, 'get_structured_value', normal_get_structured_value)
@ -257,7 +259,8 @@ def test_form_item_data_source_error_no_confirmation(pub, monkeypatch, fail_afte
NamedDataSource, 'get_structured_value', failing_get_structured_value(fail_after_count_page).method
)
resp = resp.forms[0].submit('submit')
assert 'Technical error, please try again' in resp.text
assert resp.pyquery('.global-errors summary').text() == 'Technical error, please try again.'
assert resp.pyquery('.global-errors p').text() == 'datasource is unavailable (field id: 1)'
# fix transient failure
monkeypatch.setattr(NamedDataSource, 'get_structured_value', normal_get_structured_value)

View File

@ -228,12 +228,12 @@ def test_set_backoffice_field_map(http_requests, pub):
assert pub.loggederror_class.count() == 1
logged_error = pub.loggederror_class.select()[0]
assert (
logged_error.summary
== "Failed to set Map field (bo1), error: invalid coordinates 'invalid value' (missing ;)"
logged_error.summary == "Failed to set Map field (bo1), error: invalid coordinates 'invalid value' "
'(missing ;) (field id: bo1)'
)
assert logged_error.formdata_id == str(formdata.id)
assert logged_error.exception_class == 'SetValueError'
assert logged_error.exception_message == "invalid coordinates 'invalid value' (missing ;)"
assert logged_error.exception_message == "invalid coordinates 'invalid value' (missing ;) (field id: bo1)"
pub.loggederror_class.wipe()
item.fields = [{'field_id': 'bo1', 'value': 'XXX;YYY'}]
@ -241,10 +241,13 @@ def test_set_backoffice_field_map(http_requests, pub):
formdata = formdef.data_class().get(formdata.id)
assert pub.loggederror_class.count() == 1
logged_error = pub.loggederror_class.select()[0]
assert logged_error.summary == "Failed to set Map field (bo1), error: invalid coordinates 'XXX;YYY'"
assert (
logged_error.summary
== "Failed to set Map field (bo1), error: invalid coordinates 'XXX;YYY' (field id: bo1)"
)
assert logged_error.formdata_id == str(formdata.id)
assert logged_error.exception_class == 'SetValueError'
assert logged_error.exception_message == "invalid coordinates 'XXX;YYY'"
assert logged_error.exception_message == "invalid coordinates 'XXX;YYY' (field id: bo1)"
pub.loggederror_class.wipe()
@ -1453,7 +1456,7 @@ def test_set_backoffice_field_invalid_block_value(pub):
logged_error = pub.loggederror_class.select()[0]
assert (
logged_error.summary
== 'Failed to set Field Block (foobar) field (bo1), error: invalid value for block'
== 'Failed to set Field Block (foobar) field (bo1), error: invalid value for block (field id: bo1)'
)
formdata = formdef.data_class().get(formdata.id)

View File

@ -592,7 +592,11 @@ def test_frontoffice_workflow_form_with_disappearing_option(pub, monkeypatch):
resp.form['fblah_1'] = '1'
resp = resp.form.submit('submit')
assert 'Technical error, please try again' in resp.text
assert resp.pyquery('.global-errors summary').text() == 'Technical error, please try again.'
assert (
resp.pyquery('.global-errors p').text()
== "no matching value in datasource (field id: blah_1, value: '1')"
)
formdata.refresh_from_storage()
assert formdata.status == 'wf-%s' % st1.id
assert not formdata.workflow_data

View File

@ -626,7 +626,7 @@ class Field:
if self.store_display_value:
display_value = self.store_display_value(data, self.id)
if raise_on_error and display_value is None:
raise SetValueError('a datasource is unavailable (field id: %s)' % self.id)
raise SetValueError(_('datasource is unavailable (field id: %s)') % self.id)

Je marque les chaines pour traduction vu qu'elles apparaitront dans l'interface.

Je marque les chaines pour traduction vu qu'elles apparaitront dans l'interface.
data['%s_display' % self.id] = display_value or None
if self.store_structured_value and value:
structured_value = self.store_structured_value(data, self.id, raise_on_error=raise_on_error)

View File

@ -292,7 +292,7 @@ class BlockField(WidgetField):
if isinstance(value, BlockRowValue):
value = value.make_value(block=self.block, field=self, data=data)
elif value and not (isinstance(value, dict) and 'data' in value and 'schema' in value):
raise SetValueError('invalid value for block')
raise SetValueError(_('invalid value for block (field id: %s)') % self.id)
super().set_value(data, value, **kwargs)
def get_json_value(self, value, **kwargs):

View File

@ -458,7 +458,7 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin, ItemWithImageField
# wf/backoffice_fields.py action.
if not data_source.get_structured_value(value):
raise SetValueError(
'no matching value in datasource (field id: %s, value: %r)' % (self.id, value)
_('no matching value in datasource (field id: %s, value: %r)') % (self.id, value)
)
super().set_value(data, value, raise_on_error=raise_on_error)
@ -498,7 +498,7 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin, ItemWithImageField
with get_publisher().with_language('default'):
value = data_source.get_structured_value(data.get(field_id))
if value is None and raise_on_error:
raise SetValueError('a datasource is unavailable (field id: %s)' % field_id)
raise SetValueError(_('datasource is unavailable (field id: %s)') % field_id)
if value is None or set(value.keys()) == {'id', 'text'}:
return

View File

@ -334,7 +334,7 @@ class ItemsField(WidgetField, ItemFieldMixin, ItemWithImageFieldMixin):
break
else:
if raise_on_error:
raise SetValueError('datasource is unavailable')
raise SetValueError(_('datasource is unavailable (field id: %s)') % self.id)
return ', '.join(choices)
def store_structured_value(self, data, field_id, raise_on_error=False):

View File

@ -246,13 +246,13 @@ class MapField(WidgetField, MapOptionsMixin):
if value == '':
value = None
elif value and ';' not in value:
raise SetValueError('invalid coordinates %r (missing ;)' % value)
raise SetValueError(_('invalid coordinates %r (missing ;) (field id: %s)') % (value, self.id))
elif value:
try:
dummy, dummy = (float(x) for x in value.split(';'))
except ValueError:
# will catch both "too many values to unpack" and invalid float values
raise SetValueError('invalid coordinates %r' % value)
raise SetValueError(_('invalid coordinates %r (field id: %s)') % (value, self.id))
super().set_value(data, value)

View File

@ -329,8 +329,7 @@ class FormStatusPage(Directory, FormTemplateMixin):
try:
response = self.check_submitted_form(form)
except RedisplayFormException:
# don't display errors after "add block" button has been clicked.
form.clear_errors()
pass

Cette partie se trouve intégrée dans l'exception, pour éviter la duplication.

Cette partie se trouve intégrée dans l'exception, pour éviter la duplication.
else:
if response:
return response
@ -638,8 +637,7 @@ class FormStatusPage(Directory, FormTemplateMixin):
try:
response = self.check_submitted_form(form)
except RedisplayFormException:
# don't display errors after "add block" button has been clicked.
form.clear_errors()
pass
else:
if response:
get_session().unmark_visited_object(self.filled)

View File

@ -1281,11 +1281,13 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
self.reset_locked_data(form)
try:
data = self.formdef.get_data(form, raise_on_error=True)
except SetValueError:
except SetValueError as e:
return self.page(
page,
page_change=False,
page_error_messages=[_('Technical error, please try again')],
page_error_messages=[
{'summary': _('Technical error, please try again.'), 'details': e}
],
transient_formdata=transient_formdata,
)
computed_data = self.handle_computed_fields(magictoken, submitted_fields)
@ -1311,11 +1313,13 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
try:
with get_publisher().substitutions.temporary_feed(transient_formdata, force_mode='lazy'):
data = self.formdef.get_data(form, raise_on_error=True)
except SetValueError:
except SetValueError as e:
return self.page(
page,
page_change=False,
page_error_messages=[_('Technical error, please try again')],
page_error_messages=[
{'summary': _('Technical error, please try again.'), 'details': e}
],
transient_formdata=transient_formdata,
)
form_data.update(data)
@ -1361,11 +1365,13 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
with get_publisher().substitutions.temporary_feed(transient_formdata, force_mode='lazy'):
try:
data = self.formdef.get_data(form, raise_on_error=True)
except SetValueError:
except SetValueError as e:
return self.page(
page,
page_change=False,
page_error_messages=[_('Technical error, please try again')],
page_error_messages=[
{'summary': _('Technical error, please try again.'), 'details': e}
],
transient_formdata=transient_formdata,
)
form_data.update(data)
@ -1411,11 +1417,13 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
view_form = self.create_view_form(form_data, use_tokens=False)
try:
return self.submitted_existing(view_form)
except SetValueError:
except SetValueError as e:
return self.page(
page,
page_change=False,
page_error_messages=[_('Technical error, please try again')],
page_error_messages=[
{'summary': _('Technical error, please try again.'), 'details': e}
],
transient_formdata=transient_formdata,
)
if self.has_confirmation_page():
@ -1480,18 +1488,23 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
try:
return self.submitted(form, existing_formdata)
except SetValueError:
except SetValueError as e:
if get_request().form.get('step') == '2':
# submit came from the validation page
return self.validating(
form_data, page_error_messages=[_('Technical error, please try again')]
form_data,
page_error_messages=[
{'summary': _('Technical error, please try again.'), 'details': e}
],
)
else:
# last page
return self.page(
page,
page_change=False,
page_error_messages=[_('Technical error, please try again')],
page_error_messages=[
{'summary': _('Technical error, please try again.'), 'details': e}
],
transient_formdata=transient_formdata,
)

View File

@ -496,7 +496,14 @@ class Form(QuixoteForm):
def _render_error_notice_content(self, errors):
t = TemplateIO(html=True)
for error in errors:
if isinstance(error, htmltext):
if isinstance(error, dict):
t += (
htmltext(
'<details><summary>%(summary)s</summary><p><small>%(details)s</small></p></details>'
)
% error

On peut passer en erreur un dictionnaire, {"summary": ..., "details": ...} et ça sera affiché dans une zone dépliable.

On peut passer en erreur un dictionnaire, {"summary": ..., "details": ...} et ça sera affiché dans une zone dépliable.
)
elif isinstance(error, htmltext):
t += error
else:
t += htmltext('<p>%s</p>') % error

View File

@ -16,7 +16,7 @@
import xml.etree.ElementTree as ET
from quixote import get_publisher, get_session
from quixote import get_publisher
from quixote.html import TemplateIO, htmltext
from wcs.admin.fields import FieldDefPage, FieldsDirectory
@ -326,9 +326,10 @@ class FormWorkflowStatusItem(WorkflowStatusItem):
self.prefix_form_fields()
try:
formdef_data = self.formdef.get_data(form, raise_on_error=submit)
except SetValueError:
get_session().message = ('error', _('Technical error, please try again'))
raise RedisplayFormException()
except SetValueError as e:
raise RedisplayFormException(
form=form, error={'summary': _('Technical error, please try again.'), 'details': e}
Review

Plutôt qu'utiliser l'infrastructure générale d'affichage de message de session, ça va aller le mettre correctement en erreur sur le formulaire.

Plutôt qu'utiliser l'infrastructure générale d'affichage de message de session, ça va aller le mettre correctement en erreur sur le formulaire.
)
for k, v in get_dict_with_varnames(self.formdef.fields, formdef_data, varnames_only=True).items():
workflow_data['%s_%s' % (self.varname, k)] = v
if not get_publisher().has_site_option('disable-workflow-form-to-workflow-data'):
@ -347,7 +348,7 @@ class FormWorkflowStatusItem(WorkflowStatusItem):
if isinstance(widget, WidgetList): # BlockWidget
add_element_widget = widget.get_widget('add_element')
if add_element_widget and add_element_widget.parse():
raise RedisplayFormException()
raise RedisplayFormException(form=form)
elif not form.has_errors():
button_name = form.get_submit()
button = form.get_widget(button_name)

View File

@ -148,7 +148,12 @@ class AbortActionException(Exception):
class RedisplayFormException(Exception):
pass
def __init__(self, form, error=None):
# don't display errors after "add block" button has been clicked.
form.clear_errors()
if error:
# add explicit error
form.add_global_errors([error])
def get_role_dependencies(roles):