Skip to content
Snippets Groups Projects
Unverified Commit fa8d88a3 authored by Ngoc Khuat's avatar Ngoc Khuat Committed by GitHub
Browse files

Add API to fetch and search card parameters values (#23102)

* add API to fetch and search card parameters

* remove code to support linked filters

* remove unecessary data in tests

* fix from Noah's comments

* some chain-filter -> param-values
parent f716628d
No related branches found
No related tags found
No related merge requests found
......@@ -16,12 +16,14 @@
[metabase.email.messages :as messages]
[metabase.events :as events]
[metabase.mbql.normalize :as mbql.normalize]
[metabase.mbql.util :as mbql.u]
[metabase.models.bookmark :as bookmark :refer [CardBookmark]]
[metabase.models.card :as card :refer [Card]]
[metabase.models.collection :as collection :refer [Collection]]
[metabase.models.database :refer [Database]]
[metabase.models.interface :as mi]
[metabase.models.moderation-review :as moderation-review]
[metabase.models.params.chain-filter :as chain-filter]
[metabase.models.persisted-info :as persisted-info :refer [PersistedInfo]]
[metabase.models.pulse :as pulse :refer [Pulse]]
[metabase.models.query :as query]
......@@ -32,6 +34,7 @@
[metabase.models.view-log :refer [ViewLog]]
[metabase.query-processor.async :as qp.async]
[metabase.query-processor.card :as qp.card]
[metabase.query-processor.error-type :as qp.error-type]
[metabase.query-processor.pivot :as qp.pivot]
[metabase.query-processor.util :as qp.util]
[metabase.related :as related]
......@@ -704,6 +707,118 @@
:js-int-to-string? false}))
;;; ------------------------------------------------ Parameters -------------------------------------------------
(def ^:const result-limit
"How many results to return when getting values for a parameter."
1000)
(defn- field-clause->field-id
"Find the field id in a field clause if it exists.
(field-clause->field-id [:field 3 nil])
;; -> 3"
[field-clause]
(mbql.u/match-one field-clause [:field (id :guard integer?) _] id))
(defn- template-tag-target->field-id
"Given a template tag target, find the field id that it's connected to.
Note that a target has a field id connect to it iff it's a field filter."
[card {:keys [id]}]
(if-let [template-tags (get-in card [:dataset_query :native :template-tags])]
(first (for [[_ template-tag] template-tags
:when (and (= (:id template-tag) id)
(= (:type template-tag) :dimension))
:let [field-id (field-clause->field-id (:dimension template-tag))]
:when field-id]
field-id))
(throw (ex-info (tru "Card with ID {0} does not have template tags." (:id card))
{:card-id (:id card)
:template-tag-id id}))))
(defn- target->field-id
"Find the field id that the target is connected to.
Target could be a `dimension`, in which target have the shape
[:dimension [:field 3 nil]]
Or it could be a `template-tag`, then it should have the shape
[:template-tag {:id \"6006ad7d-036e-83ec-4d6f-30f82b98ac21\"}]"
[card [target-type target-args]]
(case target-type
:dimension (field-clause->field-id target-args)
:template-tag (template-tag-target->field-id card target-args)
nil))
(defn- param-id->field-ids
"Get Field ID(s) associated with a parameter in a Card.
(param-id->field-ids (Card 62) \"ee876336\")
;; -> #{276}"
[{:keys [parameter_mappings] :as card} param-id]
(into #{} (for [{:keys [target parameter_id]} parameter_mappings
:when (= parameter_id param-id)
:let [field-id (target->field-id card target)]
:when field-id]
field-id)))
(s/defn param-values
"Given a `param-id`, returns a of possible values that it could choose from.
;; show me categories
(param-values (Card 62) \"ee876336\")
;; -> (\"African\" \"American\" \"Artisan\" ...)
;; show me categories that contains \"Ameri\"
(param-values (Card 62) \"ee876336\" \"Ameri\")
;; -> (\"American\")
"
([card param-id]
(param-values card param-id nil))
([card param-id query]
(when-not (seq (filter #(= (:id %) param-id) (:parameters card)))
(throw (ex-info (tru "Card does not have a parameter with the ID {0}" (pr-str param-id))
{:status-code 400})))
(let [field-ids (param-id->field-ids card param-id)]
(when (empty? field-ids)
(throw (ex-info (tru "Parameter {0} does not have any Fields associated with it" (pr-str param-id))
{:param-id param-id
:status-code 400})))
(try
(let [results (distinct (mapcat (if (seq query)
#(chain-filter/chain-filter-search % {} query :limit result-limit)
#(chain-filter/chain-filter % {} :limit result-limit))
field-ids))]
;; results can come back as [v ...] *or* as [[orig remapped] ...]. Sort by remapped value if that's the case
(if (sequential? (first results))
(sort-by second results)
(sort results)))
(catch clojure.lang.ExceptionInfo e
(if (= (:type (u/all-ex-data e)) qp.error-type/missing-required-permissions)
(api/throw-403 e)
(throw e)))))))
(api/defendpoint GET "/:id/params/:param-key/values"
"Fetch possible values of the parameter whose ID is `:param-id`.
;; fetch values for Card 1 parameter 'abc' that are possible
GET /api/card/1/params/abc/values"
[id param-key]
(let [card (api/read-check Card id)]
(param-values card param-key)))
(api/defendpoint GET "/:id/params/:param-key/search/:query"
"Fetch possible values of the parameter whose ID is `:param-id` that contain `:query`.
;; fetch values for Card 1 parameter 'abc' that contain 'Cam'
GET /api/card/1/params/abc/search/Cam
Currently limited to first 1000 results."
[id param-key query]
(let [card (api/read-check Card id)]
(param-values card param-key query)))
;;; ----------------------------------------------- Sharing is Caring ------------------------------------------------
(api/defendpoint POST "/:card-id/public_link"
......@@ -766,6 +881,8 @@
:qp-runner qp.pivot/run-pivot-query
:ignore_cache ignore_cache))
;;; ----------------------------------------------- Persistence ------------------------------------------------
(api/defendpoint POST "/:card-id/persist"
"Mark the model (card) as persisted. Runs the query and saves it to the database backing the card and hot swaps this
query in place of the model's query."
......
......@@ -14,6 +14,7 @@
[metabase.models :refer [Card CardBookmark Collection Dashboard Database ModerationReview Pulse PulseCard
PulseChannel PulseChannelRecipient Table Timeline TimelineEvent ViewLog]]
[metabase.models.moderation-review :as moderation-review]
[metabase.models.params.chain-filter-test :as chain-filter-test]
[metabase.models.permissions :as perms]
[metabase.models.permissions-group :as perms-group]
[metabase.models.revision :as revision :refer [Revision]]
......@@ -1838,6 +1839,167 @@
(name->position (:data (mt/user-http-request :crowberto :get 200 (format "collection/%s/items" coll-id-2)
:model "card" :archived "false"))))))))
;;; +----------------------------------------------------------------------------------------------------------------+
;;; | PARAMETER VALUES ENDPOINTS |
;;; +----------------------------------------------------------------------------------------------------------------+
(defn- do-with-param-values-fixtures
[query-type card-values f]
{:pre [(#{:native :query} query-type)]}
(mt/with-temp* [Card [card]]
(let [card-defaults
(if (= query-type :query)
;; notebook query with parameters are fields
{:database_id (mt/id)
:table_id (mt/id :venues)
:dataset_query (mt/mbql-query venues)
:parameters [{:name "Category Name"
:slug "category_name"
:id "_CATEGORY_NAME_"
:type "category"}
{:name "Category ID"
:slug "category_id"
:id "_CATEGORY_ID_"
:type "category"}]
:parameter_mappings [{:parameter_id "_CATEGORY_NAME_"
:card_id (:id card)
:target [:dimension (mt/$ids venues $category_id->categories.name)]}
{:parameter_id "_CATEGORY_ID_"
:card_id (:id card)
:target [:dimension (mt/$ids venues $category_id)]}]}
;; native query with parameters are template tags
{:database_id (mt/id)
:query_type :native
:dataset_query {:database (mt/id)
:type :native
:native
{:query (str "SELECT * FROM VENUES WHERE {{category}} and {{category_id}};")
:template-tags {"category" {:id "c7fcf1fa"
:name "category"
:display-name "Category"
:type :dimension
:dimension [:field (mt/$ids venues $category_id->categories.name) nil]
:widget-type :string/=}
"category_id" {:id "a3cd3f3b"
:name "category_id"
:display-name "Category"
:type :dimension
:dimension [:field (mt/$ids venues $category_id) nil]
:widget-type :number/=}}}}
:parameters [{:name "Category_name"
:slug "category_name"
:id "_CATEGORY_NAME_"
:type "category"}
{:name "Category ID"
:slug "category_id"
:id "_CATEGORY_ID_"
:type "category"}]
:parameter_mappings [{:parameter_id "_CATEGORY_NAME_"
:card_id (:id card)
:target [:template-tag {:id "c7fcf1fa"}]}
{:parameter_id "_CATEGORY_ID_"
:card_id (:id card)
:target [:template-tag {:id "a3cd3f3b"}]}]})]
(db/update! Card (:id card)
(merge card-defaults card-values)))
(f {:card card
:param-ids {:category-name "_CATEGORY_NAME_"
:category-id "_CATEGORY_ID_"}})))
(defmacro ^:private with-param-values-fixtures
"Create a query and its parameters."
{:style/indent 2}
[query-type [binding card-values] & body]
`(do-with-param-values-fixtures ~query-type ~card-values (fn [~binding] ~@body)))
(defn- param-values-values-url [card-or-id param-id]
(format "card/%d/params/%s/values" (u/the-id card-or-id) (name param-id)))
(defn- param-values-search-url [card-or-id param-id query]
(str (format "card/%d/params/%s/search/" (u/the-id card-or-id) (name param-id))
query))
(deftest param-values-test
(testing "GET /api/card/:id/params/:param-id/values"
(doseq [query-type [:query :native]]
(testing (format "With %s question" (name query-type))
(with-param-values-fixtures query-type [{:keys [card param-ids]}]
(testing "Show me names of categories"
(is (= ["African" "American" "Artisan"]
(take 3 (mt/user-http-request :rasta :get 200 (param-values-values-url
card
(:category-name param-ids))))))))
(testing "Should require perms for the Card"
(mt/with-non-admin-groups-no-root-collection-perms
(mt/with-temp Collection [collection]
(with-param-values-fixtures query-type [{:keys [card param-ids]} {:collection_id (:id collection)}]
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :get 403 (param-values-values-url
card
(:category-name param-ids)))))))))
(testing "should check perms for the Fields in question"
(mt/with-temp-copy-of-db
(with-param-values-fixtures query-type [{:keys [card param-ids]}]
(perms/revoke-data-perms! (perms-group/all-users) (mt/id))
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :get 403 (param-values-values-url
card
(:category-name param-ids))))))))))))
(deftest param-values-search-test
(testing "GET /api/card/:id/params/:param-id/search/:query"
(doseq [query-type [:native :query]]
(testing (format "With %s question" (name query-type))
(with-param-values-fixtures :query [{:keys [card param-ids]}]
(let [url (param-values-search-url card (:category-name param-ids) "bar")]
(testing (str "\n" url)
(testing "\nShow me names of categories that include 'bar' (case-insensitive)"
(is (= ["Bar" "Gay Bar" "Juice Bar"]
(take 3 (mt/user-http-request :rasta :get 200 url)))))))
(let [url (param-values-search-url card (:category-name param-ids) "house")]
(testing "\nShow me names of categories that include 'house' that have expensive venues (price = 4)"
(is (= ["Steakhouse"]
(take 3 (mt/user-http-request :rasta :get 200 url))))))
(testing "Should require a non-empty query"
(doseq [query [nil
""
" "
"\n"]]
(let [url (param-values-search-url card (:category-name param-ids) query)]
(is (= "API endpoint does not exist."
(mt/user-http-request :rasta :get 404 url)))))))
(testing "Should require perms for the card"
(mt/with-non-admin-groups-no-root-collection-perms
(mt/with-temp Collection [collection]
(with-param-values-fixtures :query [{:keys [card param-ids]} {:collection_id (:id collection)}]
(let [url (param-values-search-url card (:category-name param-ids) "s")]
(testing (str "\n url")
(is (= "You don't have permissions to do that."
(mt/user-http-request :rasta :get 403 url)))))))))))))
(deftest param-values-human-readable-values-remapping-test
(testing "Get param values for Fields that have Human-Readable values\n"
(doseq [query-type [:native :query]]
(testing (format "With %s question" (name query-type))
(chain-filter-test/with-human-readable-values-remapping
(with-param-values-fixtures query-type [{:keys [card param-ids]}]
(testing "GET /api/card/:id/params/:param-id/values"
(let [url (param-values-values-url card (:category-id param-ids))]
(is (= [[2 "American"]
[3 "Artisan"]]
(take 2 (mt/user-http-request :rasta :get 200 url))))))
(testing "GET /api/card/:id/params/:param-id/search/:query"
(let [url (param-values-search-url card (:category-id param-ids) "house")]
(is (= [[67 "Steakhouse"]]
(take 1 (mt/user-http-request :rasta :get 200 url))))))))))))
;;; +----------------------------------------------------------------------------------------------------------------+
;;; | PUBLIC SHARING ENDPOINTS |
;;; +----------------------------------------------------------------------------------------------------------------+
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment