écraser avec un import pour les blocs de champs (#60722) #1213

Merged
fpeters merged 2 commits from wip/60722-overwrite-block into main 2024-03-15 07:21:15 +01:00
6 changed files with 127 additions and 26 deletions

View File

@ -257,6 +257,55 @@ def test_block_delete(pub):
assert 'This block is still used' in resp
def test_block_export_overwrite(pub):
create_superuser(pub)
BlockDef.wipe()
block = BlockDef()
block.name = 'foobar'
block.fields = [fields.StringField(id='123', required=True, label='Test')]
block.store()
app = login(get_app(pub))
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
resp = resp.click(href=re.compile('^export$'))
xml_export = resp.text
block.slug = 'new-slug'
block.name = 'New foobar'
block.fields = [fields.StringField(id='123', required=True, label='Test bebore overwrite')]
block.store()
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
resp = resp.click('Overwrite')
resp = resp.form.submit('cancel').follow()
resp = resp.click('Overwrite')
resp = resp.form.submit()
assert 'There were errors processing your form.' in resp
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
resp = resp.form.submit()
resp = resp.follow()
assert BlockDef.count() == 1
block.refresh_from_storage()
assert block.fields[0].label == 'Test'
assert block.name == 'foobar'
assert block.slug == 'new-slug' # not overwritten
# unknown reference
block.fields = [fields.StringField(id='1', data_source={'type': 'foobar'})]
block.store()
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
resp = resp.click(href=re.compile('^export$'))
xml_export = resp.text
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
resp = resp.click('Overwrite')
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
resp = resp.form.submit()
assert 'Invalid File (Unknown referenced objects)' in resp
assert '<ul><li>Unknown datasources: foobar</li></ul>' in resp
def test_block_edit_duplicate_delete_field(pub):
create_superuser(pub)
BlockDef.wipe()
@ -453,14 +502,14 @@ def test_block_edit_field_warnings(pub):
blockdef.store()
resp = app.get('/backoffice/forms/blocks/%s/' % blockdef.id)
assert 'more than 30 fields' not in resp.text
assert '<div id="new-field"><h3>New Field</h3>' in resp.text
assert resp.pyquery('#new-field')
assert resp.pyquery('#fields-list a[title="Duplicate"]').length
blockdef.fields.extend([fields.StringField(id='%d' % i, label='field %d' % i) for i in range(21, 51)])
blockdef.store()
resp = app.get('/backoffice/forms/blocks/%s/' % blockdef.id)
assert 'This block of fields contains 60 fields.' in resp.text
assert '<div id="new-field"><h3>New Field</h3>' not in resp.text
assert not resp.pyquery('#new-field')
assert not resp.pyquery('#fields-list a[title="Duplicate"]').length

View File

@ -1035,7 +1035,7 @@ def test_card_edit_field_warnings(pub):
resp = app.get('/backoffice/cards/%s/fields/' % carddef.id)
assert 'more than 200 fields' not in resp.text
assert 'first field should be of type "page"' in resp.text
assert '<div id="new-field"><h3>New Field</h3>' in resp.text
assert resp.pyquery('#new-field')
carddef.fields.extend([fields.StringField(id='%d' % i, label='field %d' % i) for i in range(10, 210)])
carddef.store()
@ -1049,7 +1049,7 @@ def test_card_edit_field_warnings(pub):
resp = app.get('/backoffice/cards/%s/fields/' % carddef.id)
assert 'This card model contains 410 fields.' in resp.text
assert 'first field should be of type "page"' in resp.text
assert '<div id="new-field"><h3>New Field</h3>' not in resp.text
assert not resp.pyquery('#new-field')
assert '>Duplicate<' not in resp.text

View File

@ -3600,7 +3600,7 @@ def test_form_edit_field_warnings(pub):
resp = app.get('/backoffice/forms/%s/fields/' % formdef.id)
assert 'more than 200 fields' not in resp.text
assert 'first field should be of type "page"' in resp.text
assert '<div id="new-field"><h3>New Field</h3>' in resp.text
assert resp.pyquery('#new-field')
formdef.fields.extend([fields.StringField(id='%d' % i, label='field %d' % i) for i in range(10, 210)])
formdef.store()
@ -3615,7 +3615,7 @@ def test_form_edit_field_warnings(pub):
assert 'This form contains 410 fields.' in resp.text
assert 'no new fields can be added.' in resp.text
assert 'first field should be of type "page"' in resp.text
assert '<div id="new-field"><h3>New Field</h3>' not in resp.text
assert not resp.pyquery('#new-field')
assert '>Duplicate<' not in resp.text
assert resp.pyquery('aside .errornotice')
assert not resp.pyquery('aside form[action=new]')
@ -3628,7 +3628,7 @@ def test_form_edit_field_warnings(pub):
resp = app.get('/backoffice/forms/%s/fields/' % formdef.id)
assert 'no new fields should be added.' in resp.text
assert '<div id="new-field"><h3>New Field</h3>' in resp.text
assert resp.pyquery('#new-field')
assert '>Duplicate<' in resp.text
assert not resp.pyquery('aside .errornotice')
assert resp.pyquery('aside form[action=new]')

View File

@ -54,6 +54,7 @@ class BlockDirectory(FieldsDirectory):
'inspect',
'duplicate',
('history', 'snapshots_dir'),
'overwrite',
]
field_def_page_class = BlockFieldDefPage
blacklisted_types = ['page', 'table', 'table-select', 'tablerows', 'ranked-items', 'blocks', 'computed']
@ -106,6 +107,14 @@ class BlockDirectory(FieldsDirectory):
r = TemplateIO(html=True)
r += htmltext('<div id="appbar">')
r += htmltext('<h2>%s</h2>') % self.objectdef.name
r += htmltext('<span class="actions">')
r += htmltext('<a class="extra-actions-menu-opener"></a>')
r += htmltext('<ul class="extra-actions-menu">')
Review

Premier commit pour reprendre la barre latérale, en suivant ce qui a été fait pour l'écran d'un formulaire, quelques actions dans le menu kebab.

Premier commit pour reprendre la barre latérale, en suivant ce qui a été fait pour l'écran d'un formulaire, quelques actions dans le menu kebab.
r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
r += htmltext('</ul>')
r += htmltext('<a href="settings" rel="popup" role="button">%s</a>') % _('Settings')
r += htmltext('</span>')
r += htmltext('</div>')
r += utils.last_modification_block(obj=self.objectdef)
r += get_session().display_message()
@ -132,17 +141,25 @@ class BlockDirectory(FieldsDirectory):
def get_new_field_form_sidebar(self, page_id):
r = TemplateIO(html=True)
r += htmltext('<ul id="sidebar-actions">')
r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
r += htmltext('<li><a href="duplicate" rel="popup">%s</a></li>') % _('Duplicate')
r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
if get_publisher().snapshot_class:
r += htmltext('<li><a rel="popup" href="history/save">%s</a></li>') % _('Save snapshot')
r += htmltext('<li><a href="history/">%s</a></li>') % _('History')
r += htmltext('<li><a href="inspect">%s</a></li>') % _('Inspector')
r += htmltext('<li><a href="settings" rel="popup">%s</a></li>') % _('Settings')
r += htmltext('</ul>')
r += super().get_new_field_form_sidebar(page_id=page_id)
r += htmltext('<h3>%s</h3>') % _('Actions')
r += htmltext('<ul class="sidebar--buttons">')
r += htmltext('<li><a class="button button-paragraph" href="duplicate" rel="popup">%s</a>') % _(
Review

Et les autres actions dans la barre latérale.

Et les autres actions dans la barre latérale.
'Duplicate'
)
if get_publisher().snapshot_class:
r += htmltext('<li><a class="button button-paragraph" href="history/save">%s</a>') % _(
'Save snapshot'
)
r += htmltext('<li><a class="button button-paragraph" rel="popup" href="overwrite">%s</a>') % _(
'Overwrite'
Review

Dont la nouvelle action pour écraser.

Dont la nouvelle action pour écraser.
)
r += htmltext('</ul>')
r += htmltext('<h3>%s</h3>') % _('Navigation')
r += htmltext('<ul class="sidebar--buttons">')
r += htmltext('<li><a class="button button-paragraph" href="history/">%s</a></li>') % _('History')
r += htmltext('<li><a class="button button-paragraph" href="inspect">%s</a></li>') % _('Inspector')
r += htmltext('</ul>')
return r.getvalue()
def delete(self):
@ -209,6 +226,41 @@ class BlockDirectory(FieldsDirectory):
content_type='application/x-wcs-form',
)
def overwrite(self):
form = Form(enctype='multipart/form-data')
form.widgets.append(
HtmlWidget(
'<div class="warningnotice"><p>%s</p></div>'
% _('Field data will be lost if overwriting with an incompatible block.')
Review

Un peu du mal à trouver une phrase adéquate ici; en pratique on utiliser des uuid depuis le début pour les champs des blocs, et le stockage se fait dans des colonnes jsonb, donc un revert de l'écrasement rendrait accès aux données. Mais c'est quand même bien de décourager.

Un peu du mal à trouver une phrase adéquate ici; en pratique on utiliser des uuid depuis le début pour les champs des blocs, et le stockage se fait dans des colonnes jsonb, donc un revert de l'écrasement rendrait accès aux données. Mais c'est quand même bien de décourager.
)
)
form.add(FileWidget, 'file', title=_('File'), required=True)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.is_submitted() and not form.has_errors():
try:
return self.overwrite_submit(form)
except ValueError:
pass
get_response().breadcrumb.append(('overwrite', _('Overwrite')))
get_response().set_title(title=_('Overwrite'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Overwrite')
r += form.render()
return r.getvalue()
def overwrite_submit(self, form):
blockdef = BlocksDirectory.import_blockdef(form)
self.objectdef.name = blockdef.name
self.objectdef.digest_template = blockdef.digest_template
self.objectdef.fields = blockdef.fields
self.objectdef.store(comment=_('Overwritten'))
return redirect('.')
def settings(self):
get_response().breadcrumb.append(('settings', _('Settings')))
form = Form()
@ -409,7 +461,8 @@ class BlocksDirectory(Directory):
r += form.render()
return r.getvalue()
def import_submit(self, form):
@classmethod
def import_blockdef(cls, form):
fp = form.get_widget('file').parse().fp
error, reason = False, None
@ -433,6 +486,10 @@ class BlocksDirectory(Directory):
form.set_error('file', msg)
raise ValueError()
return blockdef
def import_submit(self, form):
blockdef = self.import_blockdef(form)
initial_blockdef_name = blockdef.name
blockdef_names = [x.name for x in BlockDef.select()]
copy_no = 1

View File

@ -527,8 +527,8 @@ class FieldsDirectory(Directory):
)
return r.getvalue()
r += htmltext('<div id="new-field">')
r += htmltext('<h3>%s</h3>') % _('New Field')
r += htmltext('<div id="new-field">')
get_request().form = None # ignore the eventual ?page=x
form = self.get_new_field_form(page_id)
r += form.render()

View File

@ -222,13 +222,8 @@ div#new-action, div#new-trigger, div#new-field {
}
}
div#new-field {
margin: 2em 0 4px 0;
padding: 5px 5px;
Review

Petit ajustement stylistique pour que le titre "Nouveau champ" apparaisse comme les titres Actions/Navigation/Applications.

Petit ajustement stylistique pour que le titre "Nouveau champ" apparaisse comme les titres Actions/Navigation/Applications.
}
div#new-field form {
margin-bottom: 2em;
form#import-fields {
margin-top: 1em;
}
aside#sidebar div.news h3,