0001-data-add-page-parameters-59798.patch
combo/data/migrations/0052_page_parameters.py | ||
---|---|---|
1 |
import django.contrib.postgres.fields.jsonb |
|
2 |
from django.db import migrations |
|
3 | ||
4 | ||
5 |
class Migration(migrations.Migration): |
|
6 | ||
7 |
dependencies = [ |
|
8 |
('data', '0051_link_cell_max_length'), |
|
9 |
] |
|
10 | ||
11 |
operations = [ |
|
12 |
migrations.AddField( |
|
13 |
model_name='page', |
|
14 |
name='parameters', |
|
15 |
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), |
|
16 |
), |
|
17 |
] |
combo/data/models.py | ||
---|---|---|
45 | 45 |
from django.dispatch import receiver |
46 | 46 |
from django.forms import models as model_forms |
47 | 47 |
from django.forms.widgets import MediaDefiningClass |
48 |
from django.template import TemplateDoesNotExist, TemplateSyntaxError, engines |
|
48 |
from django.template import RequestContext, Template, TemplateDoesNotExist, TemplateSyntaxError, engines
|
|
49 | 49 |
from django.test.client import RequestFactory |
50 | 50 |
from django.utils import timezone |
51 | 51 |
from django.utils.encoding import force_text, python_2_unicode_compatible, smart_bytes |
... | ... | |
194 | 194 |
order = models.PositiveIntegerField() |
195 | 195 |
exclude_from_navigation = models.BooleanField(_('Exclude from navigation'), default=True) |
196 | 196 |
redirect_url = models.CharField(_('Redirect URL'), max_length=200, blank=True) |
197 |
parameters = JSONField(blank=True, default=dict) |
|
197 | 198 | |
198 | 199 |
public = models.BooleanField(_('Public'), default=True) |
199 | 200 |
groups = models.ManyToManyField(Group, verbose_name=_('Groups'), blank=True) |
... | ... | |
641 | 642 |
def get_last_update_time(self): |
642 | 643 |
return self.last_update_timestamp |
643 | 644 | |
645 |
def get_parameters(self, request, original_context): |
|
646 |
result = {} |
|
647 |
context = RequestContext(request) |
|
648 |
context.push(original_context) |
|
649 |
for key, tplt in (self.parameters or {}).items(): |
|
650 |
try: |
|
651 |
template = Template(tplt) |
|
652 |
except TemplateSyntaxError: |
|
653 |
continue |
|
654 |
result[key] = template.render(context) |
|
655 |
return result |
|
656 | ||
657 |
def get_parameters_keys(self): |
|
658 |
return sorted((self.parameters or {}).keys()) |
|
659 | ||
644 | 660 |
def is_new(self): |
645 | 661 |
return self.creation_timestamp > timezone.now() - datetime.timedelta(days=7) |
646 | 662 |
combo/manager/forms.py | ||
---|---|---|
20 | 20 |
from django.conf import settings |
21 | 21 |
from django.contrib.auth.models import Group |
22 | 22 |
from django.core.exceptions import ValidationError |
23 |
from django.forms import formset_factory |
|
23 | 24 |
from django.template import Template, TemplateSyntaxError |
24 | 25 |
from django.template.loader import TemplateDoesNotExist, get_template |
25 | 26 |
from django.utils.translation import ugettext_lazy as _ |
... | ... | |
168 | 169 |
field_classes = {'picture': ImageIncludingSvgField} |
169 | 170 | |
170 | 171 | |
172 |
class PageEditParameterForm(forms.Form): |
|
173 |
key = forms.CharField(label=_('Property name'), required=False) |
|
174 |
value = forms.CharField( |
|
175 |
label=_('Value template'), widget=forms.TextInput(attrs={'size': 60}), required=False |
|
176 |
) |
|
177 | ||
178 | ||
179 |
PageEditParameterFormSet = formset_factory(PageEditParameterForm) |
|
180 | ||
181 | ||
171 | 182 |
class PageVisibilityForm(forms.ModelForm): |
172 | 183 |
class Meta: |
173 | 184 |
model = Page |
combo/manager/static/js/combo.manager.js | ||
---|---|---|
397 | 397 |
window.location = $(this).parent('div').find('option:selected').data('add-url'); |
398 | 398 |
return false; |
399 | 399 |
}); |
400 | ||
401 |
$(document).on('click', '#add-page-property-form', function() { |
|
402 |
if (typeof property_forms === "undefined") {var property_forms = $('.page-property-form');} |
|
403 |
if (typeof total_forms === "undefined") {var total_form = $('#id_form-TOTAL_FORMS');} |
|
404 |
if (typeof form_num === "undefined") {var form_num = property_forms.length - 1;} |
|
405 |
var new_form = $(property_forms[0]).clone(); |
|
406 |
var form_regex = RegExp(`form-(\\d){1}-`,'g'); |
|
407 |
form_num++; |
|
408 |
new_form.html(new_form.html().replace(form_regex, `form-${form_num}-`)); |
|
409 |
new_form.appendTo('#page-property-forms tbody'); |
|
410 |
$('#id_form-' + form_num + '-key').val(''); |
|
411 |
$('#id_form-' + form_num + '-value').val(''); |
|
412 |
total_form.val(form_num + 1); |
|
413 |
}) |
|
400 | 414 |
}); |
401 | 415 | |
402 | 416 |
combo/manager/templates/combo/page_parameters.html | ||
---|---|---|
1 |
{% extends "combo/page_add.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block content %} |
|
5 |
<form method="post" enctype="multipart/form-data"> |
|
6 |
{% csrf_token %} |
|
7 |
<h3>{% trans "Page properties" %}</h3> |
|
8 |
{{ form.management_form }} |
|
9 |
<table id="page-property-forms"> |
|
10 |
<thead> |
|
11 |
<tr> |
|
12 |
{% for field in form.0 %} |
|
13 |
<th class="column-{{ field.name }}{% if field.required %} required{% endif %}">{{ field.label }}</th> |
|
14 |
{% endfor %} |
|
15 |
</tr> |
|
16 |
</thead> |
|
17 |
<tbody> |
|
18 |
{% for sub_form in form %} |
|
19 |
<tr class='page-property-form'> |
|
20 |
{% for field in sub_form %} |
|
21 |
<td class="field-{{ field.name }}"> |
|
22 |
{{ field.errors.as_ul }} |
|
23 |
{{ field }} |
|
24 |
</td> |
|
25 |
{% endfor %} |
|
26 |
</tr> |
|
27 |
{% endfor %} |
|
28 |
</tbody> |
|
29 |
</table> |
|
30 |
<button id="add-page-property-form" type="button">{% trans "Add another property" %}</button> |
|
31 |
<div class="buttons"> |
|
32 |
<button class="submit-button">{% trans "Save" %}</button> |
|
33 |
<a class="cancel" href="{% url 'combo-manager-page-view' pk=object.pk %}">{% trans 'Cancel' %}</a> |
|
34 |
</div> |
|
35 |
</form> |
|
36 |
{% endblock %} |
combo/manager/templates/combo/page_view.html | ||
---|---|---|
90 | 90 |
(<a rel="popup" href="{% url 'combo-manager-page-edit-picture' pk=object.id %}">{% trans 'change' %}</a>) |
91 | 91 |
</p> |
92 | 92 | |
93 |
<p> |
|
94 |
<label>{% trans 'Parameters:' %}</label> |
|
95 |
{% for key in object.get_parameters_keys %}<i>{{ key }}</i>{% if not forloop.last %}, {% endif %}{% empty %}<i>{% trans 'none' context 'parameters' %}</i>{% endfor %} |
|
96 |
(<a rel="popup" href="{% url 'combo-manager-page-edit-parameters' pk=object.id %}">{% trans 'change' %}</a>) |
|
97 |
</p> |
|
98 | ||
93 | 99 |
</div> |
94 | 100 | |
95 | 101 |
{% if object.parent_id or previous_page or next_page %} |
combo/manager/urls.py | ||
---|---|---|
64 | 64 |
views.page_remove_picture, |
65 | 65 |
name='combo-manager-page-remove-picture', |
66 | 66 |
), |
67 |
url( |
|
68 |
r'^pages/(?P<pk>\d+)/parameters/$', |
|
69 |
views.page_edit_parameters, |
|
70 |
name='combo-manager-page-edit-parameters', |
|
71 |
), |
|
67 | 72 |
url(r'^pages/(?P<pk>\d+)/delete$', staff_required(views.page_delete), name='combo-manager-page-delete'), |
68 | 73 |
url(r'^pages/(?P<pk>\d+)/export$', views.page_export, name='combo-manager-page-export'), |
69 | 74 |
url( |
combo/manager/views.py | ||
---|---|---|
18 | 18 |
import hashlib |
19 | 19 |
import json |
20 | 20 |
import tarfile |
21 |
from operator import attrgetter |
|
21 |
from operator import attrgetter, itemgetter
|
|
22 | 22 | |
23 | 23 |
from django.conf import settings |
24 | 24 |
from django.contrib import messages |
... | ... | |
64 | 64 |
PageDuplicateForm, |
65 | 65 |
PageEditDescriptionForm, |
66 | 66 |
PageEditIncludeInNavigationForm, |
67 |
PageEditParameterFormSet, |
|
67 | 68 |
PageEditPictureForm, |
68 | 69 |
PageEditRedirectionForm, |
69 | 70 |
PageEditRolesForm, |
... | ... | |
366 | 367 |
page_remove_picture = PageRemovePictureView.as_view() |
367 | 368 | |
368 | 369 | |
370 |
class PageEditParametersView(PageEditView): |
|
371 |
form_class = PageEditParameterFormSet |
|
372 |
comment = _('changed parameters') |
|
373 |
template_name = 'combo/page_parameters.html' |
|
374 | ||
375 |
def get_initial(self): |
|
376 |
return sorted( |
|
377 |
({'key': k, 'value': v} for k, v in self.get_object().parameters.items()), key=itemgetter('key') |
|
378 |
) |
|
379 | ||
380 |
def get_form_kwargs(self): |
|
381 |
kwargs = super().get_form_kwargs() |
|
382 |
kwargs.pop('instance') |
|
383 |
return kwargs |
|
384 | ||
385 |
def form_valid(self, form): |
|
386 |
self.object = self.get_object() |
|
387 |
self.object.parameters = {} |
|
388 |
for sub_data in form.cleaned_data: |
|
389 |
if not sub_data.get('key'): |
|
390 |
continue |
|
391 |
self.object.parameters[sub_data['key']] = sub_data['value'] |
|
392 |
self.object.save() |
|
393 |
PageSnapshot.take(self.object, request=self.request, comment=self.comment) |
|
394 |
return HttpResponseRedirect(self.get_success_url()) |
|
395 | ||
396 | ||
397 |
page_edit_parameters = PageEditParametersView.as_view() |
|
398 | ||
399 | ||
369 | 400 |
class PageView(ManagedPageMixin, DetailView): |
370 | 401 |
model = Page |
371 | 402 |
template_name = 'combo/page_view.html' |
combo/public/views.py | ||
---|---|---|
102 | 102 |
pass |
103 | 103 |
if 'name_id' in ctx: |
104 | 104 |
ctx['selected_user'] = get_user_from_name_id(ctx['name_id']) |
105 |
if 'page' in ctx: |
|
106 |
page = ctx['page'] |
|
107 |
ctx.update(page.get_parameters(request, ctx)) |
|
105 | 108 | |
106 | 109 | |
107 | 110 |
@csrf_exempt |
tests/test_manager.py | ||
---|---|---|
571 | 571 |
assert Page.objects.get(id=page.id).picture.url in resp.text |
572 | 572 | |
573 | 573 | |
574 |
def test_page_edit_parameters(app, admin_user): |
|
575 |
app = login(app) |
|
576 |
page = Page.objects.create(title='One', slug='one', template_name='standard') |
|
577 |
assert page.parameters == {} |
|
578 |
resp = app.get('/manage/pages/%s/' % page.id) |
|
579 |
assert resp.text.count('<i>none</i>') == 4 |
|
580 |
resp = resp.click(href='.*/parameters/') |
|
581 |
resp.form['form-0-key'] = 'foo' |
|
582 |
resp.form['form-0-value'] = 'bar' |
|
583 |
resp = resp.form.submit().follow() |
|
584 |
page.refresh_from_db() |
|
585 |
assert page.parameters == {'foo': 'bar'} |
|
586 |
assert resp.text.count('<i>none</i>') == 3 |
|
587 |
assert '<i>foo</i>' in resp |
|
588 | ||
589 |
resp = resp.click(href='.*/parameters/') |
|
590 |
assert resp.form['form-TOTAL_FORMS'].value == '2' |
|
591 |
assert resp.form['form-0-key'].value == 'foo' |
|
592 |
assert resp.form['form-0-value'].value == 'bar' |
|
593 |
assert resp.form['form-1-key'].value == '' |
|
594 |
assert resp.form['form-1-value'].value == '' |
|
595 |
resp.form['form-0-value'] = 'bar-bis' |
|
596 |
resp.form['form-1-key'] = 'blah' |
|
597 |
resp.form['form-1-value'] = 'baz' |
|
598 |
resp = resp.form.submit().follow() |
|
599 |
page.refresh_from_db() |
|
600 |
assert page.parameters == { |
|
601 |
'foo': 'bar-bis', |
|
602 |
'blah': 'baz', |
|
603 |
} |
|
604 |
assert resp.text.count('<i>none</i>') == 3 |
|
605 |
assert '<i>blah</i>, <i>foo</i>' in resp |
|
606 | ||
607 |
resp = resp.click(href='.*/parameters/') |
|
608 |
assert resp.form['form-TOTAL_FORMS'].value == '3' |
|
609 |
assert resp.form['form-0-key'].value == 'blah' |
|
610 |
assert resp.form['form-0-value'].value == 'baz' |
|
611 |
assert resp.form['form-1-key'].value == 'foo' |
|
612 |
assert resp.form['form-1-value'].value == 'bar-bis' |
|
613 |
assert resp.form['form-2-key'].value == '' |
|
614 |
assert resp.form['form-2-value'].value == '' |
|
615 |
resp.form['form-1-key'] = 'foo' |
|
616 |
resp.form['form-1-value'] = 'bar' |
|
617 |
resp.form['form-0-key'] = '' |
|
618 |
resp = resp.form.submit().follow() |
|
619 |
page.refresh_from_db() |
|
620 |
assert page.parameters == { |
|
621 |
'foo': 'bar', |
|
622 |
} |
|
623 |
assert resp.text.count('<i>none</i>') == 3 |
|
624 |
assert '<i>foo</i>' in resp |
|
625 | ||
626 | ||
574 | 627 |
def test_page_placeholder_restricted_visibility(app, admin_user): |
575 | 628 |
app = login(app) |
576 | 629 |
tests/test_public.py | ||
---|---|---|
577 | 577 |
assert Page.objects.count() == 0 |
578 | 578 | |
579 | 579 | |
580 |
def test_page_async_cell(app): |
|
580 |
def test_page_async_cell(app, nocache):
|
|
581 | 581 |
Page.objects.all().delete() |
582 | 582 |
page = Page(title='Home', slug='index', template_name='standard') |
583 | 583 |
page.save() |
... | ... | |
1070 | 1070 |
assert resp.context['card_foo_bar_id'] == '42' |
1071 | 1071 | |
1072 | 1072 | |
1073 |
def test_page_parameters(app): |
|
1074 |
page = Page.objects.create( |
|
1075 |
title='Home', |
|
1076 |
slug='page', |
|
1077 |
template_name='standard', |
|
1078 |
parameters={'foo': 'bar', 'bar_id': '{{ 40|add:2 }}'}, |
|
1079 |
) |
|
1080 |
cell = JsonCell.objects.create( |
|
1081 |
page=page, |
|
1082 |
url='http://example.net', |
|
1083 |
order=0, |
|
1084 |
placeholder='content', |
|
1085 |
template_string='XX{{ foo }}YY{{ bar_id }}ZZ', |
|
1086 |
) |
|
1087 | ||
1088 |
with mock.patch('combo.utils.requests.get') as requests_get: |
|
1089 |
requests_get.return_value = mock.Mock(content='{}', status_code=200) |
|
1090 |
resp = app.get('/page/') |
|
1091 |
assert '<div>XXbarYY42ZZ</div>' in resp |
|
1092 | ||
1093 |
with mock.patch('combo.utils.requests.get') as requests_get: |
|
1094 |
requests_get.return_value = mock.Mock(content='{}', status_code=200) |
|
1095 |
resp = app.get( |
|
1096 |
reverse( |
|
1097 |
'combo-public-ajax-page-cell', |
|
1098 |
kwargs={'page_pk': page.pk, 'cell_reference': cell.get_reference()}, |
|
1099 |
) |
|
1100 |
) |
|
1101 |
assert resp.text.strip() == 'XXbarYY42ZZ' |
|
1102 | ||
1103 |
# check sub_slug/parameters override |
|
1104 |
page.sub_slug = '(?P<fooo>[a-z]+)' |
|
1105 |
page.save() |
|
1106 |
cell.template_string = 'XX{{ foo }}YY{{ bar_id }}ZZ{{ fooo }}AA' |
|
1107 |
cell.save() |
|
1108 |
with mock.patch('combo.utils.requests.get') as requests_get: |
|
1109 |
requests_get.return_value = mock.Mock(content='{}', status_code=200) |
|
1110 |
resp = app.get('/page/baz/') |
|
1111 |
assert '<div>XXbarYY42ZZbazAA</div>' in resp |
|
1112 | ||
1113 |
page.sub_slug = '(?P<foo>[a-z]+)' |
|
1114 |
page.save() |
|
1115 |
with mock.patch('combo.utils.requests.get') as requests_get: |
|
1116 |
requests_get.return_value = mock.Mock(content='{}', status_code=200) |
|
1117 |
resp = app.get('/page/baz/') |
|
1118 |
assert '<div>XXbarYY42ZZAA</div>' in resp |
|
1119 | ||
1120 | ||
1073 | 1121 |
def test_cell_slugs(app): |
1074 | 1122 |
Page.objects.all().delete() |
1075 | 1123 |
page = Page(title='Home', slug='index', template_name='standard') |
tests/test_wcs.py | ||
---|---|---|
2183 | 2183 |
assert '/api/cards/card_model_1/list' in mock_send.call_args_list[1][0][0].url |
2184 | 2184 |
assert '/api/cards/card_model_1/13/' in mock_send.call_args_list[2][0][0].url |
2185 | 2185 | |
2186 |
for card_ids in [ |
|
2187 |
'{% for card in cards|objects:"card_model_1" %}{{ card.id }},{% endfor %}', |
|
2188 |
'{{ cards|objects:"card_model_1"|getlist:"id"|join:"," }}', |
|
2189 |
]: |
|
2190 |
cell.card_ids = card_ids |
|
2191 |
cell.save() |
|
2186 |
def test_card_ids(): |
|
2192 | 2187 |
mock_send.reset_mock() |
2193 | 2188 |
resp = app.get(page.get_online_url()) |
2194 | 2189 |
assert len(resp.context['cells']) == 3 |
... | ... | |
2209 | 2204 |
in mock_send.call_args_list[i * 2 + 2][0][0].url |
2210 | 2205 |
) |
2211 | 2206 | |
2207 |
for card_ids in [ |
|
2208 |
'{% for card in cards|objects:"card_model_1" %}{{ card.id }},{% endfor %}', |
|
2209 |
'{{ cards|objects:"card_model_1"|getlist:"id"|join:"," }}', |
|
2210 |
]: |
|
2211 |
cell.card_ids = card_ids |
|
2212 |
cell.save() |
|
2213 |
test_card_ids() |
|
2214 | ||
2215 |
cell.card_ids = '{{ var1 }}' |
|
2216 |
cell.save() |
|
2217 |
page.parameters = {'var1': card_ids} |
|
2218 |
page.save() |
|
2219 |
test_card_ids() |
|
2220 |
page.parameters = {} |
|
2221 |
page.save() |
|
2222 | ||
2212 | 2223 |
# with a card_ids template, but result is empty |
2213 | 2224 |
cell.card_ids = '{{ foo }}' |
2214 | 2225 |
cell.save() |
2215 |
- |