From 75531e2882acfbd27056826e1b772ec975355a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Sun, 2 Apr 2017 13:31:16 +0200 Subject: [PATCH] general: add pre-configured json cells (#15723) --- combo/data/forms.py | 28 +++++++++- combo/data/migrations/0024_configjsoncell.py | 36 +++++++++++++ combo/data/models.py | 81 ++++++++++++++++++++++++---- combo/settings.py | 2 + tests/test_cells.py | 22 +++++++- tests/test_manager.py | 65 +++++++++++++++++++++- 6 files changed, 220 insertions(+), 14 deletions(-) create mode 100644 combo/data/migrations/0024_configjsoncell.py diff --git a/combo/data/forms.py b/combo/data/forms.py index fdf84ab..9de787b 100644 --- a/combo/data/forms.py +++ b/combo/data/forms.py @@ -14,9 +14,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import copy + from django import forms -from .models import Page, ParametersCell, MenuCell +from .models import Page, ParametersCell, MenuCell, ConfigJsonCell from jsonfield.widgets import JSONWidget class ParametersForm(forms.Form): @@ -61,3 +63,27 @@ class MenuCellForm(forms.ModelForm): pages = Page.get_as_reordered_flat_hierarchy(Page.objects.all()) choices = [(x.id, '%s %s' % (u'\u00a0' * x.level * 2, x.title)) for x in pages] self.fields['root_page'].widget = forms.Select(choices=choices) + + +class ConfigJsonForm(forms.ModelForm): + formdef = [] + + class Meta: + model = ConfigJsonCell + fields = ('parameters',) + widgets = {'parameters': forms.HiddenInput()} + + def __init__(self, *args, **kwargs): + parameters = copy.copy(kwargs['instance'].parameters or {}) + kwargs['instance'].parameters = None + super(ConfigJsonForm, self).__init__(*args, **kwargs) + for field in self.formdef: + self.fields[field['varname']] = forms.CharField( + label=field['label'], + initial=parameters.get(field['varname'])) + + def clean(self): + self.cleaned_data['parameters'] = {} + for field in self.formdef: + self.cleaned_data['parameters'][field['varname']] = self.cleaned_data[field['varname']] + return self.cleaned_data diff --git a/combo/data/migrations/0024_configjsoncell.py b/combo/data/migrations/0024_configjsoncell.py new file mode 100644 index 0000000..707602d --- /dev/null +++ b/combo/data/migrations/0024_configjsoncell.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0006_require_contenttypes_0002'), + ('data', '0023_auto_20170313_1541'), + ] + + operations = [ + migrations.CreateModel( + name='ConfigJsonCell', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('placeholder', models.CharField(max_length=20)), + ('order', models.PositiveIntegerField()), + ('slug', models.SlugField(verbose_name='Slug', blank=True)), + ('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)), + ('public', models.BooleanField(default=True, verbose_name='Public')), + ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')), + ('last_update_timestamp', models.DateTimeField(auto_now=True)), + ('key', models.CharField(max_length=50)), + ('parameters', jsonfield.fields.JSONField(default=dict, blank=True)), + ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)), + ('page', models.ForeignKey(to='data.Page')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/combo/data/models.py b/combo/data/models.py index 86071a3..922a462 100644 --- a/combo/data/models.py +++ b/combo/data/models.py @@ -804,24 +804,22 @@ class ParentContentCell(CellBase): def render(self, context): return '' -@register_cell_class -class JsonCell(CellBase): - title = models.CharField(_('Title'), max_length=150, blank=True) - url = models.URLField(_('URL'), blank=True) - template_string = models.TextField(_('Template'), blank=True, null=True) - cache_duration = models.PositiveIntegerField( - _('Cache duration'), default=60) + +class JsonCellBase(CellBase): + url = None + cache_duration = 60 + template_string = None _json_content = None class Meta: - verbose_name = _('JSON Feed') + abstract = True def is_visible(self, user=None): - return bool(self.url) and super(JsonCell, self).is_visible(user=user) + return bool(self.url) and super(JsonCellBase, self).is_visible(user=user) def get_cell_extra_context(self, context): - extra_context = super(JsonCell, self).get_cell_extra_context(context) + extra_context = super(JsonCellBase, self).get_cell_extra_context(context) self._json_content = None try: url = utils.get_templated_url(self.url, context) @@ -862,4 +860,65 @@ class JsonCell(CellBase): if self.template_string: tmpl = Template(self.template_string) return tmpl.render(context) - return super(JsonCell, self).render(context) + return super(JsonCellBase, self).render(context) + + +@register_cell_class +class JsonCell(JsonCellBase): + title = models.CharField(_('Title'), max_length=150, blank=True) + url = models.URLField(_('URL'), blank=True) + template_string = models.TextField(_('Template'), blank=True, null=True) + cache_duration = models.PositiveIntegerField( + _('Cache duration'), default=60) + + class Meta: + verbose_name = _('JSON Feed') + + +@register_cell_class +class ConfigJsonCell(JsonCellBase): + key = models.CharField(max_length=50) + parameters = JSONField(blank=True) + + @classmethod + def get_cell_types(cls): + l = [] + for key, definition in settings.JSON_CELL_TYPES.items(): + l.append({ + 'name': definition['name'], + 'variant': key, + 'group': _('Extra'), + 'cell_type_str': cls.get_cell_type_str(), + }) + l.sort(lambda x, y: cmp(x.get('name'), y.get('name'))) + return l + + def get_label(self): + return settings.JSON_CELL_TYPES[self.key]['name'] + + def set_variant(self, variant): + self.key = variant + + @property + def url(self): + url = settings.JSON_CELL_TYPES[self.key]['url'] + # + params + return url + + @property + def template_name(self): + return 'combo/json/%s.html' % self.key + + def get_default_form_class(self): + formdef = settings.JSON_CELL_TYPES[self.key].get('form') + if not formdef: + return None + from .forms import ConfigJsonForm + config_form_class = type(str('%sConfigClass' % self.key), + (ConfigJsonForm,), {'formdef': formdef}) + return config_form_class + + def get_cell_extra_context(self, context): + ctx = super(ConfigJsonCell, self).get_cell_extra_context(context) + ctx['parameters'] = self.parameters + return ctx diff --git a/combo/settings.py b/combo/settings.py index 2c31fed..7126bb9 100644 --- a/combo/settings.py +++ b/combo/settings.py @@ -264,6 +264,8 @@ MELLON_IDENTITY_PROVIDERS = [] # mapping of payment modes LINGO_NO_ONLINE_PAYMENT_REASONS = {} +JSON_CELL_TYPES = {} + local_settings_file = os.environ.get('COMBO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')) diff --git a/tests/test_cells.py b/tests/test_cells.py index b5154a8..36b9987 100644 --- a/tests/test_cells.py +++ b/tests/test_cells.py @@ -4,7 +4,7 @@ import os import pytest import requests -from combo.data.models import Page, CellBase, TextCell, LinkCell, JsonCell +from combo.data.models import Page, CellBase, TextCell, LinkCell, JsonCell, ConfigJsonCell from django.forms.widgets import Media from django.template import Context from django.test import override_settings @@ -210,3 +210,23 @@ def test_json_cell(): assert requests_get.call_count == 1 assert requests_get.call_args[0][0] == 'http://testuser?email=foo%40example.net' assert requests_get.call_args[1]['cache_duration'] == 10 + +def test_config_json_cell(): + page = Page(title='example page', slug='example-page') + page.save() + + request = RequestFactory().get('/') + + with override_settings(JSON_CELL_TYPES={'foobar': {'name': 'Foobar', 'url': 'http://test/'}}): + cell = ConfigJsonCell() + cell.key = 'foobar' + cell.parameters = {'blah': 'plop'} + assert cell.get_label() == 'Foobar' + assert cell.url == 'http://test/' + assert cell.template_name == 'combo/json/foobar.html' + + with mock.patch('combo.utils.requests.get') as requests_get: + requests_get.return_value = mock.Mock(content=json.dumps({'hello': 'world'}), status_code=200) + context = cell.get_cell_extra_context(Context({'request': request})) + assert context['json'] == {'hello': 'world'} + assert context['parameters'] == {'blah': 'plop'} diff --git a/tests/test_manager.py b/tests/test_manager.py index 82d450c..885aa05 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -6,13 +6,14 @@ import urllib from django.core.files.storage import default_storage from django.conf import settings from django.contrib.auth.models import User +from django.test import override_settings import pytest from webtest import TestApp from webtest import Upload from combo.wsgi import application -from combo.data.models import Page, CellBase, TextCell, LinkCell +from combo.data.models import Page, CellBase, TextCell, LinkCell, ConfigJsonCell from combo.apps.search.models import SearchCell pytestmark = pytest.mark.django_db @@ -423,6 +424,68 @@ def test_edit_cell_order(app, admin_user): for i, cell in enumerate(cells): assert TextCell.objects.get(id=cell.id).order == new_order[i] + +def test_edit_config_json_cell(app, admin_user): + Page.objects.all().delete() + page = Page(title='One', slug='one', template_name='standard') + page.save() + + app = login(app) + resp = app.get('/manage/pages/%s/' % page.id) + options = [x.text for x in resp.html.find_all('option')] + assert not 'Foobar' in options + + with override_settings(JSON_CELL_TYPES={'foobar': {'name': 'Foobar', 'url': 'http://test/'}}): + resp = app.get('/manage/pages/%s/' % page.id) + options = [x.text for x in resp.html.find_all('option')] + assert 'Foobar' in options + data_add_url = [x for x in resp.html.find_all('option') if x.text == 'Foobar'][0].get('data-add-url') + resp = app.get(data_add_url) + assert resp.location == 'http://testserver/manage/pages/%s/' % page.id + + cells = CellBase.get_cells(page_id=page.id) + assert len(cells) == 1 + assert isinstance(cells[0], ConfigJsonCell) + assert cells[0].key == 'foobar' + + resp = app.get('/manage/pages/%s/' % page.id) + assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.body + assert 'There are no options for this cell.' in resp.form.text + + # make it configurable + with override_settings(JSON_CELL_TYPES= + {'foobar': { + 'name': 'Foobar', + 'url': 'http://test/', + 'form': [ + { + 'label': 'Test', + 'type': 'string', + 'varname': 'test', + } + ]}}): + resp = app.get('/manage/pages/%s/' % page.id) + assert not 'There are no options for this cell.' in resp.form.text + + resp.form['c%s-test' % cells[0].get_reference()].value = 'Hello world' + resp = resp.form.submit() + assert resp.status_int == 302 + assert resp.location == 'http://testserver/manage/pages/%s/' % page.id + + resp = app.get('/manage/pages/%s/' % page.id) + assert resp.form['c%s-test' % cells[0].get_reference()].value == 'Hello world' + + resp = app.get('/manage/pages/%s/' % page.id) + assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.body + resp.forms[0]['c%s-test' % cells[0].get_reference()].value = 'World Hello' + resp = resp.form.submit(xhr=True) + assert resp.status_int == 200 + assert resp.body.startswith('