Projet

Général

Profil

0001-manager-use-sidetabs-to-navigate-between-cell-option.patch

Frédéric Péters, 29 mars 2022 12:01

Télécharger (63,1 ko)

Voir les différences:

Subject: [PATCH] manager: use sidetabs to navigate between cell options
 (#62965)

 combo/apps/maps/models.py                     |  16 ++-
 combo/data/models.py                          |  59 +++++++++-
 combo/manager/static/css/combo.manager.scss   |  90 ++++++++++------
 combo/manager/static/js/combo.manager.js      |  85 ++++++++-------
 .../templates/combo/cell_appearance.html      |   6 ++
 .../templates/combo/cell_visibility.html      |  28 +----
 .../combo/manager_edit_cell_block.html        |  53 +++++----
 combo/manager/templates/combo/page_view.html  |  28 ++---
 combo/manager/templatetags/cells.py           |  15 ++-
 combo/manager/urls.py                         |  15 ---
 combo/manager/views.py                        | 102 ++++++++++--------
 combo/settings.py                             |   1 +
 combo/utils/forms.py                          |   2 +-
 tests/test_dataviz.py                         |  64 +++++------
 tests/test_family.py                          |   5 +-
 tests/test_gallery_cell.py                    |   8 +-
 tests/test_kb.py                              |   5 +-
 tests/test_manager.py                         |  61 +++++------
 tests/test_maps_manager.py                    |   4 +-
 tests/test_wcs.py                             |  25 +++--
 tests/utils.py                                |  16 +++
 21 files changed, 398 insertions(+), 290 deletions(-)
 create mode 100644 combo/manager/templates/combo/cell_appearance.html
 create mode 100644 tests/utils.py
combo/apps/maps/models.py
403 403
        fields = (
404 404
            'title',
405 405
            'initial_state',
406
            'initial_zoom',
407
            'min_zoom',
408
            'max_zoom',
409 406
            'group_markers',
410 407
            'marker_behaviour_onclick',
411 408
        )
412 409
        return forms.models.modelform_factory(self.__class__, fields=fields)
413 410

  
411
    def get_manager_tabs(self):
412
        zoom_fields = ['initial_zoom', 'min_zoom', 'max_zoom']
413
        tabs = super().get_manager_tabs()
414
        tabs.insert(
415
            1,
416
            {
417
                'slug': 'zoom',
418
                'name': _('Zoom'),
419
                'form': forms.models.modelform_factory(self.__class__, fields=zoom_fields),
420
            },
421
        )
422
        return tabs
423

  
414 424
    @classmethod
415 425
    def is_enabled(cls):
416 426
        return MapLayer.objects.exists()
combo/data/models.py
781 781

  
782 782
@python_2_unicode_compatible
783 783
class CellBase(models.Model, metaclass=CellMeta):
784
    # noqa pylint: disable=too-many-public-methods
784 785

  
785 786
    page = models.ForeignKey(Page, on_delete=models.CASCADE)
786 787
    placeholder = models.CharField(max_length=20)
......
803 804
    default_form_class = None
804 805
    manager_form_factory_kwargs = {}
805 806
    manager_form_template = 'combo/cell_form.html'
807
    manager_visibility_template = 'combo/cell_visibility.html'
808
    manager_appearance_template = 'combo/cell_appearance.html'
806 809
    children_placeholder_prefix = None
807 810

  
808 811
    visible = True
......
841 844
    def get_additional_label(self):
842 845
        return ''
843 846

  
847
    def get_manager_visibility_css_class(self):
848
        if self.public:
849
            return 'visibility-all' if not self.restricted_to_unlogged else ''
850
        elif self.restricted_to_unlogged:
851
            return 'visibility-off'
852
        return ''
853

  
854
    def get_manager_visibility_content(self):
855
        if self.public and not self.restricted_to_unlogged:
856
            return ''
857
        group_names = ', '.join([x.name for x in self.groups.all()])
858
        if group_names:
859
            return group_names
860
        return _('unlogged users') if self.restricted_to_unlogged else _('logged users')
861

  
844 862
    @property
845 863
    def legacy_class_name(self):
846 864
        # legacy class name used in some themes
......
1063 1081
    def get_label(self):
1064 1082
        return self.get_verbose_name()
1065 1083

  
1084
    def get_manager_tabs(self):
1085
        from combo.manager.forms import CellVisibilityForm
1086

  
1087
        tabs = []
1088
        form_class = self.get_default_form_class()
1089
        if form_class:
1090
            tabs.append(
1091
                {
1092
                    'slug': 'general',
1093
                    'name': _('General'),
1094
                    'template': self.manager_form_template,
1095
                    'form': form_class,
1096
                }
1097
            )
1098
        tabs.append(
1099
            {
1100
                'slug': 'visibility',
1101
                'name': _('Visibility'),
1102
                'template': self.manager_visibility_template,
1103
                'form': CellVisibilityForm,
1104
            }
1105
        )
1106
        tabs.append(
1107
            {
1108
                'slug': 'appearance',
1109
                'name': _('Appearance'),
1110
                'template': self.manager_appearance_template,
1111
                'form': self.get_appearance_form_class(),
1112
            }
1113
        )
1114
        return tabs
1115

  
1066 1116
    def get_default_form_fields(self):
1067 1117
        return [
1068 1118
            x.name
......
1083 1133
            )
1084 1134
        ]
1085 1135

  
1086
    def get_default_form_class(self):
1136
    def get_default_form_class(self, fields=None):
1087 1137
        if self.default_form_class:
1088 1138
            return self.default_form_class
1089 1139

  
1090
        fields = self.get_default_form_fields()
1091 1140
        if not fields:
1092
            return None
1141
            fields = self.get_default_form_fields()
1142
            if not fields:
1143
                return None
1093 1144

  
1094 1145
        return model_forms.modelform_factory(
1095 1146
            self.__class__, fields=fields, **self.manager_form_factory_kwargs
1096 1147
        )
1097 1148

  
1098
    def get_options_form_class(self):
1149
    def get_appearance_form_class(self):
1099 1150
        fields = ['slug', 'extra_css_class']
1100 1151
        widgets = None
1101 1152
        extra_templates = settings.COMBO_CELL_TEMPLATES.get(self.get_cell_type_str())
combo/manager/static/css/combo.manager.scss
1
// Only display content to screen readers
2
@mixin sr-only {
3
	position: absolute !important;
4
	width: 1px !important;
5
	height: 1px !important;
6
	padding: 0 !important;
7
	margin: -1px !important;
8
	overflow: hidden !important;
9
	clip: rect(0, 0, 0, 0) !important;
10
	white-space: nowrap !important;
11
	border: 0 !important;
12
}
13

  
1 14
body.no-header #header {
2 15
	display: none;
3 16
}
......
48 61
	margin: 1ex 0;
49 62
	box-shadow: rgba(0, 0, 0, 0.04) 0px 1px 1px 0px;
50 63
	position: relative;
64
	> div {
65
		display: none;
66
		overflow: hidden;
67
	}
68
	&.toggled > div {
69
		display: block;
70
	}
