Custom values for Parameters. A parameter with custom values will need to define a source: - static-list: the values is pre-defined and stored inside parameter's config - card: the values is a column from a saved question | (ns metabase.models.params.custom-values (:require [clojure.string :as str] [metabase.legacy-mbql.util :as mbql.u] [metabase.lib.ident :as lib.ident] [metabase.models.interface :as mi] [metabase.query-processor :as qp] [metabase.query-processor.util :as qp.util] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] [toucan2.core :as t2])) |
------------------------------------------------- source=static-list -------------------------------------------------- | |
(mu/defn- normalize-query :- :string "Normalize a `query` to lower-case." [query :- :string] (u/lower-case-en (str/trim query))) | |
Filters for values that match Values could have 2 shapes - [[value1], [value2]] - [[value2, label2], [value2, label2]] - we search using label in this case | (defn- query-matches
[query values]
(let [normalized-query (normalize-query query)]
(filter (fn [v] (str/includes? (normalize-query (if (= (count v) 1)
(first v)
(second v)))
normalized-query)) values))) |
(defn- static-list-values
[{values-source-options :values_source_config :as _param} query]
(when-let [values (:values values-source-options)]
(let [wrapped-values (map (fn [v] (if-not (sequential? v) [v] v)) values)]
{:values (if query
(query-matches query wrapped-values)
wrapped-values)
:has_more_values false}))) | |
---------------------------------------------------- source=card ------------------------------------------------------ | |
Maximum number of rows returned when running a card. It's 1000 because it matches with the limit for chain-filter. Maybe we should lower it for the sake of displaying a parameter dropdown. | (def ^:dynamic *max-rows* 1000) |
(defn- values-from-card-query
[card value-field-ref opts]
(let [query-string (:query-string opts)
value-base-type (:base_type (qp.util/field->field-info value-field-ref (:result_metadata card)))
new-filter [:and
[(if (isa? value-base-type :type/Text)
:not-empty
:not-null)
value-field-ref]
(when query-string
(if-not (isa? value-base-type :type/Text)
[:= value-field-ref query-string]
[:contains [:lower value-field-ref] (u/lower-case-en query-string)]))]]
{:database (:database_id card)
:type :query
:query (if-let [inner-mbql (and (not= (:type card) :model)
(-> card :dataset_query :query))]
;; MBQL query - hijack the final stage, drop its aggregation and breakout (if any).
(let [target-stage (:stage-number opts)
last-stage (mbql.u/legacy-last-stage-number inner-mbql)
inner-mbql (if (and target-stage last-stage
(= (inc last-stage) target-stage))
{:source-query inner-mbql}
inner-mbql)]
(-> inner-mbql
(dissoc :aggregation :order-by)
(assoc :breakout [value-field-ref]
:breakout-idents (lib.ident/indexed-idents 1))
(update :limit (fnil min *max-rows*) *max-rows*)
(update :filter (fn [old]
(cond->> new-filter
old (conj [:and old]))))))
;; Model or Native query - wrap it with a new MBQL stage.
{:source-table (format "card__%d" (:id card))
:breakout [value-field-ref]
:breakout-idents (lib.ident/indexed-idents 1)
:limit *max-rows*
:filter new-filter})
:middleware {:disable-remaps? true}})) | |
Get distinct values of a field from a card. (values-from-card 1 [:field "name" nil] "red") ;; will execute a mbql that looks like ;; {:source-table (format "card__%d" card-id) ;; :fields [value-field] ;; :breakout [value-field] ;; :filter [:contains [:lower value-field] "red"] ;; :limit max-rows} => {:values [["Red Medicine"]] :hasmorevalues false} | (mu/defn values-from-card
([card value-field]
(values-from-card card value-field nil))
([card :- (ms/InstanceOf :model/Card)
value-field-ref :- ms/LegacyFieldOrExpressionReference
opts :- [:maybe :map]]
(let [mbql-query (values-from-card-query card value-field-ref opts)
result (qp/process-query mbql-query)
values (get-in result [:data :rows])]
{:values values
;; If the row_count returned = the limit we specified, then it's probably has more than that.
;; If the query has its own limit smaller than *max-rows*, then there's no more values.
:has_more_values (= (:row_count result) *max-rows*)}))) |
Given a param and query returns the values. | (defn card-values
[{config :values_source_config :as _param} query]
(let [card-id (:card_id config)
card (t2/select-one :model/Card :id card-id)]
(values-from-card card (:value_field config) {:query-string query}))) |
(defn- can-get-card-values?
[card value-field]
(boolean
(and (not (:archived card))
(some? (qp.util/field->field-info value-field (:result_metadata card)))))) | |
--------------------------------------------- Putting it together ---------------------------------------------- | |
(mu/defn parameter->values :- ms/FieldValuesResult
"Given a parameter with a custom-values source, return the values.
`default-case-thunk` is a 0-arity function that returns values list when:
- :values_source_type = card but the card is archived or the card no longer contains the value-field.
- :values_source_type = nil."
[parameter query default-case-thunk]
(case (:values_source_type parameter)
"static-list" (static-list-values parameter query)
"card" (let [card (t2/select-one :model/Card :id (get-in parameter [:values_source_config :card_id]))]
(when-not (mi/can-read? card)
(throw (ex-info "You don't have permissions to do that." {:status-code 403})))
(if (can-get-card-values? card (get-in parameter [:values_source_config :value_field]))
(card-values parameter query)
(default-case-thunk)))
nil (default-case-thunk)
(throw (ex-info (tru "Invalid parameter source {0}" (:values_source_type parameter))
{:status-code 400
:parameter parameter})))) | |