From 02eb6982d2c7bb1f29bffec9e51654232e5d6e75 Mon Sep 17 00:00:00 2001 From: metamben <103100869+metamben@users.noreply.github.com> Date: Tue, 23 Aug 2022 22:13:10 +0300 Subject: [PATCH] Hydrate app_id for collections with apps attached (#24950) * Hydrate app_id for collections with apps attached Addresses #24941, part of #24861 --- src/metabase/api/collection.clj | 27 +++--- src/metabase/models/app.clj | 20 +++++ test/metabase/api/collection_test.clj | 121 +++++++++++++++----------- 3 files changed, 102 insertions(+), 66 deletions(-) diff --git a/src/metabase/api/collection.clj b/src/metabase/api/collection.clj index cd169f17282..7b84d1cf0c5 100644 --- a/src/metabase/api/collection.clj +++ b/src/metabase/api/collection.clj @@ -65,7 +65,7 @@ (cond->> collections (mi/can-read? root) (cons root)))) - (hydrate collections :can_write) + (hydrate collections :can_write :app_id) ;; remove the :metabase.models.collection.root/is-root? tag since FE doesn't need it ;; and for personal collections we translate the name to user's locale (for [collection collections] @@ -104,17 +104,18 @@ (db/reducible-query {:select [:collection_id :dataset] :modifiers [:distinct] :from [:report_card] - :where [:= :archived false]}))] - (->> (db/select Collection - {:where [:and - (when exclude-archived - [:= :archived false]) - [:= :namespace namespace] - (collection/visible-collection-ids->honeysql-filter-clause - :id - (collection/permissions-set->visible-collection-ids @api/*current-user-permissions-set*))]}) - (map collection/personal-collection-with-ui-details) - (collection/collections->tree coll-type-ids)))) + :where [:= :archived false]})) + colls (db/select Collection + {:where [:and + (when exclude-archived + [:= :archived false]) + [:= :namespace namespace] + (collection/visible-collection-ids->honeysql-filter-clause + :id + (collection/permissions-set->visible-collection-ids @api/*current-user-permissions-set*))]}) + colls (map collection/personal-collection-with-ui-details colls) + colls (hydrate colls :app_id)] + (collection/collections->tree coll-type-ids colls))) ;;; --------------------------------- Fetching a single Collection & its 'children' ---------------------------------- @@ -604,7 +605,7 @@ [collection :- collection/CollectionWithLocationAndIDOrRoot] (-> collection collection/personal-collection-with-ui-details - (hydrate :parent_id :effective_location [:effective_ancestors :can_write] :can_write))) + (hydrate :parent_id :effective_location [:effective_ancestors :can_write] :can_write :app_id))) (api/defendpoint GET "/:id" "Fetch a specific Collection with standard details added" diff --git a/src/metabase/models/app.clj b/src/metabase/models/app.clj index 6bdd4d01979..68239422951 100644 --- a/src/metabase/models/app.clj +++ b/src/metabase/models/app.clj @@ -3,6 +3,7 @@ [metabase.models.permissions :as perms] [metabase.models.serialization.hash :as serdes.hash] [metabase.util :as u] + [toucan.db :as db] [toucan.models :as models])) (models/defmodel App :app) @@ -23,3 +24,22 @@ ;; necessary to satisfy metabase-enterprise.models.entity-id-test/comprehensive-identity-hash-test. serdes.hash/IdentityHashable {:identity-hash-fields (constantly [:entity_id])}) + +(defn add-app-id + "Add `app_id` to Collections that are linked with an App." + {:batched-hydrate :app_id} + [collections] + (if-let [coll-ids (seq (into #{} + (comp (map :id) + ;; The ID "root" breaks the query. + (filter int?)) + collections))] + (let [coll-id->app-id (into {} + (map (juxt :collection_id :id)) + (db/select [App :collection_id :id] + :collection_id [:in coll-ids]))] + (for [coll collections] + (let [app-id (coll-id->app-id (:id coll))] + (cond-> coll + app-id (assoc :app_id app-id))))) + collections)) diff --git a/test/metabase/api/collection_test.clj b/test/metabase/api/collection_test.clj index 26f6432b56e..9eb357391bd 100644 --- a/test/metabase/api/collection_test.clj +++ b/test/metabase/api/collection_test.clj @@ -4,7 +4,7 @@ [clojure.test :refer :all] [honeysql.core :as hsql] [metabase.api.collection :as api.collection] - [metabase.models :refer [Card Collection Dashboard DashboardCard ModerationReview NativeQuerySnippet + [metabase.models :refer [App Card Collection Dashboard DashboardCard ModerationReview NativeQuerySnippet PermissionsGroup PermissionsGroupMembership Pulse PulseCard PulseChannel PulseChannelRecipient Revision Timeline TimelineEvent User]] [metabase.models.collection :as collection] @@ -110,26 +110,28 @@ (str/includes? collection-name "Personal Collection")))) (map :name))))))) - (mt/with-temp* [Collection [_ {:name "Archived Collection", :archived true}] - Collection [_ {:name "Regular Collection"}]] + (mt/with-temp* [Collection [{coll1-id :id} {:name "Archived Collection", :archived true}] + Collection [{coll2-id :id} {:name "Regular Collection"}] + App [{app1-id :id} {:collection_id coll1-id}] + App [{app2-id :id} {:collection_id coll2-id}]] (letfn [(remove-other-collections [collections] (filter (fn [{collection-name :name}] (or (#{"Our analytics" "Archived Collection" "Regular Collection"} collection-name) (str/includes? collection-name "Personal Collection"))) collections))] (testing "check that we don't see collections if they're archived" - (is (= ["Our analytics" - "Rasta Toucan's Personal Collection" - "Regular Collection"] + (is (= [["Our analytics" nil] + ["Rasta Toucan's Personal Collection" nil] + ["Regular Collection" app2-id]] (->> (mt/user-http-request :rasta :get 200 "collection") remove-other-collections - (map :name))))) + (map (juxt :name :app_id)))))) (testing "Check that if we pass `?archived=true` we instead see archived Collections" - (is (= ["Archived Collection"] + (is (= [["Archived Collection" app1-id]] (->> (mt/user-http-request :rasta :get 200 "collection" :archived :true) remove-other-collections - (map :name))))))) + (map (juxt :name :app_id)))))))) (testing "?namespace= parameter" (mt/with-temp* [Collection [{normal-id :id} {:name "Normal Collection"}] @@ -163,14 +165,18 @@ (cond-> collection (:children collection) (update :children (partial collection-tree-transform xform)))))) -(defn- collection-tree-names-only - "Keep just the names of Collections in `collection-ids-to-keep` in the response returned by the Collection tree - endpoint." - [collection-ids-to-keep collections] - (collection-tree-transform (fn [collection] - (when (contains? (set collection-ids-to-keep) (:id collection)) - (select-keys collection [:name :children]))) - collections)) +(defn- collection-tree-view + "Keep just the fields specified by `fields-to-keep` of Collections in `collection-ids-to-keep` in the response + returned by the Collection tree endpoint. If `fields-to-keep` is not specified, only the names are kept." + ([collection-ids-to-keep collections] + (collection-tree-view collection-ids-to-keep [:name] collections)) + ([collection-ids-to-keep fields-to-keep collections] + (let [selection (conj fields-to-keep :children) + ids-to-keep (set collection-ids-to-keep)] + (collection-tree-transform (fn [collection] + (when (contains? ids-to-keep (:id collection)) + (select-keys collection selection))) + collections)))) (deftest collection-tree-test (testing "GET /api/collection/tree" @@ -178,34 +184,41 @@ (testing "sanity check" (is (some? personal-collection))) (with-collection-hierarchy [a b c d e f g] - (let [ids (set (map :id (cons personal-collection [a b c d e f g]))) - response (mt/user-http-request :rasta :get 200 "collection/tree")] - (testing "Make sure overall tree shape of the response is as is expected" - (is (= [{:name "A" - :children [{:name "B", :children []} - {:name "C" - :children [{:name "D" - :children [{:name "E", :children []}]} - {:name "F" - :children [{:name "G", :children []}]}]}]} - {:name "Rasta Toucan's Personal Collection", :children []}] - (collection-tree-names-only ids response)))) - (testing "Make sure each Collection comes back with the expected keys" - (is (= {:description nil - :archived false - :entity_id (:entity_id personal-collection) - :slug "rasta_toucan_s_personal_collection" - :color "#31698A" - :name "Rasta Toucan's Personal Collection" - :personal_owner_id (mt/user->id :rasta) - :id (:id (collection/user->personal-collection (mt/user->id :rasta))) - :location "/" - :namespace nil - :children [] - :authority_level nil} - (some #(when (= (:id %) (:id (collection/user->personal-collection (mt/user->id :rasta)))) - %) - response)))))) + (mt/with-temp* [App [{app-a-id :id} {:collection_id (:id a)}] + App [{app-c-id :id} {:collection_id (:id c)}] + App [{app-g-id :id} {:collection_id (:id g)}]] + (let [ids (set (map :id (cons personal-collection [a b c d e f g]))) + response (mt/user-http-request :rasta :get 200 "collection/tree")] + (testing "Make sure overall tree shape of the response is as is expected" + (is (= [{:name "A" + :app_id app-a-id + :children [{:name "B", :children []} + {:name "C" + :app_id app-c-id + :children [{:name "D" + :children [{:name "E", :children []}]} + {:name "F" + :children [{:name "G" + :app_id app-g-id + :children []}]}]}]} + {:name "Rasta Toucan's Personal Collection", :children []}] + (collection-tree-view ids [:name :app_id] response)))) + (testing "Make sure each Collection comes back with the expected keys" + (is (= {:description nil + :archived false + :entity_id (:entity_id personal-collection) + :slug "rasta_toucan_s_personal_collection" + :color "#31698A" + :name "Rasta Toucan's Personal Collection" + :personal_owner_id (mt/user->id :rasta) + :id (:id (collection/user->personal-collection (mt/user->id :rasta))) + :location "/" + :namespace nil + :children [] + :authority_level nil} + (some #(when (= (:id %) (:id (collection/user->personal-collection (mt/user->id :rasta)))) + %) + response))))))) (testing "Excludes archived collections (#19603)" (mt/with-temp* [Collection [a {:name "A"}] Collection [b {:name "B archived" @@ -217,7 +230,7 @@ response (mt/user-http-request :rasta :get 200 "collection/tree?exclude-archived=true")] (is (= [{:name "A" :children []}] - (collection-tree-names-only ids response))))))) + (collection-tree-view ids response))))))) (testing "for personal collections, it should return name and slug in user's locale" (with-french-user-and-personal-collection user collection @@ -250,7 +263,7 @@ (perms/revoke-collection-permissions! (perms-group/all-users) parent-collection) (perms/grant-collection-readwrite-permissions! (perms-group/all-users) child-collection) (is (= [{:name "Child", :children []}] - (collection-tree-names-only (map :id [parent-collection child-collection]) + (collection-tree-view (map :id [parent-collection child-collection]) (mt/user-http-request :rasta :get 200 "collection/tree"))))))) (testing "Namespace parameter" @@ -259,17 +272,17 @@ (let [ids [normal-id coins-id]] (testing "shouldn't show Collections of a different `:namespace` by default" (is (= [{:name "Normal Collection", :children []}] - (collection-tree-names-only ids (mt/user-http-request :rasta :get 200 "collection/tree"))))) + (collection-tree-view ids (mt/user-http-request :rasta :get 200 "collection/tree"))))) (perms/grant-collection-read-permissions! (perms-group/all-users) coins-id) (testing "By passing `:namespace` we should be able to see Collections of that `:namespace`" (testing "?namespace=currency" (is (= [{:name "Coin Collection", :children []}] - (collection-tree-names-only ids (mt/user-http-request :rasta :get 200 "collection/tree?namespace=currency"))))) + (collection-tree-view ids (mt/user-http-request :rasta :get 200 "collection/tree?namespace=currency"))))) (testing "?namespace=stamps" (is (= [] - (collection-tree-names-only ids (mt/user-http-request :rasta :get 200 "collection/tree?namespace=stamps"))))))))) + (collection-tree-view ids (mt/user-http-request :rasta :get 200 "collection/tree?namespace=stamps"))))))))) (testing "Tree should elide Collections for which we have no permissions (#14280)" ;; Create hierarchy like @@ -311,10 +324,12 @@ (deftest fetch-collection-test (testing "GET /api/collection/:id" (testing "check that we can see collection details" - (mt/with-temp Collection [collection {:name "Coin Collection"}] + (mt/with-temp* [Collection [collection {:name "Coin Collection"}] + App [{app-id :id} {:collection_id (:id collection)}]] (perms/grant-collection-read-permissions! (perms-group/all-users) collection) - (is (= "Coin Collection" - (:name (mt/user-http-request :rasta :get 200 (str "collection/" (u/the-id collection)))))))) + (is (= ["Coin Collection" app-id] + ((juxt :name :app_id) + (mt/user-http-request :rasta :get 200 (str "collection/" (u/the-id collection)))))))) (testing "check that collections detail properly checks permissions" (mt/with-non-admin-groups-no-root-collection-perms -- GitLab