51 71
}
52 72

  
53 73
div.cell-list > div > div form {
54 74
	padding: 1ex;
55 75
}
56 76

  
57
div.cell-list > div > div {
58
	display: none;
59
	transition: max-height linear 0.2s;
60
	overflow: hidden;
61
}
62

  
63 77
.cell-form textarea {
64 78
	width: 100%;
65 79
}
66 80

  
67
div.cell-list > div.untoggled > div {
68
	display: block;
69
	max-height: 0;
70
}
71

  
72
div.cell-list > div.toggled > div {
73
	display: block;
74
}
75

  
76 81
div.cell > h3 {
77 82
	background: #fafafa;
78 83
	margin: 0;
......
132 137

  
133 138
div.cell h3 span.visibility-summary::before,
134 139
div.page span.visibility-summary::before {
135
	content: "\f06e"; /* fa-eye */
140
	content: "\f06e "; /* fa-eye */
136 141
	font-family: FontAwesome;
137 142
}
138 143

  
139 144
div.cell h3 span.visibility-summary.visibility-off::before {
140
	content: "\f070"; /* fa-eye-slash */
145
	content: "\f070 "; /* fa-eye-slash */
141 146
	font-family: FontAwesome;
142 147
}
143 148

  
149
div.cell h3 span.visibility-summary.visibility-all::before {
150
	content: none;
151
}
152

  
144 153
div.cell h3 span.visibility-summary {
145 154
	max-width: 30%;
146 155
}
......
167 176
	content: "\f106"; /* angle-up */
168 177
}
169 178

  
170
div.cell-list button.save {
171
	position: relative;
172
	right: 2ex;
173
	bottom: 1ex;
174
	float: right;
175
}
176

  
177

  
178
div.cell div.cell-buttons {
179
	clear: both;
180
	margin-top: 2em;
181
	margin-bottom: 1ex;
182
}
183

  
184 179
div.cell-list .empty-cell {
185 180
	list-style: none;
186 181
	margin: 0;
......
682 677
.search-engine-add {
683 678
	margin: 2em 0;
684 679
}
680

  
681
.cell-properties {
682
	.django-ckeditor-widget {
683
		display: block !important;
684
	}
685
	&--buttons {
686
		margin-top: 1em;
687
		display: flex;
688
		justify-content: space-between;
689
	}
690
	.duplicate-button {
691
		&::before {
692
			font-family: FontAwesome;
693
			content: "\f24d";  /* clone */
694
		}
695
		span {
696
			@include sr-only();
697
		}
698
	}
699
	.delete-button {
700
		&::before {
701
			font-family: FontAwesome;
702
			content: "\f014";  /* trash */
703
		}
704
		span {
705
			@include sr-only();
706
		}
707
	}
708
}
709

  
710
.cell.map .pk-tabs-container--content-panels .buttons {
711
	margin: 1em 0;
712
}
combo/manager/static/js/combo.manager.js
190 190
  });
