general: add POST support on json cells (#16901)

This commit is contained in:
Frédéric Péters 2017-06-20 17:38:35 +02:00
parent 3eb6d93882
commit 0e5d85fe49
4 changed files with 155 additions and 7 deletions

View File

@ -27,7 +27,7 @@ from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import Group
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.exceptions import ObjectDoesNotExist, ValidationError, PermissionDenied
from django.core import serializers
from django.db import models
from django.db.models.base import ModelBase
@ -54,6 +54,10 @@ from combo import utils
from combo.utils import NothingInCacheException
class PostException(Exception):
pass
def element_is_visible(element, user=None):
if hasattr(element, 'restricted_to_unlogged') and element.restricted_to_unlogged:
return bool(user is None or user.is_anonymous())
@ -826,6 +830,7 @@ class JsonCellBase(CellBase):
template_string = None
varnames = None
force_async = False
actions = {}
_json_content = None
@ -835,7 +840,7 @@ class JsonCellBase(CellBase):
def is_visible(self, user=None):
return bool(self.url) and super(JsonCellBase, self).is_visible(user=user)
def get_cell_extra_context(self, context):
def get_cell_extra_context(self, context, invalidate_cache=False):
extra_context = super(JsonCellBase, self).get_cell_extra_context(context)
if self.varnames and context.get('request'):
for varname in self.varnames:
@ -855,6 +860,7 @@ class JsonCellBase(CellBase):
cache_duration=self.cache_duration,
without_user=True,
raise_if_not_cached=not(context.get('synchronous')),
invalidate_cache=invalidate_cache,
)
if json_response.status_code == 200:
try:
@ -878,6 +884,42 @@ class JsonCellBase(CellBase):
return 'combo/json-list-cell.html'
return 'combo/json-cell.html'
def post(self, request):
if not 'action' in request.POST:
raise PermissionDenied()
action = request.POST['action']
if not action in self.actions:
raise PermissionDenied()
error_message = self.actions[action].get('error-message')
logger = logging.getLogger(__name__)
context = RequestContext(request, {'request': request, 'synchronous': True})
try:
url = utils.get_templated_url(self.actions[action]['url'], context)
except utils.UnknownTemplateVariableError:
logger.warning('unknown variable in URL (%s)', self.actions[action]['url'])
raise PostException(error_message)
content = {}
for key, value in request.POST.items():
if key == 'action':
continue
content[key] = value
json_response = utils.requests.post(url,
headers={'Accept': 'application/json'},
remote_service='auto',
json=content,
without_user=True)
if json_response.status_code // 100 != 2: # 2xx
logger.error('error POSTing data to URL (%s)', url)
raise PostException(error_message)
if self.cache_duration:
self.get_cell_extra_context(context, invalidate_cache=True)
def render(self, context):
if self.force_async and not context.get('synchronous'):
raise NothingInCacheException()
@ -951,6 +993,11 @@ class ConfigJsonCell(JsonCellBase):
return settings.JSON_CELL_TYPES[self.key].get('force_async',
JsonCellBase.force_async)
@property
def actions(self):
return settings.JSON_CELL_TYPES[self.key].get('actions',
JsonCellBase.actions)
@property
def template_name(self):
return 'combo/json/%s.html' % self.key
@ -966,9 +1013,9 @@ class ConfigJsonCell(JsonCellBase):
(ConfigJsonForm,), {'formdef': formdef})
return config_form_class
def get_cell_extra_context(self, context):
def get_cell_extra_context(self, context, **kwargs):
context.update(self.parameters) # early push for templated URLs
ctx = super(ConfigJsonCell, self).get_cell_extra_context(context)
ctx = super(ConfigJsonCell, self).get_cell_extra_context(context, **kwargs)
ctx['parameters'] = self.parameters
ctx.update(self.parameters)
return ctx

View File

@ -30,6 +30,7 @@ from django.http import (Http404, HttpResponse, HttpResponseRedirect,
from django.shortcuts import render, resolve_url
from django.template import RequestContext, Template
from django.utils import lorem_ipsum, timezone
from django.views.decorators.csrf import csrf_exempt
from django.utils.translation import ugettext as _
from django.forms.widgets import Media
@ -41,7 +42,7 @@ if 'mellon' in settings.INSTALLED_APPS:
else:
get_idps = lambda: []
from combo.data.models import CellBase, Page, ParentContentCell, TextCell
from combo.data.models import CellBase, PostException, Page, ParentContentCell, TextCell
from combo.profile.models import Profile
from combo.apps.search.models import SearchCell
from combo import utils
@ -65,6 +66,7 @@ def logout(request, next_page=None):
next_page = '/'
return HttpResponseRedirect(next_page)
@csrf_exempt
def ajax_page_cell(request, page_pk, cell_reference):
try:
page = Page.objects.get(id=page_pk)
@ -80,7 +82,24 @@ def ajax_page_cell(request, page_pk, cell_reference):
if not cell.is_visible(request.user):
raise PermissionDenied()
return render_cell(request, cell)
exception = None
if request.method == 'POST':
if not hasattr(cell, 'post'):
raise PermissionDenied()
try:
cell.post(request)
except PostException as e:
exception = e
if not request.is_ajax():
messages.error(request, e.message or _('Error sending data.'))
if not request.is_ajax():
return HttpResponseRedirect(cell.page.get_online_url())
response = render_cell(request, cell)
if exception:
response['x-error-message'] = exception.message
return response
def render_cell(request, cell):
context = RequestContext(request, {

View File

@ -0,0 +1,5 @@
<form method="post" action="{% url 'combo-public-ajax-page-cell' page_pk=cell.page.id cell_reference=cell.get_reference %}">
<input type="hidden" name="action" value="create"/>
<input name="value">
<button>submit</button>
</form>

View File

@ -1,13 +1,19 @@
import mock
from webtest import TestApp
import datetime
import json
import pytest
import os
import urllib
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test import override_settings
from combo.wsgi import application
from combo.data.models import Page, CellBase, TextCell, ParentContentCell, FeedCell, LinkCell
from combo.data.models import (Page, CellBase, TextCell, ParentContentCell,
FeedCell, LinkCell, ConfigJsonCell)
pytestmark = pytest.mark.django_db
@ -299,3 +305,74 @@ def test_welcome_page(app, admin_user):
app.cookiejar.clear()
app = login(app)
resp = app.get('/', status=200)
def test_post_cell(app):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
cell = TextCell(page=page, placeholder='content', text='<p>Foobar</p>', order=0)
cell.save()
# check it's not possible to post to cell that doesn't have POST support.
resp = app.post(reverse('combo-public-ajax-page-cell',
kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()}),
params={'hello': 'world'},
status=403)
with override_settings(JSON_CELL_TYPES={
'test-post-cell': {
'name': 'Foobar',
'url': 'http://test-post-cell/',
'actions': {
'create': {
'url': 'http://test-post-cell/create/',
}
}
}},
TEMPLATE_DIRS=['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))]
):
cell = ConfigJsonCell(page=page, placeholder='content', order=0)
cell.key = 'test-post-cell'
cell.save()
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content=json.dumps({'hello': 'world'}), status_code=200)
resp = app.get('/', status=200)
resp.form['value'] = 'plop'
with mock.patch('combo.utils.requests.post') as requests_post:
requests_post.return_value = mock.Mock(content=json.dumps({'err': 0}), status_code=200)
resp2 = resp.form.submit()
assert requests_post.call_args[0][0] == 'http://test-post-cell/create/'
assert requests_post.call_args[1]['json'] == {'value': 'plop'}
assert resp2.location == 'http://testserver/'
# check ajax call
with mock.patch('combo.utils.requests.post') as requests_post:
requests_post.return_value = mock.Mock(content=json.dumps({'err': 0}), status_code=200)
resp2 = resp.form.submit(headers={'x-requested-with': 'XMLHttpRequest'})
assert requests_post.call_args[0][0] == 'http://test-post-cell/create/'
assert requests_post.call_args[1]['json'] == {'value': 'plop'}
assert resp2.content.startswith('<form')
# check error on POST
with mock.patch('combo.utils.requests.post') as requests_post:
requests_post.return_value = mock.Mock(content=json.dumps({'err': 0}), status_code=400)
resp2 = resp.form.submit()
assert resp2.location == 'http://testserver/'
resp2 = resp2.follow()
assert 'Error sending data.' in resp2.content
settings.JSON_CELL_TYPES['test-post-cell']['actions']['create']['error-message'] = 'Failed to create stuff.'
resp2 = resp.form.submit()
assert resp2.location == 'http://testserver/'
resp2 = resp2.follow()
assert 'Failed to create stuff.' in resp2.content
with mock.patch('combo.utils.requests.post') as requests_post:
requests_post.return_value = mock.Mock(content=json.dumps({'err': 0}), status_code=400)
resp2 = resp.form.submit(headers={'x-requested-with': 'XMLHttpRequest'})
assert resp2.content.startswith('<form')
assert resp2.headers['x-error-message'] == 'Failed to create stuff.'