From b7df4347268112f6f427a8bd54476e1ec442e3c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 30 Nov 2020 18:29:09 +0100
Subject: [PATCH] misc: store LoggedErrors in SQL (#48925)

---
 tests/admin_pages/test_workflow.py      |  13 +-
 tests/backoffice_pages/test_all.py      |  51 +++--
 tests/backoffice_pages/test_carddata.py |  11 +-
 tests/form_pages/test_all.py            |  65 +++---
 tests/test_formdata.py                  |  11 +-
 tests/test_mail_templates.py            |  14 +-
 tests/test_workflows.py                 | 290 ++++++++++++++----------
 tests/test_wscall.py                    |  15 +-
 tests/utilities.py                      |   8 +-
 wcs/admin/logged_errors.py              |  14 +-
 wcs/conditions.py                       |  12 +-
 wcs/data_sources.py                     |   3 +-
 wcs/fields.py                           |   6 +-
 wcs/formdata.py                         |   9 +-
 wcs/logged_errors.py                    |  42 +---
 wcs/publisher.py                        |  12 +-
 wcs/sql.py                              | 151 +++++++++++-
 wcs/variables.py                        |   3 +-
 wcs/wf/backoffice_fields.py             |  12 +-
 wcs/wf/create_formdata.py               |  17 +-
 wcs/wf/dispatch.py                      |   5 +-
 wcs/wf/external_workflow.py             |   6 +-
 wcs/wf/wscall.py                        |   3 +-
 wcs/workflows.py                        |  16 +-
 24 files changed, 497 insertions(+), 292 deletions(-)

diff --git a/tests/admin_pages/test_workflow.py b/tests/admin_pages/test_workflow.py
index 5ddf5c5c..53cdd991 100644
--- a/tests/admin_pages/test_workflow.py
+++ b/tests/admin_pages/test_workflow.py
@@ -12,7 +12,6 @@ import pytest
 from webtest import Upload
 
 from wcs.qommon.http_request import HTTPRequest
-from wcs.logged_errors import LoggedError
 from wcs.roles import Role
 from wcs.workflows import (
     Workflow, WorkflowCriticalityLevel, DisplayMessageWorkflowStatusItem,
@@ -1779,9 +1778,13 @@ def test_workflows_wscall_options(pub, value):
 
 
 def test_workflows_wscall_status_error(pub):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
     create_superuser(pub)
 
-    LoggedError.wipe()
+    pub.loggederror_class.wipe()
     Workflow.wipe()
     workflow = Workflow(name='foo')
     baz_status = workflow.add_status(name='baz')
@@ -1798,14 +1801,14 @@ def test_workflows_wscall_status_error(pub):
 
     app = login(get_app(pub))
     app.get('/backoffice/workflows/%s/' % workflow.id)
-    assert LoggedError.count() == 0
+    assert pub.loggederror_class.count() == 0
 
     # delete foo status
     del workflow.possible_status[1]
     workflow.store()
     app.get('/backoffice/workflows/%s/' % workflow.id)
-    assert LoggedError.count() == 1
-    error = LoggedError.select()[0]
+    assert pub.loggederror_class.count() == 1
+    error = pub.loggederror_class.select()[0]
     assert error.tech_id == '%s-reference-to-invalid-status-in-workflow-foo-status-baz-item-webservice' % workflow.id
     assert error.formdef_id is None
     assert error.workflow_id == workflow.id
diff --git a/tests/backoffice_pages/test_all.py b/tests/backoffice_pages/test_all.py
index 190ddfab..67022f30 100644
--- a/tests/backoffice_pages/test_all.py
+++ b/tests/backoffice_pages/test_all.py
@@ -39,7 +39,6 @@ from wcs.wf.create_carddata import CreateCarddataWorkflowStatusItem
 from wcs.carddef import CardDef
 from wcs.categories import Category
 from wcs.formdef import FormDef
-from wcs.logged_errors import LoggedError
 from wcs import fields
 from wcs.wscalls import NamedWsCall
 
@@ -2257,11 +2256,16 @@ def test_backoffice_wscall_failure_display(http_requests, pub):
 @pytest.mark.parametrize('notify_on_errors', [True, False])
 @pytest.mark.parametrize('record_on_errors', [True, False])
 def test_backoffice_wscall_on_error(http_requests, pub, emails, notify_on_errors, record_on_errors):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
     pub.cfg['debug'] = {'error_email': 'errors@localhost.invalid'}
     pub.write_cfg()
 
     create_user(pub)
     create_environment(pub)
+    pub.loggederror_class.wipe()
     formdef = FormDef.get_by_urlname('form-title')
     form_class = formdef.data_class()
 
@@ -2314,12 +2318,12 @@ def test_backoffice_wscall_on_error(http_requests, pub, emails, notify_on_errors
     else:
         assert emails.count() == 0
 
-    # check LoggedError
+    # check pub.loggederror_class
     if record_on_errors:
-        assert LoggedError.count() == 1
-        LoggedError.wipe()
+        assert pub.loggederror_class.count() == 1
+        pub.loggederror_class.wipe()
     else:
-        assert LoggedError.count() == 0
+        assert pub.loggederror_class.count() == 0
 
 
 def test_backoffice_wscall_attachment(http_requests, pub):
@@ -4709,6 +4713,10 @@ def test_backoffice_fields(pub):
 
 
 def test_backoffice_logged_errors(pub):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
     Workflow.wipe()
     workflow = Workflow.get_default_workflow()
     workflow.id = '12'
@@ -4738,7 +4746,7 @@ def test_backoffice_logged_errors(pub):
     carddef.fields = []
     carddef.store()
 
-    LoggedError.wipe()
+    pub.loggederror_class.wipe()
 
     create_superuser(pub)
     app = login(get_app(pub))
@@ -4753,7 +4761,7 @@ def test_backoffice_logged_errors(pub):
     resp = app.get('/test/')
     resp = resp.form.submit('submit').follow()
     resp = resp.form.submit('submit')
-    assert LoggedError.count() == 1
+    assert pub.loggederror_class.count() == 1
 
     app = login(get_app(pub))
     resp = app.get('/backoffice/forms/%s/' % formdef.id)
@@ -4773,9 +4781,9 @@ def test_backoffice_logged_errors(pub):
     assert not 'Acked' in resp.text
     resp = resp.click('Ack').follow()
     assert 'Acked' in resp.text
-    assert LoggedError.select()[0].acked is True
+    assert pub.loggederror_class.select()[0].acked is True
     resp = resp.click('Delete').follow()
-    assert LoggedError.count() == 0
+    assert pub.loggederror_class.count() == 0
 
     pub.cfg.update({'debug': {'error_email': None}})
     pub.write_cfg()
@@ -4784,7 +4792,7 @@ def test_backoffice_logged_errors(pub):
     resp = app.get('/test/')
     resp = resp.form.submit('submit').follow()
     resp = resp.form.submit('submit')
-    assert LoggedError.count() == 1
+    assert pub.loggederror_class.count() == 1
 
     app = login(get_app(pub))
     resp = app.get('/backoffice/workflows/%s/' % workflow.id)
@@ -4802,7 +4810,7 @@ def test_backoffice_logged_errors(pub):
     resp = app.get('/test/')
     resp = resp.form.submit('submit').follow()
     resp = resp.form.submit('submit')
-    assert LoggedError.count() == 2
+    assert pub.loggederror_class.count() == 2
 
     app = login(get_app(pub))
     resp = app.get('/backoffice/workflows/%s/' % workflow.id)
@@ -5338,11 +5346,12 @@ def create_formdata(request, pub):
     source_formdef.store()
     source_formdef.data_class().wipe()
     target_formdef.data_class().wipe()
-    LoggedError.wipe()
+    if pub.is_using_postgresql():
+        pub.loggederror_class.wipe()
     return locals()
 
 
-def test_backoffice_create_formdata_backoffice_submission(create_formdata):
+def test_backoffice_create_formdata_backoffice_submission(pub, create_formdata):
     # create submitting user
     user = create_formdata['pub'].user_class()
     user.name = 'Jean Darmette'
@@ -5373,7 +5382,8 @@ def test_backoffice_create_formdata_backoffice_submission(create_formdata):
     assert target_data_class.count() == 0
     # resubmit it through backoffice submission
     resp = resp.form.submit(name='button_resubmit')
-    assert LoggedError.count() == 0
+    if pub.is_using_postgresql():
+        assert pub.loggederror_class.count() == 0
     assert target_data_class.count() == 1
     target_formdata = target_data_class.select()[0]
 
@@ -5402,7 +5412,7 @@ def test_backoffice_create_formdata_backoffice_submission(create_formdata):
     assert pq('.field-type-file .value').text() == 'bar'
 
 
-def test_linked_forms_variables(create_formdata):
+def test_linked_forms_variables(pub, create_formdata):
     # create source formdata
     formdata = create_formdata['source_formdef'].data_class()()
     upload = PicklableUpload('/foo/bar', content_type='text/plain')
@@ -5419,9 +5429,9 @@ def test_linked_forms_variables(create_formdata):
     formdata.perform_workflow()
     formdata.store()
 
-    get_publisher().substitutions.reset()
-    get_publisher().substitutions.feed(formdata)
-    substvars = get_publisher().substitutions.get_context_variables(mode='lazy')
+    pub.substitutions.reset()
+    pub.substitutions.feed(formdata)
+    substvars = pub.substitutions.get_context_variables(mode='lazy')
     assert str(substvars['form_links_resubmitted_form_var_foo_string']) == 'coucou'
     assert 'form_links_resubmitted_form_var_foo_string' in substvars.get_flat_keys()
 
@@ -5440,7 +5450,7 @@ def test_linked_forms_variables(create_formdata):
     assert 'form_links_resubmitted_form_var_foo_string' not in resp
 
 
-def test_backoffice_create_formdata_map_fields_by_varname(create_formdata):
+def test_backoffice_create_formdata_map_fields_by_varname(pub, create_formdata):
     create_formdata['create_formdata'].map_fields_by_varname = True
     create_formdata['create_formdata'].mappings  = []
     create_formdata['wf'].store()
@@ -5490,7 +5500,8 @@ def test_backoffice_create_formdata_map_fields_by_varname(create_formdata):
     assert target_data_class.count() == 0
     # resubmit it through backoffice submission
     resp = resp.form.submit(name='button_resubmit')
-    assert LoggedError.count() == 0
+    if pub.is_using_postgresql():
+        assert pub.loggederror_class.count() == 0
     assert target_data_class.count() == 1
     target_formdata = target_data_class.select()[0]
 
diff --git a/tests/backoffice_pages/test_carddata.py b/tests/backoffice_pages/test_carddata.py
index 5234cc2b..3f68a37c 100644
--- a/tests/backoffice_pages/test_carddata.py
+++ b/tests/backoffice_pages/test_carddata.py
@@ -10,7 +10,6 @@ from wcs.blocks import BlockDef
 from wcs.carddef import CardDef
 from wcs.categories import CardDefCategory
 from wcs.formdef import FormDef
-from wcs.logged_errors import LoggedError
 from wcs.qommon.http_request import HTTPRequest
 from wcs.wf.wscall import WebserviceCallStatusItem
 from wcs.workflows import ChoiceWorkflowStatusItem, Workflow
@@ -417,7 +416,11 @@ def test_backoffice_cards_import_data_csv_invalid_columns(pub):
 
 
 def test_backoffice_cards_wscall_failure_display(http_requests, pub, studio):
-    LoggedError.wipe()
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
+    pub.loggederror_class.wipe()
     user = create_user(pub)
 
     Workflow.wipe()
@@ -473,8 +476,8 @@ def test_backoffice_cards_wscall_failure_display(http_requests, pub, studio):
     resp = resp.follow()
     assert 'Error during webservice call' in resp.text
 
-    assert LoggedError.count() == 1
-    assert LoggedError.select()[0].get_formdata().data == {'1': 'plop'}
+    assert pub.loggederror_class.count() == 1
+    assert pub.loggederror_class.select()[0].get_formdata().data == {'1': 'plop'}
 
 
 def test_block_card_item_link(pub, studio, blocks_feature):
diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py
index 0a1b5110..2e08da66 100644
--- a/tests/form_pages/test_all.py
+++ b/tests/form_pages/test_all.py
@@ -50,7 +50,6 @@ from wcs.tracking_code import TrackingCode
 from wcs.data_sources import NamedDataSource
 from wcs.wscalls import NamedWsCall
 from wcs import fields
-from wcs.logged_errors import LoggedError
 from wcs.forms.root import PublicFormStatusPage
 
 from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
@@ -2351,8 +2350,9 @@ def test_form_multi_page_post_edit(pub):
     assert formdef.data_class().get(data_id).status == 'wf-%s' % st2.id
 
     # jump to a nonexistent status == do not jump, but add a LoggedError
-    LoggedError.wipe()
-    assert LoggedError.count() == 0
+    if pub.is_using_postgresql():
+        pub.loggederror_class.wipe()
+        assert pub.loggederror_class.count() == 0
     editable.status = 'deleted_status_id'
     workflow.store()
     # go back to st1
@@ -2368,14 +2368,15 @@ def test_form_multi_page_post_edit(pub):
     resp = resp.forms[0].submit('submit')
     resp = resp.follow()
     assert formdef.data_class().get(data_id).status == 'wf-%s' % st1.id  # stay on st1
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
-    assert logged_error.formdata_id == str(formdata.id)
-    assert logged_error.formdef_id == formdef.id
-    assert logged_error.workflow_id == workflow.id
-    assert logged_error.status_id == st1.id
-    assert logged_error.status_item_id == editable.id
-    assert logged_error.occurences_count == 1
+    if pub.is_using_postgresql():
+        assert pub.loggederror_class.count() == 1
+        logged_error = pub.loggederror_class.select()[0]
+        assert logged_error.formdata_id == str(formdata.id)
+        assert logged_error.formdef_id == formdef.id
+        assert logged_error.workflow_id == workflow.id
+        assert logged_error.status_id == st1.id
+        assert logged_error.status_item_id == editable.id
+        assert logged_error.occurences_count == 1
 
     # do it again: increment logged_error.occurences_count
     page = login(get_app(pub), username='foo', password='foo').get('/test/%s/' % data_id)
@@ -2386,9 +2387,10 @@ def test_form_multi_page_post_edit(pub):
     resp = resp.forms[0].submit('submit')
     resp = resp.follow()
     assert formdef.data_class().get(data_id).status == 'wf-%s' % st1.id  # stay on st1
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
-    assert logged_error.occurences_count == 2
+    if pub.is_using_postgresql():
+        assert pub.loggederror_class.count() == 1
+        logged_error = pub.loggederror_class.select()[0]
+        assert logged_error.occurences_count == 2
 
 
 def test_form_edit_autocomplete_list(pub):
@@ -6785,6 +6787,10 @@ def test_form_data_keywords(pub):
 
 
 def test_logged_errors(pub):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
     Workflow.wipe()
     workflow = Workflow.get_default_workflow()
     workflow.id = '12'
@@ -6806,29 +6812,29 @@ def test_logged_errors(pub):
     formdef.fields = []
     formdef.store()
 
-    LoggedError.wipe()
+    pub.loggederror_class.wipe()
 
     app = get_app(pub)
     resp = app.get('/test/')
     resp = resp.form.submit('submit').follow()
     resp = resp.form.submit('submit')
-    assert LoggedError.count() == 1
+    assert pub.loggederror_class.count() == 1
 
     resp = app.get('/test/')
     resp = resp.form.submit('submit').follow()
     resp = resp.form.submit('submit')
-    assert LoggedError.count() == 1
+    assert pub.loggederror_class.count() == 1
 
-    error = LoggedError.get_on_index(
-        '34-12-just_submitted-_jump-failed-to-evaluate-condition-ZeroDivisionError-integer-division-or-modulo-by-zero',
-        'tech_id')
+    error = list(pub.loggederror_class.get_with_indexed_value(
+        'tech_id',
+        '34-12-just_submitted-_jump-failed-to-evaluate-condition-ZeroDivisionError-integer-division-or-modulo-by-zero'))[0]
     assert error.occurences_count == 2
 
-    assert len(LoggedError.get_ids_with_indexed_value('formdef_id', '34')) == 1
-    assert len(LoggedError.get_ids_with_indexed_value('formdef_id', 'X')) == 0
+    assert len(list(pub.loggederror_class.get_with_indexed_value('formdef_id', '34'))) == 1
+    assert len(list(pub.loggederror_class.get_with_indexed_value('formdef_id', 'X'))) == 0
 
-    assert len(LoggedError.get_ids_with_indexed_value('workflow_id', '12')) == 1
-    assert len(LoggedError.get_ids_with_indexed_value('workflow_id', 'X')) == 0
+    assert len(list(pub.loggederror_class.get_with_indexed_value('workflow_id', '12'))) == 1
+    assert len(list(pub.loggederror_class.get_with_indexed_value('workflow_id', 'X'))) == 0
 
 
 def test_formdata_named_wscall(http_requests, pub):
@@ -8876,8 +8882,9 @@ def test_create_formdata_anonymous_submitted(create_formdata):
     assert pq('.linked .foo_string').text() == 'zob'
 
 
-def test_create_formdata_empty_item_ds_with_id_parameter(create_formdata):
-    LoggedError.wipe()
+def test_create_formdata_empty_item_ds_with_id_parameter(pub, create_formdata):
+    if pub.is_using_postgresql():
+        pub.loggederror_class.wipe()
     NamedDataSource.wipe()
     data_source = NamedDataSource(name='foobar')
     data_source.data_source = {
@@ -8905,9 +8912,11 @@ def test_create_formdata_empty_item_ds_with_id_parameter(create_formdata):
         resp = resp.form.submit('submit')  # -> submission
         resp = resp.follow()
         assert create_formdata['target_formdef'].data_class().count() == 0
-        assert LoggedError.count() == 0
+        if pub.is_using_postgresql():
+            assert pub.loggederror_class.count() == 0
         resp = resp.form.submit('button_resubmit')
-        assert LoggedError.count() == 0
+        if pub.is_using_postgresql():
+            assert pub.loggederror_class.count() == 0
 
 
 def test_js_libraries(pub):
diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index fb8e8caf..be8f76f5 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -18,7 +18,6 @@ from wcs.categories import Category
 from wcs.conditions import Condition
 from wcs.formdef import FormDef
 from wcs.formdata import Evolution
-from wcs.logged_errors import LoggedError
 from wcs.roles import Role
 from wcs import sessions
 from wcs.variables import LazyFormData
@@ -1058,12 +1057,14 @@ def test_lazy_formdata_queryset_filter(pub, variable_test_data):
     assert queryset.count == 4
     queryset = lazy_formdata.objects.filter_by('foo_foo').apply_filter_value('X')
     assert queryset.count == 0
-    LoggedError.wipe()
+    if pub.is_using_postgresql():
+        pub.loggederror_class.wipe()
     queryset = lazy_formdata.objects.filter_by('unknown').apply_filter_value('X')
     assert queryset.count == 0
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
-    assert logged_error.summary == 'Invalid filter "unknown"'
+    if pub.is_using_postgresql():
+        assert pub.loggederror_class.count() == 1
+        logged_error = pub.loggederror_class.select()[0]
+        assert logged_error.summary == 'Invalid filter "unknown"'
 
     # filter function on backoffice field
     queryset = lazy_formdata.objects.filter_by('backoffice_blah').apply_filter_value('plop1')
diff --git a/tests/test_mail_templates.py b/tests/test_mail_templates.py
index a49be0dd..84a314bb 100644
--- a/tests/test_mail_templates.py
+++ b/tests/test_mail_templates.py
@@ -8,7 +8,6 @@ from django.utils.encoding import force_bytes
 from quixote import cleanup
 from wcs.formdef import FormDef
 from wcs.fields import FileField
-from wcs.logged_errors import LoggedError
 from wcs.mail_templates import MailTemplate
 from wcs.workflows import Workflow, SendmailWorkflowStatusItem
 from wcs.qommon.http_request import HTTPRequest
@@ -193,7 +192,12 @@ def test_admin_workflow_edit(pub, superuser, mail_templates_option):
     assert workflow.possible_status[0].items[0].mail_template == 'test-mail-template'
 
 
-def test_workflow_send_mail_template(pub, superuser, mail_templates_option, emails):
+def test_workflow_send_mail_template_with_sql(superuser, mail_templates_option, emails):
+    pub = create_temporary_pub(sql_mode=True)
+    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
+    pub.set_app_dir(req)
+    pub._set_request(req)
+
     Workflow.wipe()
     MailTemplate.wipe()
 
@@ -231,13 +235,13 @@ def test_workflow_send_mail_template(pub, superuser, mail_templates_option, emai
     # check nothing is sent and an error is logged if the mail template is
     # missing
     emails.empty()
-    LoggedError.wipe()
+    pub.loggederror_class.wipe()
     MailTemplate.wipe()
     item.perform(formdata)
     pub.get_request().response.process_after_jobs()
     assert emails.count() == 0
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
+    assert pub.loggederror_class.count() == 1
+    logged_error = pub.loggederror_class.select()[0]
     assert logged_error.summary == 'reference to invalid mail template test-mail-template in status Status1'
 
 
diff --git a/tests/test_workflows.py b/tests/test_workflows.py
index 4aa15684..1e837394 100644
--- a/tests/test_workflows.py
+++ b/tests/test_workflows.py
@@ -32,7 +32,6 @@ from wcs.fields import (StringField, DateField, MapField, FileField, ItemField,
         ItemsField, CommentField, EmailField, PageField, TitleField,
         SubtitleField, TextField, BoolField, TableField)
 from wcs.formdata import Evolution
-from wcs.logged_errors import LoggedError
 from wcs.roles import Role
 from wcs.workflows import (Workflow, WorkflowStatusItem,
         SendmailWorkflowStatusItem, SendSMSWorkflowStatusItem,
@@ -314,32 +313,36 @@ def test_jump_count_condition(pub):
     assert item.must_jump(formdata) is False
 
 
-def test_jump_bad_python_condition(pub):
+def test_jump_bad_python_condition(two_pubs):
+    if not two_pubs.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
     FormDef.wipe()
     formdef = FormDef()
     formdef.name = 'foobar'
     formdef.store()
-    pub.substitutions.feed(formdef)
+    two_pubs.substitutions.feed(formdef)
     formdef.data_class().wipe()
     formdata = formdef.data_class()()
     item = JumpWorkflowStatusItem()
 
-    LoggedError.wipe()
+    two_pubs.loggederror_class.wipe()
     item.condition = {'type': 'python', 'value': 'form_var_foobar == 0'}
     assert item.must_jump(formdata) is False
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
+    assert two_pubs.loggederror_class.count() == 1
+    logged_error = two_pubs.loggederror_class.select()[0]
     assert logged_error.summary == 'Failed to evaluate condition'
     assert logged_error.exception_class == 'NameError'
     assert logged_error.exception_message == "name 'form_var_foobar' is not defined"
     assert logged_error.expression == 'form_var_foobar == 0'
     assert logged_error.expression_type == 'python'
 
-    LoggedError.wipe()
+    two_pubs.loggederror_class.wipe()
     item.condition = {'type': 'python', 'value': '~ invalid ~'}
     assert item.must_jump(formdata) is False
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
+    assert two_pubs.loggederror_class.count() == 1
+    logged_error = two_pubs.loggederror_class.select()[0]
     assert logged_error.summary == 'Failed to evaluate condition'
     assert logged_error.exception_class == 'SyntaxError'
     assert logged_error.exception_message == 'unexpected EOF while parsing (<string>, line 1)'
@@ -347,7 +350,7 @@ def test_jump_bad_python_condition(pub):
     assert logged_error.expression_type == 'python'
 
 
-def test_jump_django_conditions(pub):
+def test_jump_django_conditions(two_pubs):
     FormDef.wipe()
     formdef = FormDef()
     formdef.name = 'foobar'
@@ -355,10 +358,11 @@ def test_jump_django_conditions(pub):
     formdef.store()
     formdata = formdef.data_class()()
     formdata.data = {'1': 'hello'}
-    pub.substitutions.feed(formdata)
+    two_pubs.substitutions.feed(formdata)
     item = JumpWorkflowStatusItem()
 
-    LoggedError.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
     item.condition = {'type': 'django', 'value': '1 < 2'}
     assert item.must_jump(formdata) is True
 
@@ -371,17 +375,19 @@ def test_jump_django_conditions(pub):
     item.condition = {'type': 'django', 'value': 'form_var_foo|first|upper == "X"'}
     assert item.must_jump(formdata) is False
 
-    assert LoggedError.count() == 0
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 0
 
     item.condition = {'type': 'django', 'value': '~ invalid ~'}
     assert item.must_jump(formdata) is False
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
-    assert logged_error.summary == 'Failed to evaluate condition'
-    assert logged_error.exception_class == 'TemplateSyntaxError'
-    assert logged_error.exception_message == "Could not parse the remainder: '~' from '~'"
-    assert logged_error.expression == '~ invalid ~'
-    assert logged_error.expression_type == 'django'
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 1
+        logged_error = two_pubs.loggederror_class.select()[0]
+        assert logged_error.summary == 'Failed to evaluate condition'
+        assert logged_error.exception_class == 'TemplateSyntaxError'
+        assert logged_error.exception_message == "Could not parse the remainder: '~' from '~'"
+        assert logged_error.expression == '~ invalid ~'
+        assert logged_error.expression_type == 'django'
 
 
 def test_check_auth(pub):
@@ -455,7 +461,8 @@ def test_dispatch(pub):
 
 
 def test_dispatch_auto(pub):
-    LoggedError.wipe()
+    if pub.is_using_postgresql():
+        pub.loggederror_class.wipe()
 
     formdef = FormDef()
     formdef.name = 'baz'
@@ -526,17 +533,19 @@ def test_dispatch_auto(pub):
     pub.substitutions.feed(formdata)
     item.perform(formdata)
     assert not formdata.workflow_roles
-    assert LoggedError.count() == 1
-    error = LoggedError.select()[0]
-    assert error.tech_id == '%s-_default-error-in-dispatch-missing-role-foobar' % formdef.id
-    assert error.formdef_id == formdef.id
-    assert error.workflow_id == '_default'
-    assert error.summary == 'error in dispatch, missing role (foobar)'
-    assert error.occurences_count == 1
+    if pub.is_using_postgresql():
+        assert pub.loggederror_class.count() == 1
+        error = pub.loggederror_class.select()[0]
+        assert error.tech_id == '%s-_default-error-in-dispatch-missing-role-foobar' % formdef.id
+        assert error.formdef_id == formdef.id
+        assert error.workflow_id == '_default'
+        assert error.summary == 'error in dispatch, missing role (foobar)'
+        assert error.occurences_count == 1
 
 
 def test_dispatch_computed(pub):
-    LoggedError.wipe()
+    if pub.is_using_postgresql():
+        pub.loggederror_class.wipe()
 
     formdef = FormDef()
     formdef.name = 'baz'
@@ -571,13 +580,14 @@ def test_dispatch_computed(pub):
     item.role_id = '="foobar"'
     item.perform(formdata)
     assert not formdata.workflow_roles
-    assert LoggedError.count() == 1
-    error = LoggedError.select()[0]
-    assert error.tech_id == '%s-_default-error-in-dispatch-missing-role-foobar' % formdef.id
-    assert error.formdef_id == formdef.id
-    assert error.workflow_id == '_default'
-    assert error.summary == 'error in dispatch, missing role (="foobar")'
-    assert error.occurences_count == 1
+    if pub.is_using_postgresql():
+        assert pub.loggederror_class.count() == 1
+        error = pub.loggederror_class.select()[0]
+        assert error.tech_id == '%s-_default-error-in-dispatch-missing-role-foobar' % formdef.id
+        assert error.formdef_id == formdef.id
+        assert error.workflow_id == '_default'
+        assert error.summary == 'error in dispatch, missing role (="foobar")'
+        assert error.occurences_count == 1
 
 
 def test_roles(pub):
@@ -2293,6 +2303,10 @@ def test_timeout_with_mark(two_pubs):
 
 
 def test_jump_missing_previous_mark(two_pubs):
+    if not two_pubs.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
     workflow = Workflow(name='jump-mark')
     st1 = workflow.add_status('Status1', 'st1')
 
@@ -2317,9 +2331,9 @@ def test_jump_missing_previous_mark(two_pubs):
     formdata.store()
 
     time.sleep(0.3)
-    LoggedError.wipe()
+    two_pubs.loggederror_class.wipe()
     _apply_timeouts(two_pubs)
-    assert LoggedError.count() == 1
+    assert two_pubs.loggederror_class.count() == 1
 
 
 def test_sms(pub, sms_mocking):
@@ -3745,7 +3759,8 @@ def test_profile(two_pubs):
 def test_set_backoffice_field(http_requests, two_pubs):
     Workflow.wipe()
     FormDef.wipe()
-    LoggedError.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
     wf = Workflow(name='xxx')
     wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
     wf.backoffice_fields_formdef.fields = [
@@ -3844,32 +3859,36 @@ def test_set_backoffice_field(http_requests, two_pubs):
     formdata = formdef.data_class().get(formdata.id)
     assert formdata.data['bo1'] == ''
 
-    assert LoggedError.count() == 0
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 0
 
     item.fields = [{'field_id': 'bo1', 'value': '= ~ invalid python ~'}]
     item.perform(formdata)
     formdata = formdef.data_class().get(formdata.id)
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
-    assert logged_error.summary == 'Failed to compute Python expression'
-    assert logged_error.formdata_id == str(formdata.id)
-    assert logged_error.expression == ' ~ invalid python ~'
-    assert logged_error.expression_type == 'python'
-    assert logged_error.exception_class == 'SyntaxError'
-    assert logged_error.exception_message == 'invalid syntax (<string>, line 1)'
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 1
+        logged_error = two_pubs.loggederror_class.select()[0]
+        assert logged_error.summary == 'Failed to compute Python expression'
+        assert logged_error.formdata_id == str(formdata.id)
+        assert logged_error.expression == ' ~ invalid python ~'
+        assert logged_error.expression_type == 'python'
+        assert logged_error.exception_class == 'SyntaxError'
+        assert logged_error.exception_message == 'invalid syntax (<string>, line 1)'
 
-    LoggedError.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
     item.fields = [{'field_id': 'bo1', 'value': '{% if bad django %}'}]
     item.perform(formdata)
     formdata = formdef.data_class().get(formdata.id)
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
-    assert logged_error.summary == 'Failed to compute template'
-    assert logged_error.formdata_id == str(formdata.id)
-    assert logged_error.expression == '{% if bad django %}'
-    assert logged_error.expression_type == 'template'
-    assert logged_error.exception_class == 'TemplateError'
-    assert logged_error.exception_message.startswith('syntax error in Django template')
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 1
+        logged_error = two_pubs.loggederror_class.select()[0]
+        assert logged_error.summary == 'Failed to compute template'
+        assert logged_error.formdata_id == str(formdata.id)
+        assert logged_error.expression == '{% if bad django %}'
+        assert logged_error.expression_type == 'template'
+        assert logged_error.exception_class == 'TemplateError'
+        assert logged_error.exception_message.startswith('syntax error in Django template')
 
 
 def test_set_backoffice_field_file(http_requests, two_pubs):
@@ -4036,17 +4055,19 @@ def test_set_backoffice_field_file(http_requests, two_pubs):
         item.parent = st1
         item.fields = [{'field_id': 'bo1', 'value': value}]
 
-        LoggedError.wipe()
+        if two_pubs.is_using_postgresql():
+            two_pubs.loggederror_class.wipe()
         item.perform(formdata)
 
         formdata = formdef.data_class().get(formdata.id)
         assert formdata.data['bo1'].base_filename == 'hello.txt'
         assert formdata.data['bo1'].get_content() == b'HELLO WORLD'
-        assert LoggedError.count() == 1
-        logged_error = LoggedError.select()[0]
-        assert logged_error.summary.startswith('Failed to convert')
-        assert logged_error.formdata_id == str(formdata.id)
-        assert logged_error.exception_class == 'ValueError'
+        if two_pubs.is_using_postgresql():
+            assert two_pubs.loggederror_class.count() == 1
+            logged_error = two_pubs.loggederror_class.select()[0]
+            assert logged_error.summary.startswith('Failed to convert')
+            assert logged_error.formdata_id == str(formdata.id)
+            assert logged_error.exception_class == 'ValueError'
 
     # check wrong field
     item = SetBackofficeFieldsWorkflowStatusItem()
@@ -4343,7 +4364,8 @@ def test_set_backoffice_field_items(two_pubs):
 def test_set_backoffice_field_date(two_pubs):
     Workflow.wipe()
     FormDef.wipe()
-    LoggedError.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
     wf = Workflow(name='xxx')
     wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
     st1 = wf.add_status('Status1')
@@ -4391,18 +4413,21 @@ def test_set_backoffice_field_date(two_pubs):
     assert datetime.date(*formdata.data['bo1'][:3]) == datetime.date(2017, 3, 23)
 
     # invalid values => do nothing
-    assert LoggedError.count() == 0
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 0
     for value in ('plop', '={}', '=[]'):
         item = SetBackofficeFieldsWorkflowStatusItem()
         item.parent = st1
         item.fields = [{'field_id': 'bo1', 'value': value}]
 
-        LoggedError.wipe()
+        if two_pubs.is_using_postgresql():
+            two_pubs.loggederror_class.wipe()
         item.perform(formdata)
         formdata = formdef.data_class().get(formdata.id)
         assert datetime.date(*formdata.data['bo1'][:3]) == datetime.date(2017, 3, 23)
-        assert LoggedError.count() == 1
-        assert LoggedError.select()[0].summary.startswith('Failed to convert')
+        if two_pubs.is_using_postgresql():
+            assert two_pubs.loggederror_class.count() == 1
+            assert two_pubs.loggederror_class.select()[0].summary.startswith('Failed to convert')
 
     # None : empty date
     item = SetBackofficeFieldsWorkflowStatusItem()
@@ -4417,7 +4442,6 @@ def test_set_backoffice_field_date(two_pubs):
 def test_set_backoffice_field_boolean(two_pubs):
     Workflow.wipe()
     FormDef.wipe()
-    LoggedError.wipe()
     wf = Workflow(name='xxx')
     wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
     st1 = wf.add_status('Status1')
@@ -4572,8 +4596,7 @@ def test_workflow_jump_condition_migration(pub):
 
 
 def test_workflow_action_condition(two_pubs):
-    pub = two_pubs
-    pub._set_request(None)  # to avoid after jobs
+    two_pubs._set_request(None)  # to avoid after jobs
     workflow = Workflow(name='jump condition migration')
     st1 = workflow.add_status('Status1', 'st1')
     workflow.store()
@@ -4581,7 +4604,7 @@ def test_workflow_action_condition(two_pubs):
     role = Role(name='bar1')
     role.store()
 
-    user = pub.user_class()
+    user = two_pubs.user_class()
     user.roles = [role.id]
     user.store()
 
@@ -4616,9 +4639,9 @@ def test_workflow_action_condition(two_pubs):
     choice.condition = {'type': 'python', 'value': 'form_var_foo == "foo"'}
     workflow.store()
 
-    with pub.substitutions.temporary_feed(formdata1):
+    with two_pubs.substitutions.temporary_feed(formdata1):
         assert FormDef.get(formdef.id).data_class().get(formdata1.id).get_actions_roles() == set([role.id])
-    with pub.substitutions.temporary_feed(formdata2):
+    with two_pubs.substitutions.temporary_feed(formdata2):
         assert FormDef.get(formdef.id).data_class().get(formdata2.id).get_actions_roles() == set()
 
     assert len(FormDef.get(formdef.id).data_class().get_actionable_ids([role.id])) == 1
@@ -4633,18 +4656,20 @@ def test_workflow_action_condition(two_pubs):
     assert len(FormDef.get(formdef.id).data_class().get_actionable_ids([role.id])) == 2
 
     # bad condition
-    LoggedError.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
     choice.condition = {'type': 'python', 'value': 'foobar == barfoo'}
     workflow.store()
     assert len(FormDef.get(formdef.id).data_class().get_actionable_ids([role.id])) == 0
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
-    assert logged_error.occurences_count > 1  # should be 2... == 12 with pickle, 4 with sql
-    assert logged_error.summary == 'Failed to evaluate condition'
-    assert logged_error.exception_class == 'NameError'
-    assert logged_error.exception_message == "name 'foobar' is not defined"
-    assert logged_error.expression == 'foobar == barfoo'
-    assert logged_error.expression_type == 'python'
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 1
+        logged_error = two_pubs.loggederror_class.select()[0]
+        assert logged_error.occurences_count > 1  # should be 2... == 12 with pickle, 4 with sql
+        assert logged_error.summary == 'Failed to evaluate condition'
+        assert logged_error.exception_class == 'NameError'
+        assert logged_error.exception_message == "name 'foobar' is not defined"
+        assert logged_error.expression == 'foobar == barfoo'
+        assert logged_error.expression_type == 'python'
 
 
 def test_notifications(pub, http_requests):
@@ -4774,6 +4799,7 @@ def test_aggregation_email(pub, emails):
     formdef.fields = []
     formdef.workflow_id = workflow.id
     formdef.store()
+    formdef.data_class().wipe()
 
     for i in range(5):
         formdata = formdef.data_class()()
@@ -4811,10 +4837,11 @@ def test_aggregation_email(pub, emails):
     assert 'http://example.net/foobar/%s/status (New)' % formdata.id in emails.emails['New arrivals']['payload']
 
 
-def test_create_formdata(pub):
+def test_create_formdata(two_pubs):
     FormDef.wipe()
-    LoggedError.wipe()
-    pub.tracking_code_class.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
+    two_pubs.tracking_code_class.wipe()
 
     target_formdef = FormDef()
     target_formdef.name = 'target form'
@@ -4849,7 +4876,8 @@ def test_create_formdata(pub):
     formdata.just_created()
 
     assert target_formdef.data_class().count() == 0
-    assert LoggedError.count() == 0
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 0
     # check unconfigure action do nothing
     formdata.perform_workflow()
     assert target_formdef.data_class().count() == 0
@@ -4860,15 +4888,16 @@ def test_create_formdata(pub):
     formdata.perform_workflow()
     assert target_formdef.data_class().count() == 1
 
-    errors = LoggedError.select()
-    assert len(errors) == 2
-    assert any('form_var_toto_string' in (error.exception_message or '') for error in errors)
-    assert any('Missing field' in error.summary for error in errors)
+    if two_pubs.is_using_postgresql():
+        errors = two_pubs.loggederror_class.select()
+        assert len(errors) == 2
+        assert any('form_var_toto_string' in (error.exception_message or '') for error in errors)
+        assert any('Missing field' in error.summary for error in errors)
 
     # no tracking code has been created
     created_formdata = target_formdef.data_class().select()[0]
     assert created_formdata.tracking_code is None
-    assert pub.tracking_code_class.count() == 0
+    assert two_pubs.tracking_code_class.count() == 0
     # now we want one
     target_formdef.enable_tracking_codes = True
     target_formdef.store()
@@ -4878,9 +4907,9 @@ def test_create_formdata(pub):
     assert target_formdef.data_class().count() == 1
     created_formdata = target_formdef.data_class().select()[0]
     assert created_formdata.tracking_code is not None
-    assert pub.tracking_code_class.count() == 1
-    assert pub.tracking_code_class.select()[0].formdef_id == target_formdef.id
-    assert pub.tracking_code_class.select()[0].formdata_id == created_formdata.id
+    assert two_pubs.tracking_code_class.count() == 1
+    assert two_pubs.tracking_code_class.select()[0].formdef_id == target_formdef.id
+    assert two_pubs.tracking_code_class.select()[0].formdata_id == str(created_formdata.id)
 
     create.condition = {'type': 'python', 'value': '1 == 2'}
     wf.store()
@@ -4891,10 +4920,11 @@ def test_create_formdata(pub):
     assert target_formdef.data_class().count() == 0
 
 
-def test_create_carddata(pub):
+def test_create_carddata(two_pubs):
     CardDef.wipe()
     FormDef.wipe()
-    LoggedError.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
 
     carddef = CardDef()
     carddef.name = 'My card'
@@ -4946,17 +4976,18 @@ def test_create_carddata(pub):
 
     assert carddef.data_class().count() == 1
 
-    errors = LoggedError.select()
-    assert len(errors) == 2
-    assert any('form_var_undefined' in (error.exception_message or '') for error in errors)
-    assert any('invalid date value' in (error.exception_message or '') for error in errors)
+    if two_pubs.is_using_postgresql():
+        errors = two_pubs.loggederror_class.select()
+        assert len(errors) == 2
+        assert any('form_var_undefined' in (error.exception_message or '') for error in errors)
+        assert any('invalid date value' in (error.exception_message or '') for error in errors)
 
     formdata = formdef.data_class()()
     today = datetime.date.today()
 
     formdata.data = {'1': 'item1',
                      '1_display': 'item1',
-                     '2': today}
+                     '2': today.timetuple()}
     formdata.just_created()
     formdata.perform_workflow()
 
@@ -4974,10 +5005,11 @@ def test_create_carddata(pub):
     assert carddef.data_class().count() == 0
 
 
-def test_call_external_workflow_with_evolution_linked_object(pub):
+def test_call_external_workflow_with_evolution_linked_object(two_pubs):
     FormDef.wipe()
     CardDef.wipe()
-    LoggedError.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
 
     external_wf = Workflow(name='External Workflow')
     st1 = external_wf.add_status(name='New')
@@ -5056,17 +5088,19 @@ def test_call_external_workflow_with_evolution_linked_object(pub):
 
     # remove external formdata
     perform_items([action], formdata)
-    assert LoggedError.count() == 0
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 0
     assert external_formdef.data_class().count() == 0
     assert external_carddef.data_class().count() == 1
 
     # formdata is already deleted: cannot find it again
     perform_items([action], formdata)
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
-    assert logged_error.summary == 'Could not find linked "External Form" object by id %s' % external_formdata.id
-    assert logged_error.exception_class == 'KeyError'
-    assert logged_error.status_item_id == action.id
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 1
+        logged_error = two_pubs.loggederror_class.select()[0]
+        assert logged_error.summary == 'Could not find linked "External Form" object by id %s' % external_formdata.id
+        assert logged_error.exception_class == 'KeyError'
+        assert logged_error.status_item_id == action.id
 
     # try remove an unexisting carddef: do nothing
     unused_carddef = CardDef()
@@ -5074,26 +5108,30 @@ def test_call_external_workflow_with_evolution_linked_object(pub):
     unused_carddef.fields = []
     unused_carddef.workflow = external_wf
     unused_carddef.store()
-    LoggedError.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
     action.slug = 'carddef:%s' % unused_carddef.url_name
     wf.store()
     perform_items([action], formdata)
-    assert LoggedError.count() == 0
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 0
     assert external_formdef.data_class().count() == 0
     assert external_carddef.data_class().count() == 1
     # remove the right carddef
     action.slug = 'carddef:%s' % external_carddef.url_name
     wf.store()
     perform_items([action], formdata)
-    assert LoggedError.count() == 0
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 0
     assert external_formdef.data_class().count() == 0
     assert external_carddef.data_class().count() == 0
 
 
-def test_call_external_workflow_with_data_sourced_object(pub):
+def test_call_external_workflow_with_data_sourced_object(two_pubs):
     FormDef.wipe()
     CardDef.wipe()
-    LoggedError.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
 
     carddef_wf = Workflow(name='Carddef Workflow')
     carddef_wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(carddef_wf)
@@ -5154,7 +5192,8 @@ def test_call_external_workflow_with_data_sourced_object(pub):
     formdef.workflow = wf
     formdef.store()
 
-    assert LoggedError.count() == 0
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 0
     assert carddef.data_class().count() == 1
 
     formdata = formdef.data_class()()
@@ -5164,7 +5203,8 @@ def test_call_external_workflow_with_data_sourced_object(pub):
     formdata.perform_workflow()
 
     perform_items([update_action], formdata)
-    assert LoggedError.count() == 0
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 0
     assert carddef.data_class().count() == 1
     data = carddef.data_class().select()[0]
     assert data.data['bo0'] == '1'
@@ -5174,20 +5214,21 @@ def test_call_external_workflow_with_data_sourced_object(pub):
     assert data.data['bo0'] == '2'
 
     perform_items([delete_action], formdata)
-    assert LoggedError.count() == 0
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 0
     assert carddef.data_class().count() == 0
 
     perform_items([delete_action], formdata)
-    assert LoggedError.count() == 1
-    logged_error = LoggedError.select()[0]
-    assert logged_error.summary == 'Could not find linked "Data" object by id %s' % carddata.id
-    assert logged_error.exception_class == 'KeyError'
+    if two_pubs.is_using_postgresql():
+        assert two_pubs.loggederror_class.count() == 1
+        logged_error = two_pubs.loggederror_class.select()[0]
+        assert logged_error.summary == 'Could not find linked "Data" object by id %s' % carddata.id
+        assert logged_error.exception_class == 'KeyError'
 
 
 def test_call_external_workflow_with_parent_object(pub):
     FormDef.wipe()
     CardDef.wipe()
-    LoggedError.wipe()
 
     # carddef workflow, with global action to increment a counter in its
     # backoffice fields.
@@ -5274,7 +5315,6 @@ def test_call_external_workflow_with_parent_object(pub):
 def test_call_external_workflow_use_caller_variable(pub):
     FormDef.wipe()
     CardDef.wipe()
-    LoggedError.wipe()
 
     # carddef workflow, with global action to set a value in a backoffice field
     carddef_wf = Workflow(name='Carddef Workflow')
@@ -5349,7 +5389,6 @@ def test_call_external_workflow_use_caller_variable(pub):
 def test_edit_carddata_with_data_sourced_object(pub):
     FormDef.wipe()
     CardDef.wipe()
-    LoggedError.wipe()
 
     datasource = {
         'type': 'formula',
@@ -5462,7 +5501,6 @@ def test_edit_carddata_with_data_sourced_object(pub):
 def test_edit_carddata_with_linked_object(pub):
     FormDef.wipe()
     CardDef.wipe()
-    LoggedError.wipe()
 
     carddef = CardDef()
     carddef.name = 'Parent'
diff --git a/tests/test_wscall.py b/tests/test_wscall.py
index a33cea6c..56b68734 100644
--- a/tests/test_wscall.py
+++ b/tests/test_wscall.py
@@ -3,7 +3,6 @@ import pytest
 
 from wcs import fields
 from wcs.formdef import FormDef
-from wcs.logged_errors import LoggedError
 from wcs.qommon.http_request import HTTPRequest
 from wcs.qommon.template import Template
 from wcs.wscalls import NamedWsCall
@@ -27,6 +26,7 @@ def teardown_module(module):
 
 def test_named_wscall(pub):
     # create object
+    NamedWsCall.wipe()
     wscall = NamedWsCall()
     wscall.name = 'Hello'
     wscall.request = {'url': 'http://example.net', 'qs_data': {'a': 'b'}}
@@ -172,12 +172,13 @@ def test_webservice_delete(http_requests, pub):
 
 @pytest.mark.parametrize('notify_on_errors', [True, False])
 @pytest.mark.parametrize('record_on_errors', [True, False])
-def test_webservice_on_error(http_requests, pub, emails, notify_on_errors, record_on_errors):
+def test_webservice_on_error_with_sql(http_requests, emails, notify_on_errors, record_on_errors):
+    pub = create_temporary_pub(sql_mode=True)
     pub.cfg['debug'] = {'error_email': 'errors@localhost.invalid'}
     pub.write_cfg()
 
     NamedWsCall.wipe()
-    LoggedError.wipe()
+    pub.loggederror_class.wipe()
     FormDef.wipe()
 
     wscall = NamedWsCall()
@@ -200,7 +201,7 @@ def test_webservice_on_error(http_requests, pub, emails, notify_on_errors, recor
         resp = get_app(pub).get('/foobar/')
         assert 'Foo Bar ' in resp.text
         assert emails.count() == 0
-        assert LoggedError.count() == 0
+        assert pub.loggederror_class.count() == 0
 
     for url_part in ['400', '400-json', '404', '404-json', '500', 'json-err1', 'json-errheader1']:
         status_code = 200
@@ -217,7 +218,7 @@ def test_webservice_on_error(http_requests, pub, emails, notify_on_errors, recor
         else:
             assert emails.count() == 0
         if record_on_errors:
-            assert LoggedError.count() == 1
-            LoggedError.wipe()
+            assert pub.loggederror_class.count() == 1
+            pub.loggederror_class.wipe()
         else:
-            assert LoggedError.count() == 0
+            assert pub.loggederror_class.count() == 0
diff --git a/tests/utilities.py b/tests/utilities.py
index 9ed74156..87d78e60 100644
--- a/tests/utilities.py
+++ b/tests/utilities.py
@@ -4,10 +4,8 @@ import os
 import tempfile
 import random
 import psycopg2
-import pytest
 import shutil
 import sys
-import threading
 
 from wcs import sql, sessions, custom_views
 
@@ -20,8 +18,7 @@ from django.utils.six.moves.urllib import parse as urlparse
 from wcs.qommon import force_str
 import wcs
 import wcs.wsgi
-from wcs import publisher, compat
-from wcs.qommon.http_request import HTTPRequest
+from wcs import compat
 from wcs.users import User
 from wcs.tracking_code import TrackingCode
 import wcs.qommon.emails
@@ -81,13 +78,13 @@ def create_temporary_pub(sql_mode=False, templates_mode=False, lazy_mode=False):
         pub.session_class = sql.Session
         pub.custom_view_class = sql.CustomView
         pub.snapshot_class = sql.Snapshot
+        pub.loggederror_class = sql.LoggedError
         pub.is_using_postgresql = lambda: True
     else:
         pub.user_class = User
         pub.tracking_code_class = TrackingCode
         pub.session_class = sessions.BasicSession
         pub.custom_view_class = custom_views.CustomView
-        pub.snapshot_class = None
         pub.is_using_postgresql = lambda: False
 
     pub.session_manager_class = sessions.StorageSessionManager
@@ -172,6 +169,7 @@ def create_temporary_pub(sql_mode=False, templates_mode=False, lazy_mode=False):
         sql.do_session_table()
         sql.do_custom_views_table()
         sql.do_snapshots_table()
+        sql.do_loggederrors_table()
         sql.do_meta_table()
 
         conn.close()
diff --git a/wcs/admin/logged_errors.py b/wcs/admin/logged_errors.py
index ed93a7b1..c0f25b68 100644
--- a/wcs/admin/logged_errors.py
+++ b/wcs/admin/logged_errors.py
@@ -24,8 +24,6 @@ from wcs.qommon import _, ngettext, N_, template
 from wcs.qommon import errors, get_cfg
 from wcs.qommon.misc import localstrftime
 
-from wcs.logged_errors import LoggedError
-
 
 class LoggedErrorDirectory(Directory):
     _q_exports = ['', 'delete', 'ack']
@@ -107,7 +105,7 @@ class LoggedErrorDirectory(Directory):
         return redirect('.')
 
     def delete(self):
-        self.error.remove_self()
+        get_publisher().loggederror_class.remove_object(self.error.id)
         return redirect('..')
 
 
@@ -117,10 +115,14 @@ class LoggedErrorsDirectory(Directory):
     @classmethod
     def get_errors(cls, formdef_class=None, formdef_id=None, workflow_id=None):
         errors = []
+        if not get_publisher().loggederror_class:
+            return errors
         if formdef_id and formdef_class:
-            errors = [e for e in LoggedError.get_with_indexed_value('formdef_id', formdef_id) if e.formdef_class == formdef_class.__name__]
+            errors = [
+                e for e in get_publisher().loggederror_class.get_with_indexed_value('formdef_id', formdef_id)
+                if e.formdef_class == formdef_class.__name__]
         elif workflow_id:
-            errors = LoggedError.get_with_indexed_value('workflow_id', workflow_id)
+            errors = get_publisher().loggederror_class.get_with_indexed_value('workflow_id', workflow_id)
         return list(errors)
 
     @classmethod
@@ -168,7 +170,7 @@ class LoggedErrorsDirectory(Directory):
 
     def _q_lookup(self, component):
         try:
-            error = LoggedError.get(component)
+            error = get_publisher().loggederror_class.get(component)
         except KeyError:
             raise errors.TraversalError()
         get_response().breadcrumb.append(('logged-errors/', _('Logged Errors')))
diff --git a/wcs/conditions.py b/wcs/conditions.py
index 594980f5..08677f64 100644
--- a/wcs/conditions.py
+++ b/wcs/conditions.py
@@ -56,13 +56,13 @@ class Condition(object):
             if self.log_errors:
                 get_logger().warning('failed to evaluate %r (%r)', self, e)
             if self.record_errors:
-                from wcs.logged_errors import LoggedError
                 summary = _('Failed to evaluate condition')
-                LoggedError.record(summary,
-                                   formdata=self.context.get('formdata'),
-                                   status_item=self.context.get('status_item'),
-                                   expression=self.value, expression_type=self.type,
-                                   exception=e)
+                get_publisher().record_error(
+                    summary,
+                    formdata=self.context.get('formdata'),
+                    status_item=self.context.get('status_item'),
+                    expression=self.value, expression_type=self.type,
+                    exception=e)
             raise RuntimeError()
 
     def evaluate_python(self, local_variables):
diff --git a/wcs/data_sources.py b/wcs/data_sources.py
index 2afc5f6b..21c4fc6a 100644
--- a/wcs/data_sources.py
+++ b/wcs/data_sources.py
@@ -512,12 +512,11 @@ class NamedDataSource(XmlStorableObject):
         url += param_name + '=' + urllib.quote(param_value)
 
         def find_item(items, name, value):
-            from wcs.logged_errors import LoggedError
             for item in items:
                 if str(item.get(name)) == str(value):
                     return item
             # not found
-            LoggedError.record(_('Could not find element by id "%s"') % value)
+            get_publisher().record_error(_('Could not find element by id "%s"') % value)
             return None
 
         request = get_request()
diff --git a/wcs/fields.py b/wcs/fields.py
index 23c89a86..d362aed2 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -387,8 +387,10 @@ class Field(object):
             except TemplateError:
                 return ('', explicit_lock)
             except AttributeError as e:
-                from wcs.logged_errors import LoggedError
-                LoggedError.record(_('Failed to evaluate prefill on field "%s"') % self.label, formdef=getattr(self, 'formdef', None), exception=e)
+                get_publisher().record_error(
+                    _('Failed to evaluate prefill on field "%s"') % self.label,
+                    formdef=getattr(self, 'formdef', None),
+                    exception=e)
                 return ('', explicit_lock)
 
         elif t == 'user' and user:
diff --git a/wcs/formdata.py b/wcs/formdata.py
index 8396d2e7..722974f7 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -601,9 +601,8 @@ class FormData(StorableObject):
         if status_id == '_previous':
             previous_status = self.pop_previous_marked_status()
             if not previous_status:
-                from wcs.logged_errors import LoggedError
                 summary = _('Failed to compute previous status')
-                LoggedError.record(summary, formdata=self)
+                get_publisher().record_error(summary, formdata=self)
                 return
             status_id = previous_status.id
         status = 'wf-%s' % status_id
@@ -1214,7 +1213,6 @@ class FormData(StorableObject):
     def iter_target_datas(self, objectdef=None, object_type=None, status_item=None):
         # objectdef, object_type and status_item are provided when called from a workflow action
         from wcs.wf.create_formdata import LinkedFormdataEvolutionPart
-        from wcs.logged_errors import LoggedError
         from .carddef import CardDef
         from .formdef import FormDef
 
@@ -1268,8 +1266,9 @@ class FormData(StorableObject):
                     yield objectdef.data_class().get(target_id)
                 except KeyError as e:
                     # use custom error message depending on target type
-                    LoggedError.record(_('Could not find linked "%(object_name)s" object by id %(object_id)s') % {
-                        'object_name': objectdef.name, 'object_id': target_id},
+                    get_publisher().record_error(
+                        _('Could not find linked "%(object_name)s" object by id %(object_id)s') % {
+                            'object_name': objectdef.name, 'object_id': target_id},
                         formdata=self, status_item=status_item, exception=e)
             else:
                 # inspect page
diff --git a/wcs/logged_errors.py b/wcs/logged_errors.py
index 3cdc0efa..b678537b 100644
--- a/wcs/logged_errors.py
+++ b/wcs/logged_errors.py
@@ -17,18 +17,16 @@
 import datetime
 
 from .qommon.misc import simplify
-from .qommon.xml_storage import XmlStorableObject
 from wcs.carddef import CardDef
 from wcs.formdef import FormDef
 from wcs.workflows import Workflow
 
 
-class LoggedError(XmlStorableObject):
+class LoggedError(object):
     _names = 'logged-errors'
-    xml_root_node = 'error'
-    _indexes = ['tech_id']
-    _hashed_indexes = ['formdef_id', 'workflow_id']
 
+    id = None
+    tech_id = None
     summary = None
     formdef_class = 'FormDef'
     formdata_id = None
@@ -46,19 +44,6 @@ class LoggedError(XmlStorableObject):
     latest_occurence_timestamp = None
     acked = False
 
-    # declarations for serialization
-    XML_NODES = [
-            ('summary', 'str'), ('traceback', 'str'),
-            ('exception_class', 'str'), ('exception_message', 'str'),
-            ('expression', 'str'), ('expression_type', 'str'),
-            ('formdata_id', 'str'), ('formdef_id', 'str'), ('workflow_id', 'str'),
-            ('formdef_class', 'str'),
-            ('status_id', 'str'), ('status_item_id', 'str'),
-            ('occurences_count', 'int'),
-            ('first_occurence_timestamp', 'datetime'),
-            ('latest_occurence_timestamp', 'datetime'),
-            ('acked', 'bool')]
-
     @classmethod
     def record(cls, error_summary, plain_error_msg=None, formdata=None,
                formdef=None, workflow=None, status=None, status_item=None,
@@ -96,13 +81,10 @@ class LoggedError(XmlStorableObject):
             error.status_id = status.id
 
         error.first_occurence_timestamp = datetime.datetime.now()
-        error.id = '%s-%s' % (
-                error.first_occurence_timestamp.strftime('%Y%m%d-%H%M%S'),
-                error.tech_id,
-                )
-        existing_error = cls.get_on_index(error.tech_id, 'tech_id', ignore_errors=True)
-        if existing_error:
-            error = existing_error
+        error.tech_id = error.build_tech_id()
+        existing_errors = list(cls.get_with_indexed_value('tech_id', error.tech_id))
+        if existing_errors:
+            error = existing_errors[0]
         error.occurences_count += 1
         error.latest_occurence_timestamp = datetime.datetime.now()
         error.store()
@@ -112,7 +94,7 @@ class LoggedError(XmlStorableObject):
     def record_exception(cls, error_summary, plain_error_msg, publisher):
         try:
             context = publisher.substitutions.get_context_variables()
-        except Exception as e:
+        except Exception:
             return
         formdata_id = context.get('form_number_raw')
         formdef_urlname = context.get('form_slug')
@@ -125,11 +107,11 @@ class LoggedError(XmlStorableObject):
             klass = CardDef
         formdef = klass.get_by_urlname(formdef_urlname)
         formdata = formdef.data_class().get(formdata_id, ignore_errors=True)
-        return cls.record(error_summary, plain_error_msg, formdata=formdata,
-                   formdef=formdef, workflow=formdef.workflow)
+        return cls.record(
+            error_summary, plain_error_msg, formdata=formdata,
+            formdef=formdef, workflow=formdef.workflow)
 
-    @property
-    def tech_id(self):
+    def build_tech_id(self):
         tech_id = ''
         if self.formdef_id:
             tech_id += '%s-' % self.formdef_id
diff --git a/wcs/publisher.py b/wcs/publisher.py
index e5e6c806..b158047d 100644
--- a/wcs/publisher.py
+++ b/wcs/publisher.py
@@ -52,7 +52,6 @@ from .qommon.cron import CronJob
 
 from .users import User
 from .tracking_code import TrackingCode
-from .logged_errors import LoggedError
 
 import pickle
 
@@ -153,6 +152,7 @@ class WcsPublisher(StubWcsPublisher):
             self.session_class = sql.Session
             self.custom_view_class = sql.CustomView
             self.snapshot_class = sql.Snapshot
+            self.loggederror_class = sql.LoggedError
             sql.get_connection(new=True)
         else:
             self.user_class = User
@@ -160,6 +160,7 @@ class WcsPublisher(StubWcsPublisher):
             self.session_class = sessions.BasicSession
             self.custom_view_class = custom_views.CustomView
             self.snapshot_class = None
+            self.loggederror_class = None
 
         self.session_manager_class = sessions.StorageSessionManager
         self.set_session_manager(self.session_manager_class(session_class=self.session_class))
@@ -312,6 +313,7 @@ class WcsPublisher(StubWcsPublisher):
         sql.do_tracking_code_table()
         sql.do_custom_views_table()
         sql.do_snapshots_table()
+        sql.do_loggederrors_table()
         sql.do_meta_table()
         from .formdef import FormDef
         from .carddef import CardDef
@@ -340,8 +342,8 @@ class WcsPublisher(StubWcsPublisher):
 
     def log_internal_error(self, error_summary, plain_error_msg, record=False, notify=True):
         tech_id = None
-        if record:
-            logged_exception = LoggedError.record_exception(
+        if record and self.loggederror_class:
+            logged_exception = self.loggederror_class.record_exception(
                     error_summary, plain_error_msg, publisher=self)
             if logged_exception:
                 tech_id = logged_exception.tech_id
@@ -357,6 +359,10 @@ class WcsPublisher(StubWcsPublisher):
             # this could happen on file descriptor exhaustion
             pass
 
+    def record_error(self, *args, **kwargs):
+        if self.loggederror_class:
+            self.loggederror_class.record(*args, **kwargs)
+
     def apply_global_action_timeouts(self):
         from wcs.workflows import Workflow, WorkflowGlobalActionTimeoutTrigger
         for workflow in Workflow.select():
diff --git a/wcs/sql.py b/wcs/sql.py
index 3f215173..6fd85d91 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -45,6 +45,7 @@ import wcs.categories
 import wcs.carddata
 import wcs.custom_views
 import wcs.formdata
+import wcs.logged_errors
 import wcs.snapshots
 import wcs.tracking_code
 import wcs.users
@@ -859,6 +860,67 @@ def do_snapshots_table():
     cur.close()
 
 
+def do_loggederrors_table(concurrently=False):
+    conn, cur = get_connection_and_cursor()
+    table_name = 'loggederrors'
+
+    cur.execute('''SELECT COUNT(*) FROM information_schema.tables
+                    WHERE table_schema = 'public'
+                      AND table_name = %s''', (table_name,))
+    if cur.fetchone()[0] == 0:
+        cur.execute('''CREATE TABLE %s (id SERIAL PRIMARY KEY,
+                                        tech_id VARCHAR UNIQUE,
+                                        summary VARCHAR,
+                                        formdef_class VARCHAR,
+                                        formdata_id VARCHAR,
+                                        formdef_id VARCHAR,
+                                        workflow_id VARCHAR,
+                                        status_id VARCHAR,
+                                        status_item_id VARCHAR,
+                                        expression VARCHAR,
+                                        expression_type VARCHAR,
+                                        traceback TEXT,
+                                        exception_class VARCHAR,
+                                        exception_message VARCHAR,
+                                        occurences_count INTEGER,
+                                        first_occurence_timestamp TIMESTAMP WITH TIME ZONE,
+                                        latest_occurence_timestamp TIMESTAMP WITH TIME ZONE,
+                                        acked BOOLEAN
+                                        )''' % table_name)
+    cur.execute('''SELECT column_name FROM information_schema.columns
+                    WHERE table_schema = 'public'
+                      AND table_name = %s''', (table_name,))
+    existing_fields = set([x[0] for x in cur.fetchall()])
+
+    needed_fields = set([x[0] for x in LoggedError._table_static_fields])
+
+    # delete obsolete fields
+    for field in (existing_fields - needed_fields):
+        cur.execute('''ALTER TABLE %s DROP COLUMN %s''' % (table_name, field))
+
+    create_index = 'CREATE INDEX'
+    if concurrently:
+        create_index = 'CREATE INDEX CONCURRENTLY'
+
+    # build indexes
+    existing_indexes = set()
+    cur.execute('''SELECT indexname
+                     FROM pg_indexes
+                    WHERE schemaname = 'public'
+                      AND tablename = %s''', (table_name,))
+    existing_indexes = set([x[0] for x in cur.fetchall()])
+
+    for attr in ('formdef_id', 'workflow_id'):
+        if not table_name + '_' + attr + '_idx' in existing_indexes:
+            cur.execute('%(create_index)s %(table_name)s_%(attr)s_idx ON %(table_name)s (%(attr)s)' % {
+                'create_index': create_index,
+                'table_name': table_name,
+                'attr': attr})
+
+    conn.commit()
+    cur.close()
+
+
 @guard_postgres
 def do_meta_table(conn=None, cur=None, insert_current_sql_level=True):
     own_conn = False
@@ -2458,6 +2520,90 @@ class Snapshot(SqlMixin, wcs.snapshots.Snapshot):
         return cls.get(row[0])
 
 
+class LoggedError(SqlMixin, wcs.logged_errors.LoggedError):
+    _table_name = 'loggederrors'
+    _table_static_fields = [
+        ('id', 'serial'),
+        ('tech_id', 'varchar'),
+        ('summary', 'varchar'),
+        ('formdef_class', 'varchar'),
+        ('formdata_id', 'varchar'),
+        ('formdef_id', 'varchar'),
+        ('workflow_id', 'varchar'),
+        ('status_id', 'varchar'),
+        ('status_item_id', 'varchar'),
+        ('expression', 'varchar'),
+        ('expression_type', 'varchar'),
+        ('traceback', 'text'),
+        ('exception_class', 'varchar'),
+        ('exception_message', 'varchar'),
+        ('occurences_count', 'integer'),
+        ('first_occurence_timestamp', 'timestamptz'),
+        ('latest_occurence_timestamp', 'timestamptz'),
+        ('acked', 'boolean'),
+    ]
+
+    _numerical_id = False
+
+    @guard_postgres
+    @invalidate_substitution_cache
+    def store(self):
+        sql_dict = {x: getattr(self, x) for x, y in self._table_static_fields}
+
+        conn, cur = get_connection_and_cursor()
+        if not self.id:
+            column_names = [x for x in sql_dict.keys() if x != 'id']
+            sql_statement = '''INSERT INTO %s (%s)
+                               VALUES (%s)
+                               RETURNING id''' % (
+                                       self._table_name,
+                                       ', '.join(column_names),
+                                       ', '.join(['%%(%s)s' % x for x in column_names]))
+            cur.execute(sql_statement, sql_dict)
+            self.id = cur.fetchone()[0]
+        else:
+            column_names = sql_dict.keys()
+            sql_statement = '''UPDATE %s SET %s WHERE id = %%(id)s RETURNING id''' % (
+                                       self._table_name,
+                                       ', '.join(['%s = %%(%s)s' % (x, x) for x in column_names]))
+            cur.execute(sql_statement, sql_dict)
+            if cur.fetchone() is None:
+                raise AssertionError()
+
+        conn.commit()
+        cur.close()
+
+    @classmethod
+    def _row2ob(cls, row, **kwargs):
+        o = cls()
+        for field, value in zip(cls._table_static_fields, tuple(row)):
+            if field[1] in ('varchar', 'text'):
+                setattr(o, field[0], str_encode(value))
+            else:
+                setattr(o, field[0], value)
+        return o
+
+    @classmethod
+    def get_data_fields(cls):
+        return []
+
+    @classmethod
+    @guard_postgres
+    def fix_sequences(cls):
+        conn, cur = get_connection_and_cursor()
+
+        sql_statement = '''select max(id) from %s''' % cls._table_name
+        cur.execute(sql_statement)
+        max_id = cur.fetchone()[0]
+        if max_id is not None:
+            sql_statement = '''ALTER SEQUENCE %s_id_seq RESTART %s''' % (
+                    cls._table_name, max_id+1)
+            cur.execute(sql_statement)
+
+        conn.commit()
+        cur.close()
+
+
 class classproperty(object):
     def __init__(self, f):
         self.f = f
@@ -2716,7 +2862,7 @@ def get_period_total(period_start=None, include_start=True, period_end=None, inc
 # latest migration, number + description (description is not used
 # programmaticaly but will make sure git conflicts if two migrations are
 # separately added with the same number)
-SQL_LEVEL = (46, 'add index on formdata(status) - fix')
+SQL_LEVEL = (47, 'use SQL to store LoggedError')
 
 
 def migrate_global_views(conn, cur):
@@ -2871,6 +3017,9 @@ def migrate():
     if sql_level < 42:
         # 42: create snapshots table
         do_snapshots_table()
+    if sql_level < 47:
+        # 47: store LoggedErrors in SQL
+        do_loggederrors_table()
 
     cur.execute('''UPDATE wcs_meta SET value = %s WHERE key = %s''', (
         str(SQL_LEVEL[0]), 'sql_level'))
diff --git a/wcs/variables.py b/wcs/variables.py
index 194beb71..f7308a37 100644
--- a/wcs/variables.py
+++ b/wcs/variables.py
@@ -24,7 +24,6 @@ from quixote import get_publisher, get_request
 
 from pyproj import Geod
 
-from .logged_errors import LoggedError
 from .qommon import misc, force_str, _
 from .qommon.evalutils import make_datetime
 from .qommon.templatetags.qommon import parse_datetime
@@ -169,7 +168,7 @@ class LazyFormDefObjectsManager(object):
                 from wcs import sql
                 criteria = Equal(sql.get_field_id(field), value)
                 return self._clone(self._criterias + [criteria])
-        LoggedError.record(_('Invalid filter "%s"') % self.pending_attr, formdata=self._formdata)
+        get_publisher().record_error(_('Invalid filter "%s"') % self.pending_attr, formdata=self._formdata)
         return self.none()
 
     def __getattr__(self, attribute):
diff --git a/wcs/wf/backoffice_fields.py b/wcs/wf/backoffice_fields.py
index 82215d39..be4f7196 100644
--- a/wcs/wf/backoffice_fields.py
+++ b/wcs/wf/backoffice_fields.py
@@ -15,6 +15,8 @@
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
 import xml.etree.ElementTree as ET
+
+from quixote import get_publisher
 from quixote.html import htmltext
 
 from ..qommon import _, N_
@@ -133,17 +135,17 @@ class SetBackofficeFieldsWorkflowStatusItem(WorkflowStatusItem):
                 try:
                     new_value = formdef_field.convert_value_from_anything(new_value)
                 except ValueError as e:
-                    from wcs.logged_errors import LoggedError
                     summary = _('Failed to convert %(class)s value to %(kind)s field (%(id)s)') % {
                                     'class': type(new_value),
                                     'kind': _(getattr(formdef_field, 'description', 'unknown')),
                                     'id': field['field_id'],
                                 }
                     expression_dict = self.get_expression(field['value'])
-                    LoggedError.record(summary, formdata=formdata, status_item=self,
-                                       expression=expression_dict['value'],
-                                       expression_type=expression_dict['type'],
-                                       exception=e)
+                    get_publisher().record_error(
+                        summary, formdata=formdata, status_item=self,
+                        expression=expression_dict['value'],
+                        expression_type=expression_dict['type'],
+                        exception=e)
                     continue
 
             formdata.data['%s' % field['field_id']] = new_value
diff --git a/wcs/wf/create_formdata.py b/wcs/wf/create_formdata.py
index eaba7d20..0cd7c5a4 100644
--- a/wcs/wf/create_formdata.py
+++ b/wcs/wf/create_formdata.py
@@ -27,7 +27,6 @@ from wcs.qommon.form import (WidgetListAsTable, CompositeWidget,
                              SingleSelectWidget, ComputedExpressionWidget,
                              CheckboxWidget, VarnameWidget, HtmlWidget)
 
-from wcs.logged_errors import LoggedError
 from wcs.workflows import WorkflowStatusItem, register_item_class
 from wcs.formdef import FormDef
 
@@ -391,8 +390,9 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
                         field=dest_field,
                         value=src.data.get(field.id))
                 except Exception as e:
-                    LoggedError.record(_('Could not copy field by varname for "%s"') % field.varname,
-                                       formdata=src, status_item=self, exception=e)
+                    get_publisher().record_error(
+                        _('Could not copy field by varname for "%s"') % field.varname,
+                        formdata=src, status_item=self, exception=e)
 
         # field.id can be serialized to xml, so we must always convert them to
         # str when matching
@@ -418,14 +418,15 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
                     value=value)
             except Exception as e:
                 expression = self.get_expression(mapping.expression)
-                LoggedError.record(_('Could not assign value to field "%s"') % dest_field.label,
-                                   formdata=src, status_item=self,
-                                   expression=expression['value'], expression_type=expression['type'],
-                                   exception=e)
+                get_publisher().record_error(
+                    _('Could not assign value to field "%s"') % dest_field.label,
+                    formdata=src, status_item=self,
+                    expression=expression['value'], expression_type=expression['type'],
+                    exception=e)
 
         if missing_fields:
             summary = _('Missing field %r') % missing_fields
-            LoggedError.record(summary, formdata=src, status_item=self)
+            get_publisher().record_error(summary, formdata=src, status_item=self)
 
     def _set_value(self, formdata, field, value):
         if field.convert_value_from_anything:
diff --git a/wcs/wf/dispatch.py b/wcs/wf/dispatch.py
index 458b2a40..ea084ba9 100644
--- a/wcs/wf/dispatch.py
+++ b/wcs/wf/dispatch.py
@@ -188,7 +188,6 @@ class DispatchWorkflowStatusItem(WorkflowStatusItem):
         return htmltext('<ul class="rules">%s</ul>') % htmltext('').join(result)
 
     def perform(self, formdata):
-        from wcs.logged_errors import LoggedError
         if not formdata.workflow_roles:
             formdata.workflow_roles = {}
 
@@ -199,7 +198,7 @@ class DispatchWorkflowStatusItem(WorkflowStatusItem):
                 return
             new_role_id = self.get_computed_role_id(self.role_id)
             if not new_role_id:
-                LoggedError.record(_('error in dispatch, missing role (%s)') % self.role_id, formdata=formdata)
+                get_publisher().record_error(_('error in dispatch, missing role (%s)') % self.role_id, formdata=formdata)
         elif self.dispatch_type == 'automatic':
             if not (self.role_key and self.variable and self.rules):
                 return
@@ -226,7 +225,7 @@ class DispatchWorkflowStatusItem(WorkflowStatusItem):
 
         if new_role_id:
             if not Role.has_key(new_role_id):
-                LoggedError.record(_('error in dispatch, missing role (%s)') % new_role_id, formdata=formdata)
+                get_publisher().record_error(_('error in dispatch, missing role (%s)') % new_role_id, formdata=formdata)
             else:
                 formdata.workflow_roles[self.role_key] = str(new_role_id)
                 formdata.store()
diff --git a/wcs/wf/external_workflow.py b/wcs/wf/external_workflow.py
index ac016465..29617676 100644
--- a/wcs/wf/external_workflow.py
+++ b/wcs/wf/external_workflow.py
@@ -19,7 +19,6 @@ from quixote import get_publisher
 from wcs.qommon import _
 from wcs.qommon.form import SingleSelectWidget
 
-from wcs.logged_errors import LoggedError
 from wcs.workflows import WorkflowStatusItem, perform_items, register_item_class
 from wcs.workflows import WorkflowGlobalActionWebserviceTrigger, Workflow
 from wcs.carddef import CardDef
@@ -126,8 +125,9 @@ class ExternalWorkflowGlobalAction(WorkflowStatusItem):
 
         trigger = self.get_trigger(objectdef.workflow)
         if not trigger:
-            LoggedError.record(_('No trigger with id "%s" found in workflow') % self.trigger_id,
-                    formdata=formdata, status_item=self)
+            get_publisher().record_error(
+                _('No trigger with id "%s" found in workflow') % self.trigger_id,
+                formdata=formdata, status_item=self)
             return
 
         class CallerSource:
diff --git a/wcs/wf/wscall.py b/wcs/wf/wscall.py
index 02f7060c..ffa0db3a 100644
--- a/wcs/wf/wscall.py
+++ b/wcs/wf/wscall.py
@@ -447,13 +447,12 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
             try:
                 target = self.parent.parent.get_status(value)
             except KeyError:
-                from wcs.logged_errors import LoggedError
                 message = _('reference to invalid status in workflow %(workflow)s, status %(status)s, item %(item)s') % {
                     'workflow': self.parent.parent.name,
                     'status': self.parent.name,
                     'item': self.description,
                 }
-                LoggedError.record(message, workflow=self.parent.parent)
+                get_publisher().record_error(message, workflow=self.parent.parent)
                 continue
             targets.append(target)
         return targets
diff --git a/wcs/workflows.py b/wcs/workflows.py
index 94cade90..6fb0ef04 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -1982,15 +1982,15 @@ class WorkflowStatusItem(XmlSerialisable):
         vars.update(context or {})
 
         def log_exception(exception):
-            from wcs.logged_errors import LoggedError
             if expression['type'] == 'template':
                 summary = _('Failed to compute template')
             else:
                 summary = _('Failed to compute Python expression')
-            LoggedError.record(summary, formdata=formdata, status_item=status_item,
-                               expression=expression['value'],
-                               expression_type=expression['type'],
-                               exception=exception)
+            get_publisher().record_error(
+                summary, formdata=formdata, status_item=status_item,
+                expression=expression['value'],
+                expression_type=expression['type'],
+                exception=exception)
 
         if expression['type'] == 'template':
             try:
@@ -2043,14 +2043,13 @@ class WorkflowStatusItem(XmlSerialisable):
 
         targets = [x for x in self.parent.parent.possible_status if x.id == self.status]
         if not targets and formdata:  # do not log in presentation context: formdata is needed
-            from wcs.logged_errors import LoggedError
             message = _('reference to invalid status %(target)s in status %(status)s, '
                         'action %(status_item)s') % {
                             'target': self.status,
                             'status': self.parent.name,
                             'status_item': _(self.description)
                         }
-            LoggedError.record(message, formdata=formdata, status_item=self)
+            get_publisher().record_error(message, formdata=formdata, status_item=self)
 
         return targets
 
@@ -2677,12 +2676,11 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
                 subject = mail_template.subject
                 extra_attachments = mail_template.attachments
             else:
-                from wcs.logged_errors import LoggedError
                 message = _('reference to invalid mail template %(mail_template)s in status %(status)s') % {
                             'status': self.parent.name,
                             'mail_template': self.mail_template,
                             }
-                LoggedError.record(message, formdata=formdata, status_item=self)
+                get_publisher().record_error(message, formdata=formdata, status_item=self)
                 return
 
         try:
-- 
2.28.0