191 191
}
192 192

  
193
function compute_max_height($cell) {
194
  var cell_id = $cell.attr('id');
195
  $('style#for-' + cell_id).remove();
196
  var h = $cell.find('h3 + div').height() + 40;
197
  h += $cell.find('.multisort').length * 250;
198
  h += $cell.find('.django-ckeditor-widget').length * 200;
199
  var style = '<style id="for-' + cell_id + '">div#' + cell_id + '.toggled h3 + div { max-height: '+h+'px; }</style>';
200
  $(style).appendTo('head');
201
}
202

  
203 193
$(function() {
204 194
  $('div.cell-list h3').on('click', function() {
205 195
    $(this).parent().toggleClass('toggled').toggleClass('untoggled');
......
270 260
  $('div.cell button.save').on('click', function(event) {
271 261
     var $button = $(this);
272 262
     var $form = $(this).closest('form');
263
     var $cell = $(this).closest('div.cell');
273 264
     var button_label = $button.text();
274 265
     for (instance in CKEDITOR.instances) {
275 266
        CKEDITOR.instances[instance].updateElement();
......
281 272
         beforeSend: function() { $button.attr('disabled', 'disabled'); },
282 273
         success: function(data) {
283 274
            $button.attr('disabled', null);
284
            if (data.indexOf('ckeditortype') == -1) {
285
              /* update form with new content, unless it has a ckeditor, as
286
               * this causes an unpleasant flickering */
287
              $button.parents('form').find('div.cell-form').html(data);
288
              compute_max_height($form.parents('div.cell'));
289
            }
290
            $form.parents('div.cell').trigger('combo:cellform-reloaded');
291
            if (data.indexOf('class="errorlist"') == -1) {
292
              $.getJSON($form.data('label-url'),
293
                function(data) {
294
                    $form.parents('div.cell').find('.additional-label i').text(data['label']);
295
                    if (data['invalid_reason']) {
296
                      if (! $form.parents('div.cell').find('.invalid').length) {
297
                        $('<span class="invalid"></span>').insertAfter($form.parents('div.cell').find('.additional-label'));
298
                      }
299
                      $form.parents('div.cell').find('.invalid').text(data['invalid_reason']);
300
                    } else {
301
                      $form.parents('div.cell').find('.invalid').remove();
302
                    }
275
            for (const tab_slug in data.tabs) {
276
              var $tab_content = $form.find('[data-tab-slug="' + tab_slug + '"]');
277
              if (data.tabs[tab_slug].indexOf('ckeditortype') == -1) {
278
                /* update form with new content, unless it has a ckeditor, as
279
                 * this causes an unpleasant flickering */
280
                $tab_content.html(data.tabs[tab_slug]);
281
              } else {
282
                for (const error in data.errorlist[tab_slug]) {
283
                  var $widget_p = $('[name="' + data.prefix + '-' + error).closest('p');
284
                  var $widget_ul = $('<ul class="errorlist"></ul>');
285
                  for (idx in data.errorlist[form_slug][error]) {
286
                    $widget_ul.append($('<li>', {text: data.errorlist[form_slug][error][idx]}));
287
                  }
288
                  $widget_p.before($widget_ul);
303 289
                }
304
              );
290
              }
291
            }
292
            // update title labels
293
            $cell.find('.visibility-summary').removeClass(
294
              ).addClass('visibility-summary').addClass(data.visibility_css_class
295
              ).text(data.visibility_content);
296
            function wrap(label, sign) {
297
              if (! label) return '';
298
              if (sign == '(') return '(' + label + ')';
299
              if (sign == '[') return '[' + label + ']';
300
              return label;
301
            }
302
            $cell.find('h3 .cell-slug').text(wrap(data.slug, '['));
303
            $cell.find('h3 .cell-template-label').text(wrap(data.template_label, '('));
304
            $cell.find('h3 .extra-css-class').text(wrap(data.extra_css_class, '['));
305
            $cell.find('h3 .additional-label i').text(wrap(data.additional_label));
306
            if (data['invalid_reason']) {
307
              if (! $cell.find('.invalid').length) {
308
                $('<span class="invalid"></span>').insertAfter($cell.find('.additional-label'));
309
              }
310
              $cell.find('.invalid').text(data['invalid_reason']);
311
            } else {
312
              $cell.find('.invalid').remove();
313
            }
314

  
315
            // select first panel with errors (if any)
316
            const $current_tab = $form.find('[role=tabpanel]:not([hidden])');
317
            if ($current_tab.find('.errorlist').length == 0) {
318
              const $panel_with_error = $form.find('[role=tabpanel] .errorlist').first().parents('[role=tabpanel]').first();
319
              if ($panel_with_error.length) {
320
                const $tab_button = $('[role=tab][aria-controls="' + $panel_with_error.attr('id') + '"]');
321
                window.selectTab($tab_button[0]);
322
              }
305 323
            }
306 324
         }
307 325
     });
......
334 352

  
335 353
  $('.cell.tipi-payment-form-cell select').on('change', function() {
336 354
      handle_tipi_form($(this));
337
      compute_max_height($(this).parents('div.cell'));
338 355
  });
339 356
  $('.cell.tipi-payment-form-cell select').trigger('change');
340 357

  
......
348 365

  
349 366
  $('div.cell').each(function(i, x) {
350 367
    const $cell = $(this);
351
    compute_max_height($cell);
352 368
    if (window.location.hash == '#' + $cell.attr('id')) {
353 369
      $cell.addClass('toggled');
354 370
    } else {
......
720 736

  
721 737
    this.grid_cell__add(schema_cell);
722 738
    $(this.cell).trigger("custom_cell:change");
723
    compute_max_height($(this.cell));
724 739
  },
725 740
  grid_cell__init: function() {
726 741
    if (!this.gridSchema_existing) return;
......
729 744
    this.gridSchema.cells.forEach(function(el){
730 745
      _self.grid_cell__add(el);
731 746
    });
732

  
733
    $(this.cell).on("cell:open", function(){
734
      compute_max_height($(this));
735
    })
736 747
  },
737 748
  // Init methods
738 749
  on: function() {
combo/manager/templates/combo/cell_appearance.html
1
{% load i18n %}
2
{{ appearance_form.as_p }}
3
{% if cell.can_have_assets %}
4
<p><a rel="popup" data-selector="div#assets-listing" href="{% url 'combo-manager-slot-assets' cell_reference=cell.get_reference %}"
5
     >{% trans 'Assets' %}</a></p>
6
{% endif %}
combo/manager/templates/combo/cell_visibility.html
1
{% extends "combo/manager_base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
<h2>{% trans 'Cell Visibility' %}</h2>
6
{% endblock %}
7

  
8
{% block content %}
9

  
10
<form method="post" enctype="multipart/form-data" id="visibility-form">
11
  {% csrf_token %}
12
  {{ form.as_p }}
13
  <div class="buttons">
14
    <button class="submit-button">{% trans "Save" %}</button>
15
    {% if object.id %}
16
    <a class="cancel" href="{{ object.get_absolute_url }}">{% trans 'Cancel' %}</a>
17
    {% else %}
18
    <a class="cancel" href="{% url 'combo-manager-homepage' %}">{% trans 'Cancel' %}</a>
19
    {% endif %}
20
  </div>
1
<div id="visibility-form-{{cell.get_reference}}">
2
  {{ visibility_form.as_p }}
21 3
  <script>
22 4
  $(function() {
23
    var $form = $('#visibility-form');
5
    var $form = $('#visibility-form-{{cell.get_reference}}');
24 6
    $form.find('select').first().on('change', function() {
25 7
      var val = $(this).val();
26 8
      var $groups = $form.find('select').last().parent('p');
......
33 15
    $form.find('select').first().trigger('change');
34 16
  });
35 17
  </script>
36
</form>
37

  
38
{% endblock %}
18
</div>
combo/manager/templates/combo/manager_edit_cell_block.html
1 1
{% load i18n %}
2 2
{% block cell-form %}
3
<form action="{{ url }}" method="post" data-label-url="{% url 'combo-manager-page-get-additional-label' page_pk=page.id cell_reference=cell.get_reference %}">
4
{% csrf_token %}
5
{% if form %}
6
  <div class="cell-form">
7
    {% block cell-form-content %}
8
      {% include cell.manager_form_template %}
9
    {% endblock %}
3
<div class="cell-properties pk-tabs-container">
4
  <div class="pk-tabs-container--tablist" role="tablist" aria-label="{% trans "Cell Properties" %}">
5
   {% for tab in manager_tabs %}
6
   <button role="tab"
7
      aria-selected="{{ forloop.first|yesno:"true,false" }}"
8
      aria-controls="panel-{{cell.get_reference}}-{{ forloop.counter }}"
9
      id="tab-{{cell.get_reference}}-{{ forloop.counter }}"
10
      tabindex="{{ forloop.first|yesno:"0,-1" }}">{{ tab.name }}</button>
11
   {% endfor %}
10 12
  </div>
11
{% else %}
12
  <p>{% trans "There are no options for this cell." %}</p>
13
{% endif %}
14
{% endblock %}
15
<div class="cell-buttons">
13

  
14
<form class="pk-tabs-container--panels" action="{{ url }}" method="post">
15
{% csrf_token %}
16
   {% for tab in manager_tabs %}
17
   <div id="panel-{{cell.get_reference}}-{{forloop.counter}}"
18
        role="tabpanel" tabindex="0" {% if not forloop.first %}hidden{% endif %}
19
        data-tab-slug="{{ tab.slug }}"
20
        aria-labelledby="tab-{{cell.get_reference}}-{{ forloop.counter }}">
21
     {% if tab.template %}{% include tab.template %}{% else %}{{ tab.form_instance.as_p }}{% endif %}
22
   </div>
23
   {% endfor %}
24
<div class="cell-properties--buttons">
16 25
{% block cell-buttons %}
17
<a rel="popup" href="{% url 'combo-manager-page-delete-cell' page_pk=page.id cell_reference=cell.get_reference %}">{% trans 'Delete' %}</a> |
18
<a rel="popup" href="{% url 'combo-manager-page-visibility-cell' page_pk=page.id cell_reference=cell.get_reference %}">{% trans 'Visibility' %}</a> |
19
<a rel="popup" href="{% url 'combo-manager-page-options-cell' page_pk=page.id cell_reference=cell.get_reference %}">{% trans 'Options' %}</a> |
20
{% if cell.can_have_assets %}<a rel="popup" data-selector="div#assets-listing" href="{% url 'combo-manager-slot-assets' cell_reference=cell.get_reference %}">{% trans 'Assets' %}</a> |{% endif %}
21
<a rel="popup" href="{% url 'combo-manager-page-duplicate-cell' page_pk=page.id cell_reference=cell.get_reference %}">{% trans 'Duplicate' %}</a> |
22
<a class="close-button" href="#">{% trans 'Close' %}</a>
23
{% if form %}
24
<button class="save submit-button">{% trans 'Save' %}</button>
25
{% endif %}
26
<button class="submit-button save">{% trans 'Save' %}</button>
27
<span>
28
<a class="pk-button duplicate-button" rel="popup" title="{% trans 'Duplicate' %}"
29
   href="{% url 'combo-manager-page-duplicate-cell' page_pk=page.id cell_reference=cell.get_reference %}"
30
   ><span>{% trans 'Duplicate' %}</span></a>
31
<a class="pk-button delete-button" rel="popup" title="{% trans 'Delete' %}"
32
   href="{% url 'combo-manager-page-delete-cell' page_pk=page.id cell_reference=cell.get_reference %}"
33
   ><span>{% trans 'Delete' %}</span></a>
34
</span>
26 35
{% endblock %}
27 36
</div>
28 37
</form>
38
</div>
39
{% endblock %}
combo/manager/templates/combo/page_view.html
161 161
      <div id="cell-{{cell.get_reference}}" class="cell {{cell.class_name}}" data-cell-reference="{{ cell.get_reference }}">
162 162
        <h3><span class="handle">⣿</span>
163 163
                <span class="group1">
164
                        {{ cell.get_label }}
165
                        {% if cell.template_name %} ({{cell.get_template_label}}){% endif %}
166
                        {% if cell.slug %} [{{cell.slug}}] {% endif %}
167
                  {% if cell.extra_css_class %}
168
                  <span class="extra-css-class">[{{ cell.extra_css_class }}]</span>
169
                  {% endif %}
164
                   {{ cell.get_label }}
165
                   <span class="cell-template-label"
166
                     >{% if cell.template_name %}({{cell.get_template_label}}){% endif %}</span>
167
                   <span class="cell-slug">{% if cell.slug %}[{{cell.slug}}]{% endif %}</span>
168
                   <span class="extra-css-class">{% if cell.extra_css_class %}[{{ cell.extra_css_class }}]{% endif %}</span>
170 169
                  <span class="additional-label"><i>{{cell.get_additional_label|default_if_none:""}}</i></span>
171 170
                  {% if cell.get_invalid_reason %}
172 171
                  <span class="invalid">{{ cell.get_invalid_reason }}{% if cell.class_name != 'link-list-cell' %} -
......
178 177
                    {% endif %}</span>
179 178
                  {% endif %}
180 179
                </span>
181
                {% if not cell.public %}
182
                  <span class="visibility-summary
183
                    {% if cell.restricted_to_unlogged %}visibility-off{% endif %}" title="{% trans 'Restricted visibility' %}">
184
                  {% if cell.groups.all|length %}
185
                  {% for group in cell.groups.all %}{{group.name}}{% if not forloop.last %}, {% endif %}{% endfor %}
186
                  {% else %}
187
                  {% trans "logged users" %}
188
                  {% endif %}
189
                  </span>
190
                {% elif cell.restricted_to_unlogged %}
191
                  <span class="visibility-summary" title="{% trans 'Restricted visibility' %}">
192
                  {% trans "unlogged users" %}
193
                  </span>
194
                {% endif %}
180
                <span class="visibility-summary {{ cell.get_manager_visibility_css_class }}"
181
                      title="{% trans 'Restricted visibility' %}"
182
                        >{{ cell.get_manager_visibility_content }}</span>
195 183
        </h3>
196 184
        <div>{% cell_form cell %}</div>
197 185
      </div>
combo/manager/templatetags/cells.py
26 26
        'combo-manager-page-edit-cell',
27 27
        kwargs={'page_pk': cell.page_id, 'cell_reference': cell.get_reference()},
28 28
    )
29
    form_class = cell.get_default_form_class()
30
    if cell.is_enabled() and form_class:
31
        context['form'] = form_class(instance=cell, prefix='c%s' % cell.get_reference())
32
    else:
33
        context['form'] = None
29
    context['manager_tabs'] = cell.get_manager_tabs()
30
    for tab in context['manager_tabs']:
31
        if tab['slug'] == 'general':
32
            form_name = 'form'
33
            if not cell.is_enabled():
34
                continue
35
        else:
36
            form_name = '%s_form' % tab['slug']
37
        tab['form_instance'] = tab['form'](initial={}, instance=cell, prefix='c%s' % cell.get_reference())
38
        context[form_name] = tab['form_instance']
34 39
    context['cell'] = cell
35 40
    cell_form_template = template.loader.get_template('combo/manager_edit_cell_block.html')
36 41
    with context.push():
combo/manager/urls.py
116 116
        views.page_duplicate_cell,
117 117
        name='combo-manager-page-duplicate-cell',
118 118
    ),
119
    url(
120
        r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/options$',
121
        views.page_cell_options,
122
        name='combo-manager-page-options-cell',
123
    ),
124
    url(
125
        r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/visibility$',
126
        views.page_cell_visibility,
127
        name='combo-manager-page-visibility-cell',
128
    ),
129
    url(
130
        r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/label$',
131
        views.page_get_additional_label,
132
        name='combo-manager-page-get-additional-label',
133
    ),
134 119
    url(
135 120
        r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/add-link/(?P<link_code>[\w-]+)$',
136 121
        views.page_list_cell_add_link,
combo/manager/views.py
20 20
import tarfile
21 21
from operator import attrgetter, itemgetter
22 22

  
23
from django import template
23 24
from django.conf import settings
24 25
from django.contrib import messages
25 26
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
26 27
from django.db import transaction
27
from django.http import Http404, HttpResponse, HttpResponseRedirect
28
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
28 29
from django.shortcuts import get_object_or_404, redirect, render
30
from django.template import engines
29 31
from django.urls import reverse, reverse_lazy
30 32
from django.utils.encoding import force_bytes, force_text
31 33
from django.utils.formats import date_format
......
61 63

  
62 64
from .forms import (
63 65
    CellDuplicateForm,
64
    CellVisibilityForm,
65 66
    PageAddForm,
66 67
    PageDuplicateForm,
67 68
    PageEditDescriptionForm,
......
664 665

  
665 666

  
666 667
class PageEditCellView(ManagedPageMixin, UpdateView):
668
    http_method_names = ['post']
669

  
667 670
    def get_template_names(self):
668 671
        return [self.template_name or self.object.manager_form_template]
669 672

  
......
685 688
    def get_prefix(self):
686 689
        return 'c%s' % self.kwargs.get('cell_reference')
687 690

  
688
    def get_form_class(self):
689
        return self.object.get_default_form_class()
691
    def post(self, request, *args, **kwargs):
692
        self.object = self.get_object()
693
        response = {'errorlist': {}, 'tabs': {}, 'prefix': self.get_prefix()}
694
        context = {}
695
        context['page'] = self.object.page
696
        context['cell'] = self.object
697
        context.update(self.object.get_extra_manager_context())
698
        tab_error_forms = {}
690 699

  
691
    def get_success_url(self):
692
        return (
693
            reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')})
694
            + '#cell-'
695
            + self.object.get_reference()
696
        )
700
        class Rollback(Exception):
701
            pass
702

  
703
        try:
704
            with transaction.atomic():
705
                for tab in self.object.get_manager_tabs():
706
                    form = tab['form'](**self.get_form_kwargs())
707
                    if form.is_valid():
708
                        self.object = form.save()
709
                    else:
710
                        tab_error_forms[tab['slug']] = form
711
                        response['errorlist'][tab['slug']] = form.errors
712
                        self.object.refresh_from_db()
713
                if response['errorlist']:
714
                    raise Rollback()
715
        except Rollback:
716
            pass
717

  
718
        for tab in self.object.get_manager_tabs():
719
            # if current form had no errors, create it anew
720
            # so it can get new dynamic fields
721
            form = tab_error_forms.get(tab['slug']) or tab['form'](**self.get_form_kwargs())
722
            if tab['slug'] == 'general':
723
                form_name = 'form'
724
            else:
725
                form_name = '%s_form' % tab['slug']
726
            context[form_name] = form
727
            if tab.get('template'):
728
                cell_form_template = template.loader.get_template(tab['template'])
729
            else:
730
                cell_form_template = engines['django'].from_string('{{ %s.as_p }}' % form_name)
731
            response['tabs'][tab['slug']] = cell_form_template.render(context)
732

  
733
        response['visibility_css_class'] = self.object.get_manager_visibility_css_class()
734
        response['visibility_content'] = self.object.get_manager_visibility_content()
735
        response['extra_css_class'] = self.object.extra_css_class
736
        response['slug'] = self.object.slug
737
        response['template_label'] = self.object.get_template_label()
738
        response['additional_label'] = self.object.get_additional_label()
739
        response['invalid_reason'] = self.object.get_invalid_reason()
740

  
741
        if not response['errorlist']:
742
            PageSnapshot.take(
743
                self.object.page, request=self.request, comment=_('changed cell "%s"') % self.object
744
            )
745
        return JsonResponse(response)
697 746

  
698 747
    def form_valid(self, form):
699 748
        if self.request.is_ajax():
......
787 836
page_duplicate_cell = PageDuplicateCellView.as_view()
788 837

  
789 838

  
790
class PageCellVisibilityView(PageEditCellView):
791
    template_name = 'combo/cell_visibility.html'
792

  
793
    def get_form_class(self):
794
        return CellVisibilityForm
795

  
796

  
797
page_cell_visibility = PageCellVisibilityView.as_view()
798

  
799

  
800
class PageCellOptionsView(PageEditCellView):
801
    template_name = 'combo/cell_options.html'
802

  
803
    def get_form_class(self):
804
        return self.object.get_options_form_class()
805

  
806

  
807
page_cell_options = PageCellOptionsView.as_view()
808

  
809

  
810 839
class PageCellOrder(ManagedPageMixin, View):
811 840
    def get(self, *args, **kwargs):
812 841
        request = self.request
......
877 906
    return redirect(reverse('combo-manager-homepage'))
878 907

  
879 908

  
880
def page_get_additional_label(request, page_pk, cell_reference):
881
    cell = CellBase.get_cell(cell_reference, page_id=page_pk)
882
    response = HttpResponse(content_type='application/json')
883
    json.dump(
884
        {
885
            'label': force_text(cell.get_additional_label()) or '',
886
            'invalid_reason': force_text(cell.get_invalid_reason() or ''),
887
        },
888
        response,
889
    )
890
    return response
891

  
892

  
893 909
def menu_json(request):
894 910
    if settings.TEMPLATE_VARS.get('site_title'):
895 911
        label = _('Editing "%(site_title)s"') % settings.TEMPLATE_VARS
combo/settings.py
186 186
        ],
187 187
        'toolbar': 'Own',
188 188
        'resize_enabled': False,
189
        'width': '100%',
189 190
    },
190 191
}
191 192

  
combo/utils/forms.py
65 65

  
66 66
    def value_from_datadict(self, data, files, name):
67 67
        if isinstance(data, MultiValueDict):
68
            return {'data': data.getlist(name)}
68
            return json.dumps({'data': data.getlist(name)})
69 69
        return data.get(name, None)
70 70

  
71 71
    def format_value(self, value):
tests/test_dataviz.py
15 15
from combo.utils.spooler import refresh_statistics_data
16 16

  
17 17
from .test_public import login
18
from .utils import manager_submit_cell
18 19

  
19 20
pytestmark = pytest.mark.django_db
20 21

  
......
1296 1297
        ('combo', False, 'Combo'),
1297 1298
    ]
1298 1299
    resp.form[field_prefix + 'service'] = ''
1299
    resp = resp.form.submit().follow()
1300
    manager_submit_cell(resp.form)
1300 1301
    assert resp.form[field_prefix + 'service'].value == ''
1301 1302

  
1302 1303
    resp.form[field_prefix + 'ou'] = 'default'
1303
    resp = resp.form.submit().follow()
1304
    manager_submit_cell(resp.form)
1304 1305
    assert resp.form[field_prefix + 'ou'].value == 'default'
1305 1306
    cell.refresh_from_db()
1306 1307
    assert cell.get_filter_params() == {'ou': 'default', 'time_interval': 'month'}
......
1318 1319
    ]
1319 1320

  
1320 1321
    resp.form[field_prefix + 'ou'] = ''
1321
    resp = resp.form.submit().follow()
1322
    manager_submit_cell(resp.form)
1322 1323
    assert resp.form[field_prefix + 'ou'].value == ''
1323 1324
    cell.refresh_from_db()
1324 1325
    assert cell.get_filter_params() == {'time_interval': 'month'}
1325 1326

  
1326 1327
    resp.form[field_prefix + 'time_range'] = 'previous-year'
1327
    resp = resp.form.submit().follow()
1328
    manager_submit_cell(resp.form)
1328 1329
    cell.refresh_from_db()
1329 1330
    assert cell.time_range == 'previous-year'
1330 1331

  
1331 1332
    resp.form[field_prefix + 'time_range'] = 'range'
1332 1333
    resp.form[field_prefix + 'time_range_start'] = '2020-10-01'
1333 1334
    resp.form[field_prefix + 'time_range_end'] = '2020-11-03'
1334
    resp = resp.form.submit().follow()
1335
    manager_submit_cell(resp.form)
1335 1336
    cell.refresh_from_db()
1336 1337
    assert cell.time_range == 'range'
1337 1338
    assert cell.time_range_start == date(year=2020, month=10, day=1)
......
1339 1340

  
1340 1341
    resp.form[field_prefix + 'time_range_start'] = ''
1341 1342
    resp.form[field_prefix + 'time_range_end'] = ''
1342
    resp = resp.form.submit().follow()
1343
    manager_submit_cell(resp.form)
1343 1344
    cell.refresh_from_db()
1344 1345
    assert cell.time_range_start is None
1345 1346
    assert cell.time_range_end is None
1346 1347

  
1347 1348
    no_filters_stat = Statistic.objects.get(slug='two-series')
1348 1349
    resp.form[field_prefix + 'statistic'] = no_filters_stat.pk
1349
    resp = resp.form.submit().follow()
1350
    manager_submit_cell(resp.form)
1350 1351
    assert resp.form[field_prefix + 'statistic'].value == str(no_filters_stat.pk)
1351 1352
    assert field_prefix + 'time_interval' not in resp.form.fields
1352 1353
    assert field_prefix + 'ou' not in resp.form.fields
......
1356 1357

  
1357 1358
    filter_multiple_stat = Statistic.objects.get(slug='filter-multiple')
1358 1359
    resp.form[field_prefix + 'statistic'] = filter_multiple_stat.pk
1359
    resp = resp.form.submit().follow()
1360
    manager_submit_cell(resp.form)
1360 1361
    resp.form[field_prefix + 'color'].select_multiple(texts=['Blue', 'Green'])
1361
    resp = resp.form.submit().follow()
1362
    manager_submit_cell(resp.form)
1362 1363
    assert resp.form[field_prefix + 'color'].value == ['green', 'blue']
1363 1364
    cell.refresh_from_db()
1364 1365
    assert cell.filter_params == {'color': ['green', 'blue']}
......
1376 1377
    ]
1377 1378

  
1378 1379
    resp.form[field_prefix + 'color'].select_multiple(texts=[])
1379
    resp = resp.form.submit().follow()
1380
    manager_submit_cell(resp.form)
1380 1381
    assert resp.form[field_prefix + 'color'].value is None
1381 1382
    cell.refresh_from_db()
1382 1383
    assert cell.get_filter_params() == {}
......
1440 1441

  
1441 1442
    # choice with no subfilter
1442 1443
    resp.form[field_prefix + 'form'] = 'contact'
1443
    resp = resp.form.submit().follow()
1444
    manager_submit_cell(resp.form)
1444 1445

  
1445 1446
    assert len(new_api_mock.call['requests']) == 1
1446 1447
    assert 'menu' not in resp.form.fields
1447 1448

  
1448 1449
    resp.form[field_prefix + 'form'] = 'error'
1449
    resp = resp.form.submit().follow()
1450
    manager_submit_cell(resp.form)
1450 1451

  
1451 1452
    assert len(new_api_mock.call['requests']) == 2
1452 1453
    assert 'menu' not in resp.form.fields
1453 1454

  
1454 1455
    # choice with subfilter
1455 1456
    resp.form[field_prefix + 'form'] = 'food-request'
1456
    resp = resp.form.submit().follow()
1457
    manager_submit_cell(resp.form)
1457 1458

  
1458 1459
    assert len(new_api_mock.call['requests']) == 3
1459 1460
    menu_field = resp.form[field_prefix + 'menu']
......
1465 1466
    ]
1466 1467

  
1467 1468
    resp.form[field_prefix + 'menu'] = 'meat'
1468
    resp = resp.form.submit().follow()
1469
    manager_submit_cell(resp.form)
1469 1470
    assert resp.form[field_prefix + 'menu'].value == 'meat'
1470 1471
    cell.refresh_from_db()
1471 1472
    assert cell.get_filter_params() == {'form': 'food-request', 'menu': 'meat'}
1472 1473

  
1473 1474
    # choice with no subfilter
1474 1475
    resp.form[field_prefix + 'form'] = 'contact'
1475
    resp = resp.form.submit().follow()
1476
    manager_submit_cell(resp.form)
1476 1477

  
1477 1478
    assert len(new_api_mock.call['requests']) == 4
1478 1479
    assert 'menu' not in resp.form.fields
......
1481 1482

  
1482 1483
    # changing another filter doesn't trigger request
1483 1484
    resp.form[field_prefix + 'other'] = 'one'
1484
    resp = resp.form.submit().follow()
1485
    resp = resp.form.submit()
1485 1486
    assert len(new_api_mock.call['requests']) == 4
1486 1487

  
1487 1488

  
......
1500 1501
    resp.form[field_prefix + 'time_range'] = 'range-template'
1501 1502
    resp.form[field_prefix + 'time_range_start_template'] = 'today|add_days:"7"|adjust_to_week_monday'
1502 1503
    resp.form[field_prefix + 'time_range_end_template'] = 'now|add_days:"14"|adjust_to_week_monday'
1503
    resp = resp.form.submit().follow()
1504
    manager_submit_cell(resp.form)
1504 1505
    cell.refresh_from_db()
1505 1506
    assert cell.time_range == 'range-template'
1506 1507
    assert cell.time_range_start_template == 'today|add_days:"7"|adjust_to_week_monday'
......
1508 1509

  
1509 1510
    resp.form[field_prefix + 'time_range_start_template'] = ''
1510 1511
    resp.form[field_prefix + 'time_range_end_template'] = ''
1511
    resp = resp.form.submit().follow()
1512
    manager_submit_cell(resp.form)
1512 1513
    cell.refresh_from_db()
1513 1514
    assert cell.time_range_start_template == ''
1514 1515
    assert cell.time_range_end_template == ''
1515 1516

  
1516 1517
    resp.form[field_prefix + 'time_range_start_template'] = 'xxx'
1517
    resp = resp.form.submit()
1518
    manager_submit_cell(resp.form, expect_errors=True)
1518 1519
    assert 'Template does not evaluate to a valid date.' in resp.text
1519 1520

  
1520 1521
    resp = app.get('/manage/pages/%s/' % page.id)
1521 1522
    resp.form[field_prefix + 'time_range_start_template'] = 'today|xxx'
1522
    resp = resp.form.submit()
1523
    manager_submit_cell(resp.form, expect_errors=True)
1523 1524
    assert 'Invalid filter' in resp.text
1524 1525

  
1525 1526
    resp = app.get('/manage/pages/%s/' % page.id)
1526 1527
    resp.form[field_prefix + 'time_range_start_template'] = 'today|date:xxx'
1527
    resp = resp.form.submit()
1528
    manager_submit_cell(resp.form, expect_errors=True)
1528 1529
    assert 'Failed lookup for key [xxx]' in resp.text
1529 1530

  
1530 1531

  
......
1539 1540
    field_prefix = 'cdataviz_chartngcell-%s-' % cell.id
1540 1541
    resp.form[field_prefix + 'statistic'] = statistic.pk
1541 1542
    resp = app.post(resp.form.action, params=resp.form.submit_fields(), xhr=True)
1542
    assert 'time_interval' in resp.text
1543
    assert 'time_interval' in resp.json['tabs']['general']
1543 1544

  
1544 1545

  
1545 1546
@with_httmock(new_api_mock)
......
1575 1576
    ]
1576 1577

  
1577 1578
    resp.form[field_prefix + 'ou'] = 'variable:foo'
1578
    resp = resp.form.submit().follow()
1579
    manager_submit_cell(resp.form)
1579 1580
    assert resp.form[field_prefix + 'ou'].value == 'variable:foo'
1580 1581
    cell.refresh_from_db()
1581 1582
    assert cell.filter_params['ou'] == 'variable:foo'
......
2043 2044
        ('year', False, 'Year'),
2044 2045
        ('weekday', False, 'Week day'),
2045 2046
    ]
2046
    resp.form.submit()
2047
    manager_submit_cell(resp.form)
2047 2048
    cell.refresh_from_db()
2048 2049

  
2049 2050
    chart = cell.get_chart()
......
2053 2054
    assert chart.raw_series[0][0][:8] == [0, 0, 0, 0, 0, 0, 0, 1]
2054 2055
    assert chart.raw_series[1][0][:8] == [2, 0, 0, 0, 0, 0, 0, 2]
2055 2056

  
2057
    time_interval_field = resp.form['cdataviz_chartngcell-%s-time_interval' % cell.id]
2056 2058
    time_interval_field.value = 'month'
2057
    resp = resp.form.submit().follow()
2059
    manager_submit_cell(resp.form)
2058 2060
    time_interval_field = resp.form['cdataviz_chartngcell-%s-time_interval' % cell.id]
2059 2061
    assert time_interval_field.options == [
2060 2062
        ('day', False, 'Day'),
......
2075 2077
        ([4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], {'title': 'Serie 2'}),
2076 2078
    ]
2077 2079

  
2080
    time_interval_field = resp.form['cdataviz_chartngcell-%s-time_interval' % cell.id]
2078 2081
    time_interval_field.value = 'year'
2079 2082
    resp.form.submit()
2080 2083
    cell.refresh_from_db()
......
2087 2090
        ([5, 0, 0], {'title': 'Serie 2'}),
2088 2091
    ]
2089 2092

  
2093
    time_interval_field = resp.form['cdataviz_chartngcell-%s-time_interval' % cell.id]
2090 2094
    time_interval_field.value = 'weekday'
2091
    resp.form.submit()
2095
    manager_submit_cell(resp.form)
2092 2096
    cell.refresh_from_db()
2093 2097

  
2094 2098
    chart = cell.get_chart()
......
2099 2103
        ([1, 4, 0, 0, 0, 0, 0], {'title': 'Serie 2'}),
2100 2104
    ]
2101 2105

  
2106
    time_interval_field = resp.form['cdataviz_chartngcell-%s-time_interval' % cell.id]
2102 2107
    time_interval_field.value = 'week'
2103
    resp.form.submit()
2108
    manager_submit_cell(resp.form)
2104 2109
    cell.refresh_from_db()
2105 2110

  
2106 2111
    chart = cell.get_chart()
......
2115 2120

  
2116 2121
    cell.statistic = Statistic.objects.get(slug='leap-week')
2117 2122
    cell.save()
2118
    resp = app.get('/manage/pages/%s/' % page.id)
2119
    resp.form.submit()
2123
    manager_submit_cell(resp.form)
2120 2124
    chart = cell.get_chart()
2121 2125
    assert 'time_interval=day' in new_api_mock.call['requests'][1].url
2122 2126
    assert len(chart.x_labels) == 1
......
2335 2339
    # select a choice with subfilters in manager
2336 2340
    resp = app.get('/manage/pages/%s/' % page.id)
2337 2341
    resp.forms[1]['cdataviz_chartngcell-%s-form' % cell.id] = 'food-request'
2338
    resp = resp.forms[1].submit().follow()
2342
    manager_submit_cell(resp.forms[1])
2339 2343

  
2340 2344
    resp = app.get('/')
2341 2345
    assert 'form' in resp.form.fields
tests/test_family.py
9 9
from combo.data.models import Page
10 10

  
11 11
from .test_manager import login
12
from .utils import manager_submit_cell
12 13

  
13 14
pytestmark = pytest.mark.django_db
14 15

  
......
63 64
    resp.forms[0]['c%s-agenda_categories' % cell.get_reference()].value = 'bar,foo'
64 65
    resp.forms[0]['c%s-start_date_filter' % cell.get_reference()].value = '{{ start_date }}'
65 66
    resp.forms[0]['c%s-end_date_filter' % cell.get_reference()].value = '{{ end_date }}'
66
    resp = resp.forms[0].submit().follow()
67
    manager_submit_cell(resp.form)
67 68
    cell.refresh_from_db()
68 69
    assert cell.agenda_type == 'subscribed'
69 70
    assert cell.agenda_references_template == ''
......
74 75
    resp.forms[0]['c%s-agenda_type' % cell.get_reference()].value = 'manual'
75 76
    resp.forms[0]['c%s-agenda_references_template' % cell.get_reference()].value = 'foo,bar'
76 77
    resp.forms[0]['c%s-agenda_categories' % cell.get_reference()].value = 'bar,foo'
77
    resp = resp.forms[0].submit().follow()
78
    manager_submit_cell(resp.form)
78 79
    cell.refresh_from_db()
79 80
    assert cell.agenda_type == 'manual'
80 81
    assert cell.agenda_references_template == 'foo,bar'
tests/test_gallery_cell.py
26 26
    )
27 27

  
28 28
    app = login(app)
29
    resp = app.get(
30
        reverse(
31
            'combo-manager-page-edit-cell',
32
            kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()},
33
        ),
34
        status=200,
35
    )
29
    resp = app.get('/manage/pages/%s/' % page.id)
36 30
    assert '<ul class="gallery"' in resp.text
37 31
    assert 'js/combo.gallery.js' in resp.text
38 32

  
tests/test_kb.py
22 22
from combo.apps.kb.models import LatestPageUpdatesCell
23 23
from combo.data.models import CellBase, Page, TextCell
24 24

  
25
from .utils import manager_submit_cell
26

  
25 27
pytestmark = pytest.mark.django_db
26 28

  
27 29

  
......
49 51
    resp = resp.follow()
50 52
    cells = CellBase.get_cells(page_id=page.id)
51 53
    assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text
52
    resp = resp.forms[0].submit()
53
    assert resp.status_int == 302
54
    manager_submit_cell(resp.forms[0])
54 55

  
55 56
    resp = app.get('/example-page/', status=200)
56 57
    div = resp.html.find('div', attrs={'class': 'latest-page-updates-cell'})
tests/test_manager.py
46 46
)
47 47
from combo.manager.forms import PageAddForm, PageSelectTemplateForm
48 48

  
49
from .utils import manager_submit_cell
50

  
49 51
pytestmark = pytest.mark.django_db
50 52

  
51 53
TESTS_DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
......
699 701
    app.get('/manage/pages/%s/' % page.pk)  # load once to populate caches
