From e91e00b395f585d2aaee271de0c8247c092d0a0b Mon Sep 17 00:00:00 2001 From: Ngoc Khuat <qn.khuat@gmail.com> Date: Thu, 11 May 2023 11:50:30 +0700 Subject: [PATCH] Fix custom mapping doesn't work on dashboard (#30217) * revert remove hydrate param values in #22836 * fix tests in ee ns * fix param field tests * Add e2e test * Add e2e tests * Add e2e tests * fix failed test and use t2 instead of db * fix wrong t2 call spec * fix failing tests * Merge branch 'master' into ngoc-revert-remove-hydrate-param-values * Merge branch 'master' into ngoc-revert-remove-hydrate-param-values * apply Tamas's suggestions and unskip e2e tests * delete the e2e tests that AlexP previously added in this PR but brought it into a seperated PR" " * create Field values if not found * add a tests for when field values is not existed, we create them * Merge branch 'master' into ngoc-revert-remove-hydrate-param-values * Fix the e2e test suite for remapping * Fix the e2e test suite for remapping * Fix the e2e test suite for remapping * Fix the e2e test suite for remapping * Fix the e2e test suite for remapping * revert the changes to create full values on demand * remove unused requires * Merge branch 'master' into ngoc-revert-remove-hydrate-param-values * fix tests * Merge branch 'master' into ngoc-revert-remove-hydrate-param-values * Merge branch 'master' into ngoc-revert-remove-hydrate-param-values Co-Authored-By: Alexander Polyankin <alexander.polyankin@metabase.com> --- .../29347-remapped-field-values.cy.spec.js | 53 ++++++++++++-- .../sandbox/models/params/field_values.clj | 21 ++++++ .../sandbox/api/dashboard_test.clj | 41 +++++++++++ src/metabase/api/dashboard.clj | 2 +- src/metabase/api/public.clj | 38 +++++----- src/metabase/models/params.clj | 49 ++++++++++++- src/metabase/models/params/field_values.clj | 26 +++++++ test/metabase/api/dashboard_test.clj | 31 ++++++++ test/metabase/api/embed_test.clj | 3 +- test/metabase/api/public_test.clj | 70 ++++++++++++++++++- 10 files changed, 307 insertions(+), 27 deletions(-) diff --git a/e2e/test/scenarios/dashboard-filters/reproductions/29347-remapped-field-values.cy.spec.js b/e2e/test/scenarios/dashboard-filters/reproductions/29347-remapped-field-values.cy.spec.js index 58ea760d6b9..4b535b8c3da 100644 --- a/e2e/test/scenarios/dashboard-filters/reproductions/29347-remapped-field-values.cy.spec.js +++ b/e2e/test/scenarios/dashboard-filters/reproductions/29347-remapped-field-values.cy.spec.js @@ -27,7 +27,7 @@ const questionDetails = { }, }; -const dashboardDetails = { +const editableDashboardDetails = { parameters: [filterDetails], enable_embedding: true, embedding_params: { @@ -35,12 +35,19 @@ const dashboardDetails = { }, }; -describe.skip("issues 29347, 29346", () => { +const lockedDashboardDetails = { + parameters: [filterDetails], + enable_embedding: true, + embedding_params: { + [filterDetails.slug]: "locked", + }, +}; + +describe("issues 29347, 29346", () => { beforeEach(() => { restore(); cy.signInAsAdmin(); addFieldRemapping(ORDERS.QUANTITY); - createDashboard(); }); describe("regular dashboards", () => { @@ -50,6 +57,7 @@ describe.skip("issues 29347, 29346", () => { }); it("should be able to filter on remapped values (metabase#29347, metabase#29346)", () => { + createDashboard(); cy.get("@dashboardId").then(dashboardId => visitDashboard(dashboardId)); cy.wait("@dashboard"); cy.wait("@cardQuery"); @@ -61,6 +69,7 @@ describe.skip("issues 29347, 29346", () => { }); it("should be able to filter on remapped values in the url (metabase#29347, metabase#29346)", () => { + createDashboard(); cy.get("@dashboardId").then(dashboardId => { visitDashboard(dashboardId, { params: { [filterDetails.slug]: filterValue }, @@ -80,6 +89,7 @@ describe.skip("issues 29347, 29346", () => { }); it("should be able to filter on remapped values (metabase#29347, metabase#29346)", () => { + createDashboard(); cy.get("@dashboardId").then(dashboardId => visitEmbeddedPage({ resource: { dashboard: dashboardId }, @@ -96,15 +106,37 @@ describe.skip("issues 29347, 29346", () => { }); it("should be able to filter on remapped values in the token (metabase#29347, metabase#29346)", () => { + createDashboard({ dashboardDetails: lockedDashboardDetails }); cy.get("@dashboardId").then(dashboardId => { visitEmbeddedPage({ resource: { dashboard: dashboardId }, - params: { [filterDetails.slug]: filterValue }, + params: { + [filterDetails.slug]: filterValue, + }, }); }); cy.wait("@dashboard"); cy.wait("@cardQuery"); + verifyRemappedCardValues(filterValue); + }); + + it("should be able to filter on remapped values in the url (metabase#29347, metabase#29346)", () => { + createDashboard(); + cy.get("@dashboardId").then(dashboardId => { + visitEmbeddedPage( + { + resource: { dashboard: dashboardId }, + params: {}, + }, + { + setFilters: `${filterDetails.slug}=${filterValue}`, + }, + ); + }); + cy.wait("@dashboard"); + cy.wait("@cardQuery"); + verifyRemappedValues(filterValue); }); }); @@ -116,6 +148,7 @@ describe.skip("issues 29347, 29346", () => { }); it("should be able to filter on remapped values (metabase#29347, metabase#29346)", () => { + createDashboard(); cy.get("@dashboardId").then(dashboardId => visitPublicDashboard(dashboardId), ); @@ -129,6 +162,7 @@ describe.skip("issues 29347, 29346", () => { }); it("should be able to filter on remapped values in the url (metabase#29347, metabase#29346)", () => { + createDashboard(); cy.get("@dashboardId").then(dashboardId => { visitPublicDashboard(dashboardId, { params: { [filterDetails.slug]: filterValue }, @@ -165,7 +199,9 @@ const addFieldRemapping = fieldId => { ); }; -const createDashboard = () => { +const createDashboard = ({ + dashboardDetails = editableDashboardDetails, +} = {}) => { cy.createQuestionAndDashboard({ questionDetails, dashboardDetails }).then( ({ body: { id, card_id, dashboard_id } }) => { cy.request("PUT", `/api/dashboard/${dashboard_id}/cards`, { @@ -205,10 +241,17 @@ const filterOnRemappedValues = fieldValue => { }; const verifyRemappedValues = fieldValue => { + verifyRemappedFilterValues(filterValue); + verifyRemappedCardValues(fieldValue); +}; + +const verifyRemappedFilterValues = fieldValue => { filterWidget().within(() => { cy.findByText(getRemappedValue(fieldValue)).should("be.visible"); }); +}; +const verifyRemappedCardValues = fieldValue => { getDashboardCard().within(() => { cy.findAllByText(getRemappedValue(fieldValue)).should("have.length", 2); }); diff --git a/enterprise/backend/src/metabase_enterprise/sandbox/models/params/field_values.clj b/enterprise/backend/src/metabase_enterprise/sandbox/models/params/field_values.clj index c04d8734fa8..c31a340e45e 100644 --- a/enterprise/backend/src/metabase_enterprise/sandbox/models/params/field_values.clj +++ b/enterprise/backend/src/metabase_enterprise/sandbox/models/params/field_values.clj @@ -10,6 +10,7 @@ [metabase.models.field-values :as field-values] [metabase.models.params.field-values :as params.field-values] [metabase.public-settings.premium-features :refer [defenterprise]] + [metabase.util :as u] [toucan.hydrate :refer [hydrate]] [toucan2.core :as t2])) @@ -75,6 +76,26 @@ (mbql.u/match-one v [:dimension [:field field-id _]] field-id))] {k (get login-attributes k)})))]))) +(defenterprise field-id->field-values-for-current-user + "Fetch *existing* FieldValues for a sequence of `field-ids` for the current User. Values are returned as a map of + {field-id FieldValues-instance} + Returns `nil` if `field-ids` is empty or no matching FieldValues exist." + :feature :sandboxes + [field-ids] + (let [fields (when (seq field-ids) + (hydrate (t2/select Field :id [:in (set field-ids)]) :table)) + {unsandboxed-fields false + sandboxed-fields true} (group-by (comp boolean field-is-sandboxed?) fields)] + (merge + ;; use the normal OSS batched implementation for any Fields that aren't subject to sandboxing. + (when (seq unsandboxed-fields) + (params.field-values/default-field-id->field-values-for-current-user + (map u/the-id unsandboxed-fields))) + ;; for sandboxed fields, fetch the sandboxed values individually. + (into {} (for [{field-id :id, :as field} sandboxed-fields] + [field-id (select-keys (params.field-values/get-or-create-advanced-field-values! :sandbox field) + [:values :human_readable_values :field_id])]))))) + (defenterprise get-or-create-field-values-for-current-user!* "Fetch cached FieldValues for a `field`, creating them if needed if the Field should have FieldValues. These should be filtered as appropriate for the current User (currently this only applies to the EE impl)." diff --git a/enterprise/backend/test/metabase_enterprise/sandbox/api/dashboard_test.clj b/enterprise/backend/test/metabase_enterprise/sandbox/api/dashboard_test.clj index db835c4e582..0d455683a1d 100644 --- a/enterprise/backend/test/metabase_enterprise/sandbox/api/dashboard_test.clj +++ b/enterprise/backend/test/metabase_enterprise/sandbox/api/dashboard_test.clj @@ -2,6 +2,7 @@ "Tests for special behavior of `/api/metabase/dashboard` endpoints in the Metabase Enterprise Edition." (:require [clojure.test :refer :all] + [metabase-enterprise.sandbox.test-util :as mt.tu] [metabase-enterprise.test :as met] [metabase.api.dashboard-test :as api.dashboard-test] [metabase.models :refer [Card Dashboard DashboardCard FieldValues]] @@ -14,6 +15,46 @@ [schema.core :as s] [toucan2.core :as t2])) +(deftest params-values-test + (testing "Return sandboxed `param_values` for Fields to which the current User only has sandboxed access." + (met/with-gtaps {:gtaps {:venues + {:remappings {:cat [:variable [:field-id (mt/id :venues :category_id)]]} + :query (mt.tu/restricted-column-query (mt/id))}} + :attributes {:cat 50}} + (perms/grant-permissions! &group (perms/table-read-path (mt/id :categories))) + (mt/with-temp* [Dashboard [{dashboard-id :id} {:name "Test Dashboard"}] + Card [{card-id :id} {:name "Dashboard Test Card"}] + DashboardCard [{_ :id} {:dashboard_id dashboard-id + :card_id card-id + :parameter_mappings [{:card_id card-id + :parameter_id "foo" + :target [:dimension + [:field (mt/id :venues :name) nil]]} + ;; should be returned normally since user has non-sandbox perms + {:card_id card-id + :parameter_id "bar" + :target [:dimension + [:field (mt/id :categories :name) nil]]} + ;; shouldn't be returned since user has no perms + {:card_id card-id + :parameter_id "bax" + :target [:dimension + [:field (mt/id :users :name) nil]]}]}]] + (is (= {(mt/id :venues :name) {:values ["Garaje" + "Gordo Taqueria" + "La Tortilla"] + :human_readable_values [] + :field_id (mt/id :venues :name)} + + (mt/id :categories :name) {:values ["African" + "American" + "Artisan"] + :human_readable_values [] + :field_id (mt/id :categories :name)}} + (let [response (:param_values (mt/user-http-request :rasta :get 200 (str "dashboard/" dashboard-id)))] + (into {} (for [[field-id m] response] + [field-id (update m :values (partial take 3))]))))))))) + (deftest chain-filter-sandboxed-field-values-test (testing "When chain filter endpoints would normally return cached FieldValues (#13832), make sure sandboxing is respected" (met/with-gtaps {:gtaps {:categories {:query (mt/mbql-query categories {:filter [:< $id 3]})}}} diff --git a/src/metabase/api/dashboard.clj b/src/metabase/api/dashboard.clj index 74b0022a054..be84366f61f 100644 --- a/src/metabase/api/dashboard.clj +++ b/src/metabase/api/dashboard.clj @@ -218,7 +218,7 @@ ;; have a hydration key and an id. moderation_reviews currently aren't batch hydrated but i'm worried they ;; cannot be in this situation (hydrate [:ordered_cards [:card [:moderation_reviews :moderator_details]] :series :dashcard/action :dashcard/linkcard-info] - :collection_authority_level :can_write :param_fields) + :collection_authority_level :can_write :param_fields :param_values) api/read-check api/check-not-archived hide-unreadable-cards diff --git a/src/metabase/api/public.clj b/src/metabase/api/public.clj index 474aa1bbc89..3ea0f9920d6 100644 --- a/src/metabase/api/public.clj +++ b/src/metabase/api/public.clj @@ -83,13 +83,14 @@ (defn public-card "Return a public Card matching key-value `conditions`, removing all columns that should not be visible to the general - public. Throws a 404 if the Card doesn't exist." + public. Throws a 404 if the Card doesn't exist." [& conditions] - (-> (api/check-404 (apply t2/select-one [Card :id :dataset_query :description :display :name :parameters :visualization_settings] - :archived false, conditions)) - remove-card-non-public-columns - combine-parameters-and-template-tags - (hydrate :param_fields))) + (binding [params/*ignore-current-user-perms-and-return-all-field-values* true] + (-> (api/check-404 (apply t2/select-one [Card :id :dataset_query :description :display :name :parameters :visualization_settings] + :archived false, conditions)) + remove-card-non-public-columns + combine-parameters-and-template-tags + (hydrate :param_values :param_fields)))) (defn- card-with-uuid [uuid] (public-card :public_uuid uuid)) @@ -202,20 +203,21 @@ (defn public-dashboard "Return a public Dashboard matching key-value `conditions`, removing all columns that should not be visible to the - general public. Throws a 404 if the Dashboard doesn't exist." + general public. Throws a 404 if the Dashboard doesn't exist." [& conditions] {:pre [(even? (count conditions))]} - (-> (api/check-404 (apply t2/select-one [Dashboard :name :description :id :parameters :auto_apply_filters], :archived false, conditions)) - (hydrate [:ordered_cards :card :series :dashcard/action] :param_fields) - api.dashboard/add-query-average-durations - (update :ordered_cards (fn [dashcards] - (for [dashcard dashcards] - (-> (select-keys dashcard [:id :card :card_id :dashboard_id :series :col :row :size_x - :size_y :parameter_mappings :visualization_settings :action]) - (update :card remove-card-non-public-columns) - (update :series (fn [series] - (for [series series] - (remove-card-non-public-columns series)))))))))) + (binding [params/*ignore-current-user-perms-and-return-all-field-values* true] + (-> (api/check-404 (apply t2/select-one [Dashboard :name :description :id :parameters :auto_apply_filters], :archived false, conditions)) + (hydrate [:ordered_cards :card :series :dashcard/action] :param_values :param_fields) + api.dashboard/add-query-average-durations + (update :ordered_cards (fn [dashcards] + (for [dashcard dashcards] + (-> (select-keys dashcard [:id :card :card_id :dashboard_id :series :col :row :size_x + :size_y :parameter_mappings :visualization_settings :action]) + (update :card remove-card-non-public-columns) + (update :series (fn [series] + (for [series series] + (remove-card-non-public-columns series))))))))))) (defn- dashboard-with-uuid [uuid] (public-dashboard :public_uuid uuid)) diff --git a/src/metabase/models/params.clj b/src/metabase/models/params.clj index fe1ca37734c..a30e3190061 100644 --- a/src/metabase/models/params.clj +++ b/src/metabase/models/params.clj @@ -18,6 +18,7 @@ [metabase.mbql.schema :as mbql.s] [metabase.mbql.util :as mbql.u] [metabase.models.interface :as mi] + [metabase.models.params.field-values :as params.field-values] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.log :as log] @@ -67,6 +68,28 @@ :else field-id-or-form)) +(def ^:dynamic *ignore-current-user-perms-and-return-all-field-values* + "Whether to ignore permissions for the current User and return *all* FieldValues for the Fields being parameterized by + Cards and Dashboards. This determines how `:param_values` gets hydrated for Card and Dashboard. Normally, this is + `false`, but the public and embed versions of the API endpoints can bind this to `true` to bypass normal perms + checks (since there is no current User) and get *all* values." + false) + +(defn- field-ids->param-field-values-ignoring-current-user + [param-field-ids] + (t2/select-fn->fn :field_id identity ['FieldValues :values :human_readable_values :field_id] + :type :full + :field_id [:in param-field-ids])) + +(defn- field-ids->param-field-values + "Given a collection of `param-field-ids` return a map of FieldValues for the Fields they reference. + This map is returned by various endpoints as `:param_values`, if `param-field-ids` is empty, return `nil`" + [param-field-ids] + (when (seq param-field-ids) + ((if *ignore-current-user-perms-and-return-all-field-values* + field-ids->param-field-values-ignoring-current-user + params.field-values/field-id->field-values-for-current-user) param-field-ids))) + (defn- template-tag->field-form "Fetch the `:field` clause from `dashcard` referenced by `template-tag`. @@ -158,7 +181,20 @@ (hydrate :has_field_values :name_field [:dimensions :human_readable_field]) remove-dimensions-nonpublic-columns)))) -(defmulti ^:private param-fields + +(defmulti ^:private ^{:hydrate :param_values} param-values + "Add a `:param_values` map (Field ID -> FieldValues) containing FieldValues for the Fields referenced by the + parameters of a Card or a Dashboard. Implementations are in respective sections below." + t2/model) + +#_{:clj-kondo/ignore [:unused-private-var]} +(mi/define-simple-hydration-method ^:private hydrate-param-values + :param_values + "Hydration method for `:param_fields`." + [instance] + (param-values instance)) + +(defmulti ^:private ^{:hydrate :param_fields} param-fields "Add a `:param_fields` map (Field ID -> Field) for all of the Fields referenced by the parameters of a Card or Dashboard. Implementations are below in respective sections." t2/model) @@ -205,6 +241,15 @@ id)) (cards->card-param-field-ids (map :card dashcards)))) +(defn- dashboard->param-field-values + "Return a map of Field ID to FieldValues (if any) for any Fields referenced by Cards in `dashboard`, + or `nil` if none are referenced or none of them have FieldValues." + [dashboard] + (field-ids->param-field-values (dashcards->param-field-ids (:ordered_cards dashboard)))) + +(defmethod param-values :metabase.models.dashboard/Dashboard [dashboard] + (dashboard->param-field-values dashboard)) + (defmethod param-fields :metabase.models.dashboard/Dashboard [dashboard] (-> (hydrate dashboard [:ordered_cards :card]) :ordered_cards @@ -231,6 +276,8 @@ (set (mbql.u/match (seq (card->template-tag-field-clauses card)) [:field (id :guard integer?) _] id))) +(defmethod param-values :model/Card [card] + (-> card card->template-tag-field-ids field-ids->param-field-values)) (defmethod param-fields :model/Card [card] (-> card card->template-tag-field-ids param-field-ids->fields)) diff --git a/src/metabase/models/params/field_values.clj b/src/metabase/models/params/field_values.clj index 5fbdc58462b..5f78d25c1d1 100644 --- a/src/metabase/models/params/field_values.clj +++ b/src/metabase/models/params/field_values.clj @@ -2,6 +2,8 @@ "Code related to fetching FieldValues for Fields to populate parameter widgets. Always used by the field values (`GET /api/field/:id/values`) endpoint; used by the chain filter endpoints under certain circumstances." (:require + [medley.core :as m] + [metabase.models.field :as field :refer [Field]] [metabase.models.field-values :as field-values :refer [FieldValues]] [metabase.models.interface :as mi] [metabase.plugins.classloader :as classloader] @@ -39,6 +41,30 @@ (select-keys [:values :field_id :has_more_values])) {:values [], :field_id (u/the-id field), :has_more_values false})) +(defn default-field-id->field-values-for-current-user + "OSS implementation; used as a fallback for the EE implementation for any fields that aren't subject to sandboxing." + [field-ids] + (when (seq field-ids) + (not-empty + (let [field-values (t2/select [FieldValues :values :human_readable_values :field_id] + :type :full + :field_id [:in (set field-ids)]) + readable-fields (when (seq field-values) + (field/readable-fields-only (t2/select [Field :id :table_id] + :id [:in (set (map :field_id field-values))]))) + readable-field-ids (set (map :id readable-fields))] + (->> field-values + (filter #(contains? readable-field-ids (:field_id %))) + (m/index-by :field_id)))))) + +(defenterprise field-id->field-values-for-current-user + "Fetch *existing* FieldValues for a sequence of `field-ids` for the current User. Values are returned as a map of + {field-id FieldValues-instance} + Returns `nil` if `field-ids` is empty of no matching FieldValues exist." + metabase-enterprise.sandbox.models.params.field-values + [field-ids] + (default-field-id->field-values-for-current-user field-ids)) + ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | Advanced FieldValues | ;;; +----------------------------------------------------------------------------------------------------------------+ diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj index 228e7ea5d12..22e2759551d 100644 --- a/test/metabase/api/dashboard_test.clj +++ b/test/metabase/api/dashboard_test.clj @@ -303,6 +303,7 @@ :collection_authority_level nil :can_write false :param_fields nil + :param_values nil :last-edit-info {:timestamp true :id true :first_name "Test" :last_name "User" :email "test@example.com"} :ordered_cards [{:size_x 4 :size_y 4 @@ -383,6 +384,7 @@ :collection_id true :collection_authority_level nil :can_write false + :param_values nil :param_fields {field-id {:id field-id :table_id table-id :display_name display-name @@ -446,6 +448,35 @@ (is (= "You don't have permissions to do that." (mt/user-http-request :rasta :get 403 (format "dashboard/%d" dashboard-id))))))))) +(deftest param-values-test + (testing "Don't return `param_values` for Fields for which the current User has no data perms." + (mt/with-temp-copy-of-db + (perms/revoke-data-perms! (perms-group/all-users) (mt/id)) + (perms/grant-permissions! (perms-group/all-users) (perms/table-read-path (mt/id :venues))) + (mt/with-temp* [Dashboard [{dashboard-id :id} {:name "Test Dashboard"}] + Card [{card-id :id} {:name "Dashboard Test Card"}] + DashboardCard [{_ :id} {:dashboard_id dashboard-id + :card_id card-id + :parameter_mappings [{:card_id card-id + :parameter_id "foo" + :target [:dimension + [:field (mt/id :venues :name) nil]]} + {:card_id card-id + :parameter_id "bar" + :target [:dimension + [:field (mt/id :categories :name) nil]]}]}]] + + (is (= {(mt/id :venues :name) {:values ["20th Century Cafe" + "25°" + "33 Taps"] + :field_id (mt/id :venues :name) + :human_readable_values []}} + (let [response (:param_values (mt/user-http-request :rasta :get 200 (str "dashboard/" dashboard-id)))] + (into {} (for [[field-id m] response] + [field-id (update m :values (partial take 3))]))))))))) + + + ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | PUT /api/dashboard/:id | ;;; +----------------------------------------------------------------------------------------------------------------+ diff --git a/test/metabase/api/embed_test.clj b/test/metabase/api/embed_test.clj index d827e77bf23..ea64aa8db7b 100644 --- a/test/metabase/api/embed_test.clj +++ b/test/metabase/api/embed_test.clj @@ -146,10 +146,11 @@ :visualization_settings {} :dataset_query {:type "query"} :parameters [] + :param_values nil :param_fields nil}) (def successful-dashboard-info - {:auto_apply_filters true, :description nil, :parameters [], :ordered_cards [], :param_fields nil}) + {:auto_apply_filters true, :description nil, :parameters [], :ordered_cards [], :param_values nil, :param_fields nil}) (def ^:private yesterday (time/minus (time/now) (time/days 1))) diff --git a/test/metabase/api/public_test.clj b/test/metabase/api/public_test.clj index dacafcb9258..d254355d4b4 100644 --- a/test/metabase/api/public_test.clj +++ b/test/metabase/api/public_test.clj @@ -136,7 +136,7 @@ (with-temp-public-card [{uuid :public_uuid, card-id :id}] (testing "Happy path -- should be able to fetch the Card" - (is (= #{:dataset_query :description :display :id :name :visualization_settings :parameters :param_fields} + (is (= #{:dataset_query :description :display :id :name :visualization_settings :parameters :param_values :param_fields} (set (keys (client/client :get 200 (str "public/card/" uuid))))))) (testing "Check that we cannot fetch a public Card if public sharing is disabled" @@ -149,6 +149,30 @@ (is (= "Not found." (client/client :get 404 (str "public/card/" uuid)))))))))) +(deftest make-sure-param-values-get-returned-as-expected + (let [category-name-id (mt/id :categories :name)] + (mt/with-temp Card [card {:dataset_query + {:database (mt/id) + :type :native + :native {:query (str "SELECT COUNT(*) " + "FROM venues " + "LEFT JOIN categories ON venues.category_id = categories.id " + "WHERE {{category}}") + :collection "CATEGORIES" + :template-tags {:category {:name "category" + :display-name "Category" + :type "dimension" + :dimension ["field" category-name-id nil] + :widget-type "category" + :required true}}}}}] + (is (= {(mt/id :categories :name) {:values (t2/select-one-fn (comp count :values) + 'FieldValues :field_id category-name-id) + :human_readable_values [] + :field_id category-name-id}} + (-> (:param_values (#'api.public/public-card :id (u/the-id card))) + (update-in [category-name-id :values] count) + (update category-name-id #(into {} %)))))))) + ;;; ------------------------- GET /api/public/card/:uuid/query (and JSON/CSV/XSLX versions) -------------------------- (deftest check-that-we--cannot--execute-a-publiccard-if-the-setting-is-disabled @@ -618,6 +642,50 @@ (is (= [1 2 3 4] (t2/select-one-fn :values FieldValues :field_id (mt/id :venues :price))))) +(defn- price-param-values [] + {(mt/id :venues :price) {:values [1 2 3 4] + :human_readable_values [] + :field_id (mt/id :venues :price)}}) + +(defn- add-price-param-to-dashboard! [dashboard] + (t2/update! Dashboard (u/the-id dashboard) {:parameters [{:name "Price", :type "category", :slug "price", :id "_PRICE_"}]})) + +(defn- add-dimension-param-mapping-to-dashcard! [dashcard card dimension] + (t2/update! DashboardCard (u/the-id dashcard) {:parameter_mappings [{:card_id (u/the-id card) + :target ["dimension" dimension]}]})) + +(defn- GET-param-values [dashboard] + (mt/with-temporary-setting-values [enable-public-sharing true] + (:param_values (client/client :get 200 (str "public/dashboard/" (:public_uuid dashboard)))))) + +(deftest check-that-param-info-comes-back-for-sql-cards + (with-temp-public-dashboard-and-card [dash card dashcard] + (t2/update! Card (u/the-id card) + {:dataset_query {:database (mt/id) + :type :native + :native {:template-tags {:price {:name "price" + :display-name "Price" + :type "dimension" + :dimension ["field" (mt/id :venues :price) nil]}}}}}) + (add-price-param-to-dashboard! dash) + (add-dimension-param-mapping-to-dashcard! dashcard card ["template-tag" "price"]) + (is (= (price-param-values) + (GET-param-values dash))))) + +(deftest check-that-param-info-comes-back-for-mbql-cards--field-id- + (with-temp-public-dashboard-and-card [dash card dashcard] + (add-price-param-to-dashboard! dash) + (add-dimension-param-mapping-to-dashcard! dashcard card ["field" (mt/id :venues :price) nil]) + (is (= (price-param-values) + (GET-param-values dash))))) + +(deftest check-that-param-info-comes-back-for-mbql-cards--fk--- + (with-temp-public-dashboard-and-card [dash card dashcard] + (add-price-param-to-dashboard! dash) + (add-dimension-param-mapping-to-dashcard! dashcard card [:field (mt/id :venues :price) {:source-field (mt/id :checkins :venue_id)}]) + (is (= (price-param-values) + (GET-param-values dash))))) + ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | New FieldValues search endpoints | ;;; +----------------------------------------------------------------------------------------------------------------+ -- GitLab