mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
feat: introduce hashids permalink keys (#19324)
* feat: introduce hashids permalink keys
* implement dashboard permalinks
* remove shorturl notice from UPDATING.md
* lint
* fix test
* introduce KeyValueResource
* make filterState optional
* fix test
* fix resource names
(cherry picked from commit f4b71abb22)
This commit is contained in:
committed by
Ville Brofeldt
parent
18f82411c9
commit
a6a2def6d3
@@ -15,7 +15,9 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import json
|
||||
from typing import Iterator
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid3
|
||||
|
||||
import pytest
|
||||
from flask_appbuilder.security.sqla.models import User
|
||||
@@ -24,8 +26,9 @@ from sqlalchemy.orm import Session
|
||||
from superset import db
|
||||
from superset.dashboards.commands.exceptions import DashboardAccessDeniedError
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
from superset.key_value.types import KeyValueResource
|
||||
from superset.key_value.utils import decode_permalink_id
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.slice import Slice
|
||||
from tests.integration_tests.base_tests import login
|
||||
from tests.integration_tests.fixtures.client import client
|
||||
from tests.integration_tests.fixtures.world_bank_dashboard import (
|
||||
@@ -35,7 +38,7 @@ from tests.integration_tests.fixtures.world_bank_dashboard import (
|
||||
from tests.integration_tests.test_app import app
|
||||
|
||||
STATE = {
|
||||
"filterState": {"FILTER_1": "foo",},
|
||||
"filterState": {"FILTER_1": "foo"},
|
||||
"hash": "my-anchor",
|
||||
}
|
||||
|
||||
@@ -48,7 +51,22 @@ def dashboard_id(load_world_bank_dashboard_with_slices) -> int:
|
||||
return dashboard.id
|
||||
|
||||
|
||||
def test_post(client, dashboard_id: int):
|
||||
@pytest.fixture
|
||||
def permalink_salt() -> Iterator[str]:
|
||||
from superset.key_value.shared_entries import get_permalink_salt, get_uuid_namespace
|
||||
from superset.key_value.types import SharedKey
|
||||
|
||||
key = SharedKey.DASHBOARD_PERMALINK_SALT
|
||||
salt = get_permalink_salt(key)
|
||||
yield salt
|
||||
namespace = get_uuid_namespace(salt)
|
||||
db.session.query(KeyValueEntry).filter_by(
|
||||
resource=KeyValueResource.APP, uuid=uuid3(namespace, key),
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def test_post(client, dashboard_id: int, permalink_salt: str) -> None:
|
||||
login(client, "admin")
|
||||
resp = client.post(f"api/v1/dashboard/{dashboard_id}/permalink", json=STATE)
|
||||
assert resp.status_code == 201
|
||||
@@ -56,7 +74,8 @@ def test_post(client, dashboard_id: int):
|
||||
key = data["key"]
|
||||
url = data["url"]
|
||||
assert key in url
|
||||
db.session.query(KeyValueEntry).filter_by(uuid=key).delete()
|
||||
id_ = decode_permalink_id(key, permalink_salt)
|
||||
db.session.query(KeyValueEntry).filter_by(id=id_).delete()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@@ -76,7 +95,7 @@ def test_post_invalid_schema(client, dashboard_id: int):
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_get(client, dashboard_id: int):
|
||||
def test_get(client, dashboard_id: int, permalink_salt: str):
|
||||
login(client, "admin")
|
||||
resp = client.post(f"api/v1/dashboard/{dashboard_id}/permalink", json=STATE)
|
||||
data = json.loads(resp.data.decode("utf-8"))
|
||||
@@ -86,5 +105,6 @@ def test_get(client, dashboard_id: int):
|
||||
result = json.loads(resp.data.decode("utf-8"))
|
||||
assert result["dashboardId"] == str(dashboard_id)
|
||||
assert result["state"] == STATE
|
||||
db.session.query(KeyValueEntry).filter_by(uuid=key).delete()
|
||||
id_ = decode_permalink_id(key, permalink_salt)
|
||||
db.session.query(KeyValueEntry).filter_by(id=id_).delete()
|
||||
db.session.commit()
|
||||
|
||||
@@ -16,14 +16,16 @@
|
||||
# under the License.
|
||||
import json
|
||||
import pickle
|
||||
from typing import Any, Dict
|
||||
from uuid import UUID
|
||||
from typing import Any, Dict, Iterator
|
||||
from uuid import uuid3
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from superset import db
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
from superset.key_value.types import KeyValueResource
|
||||
from superset.key_value.utils import decode_permalink_id, encode_permalink_key
|
||||
from superset.models.slice import Slice
|
||||
from tests.integration_tests.base_tests import login
|
||||
from tests.integration_tests.fixtures.client import client
|
||||
@@ -51,7 +53,22 @@ def form_data(chart) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def test_post(client, form_data):
|
||||
@pytest.fixture
|
||||
def permalink_salt() -> Iterator[str]:
|
||||
from superset.key_value.shared_entries import get_permalink_salt, get_uuid_namespace
|
||||
from superset.key_value.types import SharedKey
|
||||
|
||||
key = SharedKey.EXPLORE_PERMALINK_SALT
|
||||
salt = get_permalink_salt(key)
|
||||
yield salt
|
||||
namespace = get_uuid_namespace(salt)
|
||||
db.session.query(KeyValueEntry).filter_by(
|
||||
resource=KeyValueResource.APP, uuid=uuid3(namespace, key),
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def test_post(client, form_data: Dict[str, Any], permalink_salt: str):
|
||||
login(client, "admin")
|
||||
resp = client.post(f"api/v1/explore/permalink", json={"formData": form_data})
|
||||
assert resp.status_code == 201
|
||||
@@ -59,7 +76,8 @@ def test_post(client, form_data):
|
||||
key = data["key"]
|
||||
url = data["url"]
|
||||
assert key in url
|
||||
db.session.query(KeyValueEntry).filter_by(uuid=key).delete()
|
||||
id_ = decode_permalink_id(key, permalink_salt)
|
||||
db.session.query(KeyValueEntry).filter_by(id=id_).delete()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@@ -69,21 +87,18 @@ def test_post_access_denied(client, form_data):
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_get_missing_chart(client, chart):
|
||||
def test_get_missing_chart(client, chart, permalink_salt: str) -> None:
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
key = 1234
|
||||
uuid_key = "e2ea9d19-7988-4862-aa69-c3a1a7628cb9"
|
||||
chart_id = 1234
|
||||
entry = KeyValueEntry(
|
||||
id=int(key),
|
||||
uuid=UUID("e2ea9d19-7988-4862-aa69-c3a1a7628cb9"),
|
||||
resource="explore_permalink",
|
||||
resource=KeyValueResource.EXPLORE_PERMALINK,
|
||||
value=pickle.dumps(
|
||||
{
|
||||
"chartId": key,
|
||||
"chartId": chart_id,
|
||||
"datasetId": chart.datasource.id,
|
||||
"formData": {
|
||||
"slice_id": key,
|
||||
"slice_id": chart_id,
|
||||
"datasource": f"{chart.datasource.id}__{chart.datasource.type}",
|
||||
},
|
||||
}
|
||||
@@ -91,20 +106,21 @@ def test_get_missing_chart(client, chart):
|
||||
)
|
||||
db.session.add(entry)
|
||||
db.session.commit()
|
||||
key = encode_permalink_key(entry.id, permalink_salt)
|
||||
login(client, "admin")
|
||||
resp = client.get(f"api/v1/explore/permalink/{uuid_key}")
|
||||
resp = client.get(f"api/v1/explore/permalink/{key}")
|
||||
assert resp.status_code == 404
|
||||
db.session.delete(entry)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def test_post_invalid_schema(client):
|
||||
def test_post_invalid_schema(client) -> None:
|
||||
login(client, "admin")
|
||||
resp = client.post(f"api/v1/explore/permalink", json={"abc": 123})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_get(client, form_data):
|
||||
def test_get(client, form_data: Dict[str, Any], permalink_salt: str) -> None:
|
||||
login(client, "admin")
|
||||
resp = client.post(f"api/v1/explore/permalink", json={"formData": form_data})
|
||||
data = json.loads(resp.data.decode("utf-8"))
|
||||
@@ -113,5 +129,6 @@ def test_get(client, form_data):
|
||||
assert resp.status_code == 200
|
||||
result = json.loads(resp.data.decode("utf-8"))
|
||||
assert result["state"]["formData"] == form_data
|
||||
db.session.query(KeyValueEntry).filter_by(uuid=key).delete()
|
||||
id_ = decode_permalink_id(key, permalink_salt)
|
||||
db.session.query(KeyValueEntry).filter_by(id=id_).delete()
|
||||
db.session.commit()
|
||||
|
||||
@@ -36,12 +36,8 @@ def test_create_id_entry(app_context: AppContext, admin: User) -> None:
|
||||
from superset.key_value.commands.create import CreateKeyValueCommand
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
key = CreateKeyValueCommand(
|
||||
actor=admin, resource=RESOURCE, value=VALUE, key_type="id",
|
||||
).run()
|
||||
entry = (
|
||||
db.session.query(KeyValueEntry).filter_by(id=int(key)).autoflush(False).one()
|
||||
)
|
||||
key = CreateKeyValueCommand(actor=admin, resource=RESOURCE, value=VALUE).run()
|
||||
entry = db.session.query(KeyValueEntry).filter_by(id=key.id).autoflush(False).one()
|
||||
assert pickle.loads(entry.value) == VALUE
|
||||
assert entry.created_by_fk == admin.id
|
||||
db.session.delete(entry)
|
||||
@@ -52,11 +48,9 @@ def test_create_uuid_entry(app_context: AppContext, admin: User) -> None:
|
||||
from superset.key_value.commands.create import CreateKeyValueCommand
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
key = CreateKeyValueCommand(
|
||||
actor=admin, resource=RESOURCE, value=VALUE, key_type="uuid",
|
||||
).run()
|
||||
key = CreateKeyValueCommand(actor=admin, resource=RESOURCE, value=VALUE).run()
|
||||
entry = (
|
||||
db.session.query(KeyValueEntry).filter_by(uuid=UUID(key)).autoflush(False).one()
|
||||
db.session.query(KeyValueEntry).filter_by(uuid=key.uuid).autoflush(False).one()
|
||||
)
|
||||
assert pickle.loads(entry.value) == VALUE
|
||||
assert entry.created_by_fk == admin.id
|
||||
|
||||
@@ -30,8 +30,8 @@ from tests.integration_tests.key_value.commands.fixtures import admin, RESOURCE,
|
||||
if TYPE_CHECKING:
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
ID_KEY = "234"
|
||||
UUID_KEY = "5aae143c-44f1-478e-9153-ae6154df333a"
|
||||
ID_KEY = 234
|
||||
UUID_KEY = UUID("5aae143c-44f1-478e-9153-ae6154df333a")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -39,10 +39,7 @@ def key_value_entry() -> KeyValueEntry:
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
entry = KeyValueEntry(
|
||||
id=int(ID_KEY),
|
||||
uuid=UUID(UUID_KEY),
|
||||
resource=RESOURCE,
|
||||
value=pickle.dumps(VALUE),
|
||||
id=ID_KEY, uuid=UUID_KEY, resource=RESOURCE, value=pickle.dumps(VALUE),
|
||||
)
|
||||
db.session.add(entry)
|
||||
db.session.commit()
|
||||
@@ -55,10 +52,7 @@ def test_delete_id_entry(
|
||||
from superset.key_value.commands.delete import DeleteKeyValueCommand
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
assert (
|
||||
DeleteKeyValueCommand(resource=RESOURCE, key=ID_KEY, key_type="id",).run()
|
||||
is True
|
||||
)
|
||||
assert DeleteKeyValueCommand(resource=RESOURCE, key=ID_KEY).run() is True
|
||||
|
||||
|
||||
def test_delete_uuid_entry(
|
||||
@@ -67,10 +61,7 @@ def test_delete_uuid_entry(
|
||||
from superset.key_value.commands.delete import DeleteKeyValueCommand
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
assert (
|
||||
DeleteKeyValueCommand(resource=RESOURCE, key=UUID_KEY, key_type="uuid").run()
|
||||
is True
|
||||
)
|
||||
assert DeleteKeyValueCommand(resource=RESOURCE, key=UUID_KEY).run() is True
|
||||
|
||||
|
||||
def test_delete_entry_missing(
|
||||
@@ -79,7 +70,4 @@ def test_delete_entry_missing(
|
||||
from superset.key_value.commands.delete import DeleteKeyValueCommand
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
assert (
|
||||
DeleteKeyValueCommand(resource=RESOURCE, key="456", key_type="id").run()
|
||||
is False
|
||||
)
|
||||
assert DeleteKeyValueCommand(resource=RESOURCE, key=456).run() is False
|
||||
|
||||
@@ -26,14 +26,15 @@ from flask_appbuilder.security.sqla.models import User
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from superset.extensions import db
|
||||
from superset.key_value.types import KeyValueResource
|
||||
from tests.integration_tests.test_app import app
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
ID_KEY = "123"
|
||||
UUID_KEY = "3e7a2ab8-bcaf-49b0-a5df-dfb432f291cc"
|
||||
RESOURCE = "my_resource"
|
||||
ID_KEY = 123
|
||||
UUID_KEY = UUID("3e7a2ab8-bcaf-49b0-a5df-dfb432f291cc")
|
||||
RESOURCE = KeyValueResource.APP
|
||||
VALUE = {"foo": "bar"}
|
||||
|
||||
|
||||
@@ -42,10 +43,7 @@ def key_value_entry() -> Generator[KeyValueEntry, None, None]:
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
entry = KeyValueEntry(
|
||||
id=int(ID_KEY),
|
||||
uuid=UUID(UUID_KEY),
|
||||
resource=RESOURCE,
|
||||
value=pickle.dumps(VALUE),
|
||||
id=ID_KEY, uuid=UUID_KEY, resource=RESOURCE, value=pickle.dumps(VALUE),
|
||||
)
|
||||
db.session.add(entry)
|
||||
db.session.commit()
|
||||
|
||||
@@ -39,7 +39,7 @@ if TYPE_CHECKING:
|
||||
def test_get_id_entry(app_context: AppContext, key_value_entry: KeyValueEntry) -> None:
|
||||
from superset.key_value.commands.get import GetKeyValueCommand
|
||||
|
||||
value = GetKeyValueCommand(resource=RESOURCE, key=ID_KEY, key_type="id").run()
|
||||
value = GetKeyValueCommand(resource=RESOURCE, key=ID_KEY).run()
|
||||
assert value == VALUE
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def test_get_uuid_entry(
|
||||
) -> None:
|
||||
from superset.key_value.commands.get import GetKeyValueCommand
|
||||
|
||||
value = GetKeyValueCommand(resource=RESOURCE, key=UUID_KEY, key_type="uuid").run()
|
||||
value = GetKeyValueCommand(resource=RESOURCE, key=UUID_KEY).run()
|
||||
assert value == VALUE
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ def test_get_id_entry_missing(
|
||||
) -> None:
|
||||
from superset.key_value.commands.get import GetKeyValueCommand
|
||||
|
||||
value = GetKeyValueCommand(resource=RESOURCE, key="456", key_type="id").run()
|
||||
value = GetKeyValueCommand(resource=RESOURCE, key=456).run()
|
||||
assert value is None
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ def test_get_expired_entry(app_context: AppContext) -> None:
|
||||
)
|
||||
db.session.add(entry)
|
||||
db.session.commit()
|
||||
value = GetKeyValueCommand(resource=RESOURCE, key=ID_KEY, key_type="id").run()
|
||||
value = GetKeyValueCommand(resource=RESOURCE, key=ID_KEY).run()
|
||||
assert value is None
|
||||
db.session.delete(entry)
|
||||
db.session.commit()
|
||||
@@ -94,7 +94,7 @@ def test_get_future_expiring_entry(app_context: AppContext) -> None:
|
||||
)
|
||||
db.session.add(entry)
|
||||
db.session.commit()
|
||||
value = GetKeyValueCommand(resource=RESOURCE, key=str(id_), key_type="id").run()
|
||||
value = GetKeyValueCommand(resource=RESOURCE, key=id_).run()
|
||||
assert value == VALUE
|
||||
db.session.delete(entry)
|
||||
db.session.commit()
|
||||
|
||||
@@ -46,12 +46,10 @@ def test_update_id_entry(
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
key = UpdateKeyValueCommand(
|
||||
actor=admin, resource=RESOURCE, key=ID_KEY, value=NEW_VALUE, key_type="id",
|
||||
actor=admin, resource=RESOURCE, key=ID_KEY, value=NEW_VALUE,
|
||||
).run()
|
||||
assert key == ID_KEY
|
||||
entry = (
|
||||
db.session.query(KeyValueEntry).filter_by(id=int(ID_KEY)).autoflush(False).one()
|
||||
)
|
||||
assert key.id == ID_KEY
|
||||
entry = db.session.query(KeyValueEntry).filter_by(id=ID_KEY).autoflush(False).one()
|
||||
assert pickle.loads(entry.value) == NEW_VALUE
|
||||
assert entry.changed_by_fk == admin.id
|
||||
|
||||
@@ -63,25 +61,20 @@ def test_update_uuid_entry(
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
key = UpdateKeyValueCommand(
|
||||
actor=admin, resource=RESOURCE, key=UUID_KEY, value=NEW_VALUE, key_type="uuid",
|
||||
actor=admin, resource=RESOURCE, key=UUID_KEY, value=NEW_VALUE,
|
||||
).run()
|
||||
assert key == UUID_KEY
|
||||
assert key.uuid == UUID_KEY
|
||||
entry = (
|
||||
db.session.query(KeyValueEntry)
|
||||
.filter_by(uuid=UUID(UUID_KEY))
|
||||
.autoflush(False)
|
||||
.one()
|
||||
db.session.query(KeyValueEntry).filter_by(uuid=UUID_KEY).autoflush(False).one()
|
||||
)
|
||||
assert pickle.loads(entry.value) == NEW_VALUE
|
||||
assert entry.changed_by_fk == admin.id
|
||||
|
||||
|
||||
def test_update_missing_entry(
|
||||
app_context: AppContext, admin: User, key_value_entry: KeyValueEntry,
|
||||
) -> None:
|
||||
def test_update_missing_entry(app_context: AppContext, admin: User) -> None:
|
||||
from superset.key_value.commands.update import UpdateKeyValueCommand
|
||||
|
||||
key = UpdateKeyValueCommand(
|
||||
actor=admin, resource=RESOURCE, key="456", value=NEW_VALUE, key_type="id",
|
||||
actor=admin, resource=RESOURCE, key=456, value=NEW_VALUE,
|
||||
).run()
|
||||
assert key is None
|
||||
|
||||
@@ -46,9 +46,9 @@ def test_upsert_id_entry(
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
key = UpsertKeyValueCommand(
|
||||
actor=admin, resource=RESOURCE, key=ID_KEY, value=NEW_VALUE, key_type="id",
|
||||
actor=admin, resource=RESOURCE, key=ID_KEY, value=NEW_VALUE,
|
||||
).run()
|
||||
assert key == ID_KEY
|
||||
assert key.id == ID_KEY
|
||||
entry = (
|
||||
db.session.query(KeyValueEntry).filter_by(id=int(ID_KEY)).autoflush(False).one()
|
||||
)
|
||||
@@ -63,28 +63,23 @@ def test_upsert_uuid_entry(
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
key = UpsertKeyValueCommand(
|
||||
actor=admin, resource=RESOURCE, key=UUID_KEY, value=NEW_VALUE, key_type="uuid",
|
||||
actor=admin, resource=RESOURCE, key=UUID_KEY, value=NEW_VALUE,
|
||||
).run()
|
||||
assert key == UUID_KEY
|
||||
assert key.uuid == UUID_KEY
|
||||
entry = (
|
||||
db.session.query(KeyValueEntry)
|
||||
.filter_by(uuid=UUID(UUID_KEY))
|
||||
.autoflush(False)
|
||||
.one()
|
||||
db.session.query(KeyValueEntry).filter_by(uuid=UUID_KEY).autoflush(False).one()
|
||||
)
|
||||
assert pickle.loads(entry.value) == NEW_VALUE
|
||||
assert entry.changed_by_fk == admin.id
|
||||
|
||||
|
||||
def test_upsert_missing_entry(
|
||||
app_context: AppContext, admin: User, key_value_entry: KeyValueEntry,
|
||||
) -> None:
|
||||
def test_upsert_missing_entry(app_context: AppContext, admin: User) -> None:
|
||||
from superset.key_value.commands.upsert import UpsertKeyValueCommand
|
||||
from superset.key_value.models import KeyValueEntry
|
||||
|
||||
key = UpsertKeyValueCommand(
|
||||
actor=admin, resource=RESOURCE, key="456", value=NEW_VALUE, key_type="id",
|
||||
actor=admin, resource=RESOURCE, key=456, value=NEW_VALUE,
|
||||
).run()
|
||||
assert key == "456"
|
||||
assert key.id == 456
|
||||
db.session.query(KeyValueEntry).filter_by(id=456).delete()
|
||||
db.session.commit()
|
||||
|
||||
Reference in New Issue
Block a user