0001-misc-split-backoffice_pages-tests-carddata.patch
tests/backoffice_pages/test_all.py | ||
---|---|---|
18 | 18 |
from django.utils.six import StringIO, BytesIO |
19 | 19 |
from django.utils.six.moves.urllib import parse as urllib |
20 | 20 | |
21 |
from webtest import Upload |
|
22 | ||
23 | 21 |
from quixote import get_publisher |
24 | 22 |
from quixote.http_request import Upload as QuixoteUpload |
25 | 23 |
from wcs.api_utils import sign_url |
... | ... | |
46 | 44 |
from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping |
47 | 45 |
from wcs.wf.create_carddata import CreateCarddataWorkflowStatusItem |
48 | 46 |
from wcs.carddef import CardDef |
49 |
from wcs.categories import Category, CardDefCategory
|
|
47 |
from wcs.categories import Category |
|
50 | 48 |
from wcs.formdef import FormDef |
51 | 49 |
from wcs.logged_errors import LoggedError |
52 | 50 |
from wcs import fields |
... | ... | |
6287 | 6285 |
assert 'HELLO WORLD 2' in resp.text |
6288 | 6286 | |
6289 | 6287 | |
6290 |
def test_carddata_management(pub, studio): |
|
6291 |
CardDef.wipe() |
|
6292 |
user = create_user(pub) |
|
6293 |
app = login(get_app(pub)) |
|
6294 |
resp = app.get('/backoffice/') |
|
6295 |
assert 'Cards' not in resp.text |
|
6296 |
carddef = CardDef() |
|
6297 |
carddef.name = 'foo' |
|
6298 |
carddef.fields = [ |
|
6299 |
fields.StringField(id='1', label='Test', type='string', varname='foo'), |
|
6300 |
fields.StringField(id='2', label='Condi', type='string', varname='bar', |
|
6301 |
required=True, condition={'type': 'django', 'value': 'form_var_foo == "ok"'}), |
|
6302 |
] |
|
6303 |
carddef.store() |
|
6304 |
carddef.data_class().wipe() |
|
6305 | ||
6306 |
resp = app.get('/backoffice/') |
|
6307 |
assert 'Cards' not in resp.text |
|
6308 | ||
6309 |
carddef.backoffice_submission_roles = user.roles |
|
6310 |
carddef.store() |
|
6311 |
resp = app.get('/backoffice/') |
|
6312 |
assert 'Cards' in resp.text |
|
6313 | ||
6314 |
carddef.backoffice_submission_roles = None |
|
6315 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
6316 |
carddef.store() |
|
6317 |
resp = app.get('/backoffice/') |
|
6318 |
assert 'Cards' in resp.text |
|
6319 | ||
6320 |
resp = app.get('/backoffice/data/') |
|
6321 |
resp = resp.click('foo') |
|
6322 |
assert 'Add' not in resp.text |
|
6323 | ||
6324 |
carddef.backoffice_submission_roles = user.roles |
|
6325 |
carddef.store() |
|
6326 | ||
6327 |
resp = app.get('/backoffice/data/') |
|
6328 |
resp = resp.click('foo') |
|
6329 |
assert resp.text.count('<tr') == 1 # header |
|
6330 |
assert 'Add' in resp.text |
|
6331 |
resp = resp.click('Add') |
|
6332 |
resp.form['f1'] = 'blah' |
|
6333 | ||
6334 |
live_url = resp.html.find('form').attrs['data-live-url'] |
|
6335 |
assert '/backoffice/data/foo/add/live' in live_url |
|
6336 |
live_resp = app.post(live_url, params=resp.form.submit_fields()) |
|
6337 |
assert live_resp.json['result']['1']['visible'] |
|
6338 |
assert not live_resp.json['result']['2']['visible'] |
|
6339 |
resp.form['f1'] = 'ok' |
|
6340 |
live_resp = app.post(live_url, params=resp.form.submit_fields()) |
|
6341 |
assert live_resp.json['result']['1']['visible'] |
|
6342 |
assert live_resp.json['result']['2']['visible'] |
|
6343 |
resp.form['f2'] = 'blah' |
|
6344 | ||
6345 |
resp = resp.form.submit('submit') |
|
6346 |
assert resp.location.endswith('/backoffice/data/foo/1/') |
|
6347 |
resp = resp.follow() |
|
6348 |
assert 'Edit Card' in resp.text |
|
6349 |
assert 'Delete Card' in resp.text |
|
6350 | ||
6351 |
carddata = carddef.data_class().select()[0] |
|
6352 |
assert carddata.data == {'1': 'ok', '2': 'blah'} |
|
6353 |
assert carddata.user_id is None |
|
6354 |
assert carddata.submission_agent_id == str(user.id) |
|
6355 |
assert carddata.evolution[0].who == str(user.id) |
|
6356 |
assert 'Original Submitter' not in resp.text |
|
6357 | ||
6358 |
resp = app.get('/backoffice/data/') |
|
6359 |
resp = resp.click('foo') |
|
6360 |
assert resp.text.count('<tr') == 2 # header + row of data |
|
6361 | ||
6362 | ||
6363 |
def test_carddata_management_categories(pub, studio): |
|
6364 |
user = create_user(pub) |
|
6365 | ||
6366 |
CardDef.wipe() |
|
6367 |
carddef = CardDef() |
|
6368 |
carddef.name = 'foo' |
|
6369 |
carddef.fields = [] |
|
6370 |
carddef.backoffice_submission_roles = None |
|
6371 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
6372 |
carddef.store() |
|
6373 | ||
6374 |
carddef2 = CardDef() |
|
6375 |
carddef2.name = 'card title 2' |
|
6376 |
carddef2.fields = [] |
|
6377 |
carddef2.backoffice_submission_roles = None |
|
6378 |
carddef2.workflow_roles = {'_editor': user.roles[0]} |
|
6379 |
carddef2.store() |
|
6380 | ||
6381 |
CardDefCategory.wipe() |
|
6382 |
cat = CardDefCategory(name='Foo') |
|
6383 |
cat.store() |
|
6384 |
cat2 = CardDefCategory(name='Bar') |
|
6385 |
cat2.store() |
|
6386 | ||
6387 |
app = login(get_app(pub)) |
|
6388 |
resp = app.get('/backoffice/data/') |
|
6389 |
assert '<h3>Misc</h3>' not in resp.text |
|
6390 |
assert '<h3>Foo</h3>' not in resp.text |
|
6391 |
assert '<h3>Bar</h3>' not in resp.text |
|
6392 | ||
6393 |
carddef.category = cat2 |
|
6394 |
carddef.store() |
|
6395 |
resp = app.get('/backoffice/data/') |
|
6396 |
assert '<h3>Misc</h3>' in resp.text |
|
6397 |
assert '<h3>Foo</h3>' not in resp.text |
|
6398 |
assert '<h3>Bar</h3>' in resp.text |
|
6399 | ||
6400 |
carddef2.category = cat |
|
6401 |
carddef2.store() |
|
6402 |
resp = app.get('/backoffice/data/') |
|
6403 |
assert '<h3>Misc</h3>' not in resp.text |
|
6404 |
assert '<h3>Foo</h3>' in resp.text |
|
6405 |
assert '<h3>Bar</h3>' in resp.text |
|
6406 | ||
6407 | ||
6408 |
def test_studio_card_item_link(pub, studio): |
|
6409 |
user = create_user(pub) |
|
6410 |
CardDef.wipe() |
|
6411 |
carddef = CardDef() |
|
6412 |
carddef.name = 'foo' |
|
6413 |
carddef.fields = [ |
|
6414 |
fields.StringField(id='1', label='Test', type='string', varname='foo'), |
|
6415 |
] |
|
6416 |
carddef.backoffice_submission_roles = user.roles |
|
6417 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
6418 |
carddef.digest_template = 'card {{form_var_foo}}' |
|
6419 |
carddef.store() |
|
6420 |
carddef.data_class().wipe() |
|
6421 | ||
6422 |
card = carddef.data_class()() |
|
6423 |
card.data = {'1': 'plop'} |
|
6424 |
card.just_created() |
|
6425 |
card.store() |
|
6426 | ||
6427 |
carddef2 = CardDef() |
|
6428 |
carddef2.name = 'bar' |
|
6429 |
carddef2.fields = [ |
|
6430 |
fields.ItemField(id='1', label='Test', type='item', |
|
6431 |
data_source={'type': 'carddef:foo', 'value': ''}), |
|
6432 |
] |
|
6433 |
carddef2.backoffice_submission_roles = user.roles |
|
6434 |
carddef2.workflow_roles = {'_editor': user.roles[0]} |
|
6435 |
carddef2.store() |
|
6436 |
carddef2.data_class().wipe() |
|
6437 | ||
6438 |
app = login(get_app(pub)) |
|
6439 |
resp = app.get('/backoffice/data/') |
|
6440 |
resp = resp.click('bar') |
|
6441 |
resp = resp.click('Add') |
|
6442 |
resp.form['f1'] = card.id |
|
6443 |
resp = resp.form.submit('submit') |
|
6444 |
assert resp.location.endswith('/backoffice/data/bar/1/') |
|
6445 |
resp = resp.follow() |
|
6446 |
resp = resp.click('card plop') |
|
6447 |
assert '<div class="value">plop</div>' in resp |
|
6448 | ||
6449 |
# link to a unknown carddef |
|
6450 |
carddef2.fields = [ |
|
6451 |
fields.ItemField(id='1', label='Test', type='item', |
|
6452 |
data_source={'type': 'carddef:unknown', 'value': ''}), |
|
6453 |
] |
|
6454 |
carddef2.store() |
|
6455 |
app = login(get_app(pub)) |
|
6456 |
resp = app.get('/backoffice/data/') |
|
6457 |
resp = resp.click('bar') |
|
6458 |
resp = resp.click('Add') # no error |
|
6459 | ||
6460 |
# look without access rights |
|
6461 |
carddef.backoffice_submission_roles = None |
|
6462 |
carddef.workflow_roles = {'_editor': None} |
|
6463 |
carddef.store() |
|
6464 |
resp = app.get('/backoffice/data/bar/1/') |
|
6465 |
with pytest.raises(IndexError): |
|
6466 |
resp.click('card plop') |
|
6467 | ||
6468 | ||
6469 |
def test_backoffice_cards_import_data_from_csv(pub, studio): |
|
6470 |
user = create_user(pub) |
|
6471 | ||
6472 |
data_source = { |
|
6473 |
'type': 'formula', |
|
6474 |
'value': repr([ |
|
6475 |
{'id': '1', 'text': 'un', 'more': 'foo'}, |
|
6476 |
{'id': '2', 'text': 'deux', 'more': 'bar'}]) |
|
6477 |
} |
|
6478 | ||
6479 |
CardDef.wipe() |
|
6480 |
carddef = CardDef() |
|
6481 |
carddef.name = 'test' |
|
6482 |
carddef.fields = [ |
|
6483 |
fields.TableField(id='0', label='Table'), |
|
6484 |
fields.MapField(id='1', label='Map'), |
|
6485 |
fields.StringField(id='2', label='Test'), |
|
6486 |
fields.BoolField(id='3', label='Boolean'), |
|
6487 |
fields.ItemField(id='4', label='List', |
|
6488 |
items=['item1', 'item2']), |
|
6489 |
fields.DateField(id='5', label='Date'), |
|
6490 |
fields.TitleField(id='6', label='Title', type='title'), |
|
6491 |
fields.FileField(id='7', label='File'), |
|
6492 |
fields.EmailField(id='8', label='Email'), |
|
6493 |
fields.TextField(id='9', label='Long'), |
|
6494 |
fields.ItemField(id='10', label='List2', data_source=data_source), |
|
6495 |
] |
|
6496 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
6497 |
carddef.store() |
|
6498 |
carddef.data_class().wipe() |
|
6499 | ||
6500 |
app = login(get_app(pub)) |
|
6501 | ||
6502 |
resp = app.get(carddef.get_url()) |
|
6503 |
assert 'Import data from a CSV file' not in resp.text |
|
6504 |
resp = app.get(carddef.get_url() + 'import-csv', status=403) |
|
6505 | ||
6506 |
carddef.backoffice_submission_roles = user.roles |
|
6507 |
carddef.store() |
|
6508 | ||
6509 |
resp = app.get(carddef.get_url()) |
|
6510 |
resp = resp.click('Import data from a CSV file') |
|
6511 | ||
6512 |
assert 'Table, File are required but cannot be filled from CSV.' in resp |
|
6513 |
assert 'Download sample file for this card' not in resp |
|
6514 |
carddef.fields[0].required = False |
|
6515 |
carddef.fields[7].required = False |
|
6516 |
carddef.store() |
|
6517 | ||
6518 |
resp = app.get(carddef.get_url()) |
|
6519 |
resp = resp.click('Import data from a CSV file') |
|
6520 |
sample_resp = resp.click('Download sample file for this card') |
|
6521 |
today = datetime.date.today() |
|
6522 |
assert sample_resp.text == ( |
|
6523 |
"Table,Map,Test,Boolean,List,Date,File,Email,Long,List2\r\n" |
|
6524 |
"will be ignored - type Table not supported," |
|
6525 |
"%s," |
|
6526 |
"value," |
|
6527 |
"Yes," |
|
6528 |
"value," |
|
6529 |
"%s," |
|
6530 |
"will be ignored - type File Upload not supported," |
|
6531 |
"foo@example.com," |
|
6532 |
"value," |
|
6533 |
"value\r\n" % (pub.get_default_position(), today)) |
|
6534 | ||
6535 |
# missing file |
|
6536 |
resp = resp.forms[0].submit() |
|
6537 |
assert '>required field<' in resp |
|
6538 | ||
6539 |
resp.forms[0]['file'] = Upload('test.csv', b'\0', 'text/csv') |
|
6540 |
resp = resp.forms[0].submit() |
|
6541 |
assert 'Invalid file format.' in resp |
|
6542 | ||
6543 |
resp.forms[0]['file'] = Upload('test.csv', b'', 'text/csv') |
|
6544 |
resp = resp.forms[0].submit() |
|
6545 |
assert 'Invalid CSV file.' in resp |
|
6546 | ||
6547 |
resp.forms[0]['file'] = Upload('test.csv', |
|
6548 |
b'Test,List,Date\ndata1,item1,invalid', |
|
6549 |
'text/csv') |
|
6550 |
resp = resp.forms[0].submit() |
|
6551 |
assert 'CSV file contains less columns than card fields.' in resp.text |
|
6552 | ||
6553 |
data = [b'Table,Map,Test,Boolean,List,Date,File,Email,Long,List2'] |
|
6554 |
for i in range(1, 150): |
|
6555 |
data.append(b'table,48.81;2.37,data%d ,%s,item%d,2020-01-%02d,filename-%d,test@localhost,"plop\nplop",1' % ( |
|
6556 |
i, str(bool(i % 2)).encode('utf-8'), i, i % 31 + 1, i)) |
|
6557 | ||
6558 |
resp.forms[0]['file'] = Upload('test.csv', b'\n'.join(data), |
|
6559 |
'text/csv') |
|
6560 |
resp = resp.forms[0].submit().follow() |
|
6561 |
assert 'Importing data into cards' in resp |
|
6562 |
assert 'Column File will be ignored: type File Upload not supported.' in resp |
|
6563 |
assert carddef.data_class().count() == 149 |
|
6564 |
card1, card2 = carddef.data_class().select(order_by='id')[:2] |
|
6565 |
assert card1.data['1'] == '48.81;2.37' |
|
6566 |
assert card1.data['2'] == 'data1' |
|
6567 |
assert card1.data['3'] is True |
|
6568 |
assert card1.data['5'].tm_mday == 2 |
|
6569 |
assert card1.data['9'] == 'plop\nplop' |
|
6570 |
assert card1.data['10'] == '1' |
|
6571 |
assert card1.data['10_display'] == 'un' |
|
6572 |
assert card1.data['10_structured'] == {'id': '1', 'text': 'un', 'more': 'foo'} |
|
6573 |
assert card2.data['2'] == 'data2' |
|
6574 |
assert card2.data['3'] is False |
|
6575 |
assert card2.data['5'].tm_mday == 3 |
|
6576 | ||
6577 | ||
6578 |
def test_backoffice_cards_import_data_csv_invalid_columns(pub): |
|
6579 |
user = create_user(pub) |
|
6580 | ||
6581 |
CardDef.wipe() |
|
6582 |
carddef = CardDef() |
|
6583 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
6584 |
carddef.backoffice_submission_roles = user.roles |
|
6585 |
carddef.name = 'test' |
|
6586 |
carddef.fields = [ |
|
6587 |
fields.StringField(id='1', label='String1'), |
|
6588 |
fields.StringField(id='2', label='String2'), |
|
6589 |
fields.TextField(id='3', label='Text'), |
|
6590 |
] |
|
6591 |
carddef.store() |
|
6592 | ||
6593 |
app = login(get_app(pub)) |
|
6594 |
resp = app.get(carddef.get_url()) |
|
6595 |
resp = resp.click('Import data from a CSV file') |
|
6596 | ||
6597 |
csv_data = '''String1,String2,Text |
|
6598 |
1,2,3 |
|
6599 |
4,5,6 |
|
6600 |
7, |
|
6601 |
8,9,10,11 |
|
6602 |
12,13,14 |
|
6603 | ||
6604 |
''' |
|
6605 |
resp.forms[0]['file'] = Upload('test.csv', csv_data.encode('utf-8'), 'text/csv') |
|
6606 |
resp = resp.forms[0].submit() |
|
6607 |
assert 'CSV file contains lines with wrong number of columns.' in resp.text |
|
6608 |
assert '(line numbers 4, 5, 7)' in resp.text |
|
6609 | ||
6610 |
csv_data += '\n' * 10 |
|
6611 |
resp.forms[0]['file'] = Upload('test.csv', csv_data.encode('utf-8'), 'text/csv') |
|
6612 |
resp = resp.forms[0].submit() |
|
6613 |
assert 'CSV file contains lines with wrong number of columns.' in resp.text |
|
6614 |
assert '(line numbers 4, 5, 7, 8, 9 and more)' in resp.text |
|
6615 | ||
6616 | ||
6617 |
def test_backoffice_cards_wscall_failure_display(http_requests, pub, studio): |
|
6618 |
LoggedError.wipe() |
|
6619 |
user = create_user(pub) |
|
6620 | ||
6621 |
Workflow.wipe() |
|
6622 |
workflow = Workflow(name='wscall') |
|
6623 |
workflow.roles = { |
|
6624 |
'_viewer': 'Viewer', |
|
6625 |
'_editor': 'Editor', |
|
6626 |
} |
|
6627 |
st1 = workflow.add_status('Recorded', 'recorded') |
|
6628 | ||
6629 |
wscall = WebserviceCallStatusItem() |
|
6630 |
wscall.id = '_wscall' |
|
6631 |
wscall.varname = 'xxx' |
|
6632 |
wscall.url = 'http://remote.example.net/xml' |
|
6633 |
wscall.action_on_bad_data = ':stop' |
|
6634 |
wscall.record_errors = True |
|
6635 |
st1.items.append(wscall) |
|
6636 |
wscall.parent = st1 |
|
6637 | ||
6638 |
again = ChoiceWorkflowStatusItem() |
|
6639 |
again.id = '_again' |
|
6640 |
again.label = 'Again' |
|
6641 |
again.by = ['_editor'] |
|
6642 |
again.status = st1.id |
|
6643 |
st1.items.append(again) |
|
6644 |
again.parent = st1 |
|
6645 | ||
6646 |
workflow.store() |
|
6647 | ||
6648 |
CardDef.wipe() |
|
6649 |
carddef = CardDef() |
|
6650 |
carddef.name = 'foo' |
|
6651 |
carddef.fields = [ |
|
6652 |
fields.StringField(id='1', label='Test', type='string', varname='foo'), |
|
6653 |
] |
|
6654 |
carddef.backoffice_submission_roles = user.roles |
|
6655 |
carddef.workflow_id = workflow.id |
|
6656 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
6657 |
carddef.digest_template = 'card {{form_var_foo}}' |
|
6658 |
carddef.store() |
|
6659 |
carddef.data_class().wipe() |
|
6660 | ||
6661 |
carddata = carddef.data_class()() |
|
6662 |
carddata.data = {'1': 'plop'} |
|
6663 |
carddata.just_created() |
|
6664 |
carddata.store() |
|
6665 | ||
6666 |
app = login(get_app(pub)) |
|
6667 | ||
6668 |
resp = app.get('/backoffice/data/foo/%s/' % carddata.id) |
|
6669 |
assert 'Again' in resp.text |
|
6670 |
resp = resp.forms[0].submit('button_again') |
|
6671 |
resp = resp.follow() |
|
6672 |
assert 'Error during webservice call' in resp.text |
|
6673 | ||
6674 |
assert LoggedError.count() == 1 |
|
6675 |
assert LoggedError.select()[0].get_formdata().data == {'1': 'plop'} |
|
6676 | ||
6677 | ||
6678 | 6288 |
def test_lazy_eval_with_conditional_workflow_form(pub): |
6679 | 6289 |
role = Role(name='foobar') |
6680 | 6290 |
role.store() |
... | ... | |
7150 | 6760 | |
7151 | 6761 |
resp = resp.forms['listing-settings'].submit() |
7152 | 6762 |
assert resp.text.count('<tr') == 6 |
7153 | ||
7154 | ||
7155 |
def test_block_card_item_link(pub, studio, blocks_feature): |
|
7156 |
user = create_user(pub) |
|
7157 |
CardDef.wipe() |
|
7158 |
carddef = CardDef() |
|
7159 |
carddef.name = 'foo' |
|
7160 |
carddef.fields = [ |
|
7161 |
fields.StringField(id='1', label='Test', type='string', varname='foo'), |
|
7162 |
] |
|
7163 |
carddef.backoffice_submission_roles = user.roles |
|
7164 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
7165 |
carddef.digest_template = 'card {{form_var_foo}}' |
|
7166 |
carddef.store() |
|
7167 |
carddef.data_class().wipe() |
|
7168 | ||
7169 |
card = carddef.data_class()() |
|
7170 |
card.data = {'1': 'plop'} |
|
7171 |
card.just_created() |
|
7172 |
card.store() |
|
7173 | ||
7174 |
card2 = carddef.data_class()() |
|
7175 |
card2.data = {'1': 'plop2'} |
|
7176 |
card2.just_created() |
|
7177 |
card2.store() |
|
7178 | ||
7179 |
BlockDef.wipe() |
|
7180 |
block = BlockDef() |
|
7181 |
block.name = 'foobar' |
|
7182 |
block.fields = [ |
|
7183 |
fields.ItemField(id='1', label='Test', type='item', |
|
7184 |
data_source={'type': 'carddef:foo', 'value': ''}), |
|
7185 |
] |
|
7186 |
block.store() |
|
7187 | ||
7188 |
formdef = FormDef() |
|
7189 |
formdef.name = 'bar' |
|
7190 |
formdef.fields = [ |
|
7191 |
fields.BlockField(id='1', label='test', type='block:foobar', max_items=3), |
|
7192 |
] |
|
7193 |
formdef.store() |
|
7194 |
formdef.data_class().wipe() |
|
7195 | ||
7196 |
app = login(get_app(pub)) |
|
7197 |
resp = app.get('/bar/') |
|
7198 |
resp.form['f1$element0$f1'].value = card.id |
|
7199 |
resp = resp.form.submit('f1$add_element') |
|
7200 |
resp.form['f1$element1$f1'].value = card2.id |
|
7201 |
resp = resp.form.submit('submit') # -> validation page |
|
7202 |
assert resp.form['f1$element0$f1'].value == str(card.id) |
|
7203 |
assert resp.form['f1$element0$f1_label'].value == 'card plop' |
|
7204 |
assert resp.form['f1$element1$f1'].value == str(card2.id) |
|
7205 |
assert resp.form['f1$element1$f1_label'].value == 'card plop2' |
|
7206 |
resp = resp.form.submit('submit') # -> final submit |
|
7207 |
resp = resp.follow() |
|
7208 |
assert '<div class="value">card plop</div>' in resp |
|
7209 |
assert '<div class="value">card plop2</div>' in resp |
|
7210 | ||
7211 |
# check cards are links in backoffice |
|
7212 |
resp = app.get('/backoffice/management' + resp.request.path) |
|
7213 |
assert '<div class="value"><a href="http://example.net/backoffice/data/foo/%s/">card plop</a></div></div>' % card.id in resp |
|
7214 |
assert '<div class="value"><a href="http://example.net/backoffice/data/foo/%s/">card plop2</a></div></div>' % card2.id in resp |
tests/backoffice_pages/test_carddata.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
import datetime |
|
3 |
import os |
|
4 | ||
5 |
import pytest |
|
6 |
from webtest import Upload |
|
7 | ||
8 |
from wcs import fields |
|
9 |
from wcs.blocks import BlockDef |
|
10 |
from wcs.carddef import CardDef |
|
11 |
from wcs.categories import CardDefCategory |
|
12 |
from wcs.formdef import FormDef |
|
13 |
from wcs.logged_errors import LoggedError |
|
14 |
from wcs.qommon.http_request import HTTPRequest |
|
15 |
from wcs.wf.wscall import WebserviceCallStatusItem |
|
16 |
from wcs.workflows import ChoiceWorkflowStatusItem, Workflow |
|
17 | ||
18 |
from utilities import clean_temporary_pub, create_temporary_pub, get_app, login |
|
19 |
from .test_all import create_user |
|
20 | ||
21 | ||
22 |
def pytest_generate_tests(metafunc): |
|
23 |
if 'pub' in metafunc.fixturenames: |
|
24 |
metafunc.parametrize('pub', ['pickle', 'sql', 'pickle-templates'], indirect=True) |
|
25 | ||
26 | ||
27 |
@pytest.fixture |
|
28 |
def pub(request, emails): |
|
29 |
pub = create_temporary_pub( |
|
30 |
sql_mode=bool('sql' in request.param), |
|
31 |
templates_mode=bool('templates' in request.param) |
|
32 |
) |
|
33 | ||
34 |
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'}) |
|
35 |
pub.set_app_dir(req) |
|
36 |
pub.cfg['identification'] = {'methods': ['password']} |
|
37 |
pub.cfg['language'] = {'language': 'en'} |
|
38 |
pub.write_cfg() |
|
39 |
fd = open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') |
|
40 |
fd.write(''' |
|
41 |
[api-secrets] |
|
42 |
coucou = 1234 |
|
43 |
''') |
|
44 |
fd.close() |
|
45 | ||
46 |
return pub |
|
47 | ||
48 | ||
49 |
def teardown_module(module): |
|
50 |
clean_temporary_pub() |
|
51 | ||
52 | ||
53 |
def test_carddata_management(pub, studio): |
|
54 |
CardDef.wipe() |
|
55 |
user = create_user(pub) |
|
56 |
app = login(get_app(pub)) |
|
57 |
resp = app.get('/backoffice/') |
|
58 |
assert 'Cards' not in resp.text |
|
59 |
carddef = CardDef() |
|
60 |
carddef.name = 'foo' |
|
61 |
carddef.fields = [ |
|
62 |
fields.StringField(id='1', label='Test', type='string', varname='foo'), |
|
63 |
fields.StringField( |
|
64 |
id='2', label='Condi', type='string', varname='bar', |
|
65 |
required=True, condition={'type': 'django', 'value': 'form_var_foo == "ok"'}), |
|
66 |
] |
|
67 |
carddef.store() |
|
68 |
carddef.data_class().wipe() |
|
69 | ||
70 |
resp = app.get('/backoffice/') |
|
71 |
assert 'Cards' not in resp.text |
|
72 | ||
73 |
carddef.backoffice_submission_roles = user.roles |
|
74 |
carddef.store() |
|
75 |
resp = app.get('/backoffice/') |
|
76 |
assert 'Cards' in resp.text |
|
77 | ||
78 |
carddef.backoffice_submission_roles = None |
|
79 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
80 |
carddef.store() |
|
81 |
resp = app.get('/backoffice/') |
|
82 |
assert 'Cards' in resp.text |
|
83 | ||
84 |
resp = app.get('/backoffice/data/') |
|
85 |
resp = resp.click('foo') |
|
86 |
assert 'Add' not in resp.text |
|
87 | ||
88 |
carddef.backoffice_submission_roles = user.roles |
|
89 |
carddef.store() |
|
90 | ||
91 |
resp = app.get('/backoffice/data/') |
|
92 |
resp = resp.click('foo') |
|
93 |
assert resp.text.count('<tr') == 1 # header |
|
94 |
assert 'Add' in resp.text |
|
95 |
resp = resp.click('Add') |
|
96 |
resp.form['f1'] = 'blah' |
|
97 | ||
98 |
live_url = resp.html.find('form').attrs['data-live-url'] |
|
99 |
assert '/backoffice/data/foo/add/live' in live_url |
|
100 |
live_resp = app.post(live_url, params=resp.form.submit_fields()) |
|
101 |
assert live_resp.json['result']['1']['visible'] |
|
102 |
assert not live_resp.json['result']['2']['visible'] |
|
103 |
resp.form['f1'] = 'ok' |
|
104 |
live_resp = app.post(live_url, params=resp.form.submit_fields()) |
|
105 |
assert live_resp.json['result']['1']['visible'] |
|
106 |
assert live_resp.json['result']['2']['visible'] |
|
107 |
resp.form['f2'] = 'blah' |
|
108 | ||
109 |
resp = resp.form.submit('submit') |
|
110 |
assert resp.location.endswith('/backoffice/data/foo/1/') |
|
111 |
resp = resp.follow() |
|
112 |
assert 'Edit Card' in resp.text |
|
113 |
assert 'Delete Card' in resp.text |
|
114 | ||
115 |
carddata = carddef.data_class().select()[0] |
|
116 |
assert carddata.data == {'1': 'ok', '2': 'blah'} |
|
117 |
assert carddata.user_id is None |
|
118 |
assert carddata.submission_agent_id == str(user.id) |
|
119 |
assert carddata.evolution[0].who == str(user.id) |
|
120 |
assert 'Original Submitter' not in resp.text |
|
121 | ||
122 |
resp = app.get('/backoffice/data/') |
|
123 |
resp = resp.click('foo') |
|
124 |
assert resp.text.count('<tr') == 2 # header + row of data |
|
125 | ||
126 | ||
127 |
def test_carddata_management_categories(pub, studio): |
|
128 |
user = create_user(pub) |
|
129 | ||
130 |
CardDef.wipe() |
|
131 |
carddef = CardDef() |
|
132 |
carddef.name = 'foo' |
|
133 |
carddef.fields = [] |
|
134 |
carddef.backoffice_submission_roles = None |
|
135 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
136 |
carddef.store() |
|
137 | ||
138 |
carddef2 = CardDef() |
|
139 |
carddef2.name = 'card title 2' |
|
140 |
carddef2.fields = [] |
|
141 |
carddef2.backoffice_submission_roles = None |
|
142 |
carddef2.workflow_roles = {'_editor': user.roles[0]} |
|
143 |
carddef2.store() |
|
144 | ||
145 |
CardDefCategory.wipe() |
|
146 |
cat = CardDefCategory(name='Foo') |
|
147 |
cat.store() |
|
148 |
cat2 = CardDefCategory(name='Bar') |
|
149 |
cat2.store() |
|
150 | ||
151 |
app = login(get_app(pub)) |
|
152 |
resp = app.get('/backoffice/data/') |
|
153 |
assert '<h3>Misc</h3>' not in resp.text |
|
154 |
assert '<h3>Foo</h3>' not in resp.text |
|
155 |
assert '<h3>Bar</h3>' not in resp.text |
|
156 | ||
157 |
carddef.category = cat2 |
|
158 |
carddef.store() |
|
159 |
resp = app.get('/backoffice/data/') |
|
160 |
assert '<h3>Misc</h3>' in resp.text |
|
161 |
assert '<h3>Foo</h3>' not in resp.text |
|
162 |
assert '<h3>Bar</h3>' in resp.text |
|
163 | ||
164 |
carddef2.category = cat |
|
165 |
carddef2.store() |
|
166 |
resp = app.get('/backoffice/data/') |
|
167 |
assert '<h3>Misc</h3>' not in resp.text |
|
168 |
assert '<h3>Foo</h3>' in resp.text |
|
169 |
assert '<h3>Bar</h3>' in resp.text |
|
170 | ||
171 | ||
172 |
def test_studio_card_item_link(pub, studio): |
|
173 |
user = create_user(pub) |
|
174 |
CardDef.wipe() |
|
175 |
carddef = CardDef() |
|
176 |
carddef.name = 'foo' |
|
177 |
carddef.fields = [ |
|
178 |
fields.StringField(id='1', label='Test', type='string', varname='foo'), |
|
179 |
] |
|
180 |
carddef.backoffice_submission_roles = user.roles |
|
181 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
182 |
carddef.digest_template = 'card {{form_var_foo}}' |
|
183 |
carddef.store() |
|
184 |
carddef.data_class().wipe() |
|
185 | ||
186 |
card = carddef.data_class()() |
|
187 |
card.data = {'1': 'plop'} |
|
188 |
card.just_created() |
|
189 |
card.store() |
|
190 | ||
191 |
carddef2 = CardDef() |
|
192 |
carddef2.name = 'bar' |
|
193 |
carddef2.fields = [ |
|
194 |
fields.ItemField( |
|
195 |
id='1', label='Test', type='item', |
|
196 |
data_source={'type': 'carddef:foo', 'value': ''}), |
|
197 |
] |
|
198 |
carddef2.backoffice_submission_roles = user.roles |
|
199 |
carddef2.workflow_roles = {'_editor': user.roles[0]} |
|
200 |
carddef2.store() |
|
201 |
carddef2.data_class().wipe() |
|
202 | ||
203 |
app = login(get_app(pub)) |
|
204 |
resp = app.get('/backoffice/data/') |
|
205 |
resp = resp.click('bar') |
|
206 |
resp = resp.click('Add') |
|
207 |
resp.form['f1'] = card.id |
|
208 |
resp = resp.form.submit('submit') |
|
209 |
assert resp.location.endswith('/backoffice/data/bar/1/') |
|
210 |
resp = resp.follow() |
|
211 |
resp = resp.click('card plop') |
|
212 |
assert '<div class="value">plop</div>' in resp |
|
213 | ||
214 |
# link to a unknown carddef |
|
215 |
carddef2.fields = [ |
|
216 |
fields.ItemField( |
|
217 |
id='1', label='Test', type='item', |
|
218 |
data_source={'type': 'carddef:unknown', 'value': ''}), |
|
219 |
] |
|
220 |
carddef2.store() |
|
221 |
app = login(get_app(pub)) |
|
222 |
resp = app.get('/backoffice/data/') |
|
223 |
resp = resp.click('bar') |
|
224 |
resp = resp.click('Add') # no error |
|
225 | ||
226 |
# look without access rights |
|
227 |
carddef.backoffice_submission_roles = None |
|
228 |
carddef.workflow_roles = {'_editor': None} |
|
229 |
carddef.store() |
|
230 |
resp = app.get('/backoffice/data/bar/1/') |
|
231 |
with pytest.raises(IndexError): |
|
232 |
resp.click('card plop') |
|
233 | ||
234 | ||
235 |
def test_backoffice_cards_import_data_from_csv(pub, studio): |
|
236 |
user = create_user(pub) |
|
237 | ||
238 |
data_source = { |
|
239 |
'type': 'formula', |
|
240 |
'value': repr([ |
|
241 |
{'id': '1', 'text': 'un', 'more': 'foo'}, |
|
242 |
{'id': '2', 'text': 'deux', 'more': 'bar'}]) |
|
243 |
} |
|
244 | ||
245 |
CardDef.wipe() |
|
246 |
carddef = CardDef() |
|
247 |
carddef.name = 'test' |
|
248 |
carddef.fields = [ |
|
249 |
fields.TableField(id='0', label='Table'), |
|
250 |
fields.MapField(id='1', label='Map'), |
|
251 |
fields.StringField(id='2', label='Test'), |
|
252 |
fields.BoolField(id='3', label='Boolean'), |
|
253 |
fields.ItemField(id='4', label='List', |
|
254 |
items=['item1', 'item2']), |
|
255 |
fields.DateField(id='5', label='Date'), |
|
256 |
fields.TitleField(id='6', label='Title', type='title'), |
|
257 |
fields.FileField(id='7', label='File'), |
|
258 |
fields.EmailField(id='8', label='Email'), |
|
259 |
fields.TextField(id='9', label='Long'), |
|
260 |
fields.ItemField(id='10', label='List2', data_source=data_source), |
|
261 |
] |
|
262 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
263 |
carddef.store() |
|
264 |
carddef.data_class().wipe() |
|
265 | ||
266 |
app = login(get_app(pub)) |
|
267 | ||
268 |
resp = app.get(carddef.get_url()) |
|
269 |
assert 'Import data from a CSV file' not in resp.text |
|
270 |
resp = app.get(carddef.get_url() + 'import-csv', status=403) |
|
271 | ||
272 |
carddef.backoffice_submission_roles = user.roles |
|
273 |
carddef.store() |
|
274 | ||
275 |
resp = app.get(carddef.get_url()) |
|
276 |
resp = resp.click('Import data from a CSV file') |
|
277 | ||
278 |
assert 'Table, File are required but cannot be filled from CSV.' in resp |
|
279 |
assert 'Download sample file for this card' not in resp |
|
280 |
carddef.fields[0].required = False |
|
281 |
carddef.fields[7].required = False |
|
282 |
carddef.store() |
|
283 | ||
284 |
resp = app.get(carddef.get_url()) |
|
285 |
resp = resp.click('Import data from a CSV file') |
|
286 |
sample_resp = resp.click('Download sample file for this card') |
|
287 |
today = datetime.date.today() |
|
288 |
assert sample_resp.text == ( |
|
289 |
"Table,Map,Test,Boolean,List,Date,File,Email,Long,List2\r\n" |
|
290 |
"will be ignored - type Table not supported," |
|
291 |
"%s," |
|
292 |
"value," |
|
293 |
"Yes," |
|
294 |
"value," |
|
295 |
"%s," |
|
296 |
"will be ignored - type File Upload not supported," |
|
297 |
"foo@example.com," |
|
298 |
"value," |
|
299 |
"value\r\n" % (pub.get_default_position(), today)) |
|
300 | ||
301 |
# missing file |
|
302 |
resp = resp.forms[0].submit() |
|
303 |
assert '>required field<' in resp |
|
304 | ||
305 |
resp.forms[0]['file'] = Upload('test.csv', b'\0', 'text/csv') |
|
306 |
resp = resp.forms[0].submit() |
|
307 |
assert 'Invalid file format.' in resp |
|
308 | ||
309 |
resp.forms[0]['file'] = Upload('test.csv', b'', 'text/csv') |
|
310 |
resp = resp.forms[0].submit() |
|
311 |
assert 'Invalid CSV file.' in resp |
|
312 | ||
313 |
resp.forms[0]['file'] = Upload('test.csv', |
|
314 |
b'Test,List,Date\ndata1,item1,invalid', |
|
315 |
'text/csv') |
|
316 |
resp = resp.forms[0].submit() |
|
317 |
assert 'CSV file contains less columns than card fields.' in resp.text |
|
318 | ||
319 |
data = [b'Table,Map,Test,Boolean,List,Date,File,Email,Long,List2'] |
|
320 |
for i in range(1, 150): |
|
321 |
data.append(b'table,48.81;2.37,data%d ,%s,item%d,2020-01-%02d,filename-%d,test@localhost,"plop\nplop",1' % ( |
|
322 |
i, str(bool(i % 2)).encode('utf-8'), i, i % 31 + 1, i)) |
|
323 | ||
324 |
resp.forms[0]['file'] = Upload('test.csv', b'\n'.join(data), |
|
325 |
'text/csv') |
|
326 |
resp = resp.forms[0].submit().follow() |
|
327 |
assert 'Importing data into cards' in resp |
|
328 |
assert 'Column File will be ignored: type File Upload not supported.' in resp |
|
329 |
assert carddef.data_class().count() == 149 |
|
330 |
card1, card2 = carddef.data_class().select(order_by='id')[:2] |
|
331 |
assert card1.data['1'] == '48.81;2.37' |
|
332 |
assert card1.data['2'] == 'data1' |
|
333 |
assert card1.data['3'] is True |
|
334 |
assert card1.data['5'].tm_mday == 2 |
|
335 |
assert card1.data['9'] == 'plop\nplop' |
|
336 |
assert card1.data['10'] == '1' |
|
337 |
assert card1.data['10_display'] == 'un' |
|
338 |
assert card1.data['10_structured'] == {'id': '1', 'text': 'un', 'more': 'foo'} |
|
339 |
assert card2.data['2'] == 'data2' |
|
340 |
assert card2.data['3'] is False |
|
341 |
assert card2.data['5'].tm_mday == 3 |
|
342 | ||
343 | ||
344 |
def test_backoffice_cards_import_data_csv_invalid_columns(pub): |
|
345 |
user = create_user(pub) |
|
346 | ||
347 |
CardDef.wipe() |
|
348 |
carddef = CardDef() |
|
349 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
350 |
carddef.backoffice_submission_roles = user.roles |
|
351 |
carddef.name = 'test' |
|
352 |
carddef.fields = [ |
|
353 |
fields.StringField(id='1', label='String1'), |
|
354 |
fields.StringField(id='2', label='String2'), |
|
355 |
fields.TextField(id='3', label='Text'), |
|
356 |
] |
|
357 |
carddef.store() |
|
358 | ||
359 |
app = login(get_app(pub)) |
|
360 |
resp = app.get(carddef.get_url()) |
|
361 |
resp = resp.click('Import data from a CSV file') |
|
362 | ||
363 |
csv_data = '''String1,String2,Text |
|
364 |
1,2,3 |
|
365 |
4,5,6 |
|
366 |
7, |
|
367 |
8,9,10,11 |
|
368 |
12,13,14 |
|
369 | ||
370 |
''' |
|
371 |
resp.forms[0]['file'] = Upload('test.csv', csv_data.encode('utf-8'), 'text/csv') |
|
372 |
resp = resp.forms[0].submit() |
|
373 |
assert 'CSV file contains lines with wrong number of columns.' in resp.text |
|
374 |
assert '(line numbers 4, 5, 7)' in resp.text |
|
375 | ||
376 |
csv_data += '\n' * 10 |
|
377 |
resp.forms[0]['file'] = Upload('test.csv', csv_data.encode('utf-8'), 'text/csv') |
|
378 |
resp = resp.forms[0].submit() |
|
379 |
assert 'CSV file contains lines with wrong number of columns.' in resp.text |
|
380 |
assert '(line numbers 4, 5, 7, 8, 9 and more)' in resp.text |
|
381 | ||
382 | ||
383 |
def test_backoffice_cards_wscall_failure_display(http_requests, pub, studio): |
|
384 |
LoggedError.wipe() |
|
385 |
user = create_user(pub) |
|
386 | ||
387 |
Workflow.wipe() |
|
388 |
workflow = Workflow(name='wscall') |
|
389 |
workflow.roles = { |
|
390 |
'_viewer': 'Viewer', |
|
391 |
'_editor': 'Editor', |
|
392 |
} |
|
393 |
st1 = workflow.add_status('Recorded', 'recorded') |
|
394 | ||
395 |
wscall = WebserviceCallStatusItem() |
|
396 |
wscall.id = '_wscall' |
|
397 |
wscall.varname = 'xxx' |
|
398 |
wscall.url = 'http://remote.example.net/xml' |
|
399 |
wscall.action_on_bad_data = ':stop' |
|
400 |
wscall.record_errors = True |
|
401 |
st1.items.append(wscall) |
|
402 |
wscall.parent = st1 |
|
403 | ||
404 |
again = ChoiceWorkflowStatusItem() |
|
405 |
again.id = '_again' |
|
406 |
again.label = 'Again' |
|
407 |
again.by = ['_editor'] |
|
408 |
again.status = st1.id |
|
409 |
st1.items.append(again) |
|
410 |
again.parent = st1 |
|
411 | ||
412 |
workflow.store() |
|
413 | ||
414 |
CardDef.wipe() |
|
415 |
carddef = CardDef() |
|
416 |
carddef.name = 'foo' |
|
417 |
carddef.fields = [ |
|
418 |
fields.StringField(id='1', label='Test', type='string', varname='foo'), |
|
419 |
] |
|
420 |
carddef.backoffice_submission_roles = user.roles |
|
421 |
carddef.workflow_id = workflow.id |
|
422 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
423 |
carddef.digest_template = 'card {{form_var_foo}}' |
|
424 |
carddef.store() |
|
425 |
carddef.data_class().wipe() |
|
426 | ||
427 |
carddata = carddef.data_class()() |
|
428 |
carddata.data = {'1': 'plop'} |
|
429 |
carddata.just_created() |
|
430 |
carddata.store() |
|
431 | ||
432 |
app = login(get_app(pub)) |
|
433 | ||
434 |
resp = app.get('/backoffice/data/foo/%s/' % carddata.id) |
|
435 |
assert 'Again' in resp.text |
|
436 |
resp = resp.forms[0].submit('button_again') |
|
437 |
resp = resp.follow() |
|
438 |
assert 'Error during webservice call' in resp.text |
|
439 | ||
440 |
assert LoggedError.count() == 1 |
|
441 |
assert LoggedError.select()[0].get_formdata().data == {'1': 'plop'} |
|
442 | ||
443 | ||
444 |
def test_block_card_item_link(pub, studio, blocks_feature): |
|
445 |
user = create_user(pub) |
|
446 |
CardDef.wipe() |
|
447 |
carddef = CardDef() |
|
448 |
carddef.name = 'foo' |
|
449 |
carddef.fields = [ |
|
450 |
fields.StringField(id='1', label='Test', type='string', varname='foo'), |
|
451 |
] |
|
452 |
carddef.backoffice_submission_roles = user.roles |
|
453 |
carddef.workflow_roles = {'_editor': user.roles[0]} |
|
454 |
carddef.digest_template = 'card {{form_var_foo}}' |
|
455 |
carddef.store() |
|
456 |
carddef.data_class().wipe() |
|
457 | ||
458 |
card = carddef.data_class()() |
|
459 |
card.data = {'1': 'plop'} |
|
460 |
card.just_created() |
|
461 |
card.store() |
|
462 | ||
463 |
card2 = carddef.data_class()() |
|
464 |
card2.data = {'1': 'plop2'} |
|
465 |
card2.just_created() |
|
466 |
card2.store() |
|
467 | ||
468 |
BlockDef.wipe() |
|
469 |
block = BlockDef() |
|
470 |
block.name = 'foobar' |
|
471 |
block.fields = [ |
|
472 |
fields.ItemField( |
|
473 |
id='1', label='Test', type='item', |
|
474 |
data_source={'type': 'carddef:foo', 'value': ''}), |
|
475 |
] |
|
476 |
block.store() |
|
477 | ||
478 |
formdef = FormDef() |
|
479 |
formdef.name = 'bar' |
|
480 |
formdef.fields = [ |
|
481 |
fields.BlockField(id='1', label='test', type='block:foobar', max_items=3), |
|
482 |
] |
|
483 |
formdef.store() |
|
484 |
formdef.data_class().wipe() |
|
485 | ||
486 |
app = login(get_app(pub)) |
|
487 |
resp = app.get('/bar/') |
|
488 |
resp.form['f1$element0$f1'].value = card.id |
|
489 |
resp = resp.form.submit('f1$add_element') |
|
490 |
resp.form['f1$element1$f1'].value = card2.id |
|
491 |
resp = resp.form.submit('submit') # -> validation page |
|
492 |
assert resp.form['f1$element0$f1'].value == str(card.id) |
|
493 |
assert resp.form['f1$element0$f1_label'].value == 'card plop' |
|
494 |
assert resp.form['f1$element1$f1'].value == str(card2.id) |
|
495 |
assert resp.form['f1$element1$f1_label'].value == 'card plop2' |
|
496 |
resp = resp.form.submit('submit') # -> final submit |
|
497 |
resp = resp.follow() |
|
498 |
assert '<div class="value">card plop</div>' in resp |
|
499 |
assert '<div class="value">card plop2</div>' in resp |
|
500 | ||
501 |
# check cards are links in backoffice |
|
502 |
resp = app.get('/backoffice/management' + resp.request.path) |
|
503 |
assert '<div class="value"><a href="http://example.net/backoffice/data/foo/%s/">card plop</a></div></div>' % card.id in resp |
|
504 |
assert '<div class="value"><a href="http://example.net/backoffice/data/foo/%s/">card plop2</a></div></div>' % card2.id in resp |
|
0 |
- |