blocks: add option to have a remove button (#45368)

This commit is contained in:
Frédéric Péters 2021-02-01 19:37:42 +01:00
parent 669b338187
commit f59b73c7c6
8 changed files with 167 additions and 18 deletions

View File

@ -855,7 +855,7 @@ def test_block_repeated(pub, blocks_feature):
resp = resp.form.submit('f1$add_element')
assert resp.text.count('>Test<') == 3
assert resp.text.count('>hintblock<') == 1
assert 'Add another' not in resp
assert resp.pyquery('.list-add').attr['style'] == 'display: none'
# fill items (1st and 3rd row)
resp.form['f1$element0$f123'] = 'foo'
@ -915,7 +915,7 @@ def test_block_repeated_over_limit(pub, blocks_feature):
assert resp.text.count('>Test<') == 2
resp = resp.form.submit('f1$add_element')
assert resp.text.count('>Test<') == 3
assert 'Add another' not in resp
assert resp.pyquery('.list-add').attr['style'] == 'display: none'
# fill items
resp.form['f1$element0$f123'] = 'foo'
@ -963,7 +963,7 @@ def test_block_repeated_files(pub, blocks_feature):
assert resp.text.count('>Test<') == 2
resp = resp.form.submit('f1$add_element')
assert resp.text.count('>Test<') == 3
assert 'Add another' not in resp
assert resp.pyquery('.list-add').attr['style'] == 'display: none'
# fill items (1st and 3rd row)
resp.form['f1$element0$f123'] = 'foo'
@ -991,6 +991,84 @@ def test_block_repeated_files(pub, blocks_feature):
assert 'test2.txt' in resp
@pytest.mark.parametrize('removed_line', [0, 1, 2])
def test_block_repeated_remove_line(pub, blocks_feature, removed_line):
FormDef.wipe()
BlockDef.wipe()
block = BlockDef()
block.name = 'foobar'
block.fields = [
fields.StringField(id='123', required=True, label='Test', type='string'),
fields.StringField(id='234', required=True, label='Test2', type='string'),
]
block.store()
formdef = FormDef()
formdef.name = 'form title'
formdef.fields = [
fields.PageField(id='0', label='1st page', type='page'),
fields.BlockField(
id='1', label='test', type='block:foobar', max_items=5, hint='hintblock', remove_button=True
),
fields.PageField(id='2', label='2nd page', type='page'),
]
formdef.store()
formdef.data_class().wipe()
app = get_app(pub)
resp = app.get(formdef.get_url())
assert resp.text.count('>Test<') == 1
resp = resp.form.submit('f1$add_element')
assert resp.text.count('>Test<') == 2
resp = resp.form.submit('f1$add_element')
assert resp.text.count('>Test<') == 3
# fill items on three rows
resp.form['f1$element0$f123'] = 'foo1'
resp.form['f1$element0$f234'] = 'bar1'
resp.form['f1$element1$f123'] = 'foo2'
resp.form['f1$element1$f234'] = 'bar2'
resp.form['f1$element2$f123'] = 'foo3'
resp.form['f1$element2$f234'] = 'bar3'
resp = resp.form.submit('submit') # -> 2nd page
resp = resp.form.submit('submit') # -> validation page
assert 'Check values then click submit.' in resp.text
assert resp.form['f1$element0$f123'].value == 'foo1'
assert resp.form['f1$element0$f234'].value == 'bar1'
assert resp.form['f1$element1$f123'].value == 'foo2'
assert resp.form['f1$element1$f234'].value == 'bar2'
assert resp.form['f1$element2$f123'].value == 'foo3'
assert resp.form['f1$element2$f234'].value == 'bar3'
resp = resp.form.submit('previous') # -> 2nd page
resp = resp.form.submit('previous') # -> 1st page
# simulate javascript removing of block elements from DOM
resp.form.field_order.remove(
('f1$element%s$f123' % removed_line, resp.form.fields['f1$element%s$f123' % removed_line][0])
)
del resp.form.fields['f1$element%s$f123' % removed_line]
resp.form.field_order.remove(
('f1$element%s$f234' % removed_line, resp.form.fields['f1$element%s$f234' % removed_line][0])
)
del resp.form.fields['f1$element%s$f234' % removed_line]
resp = resp.form.submit('submit') # -> 2nd page
resp = resp.form.submit('submit') # -> validation page
values = ['1', '2', '3']
del values[removed_line]
assert resp.form['f1$element0$f123'].value == 'foo%s' % values[0]
assert resp.form['f1$element0$f234'].value == 'bar%s' % values[0]
assert resp.form['f1$element1$f123'].value == 'foo%s' % values[1]
assert resp.form['f1$element1$f234'].value == 'bar%s' % values[1]
assert 'f1$element2$f123' not in resp.form.fields
assert 'f1$element2$f234' not in resp.form.fields
resp = resp.form.submit('submit') # -> submit
assert len(formdef.data_class().select()[0].data['1']['data']) == 2
@pytest.mark.parametrize('block_name', ['foobar', 'Foo bar'])
def test_block_digest(pub, blocks_feature, block_name):
FormDef.wipe()

View File

@ -192,9 +192,12 @@ class BlockDef(StorableObject):
class BlockSubWidget(CompositeWidget):
template_name = 'qommon/forms/widgets/block_sub.html'
def __init__(self, name, value=None, *args, **kwargs):
self.block = kwargs.pop('block')
self.readonly = kwargs.get('readonly')
self.remove_button = kwargs.pop('remove_button', False)
super().__init__(name, value, *args, **kwargs)
for field in self.block.fields:
if 'readonly' in kwargs:
@ -240,19 +243,23 @@ class BlockSubWidget(CompositeWidget):
class BlockWidget(WidgetList):
template_name = 'qommon/forms/widgets/block.html'
always_include_add_button = True
def __init__(
self, name, value=None, title=None, block=None, max_items=None, add_element_label=None, **kwargs
):
self.block = block
self.readonly = kwargs.get('readonly')
self.label_display = kwargs.pop('label_display') or 'normal'
self.remove_button = kwargs.pop('remove_button', False)
element_values = None
if value:
element_values = value.get('data')
if not max_items:
max_items = 1
hint = kwargs.pop('hint', None)
element_kwargs = {'block': self.block, 'render_br': False}
element_kwargs = {'block': self.block, 'render_br': False, 'remove_button': self.remove_button}
element_kwargs.update(kwargs)
super().__init__(
name,
@ -275,8 +282,9 @@ class BlockWidget(WidgetList):
# (maybe this could be moved to WidgetList)
prefix = '%s$element' % self.name
known_prefixes = {x.split('$', 2)[1] for x in request.form.keys() if x.startswith(prefix)}
for i in range(len(known_prefixes) - len(self.element_names)):
self.add_element()
for prefix in known_prefixes:
if prefix not in self.element_names:
self.add_element(element_name=prefix)
super()._parse(request)
if self.value:
self.value = {'data': self.value}

View File

@ -3154,9 +3154,10 @@ class BlockField(WidgetField):
widget_class = BlockWidget
max_items = 1
extra_attributes = ['block', 'max_items', 'add_element_label', 'label_display']
extra_attributes = ['block', 'max_items', 'add_element_label', 'label_display', 'remove_button']
add_element_label = ''
label_display = 'normal'
remove_button = False
# cache
_block = None
@ -3191,9 +3192,15 @@ class BlockField(WidgetField):
value=self.label_display or 'normal',
options=display_options,
)
form.add(CheckboxWidget, 'remove_button', title=_('Include remove button'), value=self.remove_button)
def get_admin_attributes(self):
return super().get_admin_attributes() + ['max_items', 'add_element_label', 'label_display']
return super().get_admin_attributes() + [
'max_items',
'add_element_label',
'label_display',
'remove_button',
]
def store_display_value(self, data, field_id):
value = data.get(field_id)

View File

@ -1699,6 +1699,8 @@ class CaptchaWidget(CompositeWidget):
class WidgetList(quixote.form.widget.WidgetList):
always_include_add_button = False
def __init__(
self,
name,
@ -1737,9 +1739,9 @@ class WidgetList(quixote.form.widget.WidgetList):
for i in range(len(known_prefixes) - len(self.element_names)):
self.add_element()
# Add submit to add more element widgets
current_len = len(self.element_names)
if (not max_items) or current_len < max_items:
# Add submit to add more element widgets
if self.always_include_add_button or (not max_items) or current_len < max_items:
self.add(
SubmitWidget,
'add_element',
@ -1747,15 +1749,15 @@ class WidgetList(quixote.form.widget.WidgetList):
render_br=False,
extra_css_class='list-add',
)
if self.get('add_element'):
self.add_element()
current_len = len(self.element_names)
if max_items and current_len >= max_items:
self.widgets.remove(self.get_widget('add_element'))
del self._names['add_element']
if self.get('add_element') and (not max_items or current_len < max_items):
# add an empty row
self.add_element()
def add_element(self, value=None):
name = "element%d" % len(self.element_names)
def add_element(self, value=None, element_name=None):
if element_name:
name = element_name
else:
name = 'element%d' % len(self.element_names)
self.add(self.element_type, name, value=value, **self.element_kwargs)
self.element_names.append(name)
@ -1789,13 +1791,19 @@ class WidgetList(quixote.form.widget.WidgetList):
# values in subwidgets either, clear them instead of filling the
# screen with "required field" messages.
clear_errors = True
count = 0
for widget in self.get_widgets():
if widget is add_element_widget:
continue
if clear_errors:
widget.clear_error()
r += widget.render()
count += 1
if add_element_widget:
if self.max_items and count >= self.max_items:
add_element_widget.is_hidden = True
r += add_element_widget.render()
return r.getvalue()

View File

@ -2111,3 +2111,22 @@ div.timetable-widget {
padding-bottom: 1ex;
}
}
.wcs-block-with-remove-button {
.BlockSubWidget {
position: relative;
}
.remove-button {
position: absolute;
right: 0;
top: 0.5em;
margin-right: 0;
span {
display: none;
}
&::after {
font-family: FontAwesome;
content: "\f1f8"; // trash
}
}
}

View File

@ -470,4 +470,22 @@ $(function() {
const table = elem.querySelector('table');
new Responsive_table_widget(table);
});
function disable_single_block_remove_button() {
$('.BlockSubWidget button.remove-button').each(function(i, elem) {
if ($(this).parents('.BlockWidget').find('.BlockSubWidget').length == 1) {
$(this).prop('disabled', true);
}
});
}
disable_single_block_remove_button();
$('.BlockSubWidget button.remove-button').on('click', function() {
if ($(this).parents('.BlockWidget').find('.BlockSubWidget').length > 1) {
$(this).parents('.BlockWidget').find('.list-add').show();
$(this).parents('.BlockSubWidget').remove();
disable_single_block_remove_button();
}
return false;
});
});

View File

@ -0,0 +1,3 @@
{% extends "qommon/forms/widget.html" %}
{% block widget-css-classes %}{{ block.super }} {% if widget.remove_button %}wcs-block-with-remove-button{% endif %}{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends "qommon/forms/widget.html" %}
{% load i18n %}
{% block widget-content %}
{% for subwidget in widget.get_widgets %}
{{ subwidget.render|safe }}
{% endfor %}
{% if not widget.readonly and widget.remove_button %}<button class="remove-button" title="{% trans "Remove" %}"><span>-</span></button>{% endif %} {% endblock %}