Projet

Général

Profil

0001-misc-store-LoggedErrors-in-SQL-48925.patch

Lauréline Guérin, 01 décembre 2020 16:34

Télécharger (72,4 ko)

Voir les différences:

Subject: [PATCH] misc: store LoggedErrors in SQL (#48925)

 tests/backoffice_pages/test_all.py      |  51 +++--
 tests/backoffice_pages/test_carddata.py |  11 +-
 tests/form_pages/test_all.py            |  65 +++---
 tests/test_mail_templates.py            |  14 +-
 tests/test_workflows.py                 | 254 ++++++++++++++----------
 tests/test_wscall.py                    |  15 +-
 tests/utilities.py                      |   8 +-
 wcs/admin/logged_errors.py              |  12 +-
 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/wf/backoffice_fields.py             |  12 +-
 wcs/wf/create_formdata.py               |  17 +-
 wcs/wf/external_workflow.py             |   6 +-
 wcs/workflows.py                        |  16 +-
 19 files changed, 457 insertions(+), 259 deletions(-)
tests/backoffice_pages/test_all.py
39 39
from wcs.carddef import CardDef
40 40
from wcs.categories import Category
41 41
from wcs.formdef import FormDef
42
from wcs.logged_errors import LoggedError
43 42
from wcs import fields
44 43
from wcs.wscalls import NamedWsCall
45 44

  
......
2220 2219
@pytest.mark.parametrize('notify_on_errors', [True, False])
2221 2220
@pytest.mark.parametrize('record_on_errors', [True, False])
2222 2221
def test_backoffice_wscall_on_error(http_requests, pub, emails, notify_on_errors, record_on_errors):
2222
    if not pub.is_using_postgresql():
2223
        pytest.skip('this requires SQL')
2224
        return
2225

  
2223 2226
    pub.cfg['debug'] = {'error_email': 'errors@localhost.invalid'}
2224 2227
    pub.write_cfg()
2225 2228

  
2226 2229
    create_user(pub)
2227 2230
    create_environment(pub)
2231
    pub.loggederror_class.wipe()
2228 2232
    formdef = FormDef.get_by_urlname('form-title')
2229 2233
    form_class = formdef.data_class()
2230 2234

  
......
2277 2281
    else:
2278 2282
        assert emails.count() == 0
2279 2283

  
2280
    # check LoggedError
2284
    # check pub.loggederror_class
2281 2285
    if record_on_errors:
2282
        assert LoggedError.count() == 1
2283
        LoggedError.wipe()
2286
        assert pub.loggederror_class.count() == 1
2287
        pub.loggederror_class.wipe()
2284 2288
    else:
2285
        assert LoggedError.count() == 0
2289
        assert pub.loggederror_class.count() == 0
2286 2290

  
2287 2291

  
2288 2292
def test_backoffice_wscall_attachment(http_requests, pub):
......
4672 4676

  
4673 4677

  
4674 4678
def test_backoffice_logged_errors(pub):
4679
    if not pub.is_using_postgresql():
4680
        pytest.skip('this requires SQL')
4681
        return
4682

  
4675 4683
    Workflow.wipe()
4676 4684
    workflow = Workflow.get_default_workflow()
4677 4685
    workflow.id = '12'
......
4693 4701
    formdef.fields = []
4694 4702
    formdef.store()
4695 4703

  
4696
    LoggedError.wipe()
4704
    pub.loggederror_class.wipe()
4697 4705

  
4698 4706
    create_superuser(pub)
4699 4707
    app = login(get_app(pub))
......
4706 4714
    resp = app.get('/test/')
4707 4715
    resp = resp.form.submit('submit').follow()
4708 4716
    resp = resp.form.submit('submit')
4709
    assert LoggedError.count() == 1
4717
    assert pub.loggederror_class.count() == 1
4710 4718

  
4711 4719
    app = login(get_app(pub))
4712 4720
    resp = app.get('/backoffice/forms/%s/' % formdef.id)
......
4724 4732
    assert not 'Acked' in resp.text
4725 4733
    resp = resp.click('Ack').follow()
4726 4734
    assert 'Acked' in resp.text
4727
    assert LoggedError.select()[0].acked is True
4735
    assert pub.loggederror_class.select()[0].acked is True
4728 4736
    resp = resp.click('Delete').follow()
4729
    assert LoggedError.count() == 0
4737
    assert pub.loggederror_class.count() == 0
4730 4738

  
4731 4739
    pub.cfg.update({'debug': {'error_email': None}})
4732 4740
    pub.write_cfg()
......
4735 4743
    resp = app.get('/test/')
4736 4744
    resp = resp.form.submit('submit').follow()
4737 4745
    resp = resp.form.submit('submit')
4738
    assert LoggedError.count() == 1
4746
    assert pub.loggederror_class.count() == 1
4739 4747

  
4740 4748
    app = login(get_app(pub))
4741 4749
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
......
4753 4761
    resp = app.get('/test/')
4754 4762
    resp = resp.form.submit('submit').follow()
4755 4763
    resp = resp.form.submit('submit')
4756
    assert LoggedError.count() == 2
4764
    assert pub.loggederror_class.count() == 2
4757 4765

  
4758 4766
    app = login(get_app(pub))
4759 4767
    resp = app.get('/backoffice/workflows/%s/' % workflow.id)
......
5289 5297
    source_formdef.store()
5290 5298
    source_formdef.data_class().wipe()
5291 5299
    target_formdef.data_class().wipe()
5292
    LoggedError.wipe()
5300
    if pub.is_using_postgresql():
5301
        pub.loggederror_class.wipe()
5293 5302
    return locals()
5294 5303

  
5295 5304

  
5296
def test_backoffice_create_formdata_backoffice_submission(create_formdata):
5305
def test_backoffice_create_formdata_backoffice_submission(pub, create_formdata):
5297 5306
    # create submitting user
5298 5307
    user = create_formdata['pub'].user_class()
5299 5308
    user.name = 'Jean Darmette'
......
5324 5333
    assert target_data_class.count() == 0
5325 5334
    # resubmit it through backoffice submission
5326 5335
    resp = resp.form.submit(name='button_resubmit')
5327
    assert LoggedError.count() == 0
5336
    if pub.is_using_postgresql():
5337
        assert pub.loggederror_class.count() == 0
5328 5338
    assert target_data_class.count() == 1
5329 5339
    target_formdata = target_data_class.select()[0]
5330 5340

  
......
5353 5363
    assert pq('.field-type-file .value').text() == 'bar'
5354 5364

  
5355 5365

  
5356
def test_linked_forms_variables(create_formdata):
5366
def test_linked_forms_variables(pub, create_formdata):
5357 5367
    # create source formdata
5358 5368
    formdata = create_formdata['source_formdef'].data_class()()
5359 5369
    upload = PicklableUpload('/foo/bar', content_type='text/plain')
......
5370 5380
    formdata.perform_workflow()
5371 5381
    formdata.store()
5372 5382

  
5373
    get_publisher().substitutions.reset()
5374
    get_publisher().substitutions.feed(formdata)
5375
    substvars = get_publisher().substitutions.get_context_variables(mode='lazy')
5383
    pub.substitutions.reset()
5384
    pub.substitutions.feed(formdata)
5385
    substvars = pub.substitutions.get_context_variables(mode='lazy')
5376 5386
    assert str(substvars['form_links_resubmitted_form_var_foo_string']) == 'coucou'
5377 5387
    assert 'form_links_resubmitted_form_var_foo_string' in substvars.get_flat_keys()
5378 5388

  
......
5391 5401
    assert 'form_links_resubmitted_form_var_foo_string' not in resp
5392 5402

  
5393 5403

  
5394
def test_backoffice_create_formdata_map_fields_by_varname(create_formdata):
5404
def test_backoffice_create_formdata_map_fields_by_varname(pub, create_formdata):
5395 5405
    create_formdata['create_formdata'].map_fields_by_varname = True
5396 5406
    create_formdata['create_formdata'].mappings  = []
5397 5407
    create_formdata['wf'].store()
......
5441 5451
    assert target_data_class.count() == 0
5442 5452
    # resubmit it through backoffice submission
5443 5453
    resp = resp.form.submit(name='button_resubmit')
5444
    assert LoggedError.count() == 0
5454
    if pub.is_using_postgresql():
5455
        assert pub.loggederror_class.count() == 0
5445 5456
    assert target_data_class.count() == 1
5446 5457
    target_formdata = target_data_class.select()[0]
5447 5458

  
tests/backoffice_pages/test_carddata.py
10 10
from wcs.carddef import CardDef
11 11
from wcs.categories import CardDefCategory
12 12
from wcs.formdef import FormDef
13
from wcs.logged_errors import LoggedError
14 13
from wcs.qommon.http_request import HTTPRequest
15 14
from wcs.wf.wscall import WebserviceCallStatusItem
16 15
from wcs.workflows import ChoiceWorkflowStatusItem, Workflow
......
417 416

  
418 417

  
419 418
def test_backoffice_cards_wscall_failure_display(http_requests, pub, studio):
420
    LoggedError.wipe()
419
    if not pub.is_using_postgresql():
420
        pytest.skip('this requires SQL')
421
        return
422

  
423
    pub.loggederror_class.wipe()
421 424
    user = create_user(pub)
422 425

  
423 426
    Workflow.wipe()
......
473 476
    resp = resp.follow()
474 477
    assert 'Error during webservice call' in resp.text
475 478

  
476
    assert LoggedError.count() == 1
477
    assert LoggedError.select()[0].get_formdata().data == {'1': 'plop'}
479
    assert pub.loggederror_class.count() == 1
480
    assert pub.loggederror_class.select()[0].get_formdata().data == {'1': 'plop'}
478 481

  
479 482

  
480 483
def test_block_card_item_link(pub, studio, blocks_feature):
tests/form_pages/test_all.py
50 50
from wcs.data_sources import NamedDataSource
51 51
from wcs.wscalls import NamedWsCall
52 52
from wcs import fields
53
from wcs.logged_errors import LoggedError
54 53
from wcs.forms.root import PublicFormStatusPage
55 54

  
56 55
from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
......
2321 2320
    assert formdef.data_class().get(data_id).status == 'wf-%s' % st2.id
2322 2321

  
2323 2322
    # jump to a nonexistent status == do not jump, but add a LoggedError
2324
    LoggedError.wipe()
2325
    assert LoggedError.count() == 0
2323
    if pub.is_using_postgresql():
2324
        pub.loggederror_class.wipe()
2325
        assert pub.loggederror_class.count() == 0
2326 2326
    editable.status = 'deleted_status_id'
2327 2327
    workflow.store()
2328 2328
    # go back to st1
......
2338 2338
    resp = resp.forms[0].submit('submit')
2339 2339
    resp = resp.follow()
2340 2340
    assert formdef.data_class().get(data_id).status == 'wf-%s' % st1.id  # stay on st1
2341
    assert LoggedError.count() == 1
2342
    logged_error = LoggedError.select()[0]
2343
    assert logged_error.formdata_id == str(formdata.id)
2344
    assert logged_error.formdef_id == formdef.id
2345
    assert logged_error.workflow_id == workflow.id
2346
    assert logged_error.status_id == st1.id
2347
    assert logged_error.status_item_id == editable.id
2348
    assert logged_error.occurences_count == 1
2341
    if pub.is_using_postgresql():
2342
        assert pub.loggederror_class.count() == 1
2343
        logged_error = pub.loggederror_class.select()[0]
2344
        assert logged_error.formdata_id == str(formdata.id)
2345
        assert logged_error.formdef_id == formdef.id
2346
        assert logged_error.workflow_id == workflow.id
2347
        assert logged_error.status_id == st1.id
2348
        assert logged_error.status_item_id == editable.id
2349
        assert logged_error.occurences_count == 1
2349 2350

  
2350 2351
    # do it again: increment logged_error.occurences_count
2351 2352
    page = login(get_app(pub), username='foo', password='foo').get('/test/%s/' % data_id)
......
2356 2357
    resp = resp.forms[0].submit('submit')
2357 2358
    resp = resp.follow()
2358 2359
    assert formdef.data_class().get(data_id).status == 'wf-%s' % st1.id  # stay on st1
2359
    assert LoggedError.count() == 1
2360
    logged_error = LoggedError.select()[0]
2361
    assert logged_error.occurences_count == 2
2360
    if pub.is_using_postgresql():
2361
        assert pub.loggederror_class.count() == 1
2362
        logged_error = pub.loggederror_class.select()[0]
2363
        assert logged_error.occurences_count == 2
2362 2364

  
2363 2365

  
2364 2366
def test_form_edit_autocomplete_list(pub):
......
6714 6716

  
6715 6717

  
6716 6718
def test_logged_errors(pub):
6719
    if not pub.is_using_postgresql():
6720
        pytest.skip('this requires SQL')
6721
        return
6722

  
6717 6723
    Workflow.wipe()
6718 6724
    workflow = Workflow.get_default_workflow()
6719 6725
    workflow.id = '12'
......
6735 6741
    formdef.fields = []
6736 6742
    formdef.store()
6737 6743

  
6738
    LoggedError.wipe()
6744
    pub.loggederror_class.wipe()
6739 6745

  
6740 6746
    app = get_app(pub)
6741 6747
    resp = app.get('/test/')
6742 6748
    resp = resp.form.submit('submit').follow()
6743 6749
    resp = resp.form.submit('submit')
6744
    assert LoggedError.count() == 1
6750
    assert pub.loggederror_class.count() == 1
6745 6751

  
6746 6752
    resp = app.get('/test/')
6747 6753
    resp = resp.form.submit('submit').follow()
6748 6754
    resp = resp.form.submit('submit')
6749
    assert LoggedError.count() == 1
6755
    assert pub.loggederror_class.count() == 1
6750 6756

  
6751
    error = LoggedError.get_on_index(
6752
        '34-12-just_submitted-_jump-failed-to-evaluate-condition-ZeroDivisionError-integer-division-or-modulo-by-zero',
6753
        'tech_id')
6757
    error = list(pub.loggederror_class.get_with_indexed_value(
6758
        'tech_id',
6759
        '34-12-just_submitted-_jump-failed-to-evaluate-condition-ZeroDivisionError-integer-division-or-modulo-by-zero'))[0]
6754 6760
    assert error.occurences_count == 2
6755 6761

  
6756
    assert len(LoggedError.get_ids_with_indexed_value('formdef_id', '34')) == 1
6757
    assert len(LoggedError.get_ids_with_indexed_value('formdef_id', 'X')) == 0
6762
    assert len(list(pub.loggederror_class.get_with_indexed_value('formdef_id', '34'))) == 1
6763
    assert len(list(pub.loggederror_class.get_with_indexed_value('formdef_id', 'X'))) == 0
6758 6764

  
6759
    assert len(LoggedError.get_ids_with_indexed_value('workflow_id', '12')) == 1
6760
    assert len(LoggedError.get_ids_with_indexed_value('workflow_id', 'X')) == 0
6765
    assert len(list(pub.loggederror_class.get_with_indexed_value('workflow_id', '12'))) == 1
6766
    assert len(list(pub.loggederror_class.get_with_indexed_value('workflow_id', 'X'))) == 0
6761 6767

  
6762 6768

  
6763 6769
def test_formdata_named_wscall(http_requests, pub):
......
8805 8811
    assert pq('.linked .foo_string').text() == 'zob'
8806 8812

  
8807 8813

  
8808
def test_create_formdata_empty_item_ds_with_id_parameter(create_formdata):
8809
    LoggedError.wipe()
8814
def test_create_formdata_empty_item_ds_with_id_parameter(pub, create_formdata):
8815
    if pub.is_using_postgresql():
8816
        pub.loggederror_class.wipe()
8810 8817
    NamedDataSource.wipe()
8811 8818
    data_source = NamedDataSource(name='foobar')
8812 8819
    data_source.data_source = {
......
8834 8841
        resp = resp.form.submit('submit')  # -> submission
8835 8842
        resp = resp.follow()
8836 8843
        assert create_formdata['target_formdef'].data_class().count() == 0
8837
        assert LoggedError.count() == 0
8844
        if pub.is_using_postgresql():
8845
            assert pub.loggederror_class.count() == 0
8838 8846
        resp = resp.form.submit('button_resubmit')
8839
        assert LoggedError.count() == 0
8847
        if pub.is_using_postgresql():
8848
            assert pub.loggederror_class.count() == 0
8840 8849

  
8841 8850

  
8842 8851
def test_js_libraries(pub):
tests/test_mail_templates.py
8 8
from quixote import cleanup
9 9
from wcs.formdef import FormDef
10 10
from wcs.fields import FileField
11
from wcs.logged_errors import LoggedError
12 11
from wcs.mail_templates import MailTemplate
13 12
from wcs.workflows import Workflow, SendmailWorkflowStatusItem
14 13
from wcs.qommon.http_request import HTTPRequest
......
193 192
    assert workflow.possible_status[0].items[0].mail_template == 'test-mail-template'
194 193

  
195 194

  
196
def test_workflow_send_mail_template(pub, superuser, mail_templates_option, emails):
195
def test_workflow_send_mail_template_with_sql(superuser, mail_templates_option, emails):
196
    pub = create_temporary_pub(sql_mode=True)
197
    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
198
    pub.set_app_dir(req)
199
    pub._set_request(req)
200

  
197 201
    Workflow.wipe()
198 202
    MailTemplate.wipe()
199 203

  
......
231 235
    # check nothing is sent and an error is logged if the mail template is
232 236
    # missing
233 237
    emails.empty()
234
    LoggedError.wipe()
238
    pub.loggederror_class.wipe()
235 239
    MailTemplate.wipe()
236 240
    item.perform(formdata)
237 241
    pub.get_request().response.process_after_jobs()
238 242
    assert emails.count() == 0
239
    assert LoggedError.count() == 1
240
    logged_error = LoggedError.select()[0]
243
    assert pub.loggederror_class.count() == 1
244
    logged_error = pub.loggederror_class.select()[0]
241 245
    assert logged_error.summary == 'reference to invalid mail template test-mail-template in status Status1'
242 246

  
243 247

  
tests/test_workflows.py
32 32
        ItemsField, CommentField, EmailField, PageField, TitleField,
33 33
        SubtitleField, TextField, BoolField, TableField)
34 34
from wcs.formdata import Evolution
35
from wcs.logged_errors import LoggedError
36 35
from wcs.roles import Role
37 36
from wcs.workflows import (Workflow, WorkflowStatusItem,
38 37
        SendmailWorkflowStatusItem, SendSMSWorkflowStatusItem,
......
314 313
    assert item.must_jump(formdata) is False
315 314

  
316 315

  
317
def test_jump_bad_python_condition(pub):
316
def test_jump_bad_python_condition(two_pubs):
317
    if not two_pubs.is_using_postgresql():
318
        pytest.skip('this requires SQL')
319
        return
320

  
318 321
    FormDef.wipe()
319 322
    formdef = FormDef()
320 323
    formdef.name = 'foobar'
321 324
    formdef.store()
322
    pub.substitutions.feed(formdef)
325
    two_pubs.substitutions.feed(formdef)
323 326
    formdef.data_class().wipe()
324 327
    formdata = formdef.data_class()()
325 328
    item = JumpWorkflowStatusItem()
326 329

  
327
    LoggedError.wipe()
330
    two_pubs.loggederror_class.wipe()
328 331
    item.condition = {'type': 'python', 'value': 'form_var_foobar == 0'}
329 332
    assert item.must_jump(formdata) is False
330
    assert LoggedError.count() == 1
331
    logged_error = LoggedError.select()[0]
333
    assert two_pubs.loggederror_class.count() == 1
334
    logged_error = two_pubs.loggederror_class.select()[0]
332 335
    assert logged_error.summary == 'Failed to evaluate condition'
333 336
    assert logged_error.exception_class == 'NameError'
334 337
    assert logged_error.exception_message == "name 'form_var_foobar' is not defined"
335 338
    assert logged_error.expression == 'form_var_foobar == 0'
336 339
    assert logged_error.expression_type == 'python'
337 340

  
338
    LoggedError.wipe()
341
    two_pubs.loggederror_class.wipe()
339 342
    item.condition = {'type': 'python', 'value': '~ invalid ~'}
340 343
    assert item.must_jump(formdata) is False
341
    assert LoggedError.count() == 1
342
    logged_error = LoggedError.select()[0]
344
    assert two_pubs.loggederror_class.count() == 1
345
    logged_error = two_pubs.loggederror_class.select()[0]
343 346
    assert logged_error.summary == 'Failed to evaluate condition'
344 347
    assert logged_error.exception_class == 'SyntaxError'
345 348
    assert logged_error.exception_message == 'unexpected EOF while parsing (<string>, line 1)'
......
347 350
    assert logged_error.expression_type == 'python'
348 351

  
349 352

  
350
def test_jump_django_conditions(pub):
353
def test_jump_django_conditions(two_pubs):
351 354
    FormDef.wipe()
352 355
    formdef = FormDef()
353 356
    formdef.name = 'foobar'
......
355 358
    formdef.store()
356 359
    formdata = formdef.data_class()()
357 360
    formdata.data = {'1': 'hello'}
358
    pub.substitutions.feed(formdata)
361
    two_pubs.substitutions.feed(formdata)
359 362
    item = JumpWorkflowStatusItem()
360 363

  
361
    LoggedError.wipe()
364
    if two_pubs.is_using_postgresql():
365
        two_pubs.loggederror_class.wipe()
362 366
    item.condition = {'type': 'django', 'value': '1 < 2'}
363 367
    assert item.must_jump(formdata) is True
364 368

  
......
371 375
    item.condition = {'type': 'django', 'value': 'form_var_foo|first|upper == "X"'}
372 376
    assert item.must_jump(formdata) is False
373 377

  
374
    assert LoggedError.count() == 0
378
    if two_pubs.is_using_postgresql():
379
        assert two_pubs.loggederror_class.count() == 0
375 380

  
376 381
    item.condition = {'type': 'django', 'value': '~ invalid ~'}
377 382
    assert item.must_jump(formdata) is False
378
    assert LoggedError.count() == 1
379
    logged_error = LoggedError.select()[0]
380
    assert logged_error.summary == 'Failed to evaluate condition'
381
    assert logged_error.exception_class == 'TemplateSyntaxError'
382
    assert logged_error.exception_message == "Could not parse the remainder: '~' from '~'"
383
    assert logged_error.expression == '~ invalid ~'
384
    assert logged_error.expression_type == 'django'
383
    if two_pubs.is_using_postgresql():
384
        assert two_pubs.loggederror_class.count() == 1
385
        logged_error = two_pubs.loggederror_class.select()[0]
386
        assert logged_error.summary == 'Failed to evaluate condition'
387
        assert logged_error.exception_class == 'TemplateSyntaxError'
388
        assert logged_error.exception_message == "Could not parse the remainder: '~' from '~'"
389
        assert logged_error.expression == '~ invalid ~'
390
        assert logged_error.expression_type == 'django'
385 391

  
386 392

  
387 393
def test_check_auth(pub):
......
2268 2274

  
2269 2275

  
2270 2276
def test_jump_missing_previous_mark(two_pubs):
2277
    if not two_pubs.is_using_postgresql():
2278
        pytest.skip('this requires SQL')
2279
        return
2280

  
2271 2281
    workflow = Workflow(name='jump-mark')
2272 2282
    st1 = workflow.add_status('Status1', 'st1')
2273 2283

  
......
2292 2302
    formdata.store()
2293 2303

  
2294 2304
    time.sleep(0.3)
2295
    LoggedError.wipe()
2305
    two_pubs.loggederror_class.wipe()
2296 2306
    _apply_timeouts(two_pubs)
2297
    assert LoggedError.count() == 1
2307
    assert two_pubs.loggederror_class.count() == 1
2298 2308

  
2299 2309

  
2300 2310
def test_sms(pub, sms_mocking):
......
3720 3730
def test_set_backoffice_field(http_requests, two_pubs):
3721 3731
    Workflow.wipe()
3722 3732
    FormDef.wipe()
3723
    LoggedError.wipe()
3733
    if two_pubs.is_using_postgresql():
3734
        two_pubs.loggederror_class.wipe()
3724 3735
    wf = Workflow(name='xxx')
3725 3736
    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
3726 3737
    wf.backoffice_fields_formdef.fields = [
......
3819 3830
    formdata = formdef.data_class().get(formdata.id)
3820 3831
    assert formdata.data['bo1'] == ''
3821 3832

  
3822
    assert LoggedError.count() == 0
3833
    if two_pubs.is_using_postgresql():
3834
        assert two_pubs.loggederror_class.count() == 0
3823 3835

  
3824 3836
    item.fields = [{'field_id': 'bo1', 'value': '= ~ invalid python ~'}]
3825 3837
    item.perform(formdata)
3826 3838
    formdata = formdef.data_class().get(formdata.id)
3827
    assert LoggedError.count() == 1
3828
    logged_error = LoggedError.select()[0]
3829
    assert logged_error.summary == 'Failed to compute Python expression'
3830
    assert logged_error.formdata_id == str(formdata.id)
3831
    assert logged_error.expression == ' ~ invalid python ~'
3832
    assert logged_error.expression_type == 'python'
3833
    assert logged_error.exception_class == 'SyntaxError'
3834
    assert logged_error.exception_message == 'invalid syntax (<string>, line 1)'
3839
    if two_pubs.is_using_postgresql():
3840
        assert two_pubs.loggederror_class.count() == 1
3841
        logged_error = two_pubs.loggederror_class.select()[0]
3842
        assert logged_error.summary == 'Failed to compute Python expression'
3843
        assert logged_error.formdata_id == str(formdata.id)
3844
        assert logged_error.expression == ' ~ invalid python ~'
3845
        assert logged_error.expression_type == 'python'
3846
        assert logged_error.exception_class == 'SyntaxError'
3847
        assert logged_error.exception_message == 'invalid syntax (<string>, line 1)'
3835 3848

  
3836
    LoggedError.wipe()
3849
    if two_pubs.is_using_postgresql():
3850
        two_pubs.loggederror_class.wipe()
3837 3851
    item.fields = [{'field_id': 'bo1', 'value': '{% if bad django %}'}]
3838 3852
    item.perform(formdata)
3839 3853
    formdata = formdef.data_class().get(formdata.id)
3840
    assert LoggedError.count() == 1
3841
    logged_error = LoggedError.select()[0]
3842
    assert logged_error.summary == 'Failed to compute template'
3843
    assert logged_error.formdata_id == str(formdata.id)
3844
    assert logged_error.expression == '{% if bad django %}'
3845
    assert logged_error.expression_type == 'template'
3846
    assert logged_error.exception_class == 'TemplateError'
3847
    assert logged_error.exception_message.startswith('syntax error in Django template')
3854
    if two_pubs.is_using_postgresql():
3855
        assert two_pubs.loggederror_class.count() == 1
3856
        logged_error = two_pubs.loggederror_class.select()[0]
3857
        assert logged_error.summary == 'Failed to compute template'
3858
        assert logged_error.formdata_id == str(formdata.id)
3859
        assert logged_error.expression == '{% if bad django %}'
3860
        assert logged_error.expression_type == 'template'
3861
        assert logged_error.exception_class == 'TemplateError'
3862
        assert logged_error.exception_message.startswith('syntax error in Django template')
3848 3863

  
3849 3864

  
3850 3865
def test_set_backoffice_field_file(http_requests, two_pubs):
......
4011 4026
        item.parent = st1
4012 4027
        item.fields = [{'field_id': 'bo1', 'value': value}]
4013 4028

  
4014
        LoggedError.wipe()
4029
        if two_pubs.is_using_postgresql():
4030
            two_pubs.loggederror_class.wipe()
4015 4031
        item.perform(formdata)
4016 4032

  
4017 4033
        formdata = formdef.data_class().get(formdata.id)
4018 4034
        assert formdata.data['bo1'].base_filename == 'hello.txt'
4019 4035
        assert formdata.data['bo1'].get_content() == b'HELLO WORLD'
4020
        assert LoggedError.count() == 1
4021
        logged_error = LoggedError.select()[0]
4022
        assert logged_error.summary.startswith('Failed to convert')
4023
        assert logged_error.formdata_id == str(formdata.id)
4024
        assert logged_error.exception_class == 'ValueError'
4036
        if two_pubs.is_using_postgresql():
4037
            assert two_pubs.loggederror_class.count() == 1
4038
            logged_error = two_pubs.loggederror_class.select()[0]
4039
            assert logged_error.summary.startswith('Failed to convert')
4040
            assert logged_error.formdata_id == str(formdata.id)
4041
            assert logged_error.exception_class == 'ValueError'
4025 4042

  
4026 4043
    # check wrong field
4027 4044
    item = SetBackofficeFieldsWorkflowStatusItem()
......
4318 4335
def test_set_backoffice_field_date(two_pubs):
4319 4336
    Workflow.wipe()
4320 4337
    FormDef.wipe()
4321
    LoggedError.wipe()
4338
    if two_pubs.is_using_postgresql():
4339
        two_pubs.loggederror_class.wipe()
4322 4340
    wf = Workflow(name='xxx')
4323 4341
    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
4324 4342
    st1 = wf.add_status('Status1')
......
4366 4384
    assert datetime.date(*formdata.data['bo1'][:3]) == datetime.date(2017, 3, 23)
4367 4385

  
4368 4386
    # invalid values => do nothing
4369
    assert LoggedError.count() == 0
4387
    if two_pubs.is_using_postgresql():
4388
        assert two_pubs.loggederror_class.count() == 0
4370 4389
    for value in ('plop', '={}', '=[]'):
4371 4390
        item = SetBackofficeFieldsWorkflowStatusItem()
4372 4391
        item.parent = st1
4373 4392
        item.fields = [{'field_id': 'bo1', 'value': value}]
4374 4393

  
4375
        LoggedError.wipe()
4394
        if two_pubs.is_using_postgresql():
4395
            two_pubs.loggederror_class.wipe()
4376 4396
        item.perform(formdata)
4377 4397
        formdata = formdef.data_class().get(formdata.id)
4378 4398
        assert datetime.date(*formdata.data['bo1'][:3]) == datetime.date(2017, 3, 23)
4379
        assert LoggedError.count() == 1
4380
        assert LoggedError.select()[0].summary.startswith('Failed to convert')
4399
        if two_pubs.is_using_postgresql():
4400
            assert two_pubs.loggederror_class.count() == 1
4401
            assert two_pubs.loggederror_class.select()[0].summary.startswith('Failed to convert')
4381 4402

  
4382 4403
    # None : empty date
4383 4404
    item = SetBackofficeFieldsWorkflowStatusItem()
......
4392 4413
def test_set_backoffice_field_boolean(two_pubs):
4393 4414
    Workflow.wipe()
4394 4415
    FormDef.wipe()
4395
    LoggedError.wipe()
4396 4416
    wf = Workflow(name='xxx')
4397 4417
    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
4398 4418
    st1 = wf.add_status('Status1')
......
4547 4567

  
4548 4568

  
4549 4569
def test_workflow_action_condition(two_pubs):
4550
    pub = two_pubs
4551
    pub._set_request(None)  # to avoid after jobs
4570
    two_pubs._set_request(None)  # to avoid after jobs
4552 4571
    workflow = Workflow(name='jump condition migration')
4553 4572
    st1 = workflow.add_status('Status1', 'st1')
4554 4573
    workflow.store()
......
4556 4575
    role = Role(name='bar1')
4557 4576
    role.store()
4558 4577

  
4559
    user = pub.user_class()
4578
    user = two_pubs.user_class()
4560 4579
    user.roles = [role.id]
4561 4580
    user.store()
4562 4581

  
......
4591 4610
    choice.condition = {'type': 'python', 'value': 'form_var_foo == "foo"'}
4592 4611
    workflow.store()
4593 4612

  
4594
    with pub.substitutions.temporary_feed(formdata1):
4613
    with two_pubs.substitutions.temporary_feed(formdata1):
4595 4614
        assert FormDef.get(formdef.id).data_class().get(formdata1.id).get_actions_roles() == set([role.id])
4596
    with pub.substitutions.temporary_feed(formdata2):
4615
    with two_pubs.substitutions.temporary_feed(formdata2):
4597 4616
        assert FormDef.get(formdef.id).data_class().get(formdata2.id).get_actions_roles() == set()
4598 4617

  
4599 4618
    assert len(FormDef.get(formdef.id).data_class().get_actionable_ids([role.id])) == 1
......
4608 4627
    assert len(FormDef.get(formdef.id).data_class().get_actionable_ids([role.id])) == 2
4609 4628

  
4610 4629
    # bad condition
4611
    LoggedError.wipe()
4630
    if two_pubs.is_using_postgresql():
4631
        two_pubs.loggederror_class.wipe()
4612 4632
    choice.condition = {'type': 'python', 'value': 'foobar == barfoo'}
4613 4633
    workflow.store()
4614 4634
    assert len(FormDef.get(formdef.id).data_class().get_actionable_ids([role.id])) == 0
4615
    assert LoggedError.count() == 1
4616
    logged_error = LoggedError.select()[0]
4617
    assert logged_error.occurences_count > 1  # should be 2... == 12 with pickle, 4 with sql
4618
    assert logged_error.summary == 'Failed to evaluate condition'
4619
    assert logged_error.exception_class == 'NameError'
4620
    assert logged_error.exception_message == "name 'foobar' is not defined"
4621
    assert logged_error.expression == 'foobar == barfoo'
4622
    assert logged_error.expression_type == 'python'
4635
    if two_pubs.is_using_postgresql():
4636
        assert two_pubs.loggederror_class.count() == 1
4637
        logged_error = two_pubs.loggederror_class.select()[0]
4638
        assert logged_error.occurences_count > 1  # should be 2... == 12 with pickle, 4 with sql
4639
        assert logged_error.summary == 'Failed to evaluate condition'
4640
        assert logged_error.exception_class == 'NameError'
4641
        assert logged_error.exception_message == "name 'foobar' is not defined"
4642
        assert logged_error.expression == 'foobar == barfoo'
4643
        assert logged_error.expression_type == 'python'
4623 4644

  
4624 4645

  
4625 4646
def test_notifications(pub, http_requests):
......
4749 4770
    formdef.fields = []
4750 4771
    formdef.workflow_id = workflow.id
4751 4772
    formdef.store()
4773
    formdef.data_class().wipe()
4752 4774

  
4753 4775
    for i in range(5):
4754 4776
        formdata = formdef.data_class()()
......
4786 4808
    assert 'http://example.net/foobar/%s/status (New)' % formdata.id in emails.emails['New arrivals']['payload']
4787 4809

  
4788 4810

  
4789
def test_create_formdata(pub):
4811
def test_create_formdata(two_pubs):
4790 4812
    FormDef.wipe()
4791
    LoggedError.wipe()
4792
    pub.tracking_code_class.wipe()
4813
    if two_pubs.is_using_postgresql():
4814
        two_pubs.loggederror_class.wipe()
4815
    two_pubs.tracking_code_class.wipe()
4793 4816

  
4794 4817
    target_formdef = FormDef()
4795 4818
    target_formdef.name = 'target form'
......
4824 4847
    formdata.just_created()
4825 4848

  
4826 4849
    assert target_formdef.data_class().count() == 0
4827
    assert LoggedError.count() == 0
4850
    if two_pubs.is_using_postgresql():
4851
        assert two_pubs.loggederror_class.count() == 0
4828 4852
    # check unconfigure action do nothing
4829 4853
    formdata.perform_workflow()
4830 4854
    assert target_formdef.data_class().count() == 0
......
4835 4859
    formdata.perform_workflow()
4836 4860
    assert target_formdef.data_class().count() == 1
4837 4861

  
4838
    errors = LoggedError.select()
4839
    assert len(errors) == 2
4840
    assert any('form_var_toto_string' in (error.exception_message or '') for error in errors)
4841
    assert any('Missing field' in error.summary for error in errors)
4862
    if two_pubs.is_using_postgresql():
4863
        errors = two_pubs.loggederror_class.select()
4864
        assert len(errors) == 2
4865
        assert any('form_var_toto_string' in (error.exception_message or '') for error in errors)
4866
        assert any('Missing field' in error.summary for error in errors)
4842 4867

  
4843 4868
    # no tracking code has been created
4844 4869
    created_formdata = target_formdef.data_class().select()[0]
4845 4870
    assert created_formdata.tracking_code is None
4846
    assert pub.tracking_code_class.count() == 0
4871
    assert two_pubs.tracking_code_class.count() == 0
4847 4872
    # now we want one
4848 4873
    target_formdef.enable_tracking_codes = True
4849 4874
    target_formdef.store()
......
4853 4878
    assert target_formdef.data_class().count() == 1
4854 4879
    created_formdata = target_formdef.data_class().select()[0]
4855 4880
    assert created_formdata.tracking_code is not None
4856
    assert pub.tracking_code_class.count() == 1
4857
    assert pub.tracking_code_class.select()[0].formdef_id == target_formdef.id
4858
    assert pub.tracking_code_class.select()[0].formdata_id == created_formdata.id
4881
    assert two_pubs.tracking_code_class.count() == 1
4882
    assert two_pubs.tracking_code_class.select()[0].formdef_id == target_formdef.id
4883
    assert two_pubs.tracking_code_class.select()[0].formdata_id == str(created_formdata.id)
4859 4884

  
4860 4885
    create.condition = {'type': 'python', 'value': '1 == 2'}
4861 4886
    wf.store()
......
4866 4891
    assert target_formdef.data_class().count() == 0
4867 4892

  
4868 4893

  
4869
def test_create_carddata(pub):
4894
def test_create_carddata(two_pubs):
4870 4895
    CardDef.wipe()
4871 4896
    FormDef.wipe()
4872
    LoggedError.wipe()
4897
    if two_pubs.is_using_postgresql():
4898
        two_pubs.loggederror_class.wipe()
4873 4899

  
4874 4900
    carddef = CardDef()
4875 4901
    carddef.name = 'My card'
......
4921 4947

  
4922 4948
    assert carddef.data_class().count() == 1
4923 4949

  
4924
    errors = LoggedError.select()
4925
    assert len(errors) == 2
4926
    assert any('form_var_undefined' in (error.exception_message or '') for error in errors)
4927
    assert any('invalid date value' in (error.exception_message or '') for error in errors)
4950
    if two_pubs.is_using_postgresql():
4951
        errors = two_pubs.loggederror_class.select()
4952
        assert len(errors) == 2
4953
        assert any('form_var_undefined' in (error.exception_message or '') for error in errors)
4954
        assert any('invalid date value' in (error.exception_message or '') for error in errors)
4928 4955

  
4929 4956
    formdata = formdef.data_class()()
4930 4957
    today = datetime.date.today()
4931 4958

  
4932 4959
    formdata.data = {'1': 'item1',
4933 4960
                     '1_display': 'item1',
4934
                     '2': today}
4961
                     '2': today.timetuple()}
4935 4962
    formdata.just_created()
4936 4963
    formdata.perform_workflow()
4937 4964

  
......
4949 4976
    assert carddef.data_class().count() == 0
4950 4977

  
4951 4978

  
4952
def test_call_external_workflow_with_evolution_linked_object(pub):
4979
def test_call_external_workflow_with_evolution_linked_object(two_pubs):
4953 4980
    FormDef.wipe()
4954 4981
    CardDef.wipe()
4955
    LoggedError.wipe()
4982
    if two_pubs.is_using_postgresql():
4983
        two_pubs.loggederror_class.wipe()
4956 4984

  
4957 4985
    external_wf = Workflow(name='External Workflow')
4958 4986
    st1 = external_wf.add_status(name='New')
......
5031 5059

  
5032 5060
    # remove external formdata
5033 5061
    perform_items([action], formdata)
5034
    assert LoggedError.count() == 0
5062
    if two_pubs.is_using_postgresql():
5063
        assert two_pubs.loggederror_class.count() == 0
5035 5064
    assert external_formdef.data_class().count() == 0
5036 5065
    assert external_carddef.data_class().count() == 1
5037 5066

  
5038 5067
    # formdata is already deleted: cannot find it again
5039 5068
    perform_items([action], formdata)
5040
    assert LoggedError.count() == 1
5041
    logged_error = LoggedError.select()[0]
5042
    assert logged_error.summary == 'Could not find linked "External Form" object by id %s' % external_formdata.id
5043
    assert logged_error.exception_class == 'KeyError'
5044
    assert logged_error.status_item_id == action.id
5069
    if two_pubs.is_using_postgresql():
5070
        assert two_pubs.loggederror_class.count() == 1
5071
        logged_error = two_pubs.loggederror_class.select()[0]
5072
        assert logged_error.summary == 'Could not find linked "External Form" object by id %s' % external_formdata.id
5073
        assert logged_error.exception_class == 'KeyError'
5074
        assert logged_error.status_item_id == action.id
5045 5075

  
5046 5076
    # try remove an unexisting carddef: do nothing
5047 5077
    unused_carddef = CardDef()
......
5049 5079
    unused_carddef.fields = []
5050 5080
    unused_carddef.workflow = external_wf
5051 5081
    unused_carddef.store()
5052
    LoggedError.wipe()
5082
    if two_pubs.is_using_postgresql():
5083
        two_pubs.loggederror_class.wipe()
5053 5084
    action.slug = 'carddef:%s' % unused_carddef.url_name
5054 5085
    wf.store()
5055 5086
    perform_items([action], formdata)
5056
    assert LoggedError.count() == 0
5087
    if two_pubs.is_using_postgresql():
5088
        assert two_pubs.loggederror_class.count() == 0
5057 5089
    assert external_formdef.data_class().count() == 0
5058 5090
    assert external_carddef.data_class().count() == 1
5059 5091
    # remove the right carddef
5060 5092
    action.slug = 'carddef:%s' % external_carddef.url_name
5061 5093
    wf.store()
5062 5094
    perform_items([action], formdata)
5063
    assert LoggedError.count() == 0
5095
    if two_pubs.is_using_postgresql():
5096
        assert two_pubs.loggederror_class.count() == 0
5064 5097
    assert external_formdef.data_class().count() == 0
5065 5098
    assert external_carddef.data_class().count() == 0
5066 5099

  
5067 5100

  
5068
def test_call_external_workflow_with_data_sourced_object(pub):
5101
def test_call_external_workflow_with_data_sourced_object(two_pubs):
5069 5102
    FormDef.wipe()
5070 5103
    CardDef.wipe()
5071
    LoggedError.wipe()
5104
    if two_pubs.is_using_postgresql():
5105
        two_pubs.loggederror_class.wipe()
5072 5106

  
5073 5107
    carddef_wf = Workflow(name='Carddef Workflow')
5074 5108
    carddef_wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(carddef_wf)
......
5129 5163
    formdef.workflow = wf
5130 5164
    formdef.store()
5131 5165

  
5132
    assert LoggedError.count() == 0
5166
    if two_pubs.is_using_postgresql():
5167
        assert two_pubs.loggederror_class.count() == 0
5133 5168
    assert carddef.data_class().count() == 1
5134 5169

  
5135 5170
    formdata = formdef.data_class()()
......
5139 5174
    formdata.perform_workflow()
5140 5175

  
5141 5176
    perform_items([update_action], formdata)
5142
    assert LoggedError.count() == 0
5177
    if two_pubs.is_using_postgresql():
5178
        assert two_pubs.loggederror_class.count() == 0
5143 5179
    assert carddef.data_class().count() == 1
5144 5180
    data = carddef.data_class().select()[0]
5145 5181
    assert data.data['bo0'] == '1'
......
5149 5185
    assert data.data['bo0'] == '2'
5150 5186

  
5151 5187
    perform_items([delete_action], formdata)
5152
    assert LoggedError.count() == 0
5188
    if two_pubs.is_using_postgresql():
5189
        assert two_pubs.loggederror_class.count() == 0
5153 5190
    assert carddef.data_class().count() == 0
5154 5191

  
5155 5192
    perform_items([delete_action], formdata)
5156
    assert LoggedError.count() == 1
5157
    logged_error = LoggedError.select()[0]
5158
    assert logged_error.summary == 'Could not find linked "Data" object by id %s' % carddata.id
5159
    assert logged_error.exception_class == 'KeyError'
5193
    if two_pubs.is_using_postgresql():
5194
        assert two_pubs.loggederror_class.count() == 1
5195
        logged_error = two_pubs.loggederror_class.select()[0]
5196
        assert logged_error.summary == 'Could not find linked "Data" object by id %s' % carddata.id
5197
        assert logged_error.exception_class == 'KeyError'
5160 5198

  
5161 5199

  
5162 5200
def test_call_external_workflow_with_parent_object(pub):
5163 5201
    FormDef.wipe()
5164 5202
    CardDef.wipe()
5165
    LoggedError.wipe()
5166 5203

  
5167 5204
    # carddef workflow, with global action to increment a counter in its
5168 5205
    # backoffice fields.
......
5249 5286
def test_call_external_workflow_use_caller_variable(pub):
5250 5287
    FormDef.wipe()
5251 5288
    CardDef.wipe()
5252
    LoggedError.wipe()
5253 5289

  
5254 5290
    # carddef workflow, with global action to set a value in a backoffice field
5255 5291
    carddef_wf = Workflow(name='Carddef Workflow')
......
5324 5360
def test_edit_carddata_with_data_sourced_object(pub):
5325 5361
    FormDef.wipe()
5326 5362
    CardDef.wipe()
5327
    LoggedError.wipe()
5328 5363

  
5329 5364
    datasource = {
5330 5365
        'type': 'formula',
......
5437 5472
def test_edit_carddata_with_linked_object(pub):
5438 5473
    FormDef.wipe()
5439 5474
    CardDef.wipe()
5440
    LoggedError.wipe()
5441 5475

  
5442 5476
    carddef = CardDef()
5443 5477
    carddef.name = 'Parent'
tests/test_wscall.py
3 3

  
4 4
from wcs import fields
5 5
from wcs.formdef import FormDef
6
from wcs.logged_errors import LoggedError
7 6
from wcs.qommon.http_request import HTTPRequest
8 7
from wcs.qommon.template import Template
9 8
from wcs.wscalls import NamedWsCall
......
27 26

  
28 27
def test_named_wscall(pub):
29 28
    # create object
29
    NamedWsCall.wipe()
30 30
    wscall = NamedWsCall()
31 31
    wscall.name = 'Hello'
32 32
    wscall.request = {'url': 'http://example.net', 'qs_data': {'a': 'b'}}
......
172 172

  
173 173
@pytest.mark.parametrize('notify_on_errors', [True, False])
174 174
@pytest.mark.parametrize('record_on_errors', [True, False])
175
def test_webservice_on_error(http_requests, pub, emails, notify_on_errors, record_on_errors):
175
def test_webservice_on_error_with_sql(http_requests, emails, notify_on_errors, record_on_errors):
176
    pub = create_temporary_pub(sql_mode=True)
176 177
    pub.cfg['debug'] = {'error_email': 'errors@localhost.invalid'}
177 178
    pub.write_cfg()
178 179

  
179 180
    NamedWsCall.wipe()
180
    LoggedError.wipe()
181
    pub.loggederror_class.wipe()
181 182
    FormDef.wipe()
182 183

  
183 184
    wscall = NamedWsCall()
......
200 201
        resp = get_app(pub).get('/foobar/')
201 202
        assert 'Foo Bar ' in resp.text
202 203
        assert emails.count() == 0
203
        assert LoggedError.count() == 0
204
        assert pub.loggederror_class.count() == 0
204 205

  
205 206
    for url_part in ['400', '400-json', '404', '404-json', '500', 'json-err1', 'json-errheader1']:
206 207
        status_code = 200
......
217 218
        else:
218 219
            assert emails.count() == 0
219 220
        if record_on_errors:
220
            assert LoggedError.count() == 1
221
            LoggedError.wipe()
221
            assert pub.loggederror_class.count() == 1
222
            pub.loggederror_class.wipe()
222 223
        else:
223
            assert LoggedError.count() == 0
224
            assert pub.loggederror_class.count() == 0
tests/utilities.py
4 4
import tempfile
5 5
import random
6 6
import psycopg2
7
import pytest
8 7
import shutil
9 8
import sys
10
import threading
11 9

  
12 10
from wcs import sql, sessions, custom_views
13 11

  
......
20 18
from wcs.qommon import force_str
21 19
import wcs
22 20
import wcs.wsgi
23
from wcs import publisher, compat
24
from wcs.qommon.http_request import HTTPRequest
21
from wcs import compat
25 22
from wcs.users import User
26 23
from wcs.tracking_code import TrackingCode
27 24
import wcs.qommon.emails
......
81 78
        pub.session_class = sql.Session
82 79
        pub.custom_view_class = sql.CustomView
83 80
        pub.snapshot_class = sql.Snapshot
81
        pub.loggederror_class = sql.LoggedError
84 82
        pub.is_using_postgresql = lambda: True
85 83
    else:
86 84
        pub.user_class = User
87 85
        pub.tracking_code_class = TrackingCode
88 86
        pub.session_class = sessions.BasicSession
89 87
        pub.custom_view_class = custom_views.CustomView
90
        pub.snapshot_class = None
91 88
        pub.is_using_postgresql = lambda: False
92 89

  
93 90
    pub.session_manager_class = sessions.StorageSessionManager
......
172 169
        sql.do_session_table()
173 170
        sql.do_custom_views_table()
174 171
        sql.do_snapshots_table()
172
        sql.do_loggederrors_table()
175 173
        sql.do_meta_table()
176 174

  
177 175
        conn.close()
wcs/admin/logged_errors.py
24 24
from wcs.qommon import errors, get_cfg
25 25
from wcs.qommon.misc import localstrftime
26 26

  
27
from wcs.logged_errors import LoggedError
28

  
29 27

  
30 28
class LoggedErrorDirectory(Directory):
31 29
    _q_exports = ['', 'delete', 'ack']
......
107 105
        return redirect('.')
108 106

  
109 107
    def delete(self):
110
        self.error.remove_self()
108
        get_publisher().loggederror_class.remove_object(self.error.id)
111 109
        return redirect('..')
112 110

  
113 111

  
......
117 115
    @classmethod
118 116
    def get_errors(cls, formdef_id=None, workflow_id=None):
119 117
        errors = []
118
        if not get_publisher().loggederror_class:
119
            return errors
120 120
        if formdef_id:
121
            errors = LoggedError.get_with_indexed_value('formdef_id', formdef_id)
121
            errors = get_publisher().loggederror_class.get_with_indexed_value('formdef_id', formdef_id)
122 122
        elif workflow_id:
123
            errors = LoggedError.get_with_indexed_value('workflow_id', workflow_id)
123
            errors = get_publisher().loggederror_class.get_with_indexed_value('workflow_id', workflow_id)
124 124
        return list(errors)
125 125

  
126 126
    @classmethod
......
167 167

  
168 168
    def _q_lookup(self, component):
169 169
        try:
170
            error = LoggedError.get(component)
170
            error = get_publisher().loggederror_class.get(component)
171 171
        except KeyError:
172 172
            raise errors.TraversalError()
173 173
        get_response().breadcrumb.append(('logged-errors/', _('Logged Errors')))
wcs/conditions.py
56 56
            if self.log_errors:
57 57
                get_logger().warning('failed to evaluate %r (%r)', self, e)
58 58
            if self.record_errors:
59
                from wcs.logged_errors import LoggedError
60 59
                summary = _('Failed to evaluate condition')
61
                LoggedError.record(summary,
62
                                   formdata=self.context.get('formdata'),
63
                                   status_item=self.context.get('status_item'),
64
                                   expression=self.value, expression_type=self.type,
65
                                   exception=e)
60
                get_publisher().record_loggederror(
61
                    summary,
62
                    formdata=self.context.get('formdata'),
63
                    status_item=self.context.get('status_item'),
64
                    expression=self.value, expression_type=self.type,
65
                    exception=e)
66 66
            raise RuntimeError()
67 67

  
68 68
    def evaluate_python(self, local_variables):
wcs/data_sources.py
512 512
        url += param_name + '=' + urllib.quote(param_value)
513 513

  
514 514
        def find_item(items, name, value):
515
            from wcs.logged_errors import LoggedError
516 515
            for item in items:
517 516
                if str(item.get(name)) == str(value):
518 517
                    return item
519 518
            # not found
520
            LoggedError.record(_('Could not find element by id "%s"') % value)
519
            get_publisher().record_loggederror(_('Could not find element by id "%s"') % value)
521 520
            return None
522 521

  
523 522
        request = get_request()
wcs/fields.py
387 387
            except TemplateError:
388 388
                return ('', explicit_lock)
389 389
            except AttributeError as e:
390
                from wcs.logged_errors import LoggedError
391
                LoggedError.record(_('Failed to evaluate prefill on field "%s"') % self.label, formdef=getattr(self, 'formdef', None), exception=e)
390
                get_publisher().record_loggederror(
391
                    _('Failed to evaluate prefill on field "%s"') % self.label,
392
                    formdef=getattr(self, 'formdef', None),
393
                    exception=e)
392 394
                return ('', explicit_lock)
393 395

  
394 396
        elif t == 'user' and user:
wcs/formdata.py
601 601
        if status_id == '_previous':
602 602
            previous_status = self.pop_previous_marked_status()
603 603
            if not previous_status:
604
                from wcs.logged_errors import LoggedError
605 604
                summary = _('Failed to compute previous status')
606
                LoggedError.record(summary, formdata=self)
605
                get_publisher().record_loggederror(summary, formdata=self)
607 606
                return
608 607
            status_id = previous_status.id
609 608
        status = 'wf-%s' % status_id
......
1211 1210
    def iter_target_datas(self, objectdef=None, object_type=None, status_item=None):
1212 1211
        # objectdef, object_type and status_item are provided when called from a workflow action
1213 1212
        from wcs.wf.create_formdata import LinkedFormdataEvolutionPart
1214
        from wcs.logged_errors import LoggedError
1215 1213
        from .carddef import CardDef
1216 1214
        from .formdef import FormDef
1217 1215

  
......
1265 1263
                    yield objectdef.data_class().get(target_id)
1266 1264
                except KeyError as e:
1267 1265
                    # use custom error message depending on target type
1268
                    LoggedError.record(_('Could not find linked "%(object_name)s" object by id %(object_id)s') % {
1269
                        'object_name': objectdef.name, 'object_id': target_id},
1266
                    get_publisher().record_loggederror(
1267
                        _('Could not find linked "%(object_name)s" object by id %(object_id)s') % {
1268
                            'object_name': objectdef.name, 'object_id': target_id},
1270 1269
                        formdata=self, status_item=status_item, exception=e)
1271 1270
            else:
1272 1271
                # inspect page
wcs/logged_errors.py
17 17
import datetime
18 18

  
19 19
from .qommon.misc import simplify
20
from .qommon.xml_storage import XmlStorableObject
21 20
from wcs.carddef import CardDef
22 21
from wcs.formdef import FormDef
23 22
from wcs.workflows import Workflow
24 23

  
25 24

  
26
class LoggedError(XmlStorableObject):
25
class LoggedError(object):
27 26
    _names = 'logged-errors'
28
    xml_root_node = 'error'
29
    _indexes = ['tech_id']
30
    _hashed_indexes = ['formdef_id', 'workflow_id']
31 27

  
28
    id = None
29
    tech_id = None
32 30
    summary = None
33 31
    formdef_class = 'FormDef'
34 32
    formdata_id = None
......
46 44
    latest_occurence_timestamp = None
47 45
    acked = False
48 46

  
49
    # declarations for serialization
50
    XML_NODES = [
51
            ('summary', 'str'), ('traceback', 'str'),
52
            ('exception_class', 'str'), ('exception_message', 'str'),
53
            ('expression', 'str'), ('expression_type', 'str'),
54
            ('formdata_id', 'str'), ('formdef_id', 'str'), ('workflow_id', 'str'),
55
            ('formdef_class', 'str'),
56
            ('status_id', 'str'), ('status_item_id', 'str'),
57
            ('occurences_count', 'int'),
58
            ('first_occurence_timestamp', 'datetime'),
59
            ('latest_occurence_timestamp', 'datetime'),
60
            ('acked', 'bool')]
61

  
62 47
    @classmethod
63 48
    def record(cls, error_summary, plain_error_msg=None, formdata=None,
64 49
               formdef=None, workflow=None, status=None, status_item=None,
......
97 82
            error.status_id = status.id
98 83

  
99 84
        error.first_occurence_timestamp = datetime.datetime.now()
100
        error.id = '%s-%s' % (
101
                error.first_occurence_timestamp.strftime('%Y%m%d-%H%M%S'),
102
                error.tech_id,
103
                )
104
        existing_error = cls.get_on_index(error.tech_id, 'tech_id', ignore_errors=True)
105
        if existing_error:
106
            error = existing_error
85
        error.tech_id = error.build_tech_id()
86
        existing_errors = list(cls.get_with_indexed_value('tech_id', error.tech_id))
87
        if existing_errors:
88
            error = existing_errors[0]
107 89
        error.occurences_count += 1
108 90
        error.latest_occurence_timestamp = datetime.datetime.now()
109 91
        error.store()
......
113 95
    def record_exception(cls, error_summary, plain_error_msg, publisher):
114 96
        try:
115 97
            context = publisher.substitutions.get_context_variables()
116
        except Exception as e:
98
        except Exception:
117 99
            return
118 100
        formdata_id = context.get('form_number_raw')
119 101
        formdef_urlname = context.get('form_slug')
......
126 108
            klass = CardDef
127 109
        formdef = klass.get_by_urlname(formdef_urlname)
128 110
        formdata = formdef.data_class().get(formdata_id, ignore_errors=True)
129
        return cls.record(error_summary, plain_error_msg, formdata=formdata,
130
                   formdef=formdef, workflow=formdef.workflow)
111
        return cls.record(
112
            error_summary, plain_error_msg, formdata=formdata,
113
            formdef=formdef, workflow=formdef.workflow)
131 114

  
132
    @property
133
    def tech_id(self):
115
    def build_tech_id(self):
134 116
        tech_id = '%s-%s-' % (self.formdef_id, self.workflow_id)
135 117
        if self.status_id:
136 118
            tech_id += '%s-' % self.status_id
wcs/publisher.py
52 52

  
53 53
from .users import User
54 54
from .tracking_code import TrackingCode
55
from .logged_errors import LoggedError
56 55

  
57 56
import pickle
58 57

  
......
153 152
            self.session_class = sql.Session
154 153
            self.custom_view_class = sql.CustomView
155 154
            self.snapshot_class = sql.Snapshot
155
            self.loggederror_class = sql.LoggedError
156 156
            sql.get_connection(new=True)
157 157
        else:
158 158
            self.user_class = User
......
160 160
            self.session_class = sessions.BasicSession
161 161
            self.custom_view_class = custom_views.CustomView
162 162
            self.snapshot_class = None
163
            self.loggederror_class = None
163 164

  
164 165
        self.session_manager_class = sessions.StorageSessionManager
165 166
        self.set_session_manager(self.session_manager_class(session_class=self.session_class))
......
312 313
        sql.do_tracking_code_table()
313 314
        sql.do_custom_views_table()
314 315
        sql.do_snapshots_table()
316
        sql.do_loggederrors_table()
315 317
        sql.do_meta_table()
316 318
        from .formdef import FormDef
317 319
        from .carddef import CardDef
......
340 342

  
341 343
    def log_internal_error(self, error_summary, plain_error_msg, record=False, notify=True):
342 344
        tech_id = None
343
        if record:
344
            logged_exception = LoggedError.record_exception(
345
        if record and self.loggederror_class:
346
            logged_exception = self.loggederror_class.record_exception(
345 347
                    error_summary, plain_error_msg, publisher=self)
346 348
            if logged_exception:
347 349
                tech_id = logged_exception.tech_id
......
357 359
            # this could happen on file descriptor exhaustion
358 360
            pass
359 361

  
362
    def record_loggederror(self, *args, **kwargs):
363
        if self.loggederror_class:
364
            self.loggederror_class.record(*args, **kwargs)
365

  
360 366
    def apply_global_action_timeouts(self):
361 367
        from wcs.workflows import Workflow, WorkflowGlobalActionTimeoutTrigger
362 368
        for workflow in Workflow.select():
wcs/sql.py
45 45
import wcs.carddata
46 46
import wcs.custom_views
47 47
import wcs.formdata
48
import wcs.logged_errors
48 49
import wcs.snapshots
49 50
import wcs.tracking_code
50 51
import wcs.users
......
859 860
    cur.close()
860 861

  
861 862

  
863
def do_loggederrors_table(concurrently=False):
864
    conn, cur = get_connection_and_cursor()
865
    table_name = 'loggederrors'
866

  
867
    cur.execute('''SELECT COUNT(*) FROM information_schema.tables
868
                    WHERE table_schema = 'public'
869
                      AND table_name = %s''', (table_name,))
870
    if cur.fetchone()[0] == 0:
871
        cur.execute('''CREATE TABLE %s (id SERIAL PRIMARY KEY,
872
                                        tech_id VARCHAR UNIQUE,
873
                                        summary VARCHAR,
874
                                        formdef_class VARCHAR,
875
                                        formdata_id VARCHAR,
876
                                        formdef_id VARCHAR,
877
                                        workflow_id VARCHAR,
878
                                        status_id VARCHAR,
879
                                        status_item_id VARCHAR,
880
                                        expression VARCHAR,
881
                                        expression_type VARCHAR,
882
                                        traceback TEXT,
883
                                        exception_class VARCHAR,
884
                                        exception_message VARCHAR,
885
                                        occurences_count INTEGER,
886
                                        first_occurence_timestamp TIMESTAMP WITH TIME ZONE,
887
                                        latest_occurence_timestamp TIMESTAMP WITH TIME ZONE,
888
                                        acked BOOLEAN
889
                                        )''' % table_name)
890
    cur.execute('''SELECT column_name FROM information_schema.columns
891
                    WHERE table_schema = 'public'
892
                      AND table_name = %s''', (table_name,))
893
    existing_fields = set([x[0] for x in cur.fetchall()])
894

  
895
    needed_fields = set([x[0] for x in LoggedError._table_static_fields])
896

  
897
    # delete obsolete fields
898
    for field in (existing_fields - needed_fields):
899
        cur.execute('''ALTER TABLE %s DROP COLUMN %s''' % (table_name, field))
900

  
901
    create_index = 'CREATE INDEX'
902
    if concurrently:
903
        create_index = 'CREATE INDEX CONCURRENTLY'
904

  
905
    # build indexes
906
    existing_indexes = set()
907
    cur.execute('''SELECT indexname
908
                     FROM pg_indexes
909
                    WHERE schemaname = 'public'
910
                      AND tablename = %s''', (table_name,))
911
    existing_indexes = set([x[0] for x in cur.fetchall()])
912

  
913
    for attr in ('formdef_id', 'workflow_id'):
914
        if not table_name + '_' + attr + '_idx' in existing_indexes:
915
            cur.execute('%(create_index)s %(table_name)s_%(attr)s_idx ON %(table_name)s (%(attr)s)' % {
916
                'create_index': create_index,
917
                'table_name': table_name,
918
                'attr': attr})
919

  
920
    conn.commit()
921
    cur.close()
922

  
923

  
862 924
@guard_postgres
863 925
def do_meta_table(conn=None, cur=None, insert_current_sql_level=True):
864 926
    own_conn = False
......
2458 2520
        return cls.get(row[0])
2459 2521

  
2460 2522

  
2523
class LoggedError(SqlMixin, wcs.logged_errors.LoggedError):
2524
    _table_name = 'loggederrors'
2525
    _table_static_fields = [
2526
        ('id', 'serial'),
2527
        ('tech_id', 'varchar'),
2528
        ('summary', 'varchar'),
2529
        ('formdef_class', 'varchar'),
2530
        ('formdata_id', 'varchar'),
2531
        ('formdef_id', 'varchar'),
2532
        ('workflow_id', 'varchar'),
2533
        ('status_id', 'varchar'),
2534
        ('status_item_id', 'varchar'),
2535
        ('expression', 'varchar'),
2536
        ('expression_type', 'varchar'),
2537
        ('traceback', 'text'),
2538
        ('exception_class', 'varchar'),
2539
        ('exception_message', 'varchar'),
2540
        ('occurences_count', 'integer'),
2541
        ('first_occurence_timestamp', 'timestamptz'),
2542
        ('latest_occurence_timestamp', 'timestamptz'),
2543
        ('acked', 'boolean'),
2544
    ]
2545

  
2546
    _numerical_id = False
2547

  
2548
    @guard_postgres
2549
    @invalidate_substitution_cache
2550
    def store(self):
2551
        sql_dict = {x: getattr(self, x) for x, y in self._table_static_fields}
2552

  
2553
        conn, cur = get_connection_and_cursor()
2554
        if not self.id:
2555
            column_names = [x for x in sql_dict.keys() if x != 'id']
2556
            sql_statement = '''INSERT INTO %s (%s)
2557
                               VALUES (%s)
2558
                               RETURNING id''' % (
2559
                                       self._table_name,
2560
                                       ', '.join(column_names),
2561
                                       ', '.join(['%%(%s)s' % x for x in column_names]))
2562
            cur.execute(sql_statement, sql_dict)
2563
            self.id = cur.fetchone()[0]
2564
        else:
2565
            column_names = sql_dict.keys()
2566
            sql_statement = '''UPDATE %s SET %s WHERE id = %%(id)s RETURNING id''' % (
2567
                                       self._table_name,
2568
                                       ', '.join(['%s = %%(%s)s' % (x, x) for x in column_names]))
2569
            cur.execute(sql_statement, sql_dict)
2570
            if cur.fetchone() is None:
2571
                raise AssertionError()
2572

  
2573
        conn.commit()
2574
        cur.close()
2575

  
2576
    @classmethod
2577
    def _row2ob(cls, row, **kwargs):
2578
        o = cls()
2579
        for field, value in zip(cls._table_static_fields, tuple(row)):
2580
            if field[1] in ('varchar', 'text'):
2581
                setattr(o, field[0], str_encode(value))
2582
            else:
2583
                setattr(o, field[0], value)
2584
        return o
2585

  
2586
    @classmethod
2587
    def get_data_fields(cls):
2588
        return []
2589

  
2590
    @classmethod
2591
    @guard_postgres
2592
    def fix_sequences(cls):
2593
        conn, cur = get_connection_and_cursor()
2594

  
2595
        sql_statement = '''select max(id) from %s''' % cls._table_name
2596
        cur.execute(sql_statement)
2597
        max_id = cur.fetchone()[0]
2598
        if max_id is not None:
2599
            sql_statement = '''ALTER SEQUENCE %s_id_seq RESTART %s''' % (
2600
                    cls._table_name, max_id+1)
2601
            cur.execute(sql_statement)
2602

  
2603
        conn.commit()
2604
        cur.close()
2605

  
2606

  
2461 2607
class classproperty(object):
2462 2608
    def __init__(self, f):
2463 2609
        self.f = f
......
2691 2837
# latest migration, number + description (description is not used
2692 2838
# programmaticaly but will make sure git conflicts if two migrations are
2693 2839
# separately added with the same number)
2694
SQL_LEVEL = (46, 'add index on formdata(status) - fix')
2840
SQL_LEVEL = (47, 'use SQL to store LoggedError')
2695 2841

  
2696 2842

  
2697 2843
def migrate_global_views(conn, cur):
......
2846 2992
    if sql_level < 42:
2847 2993
        # 42: create snapshots table
2848 2994
        do_snapshots_table()
2995
    if sql_level < 47:
2996
        # 47: store LoggedErrors in SQL
2997
        do_loggederrors_table()
2849 2998

  
2850 2999
    cur.execute('''UPDATE wcs_meta SET value = %s WHERE key = %s''', (
2851 3000
        str(SQL_LEVEL[0]), 'sql_level'))
wcs/wf/backoffice_fields.py
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
import xml.etree.ElementTree as ET
18

  
19
from quixote import get_publisher
18 20
from quixote.html import htmltext
19 21

  
20 22
from ..qommon import _, N_
......
133 135
                try:
134 136
                    new_value = formdef_field.convert_value_from_anything(new_value)
135 137
                except ValueError as e:
136
                    from wcs.logged_errors import LoggedError
137 138
                    summary = _('Failed to convert %(class)s value to %(kind)s field (%(id)s)') % {
138 139
                                    'class': type(new_value),
139 140
                                    'kind': _(getattr(formdef_field, 'description', 'unknown')),
140 141
                                    'id': field['field_id'],
141 142
                                }
142 143
                    expression_dict = self.get_expression(field['value'])
143
                    LoggedError.record(summary, formdata=formdata, status_item=self,
144
                                       expression=expression_dict['value'],
145
                                       expression_type=expression_dict['type'],
146
                                       exception=e)
144
                    get_publisher().record_loggederror(
145
                        summary, formdata=formdata, status_item=self,
146
                        expression=expression_dict['value'],
147
                        expression_type=expression_dict['type'],
148
                        exception=e)
147 149
                    continue
148 150

  
149 151
            formdata.data['%s' % field['field_id']] = new_value
wcs/wf/create_formdata.py
27 27
                             SingleSelectWidget, ComputedExpressionWidget,
28 28
                             CheckboxWidget, VarnameWidget, HtmlWidget)
29 29

  
30
from wcs.logged_errors import LoggedError
31 30
from wcs.workflows import WorkflowStatusItem, register_item_class
32 31
from wcs.formdef import FormDef
33 32

  
......
391 390
                        field=dest_field,
392 391
                        value=src.data.get(field.id))
393 392
                except Exception as e:
394
                    LoggedError.record(_('Could not copy field by varname for "%s"') % field.varname,
395
                                       formdata=src, status_item=self, exception=e)
393
                    get_publisher().record_loggederror(
394
                        _('Could not copy field by varname for "%s"') % field.varname,
395
                        formdata=src, status_item=self, exception=e)
396 396

  
397 397
        # field.id can be serialized to xml, so we must always convert them to
398 398
        # str when matching
......
418 418
                    value=value)
419 419
            except Exception as e:
420 420
                expression = self.get_expression(mapping.expression)
421
                LoggedError.record(_('Could not assign value to field "%s"') % dest_field.label,
422
                                   formdata=src, status_item=self,
423
                                   expression=expression['value'], expression_type=expression['type'],
424
                                   exception=e)
421
                get_publisher().record_loggederror(
422
                    _('Could not assign value to field "%s"') % dest_field.label,
423
                    formdata=src, status_item=self,
424
                    expression=expression['value'], expression_type=expression['type'],
425
                    exception=e)
425 426

  
426 427
        if missing_fields:
427 428
            summary = _('Missing field %r') % missing_fields
428
            LoggedError.record(summary, formdata=src, status_item=self)
429
            get_publisher().record_loggederror(summary, formdata=src, status_item=self)
429 430

  
430 431
    def _set_value(self, formdata, field, value):
431 432
        if field.convert_value_from_anything:
wcs/wf/external_workflow.py
19 19
from wcs.qommon import _
20 20
from wcs.qommon.form import SingleSelectWidget
21 21

  
22
from wcs.logged_errors import LoggedError
23 22
from wcs.workflows import WorkflowStatusItem, perform_items, register_item_class
24 23
from wcs.workflows import WorkflowGlobalActionWebserviceTrigger, Workflow
25 24
from wcs.carddef import CardDef
......
126 125

  
127 126
        trigger = self.get_trigger(objectdef.workflow)
128 127
        if not trigger:
129
            LoggedError.record(_('No trigger with id "%s" found in workflow') % self.trigger_id,
130
                    formdata=formdata, status_item=self)
128
            get_publisher().record_loggederror(
129
                _('No trigger with id "%s" found in workflow') % self.trigger_id,
130
                formdata=formdata, status_item=self)
131 131
            return
132 132

  
133 133
        class CallerSource:
wcs/workflows.py
1982 1982
        vars.update(context or {})
1983 1983

  
1984 1984
        def log_exception(exception):
1985
            from wcs.logged_errors import LoggedError
1986 1985
            if expression['type'] == 'template':
1987 1986
                summary = _('Failed to compute template')
1988 1987
            else:
1989 1988
                summary = _('Failed to compute Python expression')
1990
            LoggedError.record(summary, formdata=formdata, status_item=status_item,
1991
                               expression=expression['value'],
1992
                               expression_type=expression['type'],
1993
                               exception=exception)
1989
            get_publisher().record_loggederror(
1990
                summary, formdata=formdata, status_item=status_item,
1991
                expression=expression['value'],
1992
                expression_type=expression['type'],
1993
                exception=exception)
1994 1994

  
1995 1995
        if expression['type'] == 'template':
1996 1996
            try:
......
2043 2043

  
2044 2044
        targets = [x for x in self.parent.parent.possible_status if x.id == self.status]
2045 2045
        if not targets and formdata:  # do not log in presentation context: formdata is needed
2046
            from wcs.logged_errors import LoggedError
2047 2046
            message = _('reference to invalid status %(target)s in status %(status)s, '
2048 2047
                        'action %(status_item)s') % {
2049 2048
                            'target': self.status,
2050 2049
                            'status': self.parent.name,
2051 2050
                            'status_item': _(self.description)
2052 2051
                        }
2053
            LoggedError.record(message, formdata=formdata, status_item=self)
2052
            get_publisher().record_loggederror(message, formdata=formdata, status_item=self)
2054 2053

  
2055 2054
        return targets
2056 2055

  
......
2677 2676
                subject = mail_template.subject
2678 2677
                extra_attachments = mail_template.attachments
2679 2678
            else:
2680
                from wcs.logged_errors import LoggedError
2681 2679
                message = _('reference to invalid mail template %(mail_template)s in status %(status)s') % {
2682 2680
                            'status': self.parent.name,
2683 2681
                            'mail_template': self.mail_template,
2684 2682
                            }
2685
                LoggedError.record(message, formdata=formdata, status_item=self)
2683
                get_publisher().record_loggederror(message, formdata=formdata, status_item=self)
2686 2684
                return
2687 2685

  
2688 2686
        try:
2689
-