Skip to content
Snippets Groups Projects
Unverified Commit 9bd751e4 authored by Cam Saul's avatar Cam Saul
Browse files

ALL OF THE TESTS :white_check_mark::x:

parent 4fe4c52f
Branches
Tags
No related merge requests found
......@@ -354,6 +354,7 @@
[token field-id]
(let [unsigned-token (eu/unsign token)
card-id (eu/get-in-unsigned-token-or-throw unsigned-token [:resource :question])]
(check-embedding-enabled-for-card card-id)
(public-api/card-and-field-id->values card-id field-id)))
(api/defendpoint GET "/dashboard/:token/field/:field-id/values"
......@@ -361,8 +362,7 @@
[token field-id]
(let [unsigned-token (eu/unsign token)
dashboard-id (eu/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])]
;; TODO - look at other code in this namespace and make sure the logic we're using over in `public-api` is
;; appropriate here as well !
(check-embedding-enabled-for-dashboard dashboard-id)
(public-api/dashboard-and-field-id->values dashboard-id field-id)))
......@@ -375,6 +375,7 @@
limit (s/maybe su/IntStringGreaterThanZero)}
(let [unsigned-token (eu/unsign token)
card-id (eu/get-in-unsigned-token-or-throw unsigned-token [:resource :question])]
(check-embedding-enabled-for-card card-id)
(public-api/search-card-fields card-id field-id search-field-id value (when limit (Integer/parseInt limit)))))
(api/defendpoint GET "/dashboard/:token/field/:field-id/search/:search-field-id"
......@@ -384,6 +385,7 @@
limit (s/maybe su/IntStringGreaterThanZero)}
(let [unsigned-token (eu/unsign token)
dashboard-id (eu/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])]
(check-embedding-enabled-for-dashboard dashboard-id)
(public-api/search-dashboard-fields dashboard-id field-id search-field-id value (when limit
(Integer/parseInt limit)))))
......@@ -395,6 +397,7 @@
{value su/NonBlankString}
(let [unsigned-token (eu/unsign token)
card-id (eu/get-in-unsigned-token-or-throw unsigned-token [:resource :question])]
(check-embedding-enabled-for-card card-id)
(public-api/card-field-remapped-values card-id field-id remapped-id value)))
(api/defendpoint GET "/dashboard/:token/field/:field-id/remapping/:remapped-id"
......@@ -402,6 +405,7 @@
{value su/NonBlankString}
(let [unsigned-token (eu/unsign token)
dashboard-id (eu/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])]
(check-embedding-enabled-for-dashboard dashboard-id)
(public-api/dashboard-field-remapped-values dashboard-id field-id remapped-id value)))
......
......@@ -154,11 +154,12 @@
"Fetch FieldValues, if they exist, for a `field` and return them in an appropriate format for public/embedded
use-cases."
[field]
(api/check-404 field)
(if-let [field-values (and (field-values/field-should-have-field-values? field)
(field-values/create-field-values-if-needed! field))]
(-> field-values
(assoc :values (field-values/field-values->pairs field-values))
(dissoc :human_readable_values))
(dissoc :human_readable_values :created_at :updated_at :id :field_id))
{:values []}))
(api/defendpoint GET "/:id/values"
......
......@@ -192,7 +192,8 @@
(if (instance? metabase.query_processor.interface.Field x)
(swap! field-ids conj (:field-id x))
x))
(qp/expand query))))
(qp/expand query))
@field-ids))
(defn- card->referenced-field-ids
"Return a set of all Field IDs referenced by `card`, in both the MBQL query itself and in its parameters ('template
......@@ -205,9 +206,9 @@
"Check to make sure the query for Card with `card-id` references Field with `field-id`. Otherwise, or if the Card
cannot be found, throw an Exception."
[field-id card-id]
(let [card (api/check-404 (db/select-one [Card :dataset_query] :id card-id))
referenced-fields-ids (card->referenced-field-ids card)]
(api/check-404 (contains? referenced-fields-ids field-id))))
(let [card (api/check-404 (db/select-one [Card :dataset_query] :id card-id))
referenced-field-ids (card->referenced-field-ids card)]
(api/check-404 (contains? referenced-field-ids field-id))))
(defn- check-search-field-is-allowed
"Check whether a search Field is allowed to be used in conjunction with another Field. A search Field is allowed if
......@@ -220,6 +221,7 @@
If none of these conditions are met, you are not allowed to use the search field in combination with the other
field, and an 400 exception will be thrown."
[field-id search-field-id]
{:pre [(integer? field-id) (integer? search-field-id)]}
(api/check-400
(or (= field-id search-field-id)
(db/exists? Dimension :field_id field-id, :human_readable_field_id search-field-id)
......@@ -229,7 +231,6 @@
(db/exists? Field :id search-field-id, :table_id table-id, :special_type (mdb/isa :type/Name))))))
;; FIXME
(defn- check-field-is-referenced-by-dashboard
"Check that `field-id` belongs to a Field that is used as a parameter in a Dashboard with `dashboard-id`, or throw a
404 Exception."
......
......@@ -69,11 +69,11 @@
cases where more than one name Field exists for a Table, this just adds the first one it finds."
[fields]
(when-let [table-ids (seq (map :table_id fields))]
(u/key-by :table_id (db/select (conj Field:params-columns-only :table_id)
(u/key-by :table_id (db/select Field:params-columns-only
:table_id [:in table-ids]
:special_type (mdb/isa :type/Name)))))
(defn add-name-fields
(defn add-name-field
"For all `fields` that are `:type/PK` Fields, look for a `:type/Name` Field belonging to the same Table. For each
Field, if a matching name Field exists, add it under the `:name_field` key. This is so the Fields can be used in
public/embedded field values search widgets. This only includes the information needed to power those widgets, and
......@@ -123,7 +123,7 @@
parameter widgets."
[field-ids]
(when (seq field-ids)
(u/key-by :id (-> (db/select (conj Field:params-columns-only :table_id) :id [:in field-ids])
(u/key-by :id (-> (db/select Field:params-columns-only :id [:in field-ids])
(hydrate :has_field_values :name_field [:dimensions :human_readable_field])
remove-dimensions-nonpublic-columns))))
......
......@@ -19,6 +19,7 @@
[metabase.test
[data :as data]
[util :as tu]]
[toucan.db :as db]
[toucan.util.test :as tt])
(:import java.io.ByteArrayInputStream))
......@@ -486,3 +487,273 @@
:card_id (u/get-id series-card)
:position 0}]
(:status (http/client :get 200 (str (dashcard-url (assoc dashcard :card_id (u/get-id series-card)))))))))))
;;; ------------------------------- GET /api/embed/card/:token/field/:field-id/values --------------------------------
(defn- field-values-url [card-or-dashboard field-or-id]
(str
"embed/"
(condp instance? card-or-dashboard
(class Card) (str "card/" (card-token card-or-dashboard))
(class Dashboard) (str "dashboard/" (dash-token card-or-dashboard)))
"/field/"
(u/get-id field-or-id)
"/values"))
(defn- do-with-embedding-enabled-and-temp-card-referencing {:style/indent 2} [table-kw field-kw f]
(with-embedding-enabled-and-new-secret-key
(tt/with-temp Card [card (assoc (public-test/mbql-card-referencing table-kw field-kw)
:enable_embedding true)]
(f card))))
(defmacro ^:private with-embedding-enabled-and-temp-card-referencing
{:style/indent 3}
[table-kw field-kw [card-binding] & body]
`(do-with-embedding-enabled-and-temp-card-referencing ~table-kw ~field-kw
(fn [~(or card-binding '_)]
~@body)))
;; should be able to fetch values for a Field referenced by a public Card
(expect
{:values [["20th Century Cafe"]
["25°"]
["33 Taps"]
["800 Degrees Neapolitan Pizzeria"]
["BCD Tofu House"]]}
(with-embedding-enabled-and-temp-card-referencing :venues :name [card]
(-> (http/client :get 200 (field-values-url card (data/id :venues :name)))
(update :values (partial take 5)))))
;; but for Fields that are not referenced we should get an Exception
(expect
"Not found."
(with-embedding-enabled-and-temp-card-referencing :venues :name [card]
(http/client :get 400 (field-values-url card (data/id :venues :price)))))
;; Endpoint should fail if embedding is disabled
(expect
"Embedding is not enabled."
(with-embedding-enabled-and-temp-card-referencing :venues :name [card]
(tu/with-temporary-setting-values [enable-embedding false]
(http/client :get 400 (field-values-url card (data/id :venues :name))))))
(expect
"Embedding is not enabled for this object."
(with-embedding-enabled-and-temp-card-referencing :venues :name [card]
(db/update! Card (u/get-id card) :enable_embedding false)
(http/client :get 400 (field-values-url card (data/id :venues :name)))))
;;; ----------------------------- GET /api/embed/dashboard/:token/field/:field-id/values -----------------------------
(defn- do-with-embedding-enabled-and-temp-dashcard-referencing {:style/indent 2} [table-kw field-kw f]
(with-embedding-enabled-and-new-secret-key
(tt/with-temp* [Dashboard [dashboard {:enable_embedding true}]
Card [card (public-test/mbql-card-referencing table-kw field-kw)]
DashboardCard [dashcard {:dashboard_id (u/get-id dashboard)
:card_id (u/get-id card)
:parameter_mappings [{:card_id (u/get-id card)
:target [:dimension
[:field-id
(data/id table-kw field-kw)]]}]}]]
(f dashboard card dashcard))))
(defmacro ^:private with-embedding-enabled-and-temp-dashcard-referencing
{:style/indent 3}
[table-kw field-kw [dash-binding card-binding dashcard-binding] & body]
`(do-with-embedding-enabled-and-temp-dashcard-referencing ~table-kw ~field-kw
(fn [~(or dash-binding '_) ~(or card-binding '_) ~(or dashcard-binding '_)]
~@body)))
;; should be able to use it when everything is g2g
(expect
{:values [["20th Century Cafe"]
["25°"]
["33 Taps"]
["800 Degrees Neapolitan Pizzeria"]
["BCD Tofu House"]]}
(with-embedding-enabled-and-temp-dashcard-referencing :venues :name [dashboard]
(-> (http/client :get 200 (field-values-url dashboard (data/id :venues :name)))
(update :values (partial take 5)))))
;; shound NOT be able to use the endpoint with a Field not referenced by the Dashboard
(expect
"Not found."
(with-embedding-enabled-and-temp-dashcard-referencing :venues :name [dashboard]
(http/client :get 400 (field-values-url dashboard (data/id :venues :price)))))
;; Endpoint should fail if embedding is disabled
(expect
"Embedding is not enabled."
(with-embedding-enabled-and-temp-dashcard-referencing :venues :name [dashboard]
(tu/with-temporary-setting-values [enable-embedding false]
(http/client :get 400 (field-values-url dashboard (data/id :venues :name))))))
;; Endpoint should fail if embedding is disabled for the Dashboard
(expect
"Embedding is not enabled for this object."
(with-embedding-enabled-and-temp-dashcard-referencing :venues :name [dashboard]
(db/update! Dashboard (u/get-id dashboard) :enable_embedding false)
(http/client :get 400 (field-values-url dashboard (data/id :venues :name)))))
;;; ----------------------- GET /api/embed/card/:token/field/:field-id/search/:search-field-id -----------------------
(defn- field-search-url [card-or-dashboard field-or-id search-field-or-id]
(str "embed/"
(condp instance? card-or-dashboard
(class Card) (str "card/" (card-token card-or-dashboard))
(class Dashboard) (str "dashboard/" (dash-token card-or-dashboard)))
"/field/" (u/get-id field-or-id)
"/search/" (u/get-id search-field-or-id)))
(expect
[[93 "33 Taps"]]
(with-embedding-enabled-and-temp-card-referencing :venues :id [card]
(http/client :get 200 (field-search-url card (data/id :venues :id) (data/id :venues :name))
:value "33 T")))
;; if search field isn't allowed to be used with the other Field endpoint should return exception
(expect
"Invalid Request."
(with-embedding-enabled-and-temp-card-referencing :venues :id [card]
(http/client :get 400 (field-search-url card (data/id :venues :id) (data/id :venues :price))
:value "33 T")))
;; Endpoint should fail if embedding is disabled
(expect
"Embedding is not enabled."
(with-embedding-enabled-and-temp-card-referencing :venues :id [card]
(tu/with-temporary-setting-values [enable-embedding false]
(http/client :get 400 (field-search-url card (data/id :venues :id) (data/id :venues :name))
:value "33 T"))))
;; Endpoint should fail if embedding is disabled for the Card
(expect
"Embedding is not enabled for this object."
(with-embedding-enabled-and-temp-card-referencing :venues :id [card]
(db/update! Card (u/get-id card) :enable_embedding false)
(http/client :get 400 (field-search-url card (data/id :venues :id) (data/id :venues :name))
:value "33 T")))
;;; -------------------- GET /api/embed/dashboard/:token/field/:field-id/search/:search-field-id ---------------------
(expect
[[93 "33 Taps"]]
(with-embedding-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(http/client :get (field-search-url dashboard (data/id :venues :id) (data/id :venues :name))
:value "33 T")))
;; if search field isn't allowed to be used with the other Field endpoint should return exception
(expect
"Invalid Request."
(with-embedding-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(http/client :get 400 (field-search-url dashboard (data/id :venues :id) (data/id :venues :price))
:value "33 T")))
;; Endpoint should fail if embedding is disabled
(expect
"Embedding is not enabled."
(with-embedding-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(tu/with-temporary-setting-values [enable-embedding false]
(http/client :get 400 (field-search-url dashboard (data/id :venues :name) (data/id :venues :name))
:value "33 T"))))
;; Endpoint should fail if embedding is disabled for the Dashboard
(expect
"Embedding is not enabled for this object."
(with-embedding-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(db/update! Dashboard (u/get-id dashboard) :enable_embedding false)
(http/client :get 400 (field-search-url dashboard (data/id :venues :name) (data/id :venues :name))
:value "33 T")))
;;; ----------------------- GET /api/embed/card/:token/field/:field-id/remapping/:remapped-id ------------------------
(defn- field-remapping-url [card-or-dashboard field-or-id remapped-field-or-id]
(str "embed/"
(condp instance? card-or-dashboard
(class Card) (str "card/" (card-token card-or-dashboard))
(class Dashboard) (str "dashboard/" (dash-token card-or-dashboard)))
"/field/" (u/get-id field-or-id)
"/remapping/" (u/get-id remapped-field-or-id)))
;; we should be able to use the API endpoint and get the same results we get by calling the function above directly
(expect
[10 "Fred 62"]
(with-embedding-enabled-and-temp-card-referencing :venues :id [card]
(http/client :get 200 (field-remapping-url card (data/id :venues :id) (data/id :venues :name))
:value "10")))
;; shouldn't work if Card doesn't reference the Field in question
(expect
"Not found."
(with-embedding-enabled-and-temp-card-referencing :venues :price [card]
(http/client :get 400 (field-remapping-url card (data/id :venues :id) (data/id :venues :name))
:value "10")))
;; ...or if the remapping Field isn't allowed to be used with the other Field
(expect
"Invalid Request."
(with-embedding-enabled-and-temp-card-referencing :venues :id [card]
(http/client :get 400 (field-remapping-url card (data/id :venues :id) (data/id :venues :price))
:value "10")))
;; ...or if embedding is disabled
(expect
"Embedding is not enabled."
(with-embedding-enabled-and-temp-card-referencing :venues :id [card]
(tu/with-temporary-setting-values [enable-embedding false]
(http/client :get 400 (field-remapping-url card (data/id :venues :id) (data/id :venues :name))
:value "10"))))
;; ...or if embedding is disabled for the Card
(expect
"Embedding is not enabled for this object."
(with-embedding-enabled-and-temp-card-referencing :venues :id [card]
(db/update! Card (u/get-id card) :enable_embedding false)
(http/client :get 400 (field-remapping-url card (data/id :venues :id) (data/id :venues :name))
:value "10")))
;;; --------------------- GET /api/embed/dashboard/:token/field/:field-id/remapping/:remapped-id ---------------------
;; we should be able to use the API endpoint and get the same results we get by calling the function above directly
(expect
[10 "Fred 62"]
(with-embedding-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(http/client :get 200 (field-remapping-url dashboard (data/id :venues :id) (data/id :venues :name))
:value "10")))
;; shouldn't work if Card doesn't reference the Field in question
(expect
"Not found."
(with-embedding-enabled-and-temp-dashcard-referencing :venues :price [dashboard]
(http/client :get 400 (field-remapping-url dashboard (data/id :venues :id) (data/id :venues :name))
:value "10")))
;; ...or if the remapping Field isn't allowed to be used with the other Field
(expect
"Invalid Request."
(with-embedding-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(http/client :get 400 (field-remapping-url dashboard (data/id :venues :id) (data/id :venues :price))
:value "10")))
;; ...or if embedding is disabled
(expect
"Embedding is not enabled."
(with-embedding-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(tu/with-temporary-setting-values [enable-embedding false]
(http/client :get 400 (field-remapping-url dashboard (data/id :venues :id) (data/id :venues :name))
:value "10"))))
;; ...or if embedding is disabled for the Dashboard
(expect
"Embedding is not enabled for this object."
(with-embedding-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(db/update! Dashboard (u/get-id dashboard) :enable_embedding false)
(http/client :get 400 (field-remapping-url dashboard (data/id :venues :id) (data/id :venues :name))
:value "10")))
......@@ -17,9 +17,6 @@
;; Helper Fns
(def ^:private default-field-values
{:id true, :created_at true, :updated_at true, :field_id true})
(defn- db-details []
(tu/match-$ (db)
{:created_at $
......@@ -168,7 +165,7 @@
;; ## GET /api/field/:id/values
;; Should return something useful for a field that has special_type :type/Category
(expect
(merge default-field-values {:values (mapv vector [1 2 3 4])})
{:values [[1] [2] [3] [4]]}
(do
;; clear out existing human_readable_values in case they're set
(db/update! FieldValues (field-values-id :venues :price)
......@@ -186,20 +183,15 @@
{:values []}
((user->client :rasta) :get 200 (format "field/%d/values" (id :users :password))))
(defn- num->$ [num-seq]
(mapv (fn [idx]
(vector idx (apply str (repeat idx \$))))
num-seq))
(def category-field {:name "Field Test" :base_type :type/Integer :special_type :type/Category})
(def ^:private category-field {:name "Field Test" :base_type :type/Integer :special_type :type/Category})
;; ## POST /api/field/:id/values
;; Human readable values are optional
(expect
[(merge default-field-values {:values (map vector (range 5 10))})
[{:values [[5] [6] [7] [8] [9]]}
{:status "success"}
(merge default-field-values {:values (map vector (range 1 5))})]
{:values [[1] [2] [3] [4]]}]
(tt/with-temp* [Field [{field-id :id} category-field]
FieldValues [{field-value-id :id} {:values (range 5 10), :field_id field-id}]]
(mapv tu/boolean-ids-and-timestamps
......@@ -210,34 +202,34 @@
;; Existing field values can be updated (with their human readable values)
(expect
[(merge default-field-values {:values (map vector (range 1 5))})
[{:values [[1] [2] [3] [4]]}
{:status "success"}
(merge default-field-values {:values (num->$ (range 1 5))})]
{:values [[1 "$"] [2 "$$"] [3 "$$$"] [4 "$$$$"]]}]
(tt/with-temp* [Field [{field-id :id} category-field]
FieldValues [{field-value-id :id} {:values (range 1 5), :field_id field-id}]]
(mapv tu/boolean-ids-and-timestamps
[((user->client :crowberto) :get 200 (format "field/%d/values" field-id))
((user->client :crowberto) :post 200 (format "field/%d/values" field-id)
{:values (num->$ (range 1 5))})
{:values [[1 "$"] [2 "$$"] [3 "$$$"] [4 "$$$$"]]})
((user->client :crowberto) :get 200 (format "field/%d/values" field-id))])))
;; Field values are created when not present
(expect
[(merge default-field-values {:values []})
[{:values []}
{:status "success"}
(merge default-field-values {:values (num->$ (range 1 5))})]
{:values [[1 "$"] [2 "$$"] [3 "$$$"] [4 "$$$$"]]}]
(tt/with-temp* [Field [{field-id :id} category-field]]
(mapv tu/boolean-ids-and-timestamps
[((user->client :crowberto) :get 200 (format "field/%d/values" field-id))
((user->client :crowberto) :post 200 (format "field/%d/values" field-id)
{:values (num->$ (range 1 5))})
{:values [[1 "$"] [2 "$$"] [3 "$$$"] [4 "$$$$"]]})
((user->client :crowberto) :get 200 (format "field/%d/values" field-id))])))
;; Can unset values
(expect
[(merge default-field-values {:values (mapv vector (range 1 5))})
[{:values [[1] [2] [3] [4]]}
{:status "success"}
(merge default-field-values {:values []})]
{:values []}]
(tt/with-temp* [Field [{field-id :id} category-field]
FieldValues [{field-value-id :id} {:values (range 1 5), :field_id field-id}]]
(mapv tu/boolean-ids-and-timestamps
......@@ -248,16 +240,16 @@
;; Can unset just human readable values
(expect
[(merge default-field-values {:values (num->$ (range 1 5))})
[{:values [[1 "$"] [2 "$$"] [3 "$$$"] [4 "$$$$"]]}
{:status "success"}
(merge default-field-values {:values (mapv vector (range 1 5))})]
{:values [[1] [2] [3] [4]]}]
(tt/with-temp* [Field [{field-id :id} category-field]
FieldValues [{field-value-id :id} {:values (range 1 5), :field_id field-id
:human_readable_values ["$" "$$" "$$$" "$$$$"]}]]
(mapv tu/boolean-ids-and-timestamps
[((user->client :crowberto) :get 200 (format "field/%d/values" field-id))
((user->client :crowberto) :post 200 (format "field/%d/values" field-id)
{:values (mapv vector (range 1 5))})
{:values [[1] [2] [3] [4]]})
((user->client :crowberto) :get 200 (format "field/%d/values" field-id))])))
;; Should throw when human readable values are present but not for every value
......@@ -274,7 +266,7 @@
(hydrate :dimensions)
:dimensions))
(defn dimension-post [field-id map-to-post]
(defn- create-dimension-via-API! [field-id map-to-post]
((user->client :crowberto) :post 200 (format "field/%d/dimension" field-id) map-to-post))
;; test that we can do basic field update work, including unsetting some fields such as special-type
......@@ -297,9 +289,9 @@
true]
(tt/with-temp* [Field [{field-id :id} {:name "Field Test"}]]
(let [before-creation (dimension-for-field field-id)
_ (dimension-post field-id {:name "some dimension name", :type "internal"})
_ (create-dimension-via-API! field-id {:name "some dimension name", :type "internal"})
new-dim (dimension-for-field field-id)
_ (dimension-post field-id {:name "different dimension name", :type "internal"})
_ (create-dimension-via-API! field-id {:name "different dimension name", :type "internal"})
updated-dim (dimension-for-field field-id)]
[before-creation
(tu/boolean-ids-and-timestamps new-dim)
......@@ -324,7 +316,7 @@
(tt/with-temp* [Field [{field-id-1 :id} {:name "Field Test 1"}]
Field [{field-id-2 :id} {:name "Field Test 2"}]]
(let [before-creation (dimension-for-field field-id-1)
_ (dimension-post field-id-1 {:name "some dimension name", :type "external" :human_readable_field_id field-id-2})
_ (create-dimension-via-API! field-id-1 {:name "some dimension name", :type "external" :human_readable_field_id field-id-2})
new-dim (dimension-for-field field-id-1)]
[before-creation
(tu/boolean-ids-and-timestamps new-dim)])))
......@@ -333,7 +325,7 @@
(expect
clojure.lang.ExceptionInfo
(tt/with-temp* [Field [{field-id-1 :id} {:name "Field Test 1"}]]
(dimension-post field-id-1 {:name "some dimension name", :type "external"})))
(create-dimension-via-API! field-id-1 {:name "some dimension name", :type "external"})))
;; Non-admin users can't update dimensions
(expect
......@@ -353,7 +345,7 @@
[]]
(tt/with-temp* [Field [{field-id :id} {:name "Field Test"}]]
(dimension-post field-id {:name "some dimension name", :type "internal"})
(create-dimension-via-API! field-id {:name "some dimension name", :type "internal"})
(let [new-dim (dimension-for-field field-id)]
((user->client :crowberto) :delete 204 (format "field/%d/dimension" field-id))
......@@ -380,7 +372,7 @@
:special_type :type/FK}]
Field [{field-id-2 :id} {:name "Field Test 2"}]]
(dimension-post field-id-1 {:name "fk-remove-dimension", :type "external" :human_readable_field_id field-id-2})
(create-dimension-via-API! field-id-1 {:name "fk-remove-dimension", :type "external" :human_readable_field_id field-id-2})
(let [new-dim (dimension-for-field field-id-1)
_ ((user->client :crowberto) :put 200 (format "field/%d" field-id-1) {:special_type nil})
......@@ -401,7 +393,7 @@
:special_type :type/FK}]
Field [{field-id-2 :id} {:name "Field Test 2"}]]
(dimension-post field-id-1 {:name "fk-remove-dimension", :type "external" :human_readable_field_id field-id-2})
(create-dimension-via-API! field-id-1 {:name "fk-remove-dimension", :type "external" :human_readable_field_id field-id-2})
(let [new-dim (dimension-for-field field-id-1)
_ ((user->client :crowberto) :put 200 (format "field/%d" field-id-1) {:description "something diffrent"})
......@@ -524,7 +516,7 @@
[]]
(tt/with-temp* [Field [{field-id :id} {:name "Field Test"
:base_type "type/Integer"}]]
(dimension-post field-id {:name "some dimension name", :type "internal"})
(create-dimension-via-API! field-id {:name "some dimension name", :type "internal"})
(let [new-dim (dimension-for-field field-id)]
((user->client :crowberto) :put 200 (format "field/%d" field-id) {:special_type "type/Text"})
[(tu/boolean-ids-and-timestamps new-dim)
......@@ -541,7 +533,7 @@
:field_id true})
(tt/with-temp* [Field [{field-id :id} {:name "Field Test"
:base_type "type/Integer"}]]
(dimension-post field-id {:name "some dimension name", :type "internal"})
(create-dimension-via-API! field-id {:name "some dimension name", :type "internal"})
(let [new-dim (dimension-for-field field-id)]
((user->client :crowberto) :put 200 (format "field/%d" field-id) {:special_type "type/Category"})
[(tu/boolean-ids-and-timestamps new-dim)
......
......@@ -14,6 +14,8 @@
[dashboard :refer [Dashboard]]
[dashboard-card :refer [DashboardCard]]
[dashboard-card-series :refer [DashboardCardSeries]]
[dimension :refer [Dimension]]
[field :refer [Field]]
[field-values :refer [FieldValues]]]
[metabase.test
[data :as data]
......@@ -364,3 +366,425 @@
(add-price-param-to-dashboard! dash)
(add-dimension-param-mapping-to-dashcard! dashcard card ["fk->" (data/id :checkins :venue_id) (data/id :venues :price)])
(GET-param-values dash)))
;;; +----------------------------------------------------------------------------------------------------------------+
;;; | New FieldValues search endpoints |
;;; +----------------------------------------------------------------------------------------------------------------+
(defn- mbql-card-referencing-nothing []
{:dataset_query {:database (data/id)}})
(defn mbql-card-referencing [table-kw field-kw]
{:dataset_query
{:database (data/id)
:type :query
:query {:source-table (data/id table-kw)
:filter [:= [:field-id (data/id table-kw field-kw)] "Krua Siri"]}}})
(defn- mbql-card-referencing-venue-name []
(mbql-card-referencing :venues :name))
(defn- sql-card-referencing-venue-name []
{:dataset_query
{:database (data/id)
:type :native
:native {:query "SELECT COUNT(*) FROM VENUES WHERE {{x}}"
:template_tags {:x {:name :x
:display_name "X"
:type :dimension
:dimension [:field-id (data/id :venues :name)]}}}}})
;;; ------------------------------------------- card->referenced-field-ids -------------------------------------------
(expect
#{}
(tt/with-temp Card [card (mbql-card-referencing-nothing)]
(#'public-api/card->referenced-field-ids card)))
;; It should pick up on Fields referenced in the MBQL query itself...
(expect
#{(data/id :venues :name)}
(tt/with-temp Card [card (mbql-card-referencing-venue-name)]
(#'public-api/card->referenced-field-ids card)))
;; ...as well as template tag "implict" params for SQL queries
(expect
#{(data/id :venues :name)}
(tt/with-temp Card [card (sql-card-referencing-venue-name)]
(#'public-api/card->referenced-field-ids card)))
;;; --------------------------------------- check-field-is-referenced-by-card ----------------------------------------
;; Check that the check succeeds when Field is referenced
(expect
(tt/with-temp Card [card (mbql-card-referencing-venue-name)]
(#'public-api/check-field-is-referenced-by-card (data/id :venues :name) (u/get-id card))))
;; check that exception is thrown if the Field isn't referenced
(expect
Exception
(tt/with-temp Card [card (mbql-card-referencing-venue-name)]
(#'public-api/check-field-is-referenced-by-card (data/id :venues :category_id) (u/get-id card))))
;;; ----------------------------------------- check-search-field-is-allowed ------------------------------------------
;; search field is allowed IF:
;; A) search-field is the same field as the other one
(expect
(#'public-api/check-search-field-is-allowed (data/id :venues :id) (data/id :venues :id)))
(expect
Exception
(#'public-api/check-search-field-is-allowed (data/id :venues :id) (data/id :venues :category_id)))
;; B) there's a Dimension that lists search field as the human_readable_field for the other field
(expect
(tt/with-temp Dimension [_ {:field_id (data/id :venues :id), :human_readable_field_id (data/id :venues :category_id)}]
(#'public-api/check-search-field-is-allowed (data/id :venues :id) (data/id :venues :category_id))))
;; C) search-field is a Name Field belonging to the same table as the other field, which is a PK
(expect
(do ;tu/with-temp-vals-in-db Field (data/id :venues :name) {:special_type "type/Name"}
(#'public-api/check-search-field-is-allowed (data/id :venues :id) (data/id :venues :name))))
;; not allowed if search field isn't a NAME
(expect
Exception
(tu/with-temp-vals-in-db Field (data/id :venues :name) {:special_type "type/Latitude"}
(#'public-api/check-search-field-is-allowed (data/id :venues :id) (data/id :venues :name))))
;; not allowed if search field belongs to a different TABLE
(expect
Exception
(tu/with-temp-vals-in-db Field (data/id :categories :name) {:special_type "type/Name"}
(#'public-api/check-search-field-is-allowed (data/id :venues :id) (data/id :categories :name))))
;;; ------------------------------------- check-field-is-referenced-by-dashboard -------------------------------------
(defn- dashcard-with-param-mapping-to-venue-id [dashboard card]
{:dashboard_id (u/get-id dashboard)
:card_id (u/get-id card)
:parameter_mappings [{:card_id (u/get-id card)
:target [:dimension [:field-id (data/id :venues :id)]]}]})
;; Field is "referenced" by Dashboard if it's one of the Dashboard's params...
(expect
(tt/with-temp* [Dashboard [dashboard]
Card [card]
DashboardCard [_ (dashcard-with-param-mapping-to-venue-id dashboard card)]]
(#'public-api/check-field-is-referenced-by-dashboard (data/id :venues :id) (u/get-id dashboard))))
(expect
Exception
(tt/with-temp* [Dashboard [dashboard]
Card [card]
DashboardCard [_ (dashcard-with-param-mapping-to-venue-id dashboard card)]]
(#'public-api/check-field-is-referenced-by-dashboard (data/id :venues :name) (u/get-id dashboard))))
;; ...*or* if it's a so-called "implicit" param (a Field Filter Template Tag (FFTT) in a SQL Card)
(expect
(tt/with-temp* [Dashboard [dashboard]
Card [card (sql-card-referencing-venue-name)]
DashboardCard [_ {:dashboard_id (u/get-id dashboard), :card_id (u/get-id card)}]]
(#'public-api/check-field-is-referenced-by-dashboard (data/id :venues :name) (u/get-id dashboard))))
(expect
Exception
(tt/with-temp* [Dashboard [dashboard]
Card [card (sql-card-referencing-venue-name)]
DashboardCard [_ {:dashboard_id (u/get-id dashboard), :card_id (u/get-id card)}]]
(#'public-api/check-field-is-referenced-by-dashboard (data/id :venues :id) (u/get-id dashboard))))
;;; ------------------------------------------- card-and-field-id->values --------------------------------------------
;; We should be able to get values for a Field referenced by a Card
(expect
{:values [["20th Century Cafe"]
["25°"]
["33 Taps"]
["800 Degrees Neapolitan Pizzeria"]
["BCD Tofu House"]]}
(tt/with-temp Card [card (mbql-card-referencing :venues :name)]
(-> (public-api/card-and-field-id->values (u/get-id card) (data/id :venues :name))
(update :values (partial take 5)))))
;; SQL param field references should work just as well as MBQL field referenced
(expect
{:values [["20th Century Cafe"]
["25°"]
["33 Taps"]
["800 Degrees Neapolitan Pizzeria"]
["BCD Tofu House"]]}
(tt/with-temp Card [card (sql-card-referencing-venue-name)]
(-> (public-api/card-and-field-id->values (u/get-id card) (data/id :venues :name))
(update :values (partial take 5)))))
;; But if the Field is not referenced we should get an Exception
(expect
Exception
(tt/with-temp Card [card (mbql-card-referencing :venues :price)]
(public-api/card-and-field-id->values (u/get-id card) (data/id :venues :name))))
;;; ------------------------------- GET /api/public/card/:uuid/field/:field-id/values --------------------------------
(defn- field-values-url [card-or-dashboard field-or-id]
(str "public/"
(condp instance? card-or-dashboard
(class Card) "card"
(class Dashboard) "dashboard")
"/" (or (:public_uuid card-or-dashboard)
(throw (Exception. (str "Missing public UUID: " card-or-dashboard))))
"/field/" (u/get-id field-or-id)
"/values"))
(defn- do-with-sharing-enabled-and-temp-card-referencing {:style/indent 2} [table-kw field-kw f]
(tu/with-temporary-setting-values [enable-public-sharing true]
(tt/with-temp Card [card (merge (shared-obj) (mbql-card-referencing table-kw field-kw))]
(f card))))
(defmacro ^:private with-sharing-enabled-and-temp-card-referencing
{:style/indent 3}
[table-kw field-kw [card-binding] & body]
`(do-with-sharing-enabled-and-temp-card-referencing ~table-kw ~field-kw
(fn [~card-binding]
~@body)))
;; should be able to fetch values for a Field referenced by a public Card
(expect
{:values [["20th Century Cafe"]
["25°"]
["33 Taps"]
["800 Degrees Neapolitan Pizzeria"]
["BCD Tofu House"]]}
(with-sharing-enabled-and-temp-card-referencing :venues :name [card]
(-> (http/client :get 200 (field-values-url card (data/id :venues :name)))
(update :values (partial take 5)))))
;; but for Fields that are not referenced we should get an Exception
(expect
"An error occurred."
(with-sharing-enabled-and-temp-card-referencing :venues :name [card]
(http/client :get 400 (field-values-url card (data/id :venues :price)))))
;; Endpoint should fail if public sharing is disabled
(expect
"An error occurred."
(with-sharing-enabled-and-temp-card-referencing :venues :name [card]
(tu/with-temporary-setting-values [enable-public-sharing false]
(http/client :get 400 (field-values-url card (data/id :venues :name))))))
;;; ----------------------------- GET /api/public/dashboard/:uuid/field/:field-id/values -----------------------------
(defn do-with-sharing-enabled-and-temp-dashcard-referencing {:style/indent 2} [table-kw field-kw f]
(tu/with-temporary-setting-values [enable-public-sharing true]
(tt/with-temp* [Dashboard [dashboard (shared-obj)]
Card [card (mbql-card-referencing table-kw field-kw)]
DashboardCard [dashcard {:dashboard_id (u/get-id dashboard)
:card_id (u/get-id card)
:parameter_mappings [{:card_id (u/get-id card)
:target [:dimension
[:field-id
(data/id table-kw field-kw)]]}]}]]
(f dashboard card dashcard))))
(defmacro with-sharing-enabled-and-temp-dashcard-referencing
{:style/indent 3}
[table-kw field-kw [dashboard-binding card-binding dashcard-binding] & body]
`(do-with-sharing-enabled-and-temp-dashcard-referencing ~table-kw ~field-kw
(fn [~(or dashboard-binding '_) ~(or card-binding '_) ~(or dashcard-binding '_)]
~@body)))
;; should be able to use it when everything is g2g
(expect
{:values [["20th Century Cafe"]
["25°"]
["33 Taps"]
["800 Degrees Neapolitan Pizzeria"]
["BCD Tofu House"]]}
(with-sharing-enabled-and-temp-dashcard-referencing :venues :name [dashboard]
(-> (http/client :get 200 (field-values-url dashboard (data/id :venues :name)))
(update :values (partial take 5)))))
;; shound NOT be able to use the endpoint with a Field not referenced by the Dashboard
(expect
"An error occurred."
(with-sharing-enabled-and-temp-dashcard-referencing :venues :name [dashboard]
(http/client :get 400 (field-values-url dashboard (data/id :venues :price)))))
;; Endpoint should fail if public sharing is disabled
(expect
"An error occurred."
(with-sharing-enabled-and-temp-dashcard-referencing :venues :name [dashboard]
(tu/with-temporary-setting-values [enable-public-sharing false]
(http/client :get 400 (field-values-url dashboard (data/id :venues :name))))))
;;; ----------------------------------------------- search-card-fields -----------------------------------------------
(expect
[[93 "33 Taps"]]
(with-sharing-enabled-and-temp-card-referencing :venues :id [card]
(do ;tu/with-temp-vals-in-db Field (data/id :venues :name) {:special_type "type/Name"}
(public-api/search-card-fields (u/get-id card) (data/id :venues :id) (data/id :venues :name) "33 T" 10))))
;; shouldn't work if the search-field isn't allowed to be used in combination with the other Field
(expect
Exception
(with-sharing-enabled-and-temp-card-referencing :venues :id [card]
(public-api/search-card-fields (u/get-id card) (data/id :venues :id) (data/id :venues :price) "33 T" 10)))
;; shouldn't work if the field isn't referenced by CARD
(expect
Exception
(with-sharing-enabled-and-temp-card-referencing :venues :name [card]
(public-api/search-card-fields (u/get-id card) (data/id :venues :id) (data/id :venues :id) "33 T" 10)))
;;; ----------------------- GET /api/public/card/:uuid/field/:field-id/search/:search-field-id -----------------------
(defn- field-search-url [card-or-dashboard field-or-id search-field-or-id]
(str "public/"
(condp instance? card-or-dashboard
(class Card) "card"
(class Dashboard) "dashboard")
"/" (:public_uuid card-or-dashboard)
"/field/" (u/get-id field-or-id)
"/search/" (u/get-id search-field-or-id)))
(expect
[[93 "33 Taps"]]
(with-sharing-enabled-and-temp-card-referencing :venues :id [card]
(http/client :get 200 (field-search-url card (data/id :venues :id) (data/id :venues :name))
:value "33 T")))
;; if search field isn't allowed to be used with the other Field endpoint should return exception
(expect
"An error occurred."
(with-sharing-enabled-and-temp-card-referencing :venues :id [card]
(http/client :get 400 (field-search-url card (data/id :venues :id) (data/id :venues :price))
:value "33 T")))
;; Endpoint should fail if public sharing is disabled
(expect
"An error occurred."
(with-sharing-enabled-and-temp-card-referencing :venues :id [card]
(tu/with-temporary-setting-values [enable-public-sharing false]
(http/client :get 400 (field-search-url card (data/id :venues :id) (data/id :venues :name))
:value "33 T"))))
;;; -------------------- GET /api/public/dashboard/:uuid/field/:field-id/search/:search-field-id ---------------------
(expect
[[93 "33 Taps"]]
(with-sharing-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(http/client :get (field-search-url dashboard (data/id :venues :id) (data/id :venues :name))
:value "33 T")))
;; if search field isn't allowed to be used with the other Field endpoint should return exception
(expect
"An error occurred."
(with-sharing-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(http/client :get 400 (field-search-url dashboard (data/id :venues :id) (data/id :venues :price))
:value "33 T")))
;; Endpoint should fail if public sharing is disabled
(expect
"An error occurred."
(with-sharing-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(tu/with-temporary-setting-values [enable-public-sharing false]
(http/client :get 400 (field-search-url dashboard (data/id :venues :name) (data/id :venues :name))
:value "33 T"))))
;;; --------------------------------------------- field-remapped-values ----------------------------------------------
;; `field-remapped-values` should return remappings in the expected format when the combination of Fields is allowed.
;; It should parse the value string (it comes back from the API as a string since it is a query param)
(expect
[10 "Fred 62"]
(#'public-api/field-remapped-values (data/id :venues :id) (data/id :venues :name) "10"))
;; if the Field isn't allowed
(expect
Exception
(#'public-api/field-remapped-values (data/id :venues :id) (data/id :venues :price) "10"))
;;; ----------------------- GET /api/public/card/:uuid/field/:field-id/remapping/:remapped-id ------------------------
(defn- field-remapping-url [card-or-dashboard field-or-id remapped-field-or-id]
(str "public/"
(condp instance? card-or-dashboard
(class Card) "card"
(class Dashboard) "dashboard")
"/" (:public_uuid card-or-dashboard)
"/field/" (u/get-id field-or-id)
"/remapping/" (u/get-id remapped-field-or-id)))
;; we should be able to use the API endpoint and get the same results we get by calling the function above directly
(expect
[10 "Fred 62"]
(with-sharing-enabled-and-temp-card-referencing :venues :id [card]
(http/client :get 200 (field-remapping-url card (data/id :venues :id) (data/id :venues :name))
:value "10")))
;; shouldn't work if Card doesn't reference the Field in question
(expect
"An error occurred."
(with-sharing-enabled-and-temp-card-referencing :venues :price [card]
(http/client :get 400 (field-remapping-url card (data/id :venues :id) (data/id :venues :name))
:value "10")))
;; ...or if the remapping Field isn't allowed to be used with the other Field
(expect
"An error occurred."
(with-sharing-enabled-and-temp-card-referencing :venues :id [card]
(http/client :get 400 (field-remapping-url card (data/id :venues :id) (data/id :venues :price))
:value "10")))
;; ...or if public sharing is disabled
(expect
"An error occurred."
(with-sharing-enabled-and-temp-card-referencing :venues :id [card]
(tu/with-temporary-setting-values [enable-public-sharing false]
(http/client :get 400 (field-remapping-url card (data/id :venues :id) (data/id :venues :name))
:value "10"))))
;;; --------------------- GET /api/public/dashboard/:uuid/field/:field-id/remapping/:remapped-id ---------------------
;; we should be able to use the API endpoint and get the same results we get by calling the function above directly
(expect
[10 "Fred 62"]
(with-sharing-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(http/client :get 200 (field-remapping-url dashboard (data/id :venues :id) (data/id :venues :name))
:value "10")))
;; shouldn't work if Card doesn't reference the Field in question
(expect
"An error occurred."
(with-sharing-enabled-and-temp-dashcard-referencing :venues :price [dashboard]
(http/client :get 400 (field-remapping-url dashboard (data/id :venues :id) (data/id :venues :name))
:value "10")))
;; ...or if the remapping Field isn't allowed to be used with the other Field
(expect
"An error occurred."
(with-sharing-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(http/client :get 400 (field-remapping-url dashboard (data/id :venues :id) (data/id :venues :price))
:value "10")))
;; ...or if public sharing is disabled
(expect
"An error occurred."
(with-sharing-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(tu/with-temporary-setting-values [enable-public-sharing false]
(http/client :get 400 (field-remapping-url dashboard (data/id :venues :id) (data/id :venues :name))
:value "10"))))
(ns metabase.models.params-test
"Tests for the utility functions for dealing with parameters in `metabase.models.params`."
(:require [expectations :refer :all]
[metabase.api.public-test :as public-test]
[metabase.models
[card :refer [Card]]
[field :refer [Field]]
[params :as params]]
[metabase.test.data :as data]
[toucan
[db :as db]
[hydrate :refer [hydrate]]]
[toucan.util.test :as tt]))
;;; ---------------------------------------------- name_field hydration ----------------------------------------------
;; make sure that we can hydrate the `name_field` property for PK Fields
(expect
{:name "ID"
:table_id (data/id :venues)
:special_type :type/PK
:name_field {:id (data/id :venues :name)
:table_id (data/id :venues)
:display_name "Name"
:base_type :type/Text
:special_type :type/Name}}
(-> (db/select-one [Field :name :table_id :special_type], :id (data/id :venues :id))
(hydrate :name_field)))
;; make sure it works for multiple fields efficiently. Should only require one DB call to hydrate many Fields
(expect
1
(let [venues-fields (db/select Field :table_id (data/id :venues))]
(db/with-call-counting [call-count]
(hydrate venues-fields :name_field)
(call-count))))
;; It shouldn't hydrate for Fields that aren't PKs
(expect
{:name "PRICE"
:table_id (data/id :venues)
:special_type :type/Category
:name_field nil}
(-> (db/select-one [Field :name :table_id :special_type], :id (data/id :venues :price))
(hydrate :name_field)))
;; Or if it *is* a PK, but no name Field is available for that Table, it shouldn't hydrate
(expect
{:name "ID"
:table_id (data/id :checkins)
:special_type :type/PK
:name_field nil}
(-> (db/select-one [Field :name :table_id :special_type], :id (data/id :checkins :id))
(hydrate :name_field)))
;;; -------------------------------------------------- param_fields --------------------------------------------------
;; check that we can hydrate param_fields for a Card
(expect
{(data/id :venues :id) {:id (data/id :venues :id)
:table_id (data/id :venues)
:display_name "ID"
:base_type :type/BigInteger
:special_type :type/PK
:has_field_values :search
:name_field {:id (data/id :venues :name)
:table_id (data/id :venues)
:display_name "Name"
:base_type :type/Text
:special_type :type/Name}
:dimensions []}}
(tt/with-temp Card [card {:dataset_query
{:database (data/id)
:type :native
:native {:query "SELECT COUNT(*) FROM VENUES WHERE {{x}}"
:template_tags {:name {:name :name
:display_name "Name"
:type :dimension
:dimension [:field-id (data/id :venues :id)]}}}}}]
(-> (hydrate card :param_fields)
:param_fields)))
;; check that we can hydrate param_fields for a Dashboard
(expect
{(data/id :venues :id) {:id (data/id :venues :id)
:table_id (data/id :venues)
:display_name "ID"
:base_type :type/BigInteger
:special_type :type/PK
:has_field_values :search
:name_field {:id (data/id :venues :name)
:table_id (data/id :venues)
:display_name "Name"
:base_type :type/Text
:special_type :type/Name}
:dimensions []}}
(public-test/with-sharing-enabled-and-temp-dashcard-referencing :venues :id [dashboard]
(-> (hydrate dashboard :param_fields)
:param_fields)))
......@@ -16,6 +16,7 @@
[dashboard :refer [Dashboard]]
[dashboard-card-series :refer [DashboardCardSeries]]
[database :refer [Database]]
[dimension :refer [Dimension]]
[field :refer [Field]]
[metric :refer [Metric]]
[permissions-group :refer [PermissionsGroup]]
......@@ -155,6 +156,11 @@
:is_sample false
:name (random-name)})})
(u/strict-extend (class Dimension)
test/WithTempDefaults
{:with-temp-defaults (fn [_] {:name (random-name)
:type "internal"})})
(u/strict-extend (class Field)
test/WithTempDefaults
{:with-temp-defaults (fn [_] {:database_type "VARCHAR"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment