Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fix] Ensuring cache warmup respects request form data #9665

Merged
merged 1 commit into from
Apr 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 11 additions & 12 deletions superset/jinja_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
# under the License.
"""Defines the templating context for SQL Lab"""
import inspect
import json
import re
from typing import Any, List, Optional, Tuple

Expand Down Expand Up @@ -49,7 +48,9 @@ def filter_values(column: str, default: Optional[str] = None) -> List[str]:
:return: returns a list of filter values
"""

form_data = json.loads(request.form.get("form_data", "{}"))
from superset.views.utils import get_form_data

form_data, _ = get_form_data()
convert_legacy_filters_into_adhoc(form_data)
merge_extra_filters(form_data)

Expand Down Expand Up @@ -168,18 +169,16 @@ def url_param(
:returns: The URL parameters
"""

from superset.views.utils import get_form_data

if request.args.get(param):
return request.args.get(param, default)
# Supporting POST as well as get
form_data = request.form.get("form_data")
if isinstance(form_data, str):
form_data = json.loads(form_data)
url_params = form_data.get("url_params") or {}
result = url_params.get(param, default)
if add_to_cache_keys:
self.cache_key_wrapper(result)
return result
return default
form_data, _ = get_form_data()
url_params = form_data.get("url_params") or {}
result = url_params.get(param, default)
if add_to_cache_keys:
self.cache_key_wrapper(result)
return result


class BaseTemplateProcessor: # pylint: disable=too-few-public-methods
Expand Down
4 changes: 4 additions & 0 deletions superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1703,13 +1703,17 @@ def warm_up_cache(self):
form_data["extra_filters"] = get_dashboard_extra_filters(
slc.id, dashboard_id
)

obj = get_viz(
datasource_type=slc.datasource.type,
datasource_id=slc.datasource.id,
form_data=form_data,
force=True,
)

g.form_data = form_data
payload = obj.get_payload()
delattr(g, "form_data")
error = payload["error"]
status = payload["status"]
except Exception as ex:
Expand Down
21 changes: 13 additions & 8 deletions superset/views/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from urllib import parse

import simplejson as json
from flask import request
from flask import g, request

import superset.models.core as models
from superset import app, db, is_feature_enabled
Expand Down Expand Up @@ -99,23 +99,28 @@ def get_viz(
return viz_obj


def get_form_data(slice_id=None, use_slice_data=False):
def get_form_data(
slice_id: Optional[int] = None, use_slice_data: bool = False
) -> Tuple[Dict[str, Any], Optional[Slice]]:
form_data = {}
post_data = request.form.get("form_data")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@villebro I was spinning my wheels for quite some time trying to get test_request_context() to work and then realize that this isn't explicitly related to the POST method and not is the body of the request a JSON object, i.e., the form_data field is actually further encoded as a JSON object.

The form-data is messy and we probably should strive to standardize this at some stage. For now I've renamed post_data as request_form_data which is more accurate.

request_form_data = request.form.get("form_data")
request_args_data = request.args.get("form_data")
# Supporting POST
if post_data:
form_data.update(json.loads(post_data))
# request params can overwrite post body
if request_form_data:
form_data.update(json.loads(request_form_data))
# request params can overwrite the body
if request_args_data:
form_data.update(json.loads(request_args_data))

# Fallback to using the Flask globals (used for cache warmup) if defined.
if not form_data and hasattr(g, "form_data"):
form_data = getattr(g, "form_data")

url_id = request.args.get("r")
if url_id:
saved_url = db.session.query(models.Url).filter_by(id=url_id).first()
if saved_url:
url_str = parse.unquote_plus(
saved_url.url.split("?")[1][10:], encoding="utf-8", errors=None
saved_url.url.split("?")[1][10:], encoding="utf-8"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@villebro unquote_plus was raising a typing issue as it expected errors to be of type str. Per here it seems if errors is None it gets set to "replace" which is the default, thus there's no need to specify it.

)
url_form_data = json.loads(url_str)
# allow form_date in request override saved url
Expand Down
117 changes: 63 additions & 54 deletions tests/jinja_context_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,76 +15,85 @@
# specific language governing permissions and limitations
# under the License.
import json
from unittest import mock

from superset.jinja_context import filter_values
import tests.test_app
from superset import app
from superset.jinja_context import ExtraCache, filter_values
from tests.base_tests import SupersetTestCase


class Jinja2ContextTests(SupersetTestCase):
def test_filter_values_default(self) -> None:
request = mock.MagicMock()
request.form = {}

with mock.patch("superset.jinja_context.request", request):
with app.test_request_context():
self.assertEquals(filter_values("name", "foo"), ["foo"])

def test_filter_values_no_default(self) -> None:
request = mock.MagicMock()
request.form = {}

with mock.patch("superset.jinja_context.request", request):
with app.test_request_context():
self.assertEquals(filter_values("name"), [])

def test_filter_values_adhoc_filters(self) -> None:
request = mock.MagicMock()

request.form = {
"form_data": json.dumps(
{
"adhoc_filters": [
{
"clause": "WHERE",
"comparator": "foo",
"expressionType": "SIMPLE",
"operator": "in",
"subject": "name",
}
],
}
)
}

with mock.patch("superset.jinja_context.request", request):
with app.test_request_context(
data={
"form_data": json.dumps(
{
"adhoc_filters": [
{
"clause": "WHERE",
"comparator": "foo",
"expressionType": "SIMPLE",
"operator": "in",
"subject": "name",
}
],
}
)
}
):
self.assertEquals(filter_values("name"), ["foo"])

request.form = {
"form_data": json.dumps(
{
"adhoc_filters": [
{
"clause": "WHERE",
"comparator": ["foo", "bar"],
"expressionType": "SIMPLE",
"operator": "in",
"subject": "name",
}
],
}
)
}

with mock.patch("superset.jinja_context.request", request):
with app.test_request_context(
data={
"form_data": json.dumps(
{
"adhoc_filters": [
{
"clause": "WHERE",
"comparator": ["foo", "bar"],
"expressionType": "SIMPLE",
"operator": "in",
"subject": "name",
}
],
}
)
}
):
self.assertEquals(filter_values("name"), ["foo", "bar"])

def test_filter_values_extra_filters(self) -> None:
request = mock.MagicMock()
with app.test_request_context(
data={
"form_data": json.dumps(
{"extra_filters": [{"col": "name", "op": "in", "val": "foo"}]}
)
}
):
self.assertEquals(filter_values("name"), ["foo"])

request.form = {
"form_data": json.dumps(
{"extra_filters": [{"col": "name", "op": "in", "val": "foo"}]}
)
}
def test_url_param_default(self) -> None:
with app.test_request_context():
self.assertEquals(ExtraCache().url_param("foo", "bar"), "bar")

with mock.patch("superset.jinja_context.request", request):
self.assertEquals(filter_values("name"), ["foo"])
def test_url_param_no_default(self) -> None:
with app.test_request_context():
self.assertEquals(ExtraCache().url_param("foo"), None)

def test_url_param_query(self) -> None:
with app.test_request_context(query_string={"foo": "bar"}):
self.assertEquals(ExtraCache().url_param("foo"), "bar")

def test_url_param_form_data(self) -> None:
with app.test_request_context(
query_string={"form_data": json.dumps({"url_params": {"foo": "bar"}})}
):
self.assertEquals(ExtraCache().url_param("foo"), "bar")
77 changes: 0 additions & 77 deletions tests/macro_tests.py

This file was deleted.