700 702
    with CaptureQueriesContext(connection) as ctx:
701 703
        app.get('/manage/pages/%s/' % page.pk)
702
    assert len(ctx.captured_queries) == 33
704
    assert len(ctx.captured_queries) == 41
703 705

  
704 706

  
705 707
def test_delete_page(app, admin_user):
......
1269 1271
    resp = app.get('/manage/pages/%s/' % page.id)
1270 1272
    assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text
1271 1273
    resp.forms[0]['c%s-text' % cells[0].get_reference()].value = 'Hello world'
1272
    resp = resp.forms[0].submit()
1273
    assert resp.status_int == 302
1274
    assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.id, cells[0].get_reference()))
1274
    manager_submit_cell(resp.forms[0])
1275 1275

  
1276 1276
    resp = app.get('/manage/pages/%s/' % page.id)
1277 1277
    assert resp.forms[0]['c%s-text' % cells[0].get_reference()].value == 'Hello world'
......
1279 1279
    resp = app.get('/manage/pages/%s/' % page.id)
1280 1280
    assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text
1281 1281
    resp.forms[0]['c%s-text' % cells[0].get_reference()].value = 'World Hello'
1282
    resp = resp.forms[0].submit(xhr=True)
1283
    assert resp.status_int == 200
1284
    assert resp.text.strip().startswith('<p><label')
1282
    manager_submit_cell(resp.forms[0])
1283
    assert resp.forms[0]['c%s-text' % cells[0].get_reference()].value == 'World Hello'
1285 1284

  
1286 1285
    resp = app.get('/manage/pages/%s/' % page.id)
1287 1286
    assert resp.forms[0]['c%s-text' % cells[0].get_reference()].value == 'World Hello'
......
1440 1439

  
1441 1440
    app = login(app)
1442 1441
    resp = app.get('/manage/pages/%s/' % page.id)
1443
    resp = resp.click(href='/data_textcell-%s/visibility' % cell.id)
1444 1442
    assert resp.form['cdata_textcell-%s-visibility' % cell.id].value == 'all'
1445 1443
    resp.form['cdata_textcell-%s-visibility' % cell.id] = 'logged'
1446 1444
    resp = resp.form.submit('submit')
......
1449 1447
    assert TextCell.objects.get(id=cell.id).groups.count() == 0
1450 1448

  
1451 1449
    resp = app.get('/manage/pages/%s/' % page.id)
1452
    resp = resp.click(href='/data_textcell-%s/visibility' % cell.id)
1453 1450
    assert resp.form['cdata_textcell-%s-visibility' % cell.id].value == 'logged'
1454 1451
    resp.form['cdata_textcell-%s-visibility' % cell.id] = 'unlogged'
1455 1452
    resp = resp.form.submit('submit')
......
1458 1455
    assert TextCell.objects.get(id=cell.id).groups.count() == 0
1459 1456

  
1460 1457
    resp = app.get('/manage/pages/%s/' % page.id)
1461
    resp = resp.click(href='/data_textcell-%s/visibility' % cell.id)
1462 1458
    assert resp.form['cdata_textcell-%s-visibility' % cell.id].value == 'unlogged'
1463 1459
    resp.form['cdata_textcell-%s-visibility' % cell.id] = 'all'
1464 1460
    resp = resp.form.submit('submit')
......
1472 1468
    group2.save()
1473 1469

  
1474 1470
    resp = app.get('/manage/pages/%s/' % page.id)
1475
    resp = resp.click(href='/data_textcell-%s/visibility' % cell.id)
1476 1471
    resp.form['cdata_textcell-%s-visibility' % cell.id] = 'groups-any'
1477 1472
    resp.form['cdata_textcell-%s-groups' % cell.id] = [group1.id]
1478 1473
    resp = resp.form.submit('submit')
......
1482 1477
    assert TextCell.objects.get(id=cell.id).groups.all()[0].name == 'A group'
1483 1478

  
1484 1479
    resp = app.get('/manage/pages/%s/' % page.id)
1485
    resp = resp.click(href='/data_textcell-%s/visibility' % cell.id)
1486 1480
    resp.form['cdata_textcell-%s-visibility' % cell.id] = 'groups-none'
1487 1481
    resp.form['cdata_textcell-%s-groups' % cell.id] = [group2.id]
1488 1482
    resp = resp.form.submit('submit')
......
1501 1495

  
1502 1496
    app = login(app)
1503 1497
    resp = app.get('/manage/pages/%s/' % page.id)
1504
    resp = resp.click(href='/data_textcell-%s/options' % cell.id)
1505 1498
    assert resp.form['cdata_textcell-%s-slug' % cell.id].value == ''
1506 1499
    assert resp.form['cdata_textcell-%s-extra_css_class' % cell.id].value == ''
1507 1500
    resp.form['cdata_textcell-%s-slug' % cell.id] = 'SLUG'
......
1510 1503
    assert 'SLUG' in app.get('/manage/pages/%s/' % page.id)
1511 1504

  
1512 1505
    resp = app.get('/manage/pages/%s/' % page.id)
1513
    resp = resp.click(href='/data_textcell-%s/options' % cell.id)
1514 1506
    resp.form['cdata_textcell-%s-slug' % cell.id] = ''
1515 1507
    resp = resp.form.submit('submit')
1516 1508
    assert TextCell.objects.get(id=cell.id).slug == ''
1517 1509
    assert 'SLUG' not in app.get('/manage/pages/%s/' % page.id)
1518 1510

  
1519 1511
    resp = app.get('/manage/pages/%s/' % page.id)
1520
    resp = resp.click(href='/data_textcell-%s/options' % cell.id)
1521 1512
    resp.form['cdata_textcell-%s-extra_css_class' % cell.id] = 'CSS'
1522 1513
    resp = resp.form.submit('submit')
1523 1514
    assert TextCell.objects.get(id=cell.id).extra_css_class == 'CSS'
......
1533 1524

  
1534 1525
    app = login(app)
1535 1526
    resp = app.get('/manage/pages/%s/' % page.id)
1536
    resp = resp.click(href='/data_textcell-%s/options' % cell.id)
1537 1527
    assert 'cdata_textcell-%s-template_name' % cell.id not in resp.form.fields
1538 1528
    assert resp.form['cdata_textcell-%s-slug' % cell.id].value == ''
1539 1529
    assert resp.form['cdata_textcell-%s-extra_css_class' % cell.id].value == ''
1540 1530

  
1541 1531
    with override_settings(COMBO_CELL_TEMPLATES={'data_textcell': {'extra': {'label': 'Extra'}}}):
1542 1532
        resp = app.get('/manage/pages/%s/' % page.id)
1543
        resp = resp.click(href='/data_textcell-%s/options' % cell.id)
1544 1533
        resp.form['cdata_textcell-%s-template_name' % cell.id].value = 'extra'
1545

  
1546 1534
        resp = resp.form.submit('submit')
1547 1535
        assert TextCell.objects.get(id=cell.id).template_name == 'extra'
1548 1536
        assert '(Extra)' in app.get('/manage/pages/%s/' % page.id)
......
1661 1649

  
1662 1650
        resp = app.get('/manage/pages/%s/' % page.id)
1663 1651
        assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text
1664
        assert 'There are no options for this cell.' in resp.form.text
1652
        assert [x.attrib['data-tab-slug'] for x in resp.pyquery('[data-tab-slug]')] == [
1653
            'visibility',
1654
            'appearance',
1655
        ]
1665 1656

  
1666 1657
    # make it configurable
1667 1658
    with override_settings(
......
1697 1688
        TEMPLATES=templates_settings,
1698 1689
    ):
1699 1690
        resp = app.get('/manage/pages/%s/' % page.id)
1700
        assert 'There are no options for this cell.' not in resp.form.text
1691
        assert [x.attrib['data-tab-slug'] for x in resp.pyquery('[data-tab-slug]')] == [
1692
            'general',
1693
            'visibility',
1694
            'appearance',
1695
        ]
1701 1696

  
1702 1697
        resp.form['c%s-test' % cells[0].get_reference()].value = 'Hello world'
1703 1698
        resp.form['c%s-test3' % cells[0].get_reference()].value = 'Hello again'
1704
        resp = resp.form.submit()
1705
        assert resp.status_int == 302
1706
        assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.id, cells[0].get_reference()))
1699
        manager_submit_cell(resp.form)
1707 1700

  
1708 1701
        # test form error
1709 1702
        resp = app.get('/manage/pages/%s/' % page.id)
1710 1703
        resp.form['c%s-test' % cells[0].get_reference()].value = ''
1711 1704
        resp.form['c%s-test3' % cells[0].get_reference()].value = 'Hello'
1712
        resp = resp.form.submit()
1713
        assert resp.status_int == 200
1705
        resp = manager_submit_cell(resp.form, expect_errors=True)
1714 1706
        assert resp.context['form'].errors['test'] == ['This field is required.']
1715 1707

  
1716 1708
        resp = app.get('/manage/pages/%s/' % page.id)
......
1724 1716
        resp.forms[0]['c%s-test2' % cells[0].get_reference()].checked = True
1725 1717
        assert resp.form['c%s-test4' % cells[0].get_reference()].tag == 'textarea'
1726 1718
        resp.forms[0]['c%s-test4' % cells[0].get_reference()].value = 'Text Area'
1727
        resp = resp.form.submit(xhr=True)
1728
        assert resp.status_int == 200
1729
        assert resp.text.strip().startswith('<p><label')
1719
        resp = manager_submit_cell(resp.form)
1720
        assert resp.json['tabs']['general'].strip().startswith('<p><label')
1730 1721

  
1731 1722
        resp = app.get('/manage/pages/%s/' % page.id)
1732 1723
        assert resp.form['c%s-test' % cells[0].get_reference()].value == 'World Hello'
......
2338 2329

  
2339 2330
    # change cell text
2340 2331
    resp.forms[0]['c%s-text' % cell1.get_reference()].value = 'Hello world'
2341
    resp = resp.forms[0].submit().follow()
2332
    manager_submit_cell(resp.forms[0])
2342 2333
    assert PageSnapshot.objects.all().count() == 2
2343 2334

  
2344 2335
    # reorder cells
......
2379 2370
    cell_id = JsonCell.objects.last().id
2380 2371
    resp.forms[3]['cdata_jsoncell-%s-template_string' % cell_id].value = 'A{{json.data.0.text}}B'
2381 2372
    resp.forms[3]['cdata_jsoncell-%s-url' % cell_id].value = 'http://example.com'
2382
    resp = resp.forms[3].submit().follow()
2373
    manager_submit_cell(resp.forms[3])
2383 2374
    assert PageSnapshot.objects.all().count() == 5  # add + change
2384 2375

  
2385 2376
    resp.forms[3]['cdata_jsoncell-%s-template_string' % cell_id].value = 'C{{json.data.0.text}}D'
2386
    resp = resp.forms[3].submit().follow()
2377
    manager_submit_cell(resp.forms[3])
2387 2378
    assert PageSnapshot.objects.all().count() == 6
2388 2379

  
2389 2380
    resp.forms[1]['c%s-text' % cell1.get_reference()].value = 'Foo back to 1'
2390
    resp = resp.forms[0].submit().follow()
2381
    manager_submit_cell(resp.forms[1])
2391 2382

  
2392 2383
    resp = resp.click('History')
2393 2384
    assert 'added cell' in resp.text
......
2420 2411
    resp = resp.click('restore', index=6)
2421 2412
    with CaptureQueriesContext(connection) as ctx:
2422 2413
        resp = resp.form.submit().follow()
2423
        assert len(ctx.captured_queries) == 146
2414
        assert len(ctx.captured_queries) == 152
2424 2415

  
2425 2416
    resp2 = resp.click('See online')
2426 2417
    assert resp2.text.index('Foobar1') < resp2.text.index('Foobar2') < resp2.text.index('Foobar3')
......
2537 2528
    resp.forms[0]['cdata_jsoncell-%s-template_string' % cell_id].value = '{% syntax|error %}'
2538 2529
    resp.forms[0]['cdata_jsoncell-%s-url' % cell_id].value = 'http://example.com'
2539 2530
    resp = resp.forms[0].submit()
2540
    assert 'syntax error: Invalid block tag' in resp.text
2531
    assert resp.json['errorlist']['general']['template_string']
2541 2532
    assert JsonCell.objects.count() == 1
2542 2533
    assert JsonCell.objects.first().template_string is None
2543 2534
    # valid syntax
2544 2535
    resp = app.get('/manage/pages/%s/' % page.id)
2545 2536
    resp.forms[0]['cdata_jsoncell-%s-template_string' % cell_id].value = '{{ ok }}'
2546 2537
    resp.forms[0]['cdata_jsoncell-%s-url' % cell_id].value = 'http://example.com'
2547
    resp = resp.forms[0].submit().follow()
2538
    manager_submit_cell(resp.forms[0])
2548 2539
    assert 'syntax error' not in resp.text
2549 2540
    assert JsonCell.objects.count() == 1
2550 2541
    assert JsonCell.objects.first().template_string == '{{ ok }}'
tests/test_maps_manager.py
5 5
from combo.apps.maps.models import Map, MapLayer, MapLayerOptions
6 6
from combo.data.models import Page
7 7

  
8
from .utils import manager_submit_cell
9

  
8 10
pytestmark = pytest.mark.django_db
9 11

  
10 12

  
......
254 256
    resp = resp.click('Cancel')
255 257

  
256 258
    resp.form['cmaps_map-%s-marker_behaviour_onclick' % cell.pk] = 'display_data'
257
    resp = resp.form.submit().follow()
259
    manager_submit_cell(resp.form)
258 260
    resp = resp.click(href='.*/layer/%s/edit/$' % options.pk)
259 261
    assert 'this setting has no effect' not in resp.context['form'].fields['properties'].help_text
260 262
    resp.form['properties'] = 'a, b'
tests/test_wcs.py
39 39
from combo.utils import NothingInCacheException
40 40

  
41 41
from .test_manager import login
42
from .utils import manager_submit_cell
42 43

  
43 44
pytestmark = pytest.mark.django_db
44 45

  
......
1532 1533
    resp = app.get('/manage/pages/%s/' % page.id)
1533 1534
    assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text
1534 1535
    resp.forms[0]['c%s-ordering' % cells[0].get_reference()].value = 'manual'
1535
    resp = resp.forms[0].submit()
1536
    assert resp.status_int == 302
1536
    manager_submit_cell(resp.forms[0])
1537
    cells[0].refresh_from_db()
1538
    resp.forms[0]['c%s-manual_order' % cells[0].get_reference()].select_multiple(
1539
        ['default::a-second-form-title', 'default::third-form-title']
1540
    )
1541
    manager_submit_cell(resp.forms[0])
1542
    cells[0].refresh_from_db()
1543
    assert cells[0].manual_order == {'data': ['default::a-second-form-title', 'default::third-form-title']}
1537 1544

  
1538 1545

  
1539 1546
@mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send)
......
1554 1561
    assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text
1555 1562
    assert len(resp.form['c%s-categories' % cells[0].get_reference()].options) == 4
1556 1563
    resp.form['c%s-categories' % cells[0].get_reference()].value = ['default:test-3', 'default:test-9']
1557
    resp = resp.form.submit().follow()
1564
    manager_submit_cell(resp.form)
1558 1565
    assert resp.form['c%s-categories' % cells[0].get_reference()].value == [
1559 1566
        'default:test-3',
1560 1567
        'default:test-9',
......
1565 1572
    assert 'Please choose at least one option among the following: Current Forms, Done Forms' in resp
1566 1573
    resp = app.get('/manage/pages/%s/' % page.id)
1567 1574
    resp.form['c%s-done_forms' % cells[0].get_reference()] = True
1568
    resp = resp.form.submit().follow()
1575
    manager_submit_cell(resp.form)
1569 1576

  
1570 1577
    # check wcs_site field is a select box
1571 1578
    assert resp.form['c%s-wcs_site' % cells[0].get_reference()].tag == 'select'
......
1587 1594
    assert cell.without_user is False
1588 1595
    assert resp.forms[0]['c%s-with_user' % cell.get_reference()].value == 'on'
1589 1596
    resp.forms[0]['c%s-with_user' % cell.get_reference()].value = False
1590
    resp.forms[0].submit().follow()
1597
    manager_submit_cell(resp.forms[0])
1591 1598
    cell.refresh_from_db()
1592 1599
    assert cell.without_user is True
1593 1600
    assert resp.forms[0]['c%s-with_user' % cell.get_reference()].value is None
......
1947 1954
    resp = app.get('/manage/pages/%s/' % page.pk)
1948 1955
    assert resp.forms[0]['c%s-customize_display' % cell.get_reference()].value == 'on'
1949 1956
    resp.forms[0]['c%s-customize_display' % cell.get_reference()].value = False
1950
    resp.forms[0].submit().follow()
1957
    manager_submit_cell(resp.forms[0])
1951 1958
    cell.refresh_from_db()
1952 1959
    assert cell.custom_schema == {}
1953 1960

  
......
1956 1963
    resp = app.get('/manage/pages/%s/' % page.pk)
1957 1964
    assert resp.forms[0]['c%s-related_card_path' % cell.get_reference()].value == '--'
1958 1965
    resp.forms[0]['c%s-card_ids' % cell.get_reference()].value = '42'
1959
    resp.forms[0].submit().follow()
1966
    manager_submit_cell(resp.forms[0])
1960 1967
    cell.refresh_from_db()
1961 1968
    assert cell.related_card_path == ''
1962 1969
    assert cell.card_ids == ''
......
1964 1971
    assert resp.forms[0]['c%s-related_card_path' % cell.get_reference()].value == '--'
1965 1972
    resp.forms[0]['c%s-related_card_path' % cell.get_reference()].value = ''
1966 1973
    resp.forms[0]['c%s-card_ids' % cell.get_reference()].value = '42'
1967
    resp.forms[0].submit().follow()
1974
    manager_submit_cell(resp.forms[0])
1968 1975
    cell.refresh_from_db()
1969 1976
    assert cell.related_card_path == ''
1970 1977
    assert cell.card_ids == '42'
......
2192 2199
    assert cell.without_user is False
2193 2200
    assert resp.forms[0]['c%s-with_user' % cell.get_reference()].value == 'on'
2194 2201
    resp.forms[0]['c%s-with_user' % cell.get_reference()].value = False
2195
    resp = resp.forms[0].submit().follow()
2202
    manager_submit_cell(resp.forms[0])
2196 2203
    cell.refresh_from_db()
2197 2204
    assert cell.without_user is True
2198 2205
    assert resp.forms[0]['c%s-with_user' % cell.get_reference()].value is None
tests/utils.py
1
from pyquery import PyQuery
2

  
3

  
4
def manager_submit_cell(form, expect_errors=False):
5
    # submit cell edit form and replace current page body with
6
    # new tab contents.
7
    resp = form.response
8
    action = form.action
9
    pq = PyQuery(resp.body)
10
    resp2 = form.submit()
11
    for tab in resp2.json['tabs']:
12
        pq.find('form[action="%s"] [data-tab-slug="%s"]' % (action, tab)).html(resp2.json['tabs'][tab])
13
    resp.text = pq.html()
14
    resp._forms_indexed = None
15
    assert expect_errors or not resp2.json['errorlist'], 'got unexpected errors'
16
    return resp2
0
-