Skip to content
Snippets Groups Projects
Unverified Commit c64ef909 authored by dpsutton's avatar dpsutton Committed by GitHub
Browse files

Dataset recent activity (#18967)

* Get rid of the n-queries

old logic: select last view logs, and then for each one, select its
associated model (card, table, etc).

new logic: get the last view logs, and do three batch queries, one each
to card, dashboard, table, for the ids from the view logs

* Mark datasets in api/activity/recent_views

* Get rid of `(db/delete! Activity)` in tests

* Handle datasets in activity list
parent 0a8eaab3
No related branches found
No related tags found
No related merge requests found
(ns metabase.api.activity
(:require [clojure.set :as set]
[compojure.core :refer [GET]]
[medley.core :as m]
[metabase.api.common :refer [*current-user-id* defendpoint define-routes]]
[metabase.models.activity :refer [Activity]]
[metabase.models.card :refer [Card]]
......@@ -36,28 +37,47 @@
(referenced-objects->existing-objects {\"dashboard\" #{41 42 43}, \"card\" #{100 101}, ...})
;; -> {\"dashboard\" #{41 43}, \"card\" #{101}, ...}"
[referenced-objects]
(into {} (for [[model ids] referenced-objects
:when (seq ids)]
{model (case model
"card" (db/select-ids 'Card, :id [:in ids])
"dashboard" (db/select-ids 'Dashboard, :id [:in ids])
"metric" (db/select-ids 'Metric, :id [:in ids], :archived false)
"pulse" (db/select-ids 'Pulse, :id [:in ids])
"segment" (db/select-ids 'Segment, :id [:in ids], :archived false)
nil)}))) ; don't care about other models
(merge
(when-let [card-ids (get referenced-objects "card")]
(let [id->dataset? (db/select-id->field :dataset Card
:id [:in card-ids])
{dataset-ids true card-ids' false} (group-by (comp boolean id->dataset?)
;; only existing ids go back
(keys id->dataset?))]
(cond-> {}
(seq dataset-ids) (assoc "dataset" (set dataset-ids))
(seq card-ids') (assoc "card" (set card-ids')))))
(into {} (for [[model ids] (dissoc referenced-objects "card")
:when (seq ids)]
{model (case model
"dashboard" (db/select-ids 'Dashboard, :id [:in ids])
"metric" (db/select-ids 'Metric, :id [:in ids], :archived false)
"pulse" (db/select-ids 'Pulse, :id [:in ids])
"segment" (db/select-ids 'Segment, :id [:in ids], :archived false)
nil)})))) ; don't care about other models
(defn- add-model-exists-info
"Add `:model_exists` keys to `activities`, and `:exists` keys to nested dashcards where appropriate."
[activities]
(let [existing-objects (-> activities activities->referenced-objects referenced-objects->existing-objects)]
(for [{:keys [model model_id], :as activity} activities]
(let [activity (assoc activity :model_exists (contains? (get existing-objects model) model_id))]
(if-not (dashcard-activity? activity)
activity
(update-in activity [:details :dashcards] (fn [dashcards]
(for [dashcard dashcards]
(assoc dashcard :exists (contains? (get existing-objects "card")
(:card_id dashcard)))))))))))
(let [existing-objects (-> activities activities->referenced-objects referenced-objects->existing-objects)
existing-dataset? (fn [card-id]
(contains? (get existing-objects "dataset") card-id))]
(for [{:keys [model_id], :as activity} activities]
(let [model (if (and (= (:model activity) "card")
(existing-dataset? (:model_id activity)))
"dataset"
(:model activity))]
(cond-> (assoc activity
:model_exists (contains? (get existing-objects model) model_id)
:model model)
(dashcard-activity? activity)
(update-in [:details :dashcards]
(fn [dashcards]
(for [dashcard dashcards]
(assoc dashcard :exists
(or (existing-dataset? (:card_id dashcard))
(contains? (get existing-objects "card")
(:card_id dashcard))))))))))))
(defendpoint GET "/"
"Get recent activity."
......@@ -66,14 +86,25 @@
(hydrate :user :table :database)
add-model-exists-info)))
(defn- view-log-entry->matching-object [{:keys [model model_id]}]
(when (contains? #{"card" "dashboard" "table"} model)
(db/select-one
(case model
"card" [Card :id :name :collection_id :description :display :dataset_query]
"dashboard" [Dashboard :id :name :collection_id :description]
"table" [Table :id :name :db_id :display_name])
:id model_id)))
(defn- models-for-views
"Returns a map of {model {id instance}} for activity views suitable for looking up by model and id to get a model."
[views]
(letfn [(select-items! [model ids]
(when (seq ids)
(db/select
(case model
"card" [Card :id :name :collection_id :description :display
:dataset_query :dataset]
"dashboard" [Dashboard :id :name :collection_id :description]
"table" [Table :id :name :db_id :display_name])
{:where [:in :id ids]})))
(by-id [models] (m/index-by :id models))]
(into {} (map (fn [[model models]]
[model (->> models
(map :model_id)
(select-items! model)
(by-id))]))
(group-by :model views))))
(defendpoint GET "/recent_views"
"Get the list of 10 things the current user has been viewing most recently."
......@@ -81,15 +112,19 @@
;; expected output of the query is a single row per unique model viewed by the current user
;; including a `:max_ts` which has the most recent view timestamp of the item and `:cnt` which has total views
;; and we order the results by most recently viewed then hydrate the basic details of the model
(for [view-log (db/select [ViewLog :user_id :model :model_id [:%count.* :cnt] [:%max.timestamp :max_ts]]
:user_id *current-user-id*
{:group-by [:user_id :model :model_id]
:order-by [[:max_ts :desc]]
:limit 5})
:let [model-object (view-log-entry->matching-object view-log)]
:when (and model-object
(mi/can-read? model-object))]
(assoc view-log :model_object (dissoc model-object :dataset_query))))
(let [views (db/select [ViewLog :user_id :model :model_id
[:%count.* :cnt] [:%max.timestamp :max_ts]]
{:group-by [:user_id :model :model_id]
:where [:and
[:= :user_id *current-user-id*]
[:in :model #{"card" "dashboard" "table"}]]
:order-by [[:max_ts :desc]]
:limit 5})
model->id->items (models-for-views views)]
(for [{:keys [model model_id] :as view-log} views
:let [model-object (get-in model->id->items [model model_id])]
:when (and model-object (mi/can-read? model-object))]
(cond-> (assoc view-log :model_object (dissoc model-object :dataset_query))
(:dataset model-object) (assoc :model "dataset")))))
(define-routes)
......@@ -108,11 +108,12 @@
:description "rand-name"
:creator_id (mt/user->id :crowberto)}]
Table [table1 {:name "rand-name"}]
Card [card2 {:name "rand-name"
:creator_id (mt/user->id :crowberto)
:display "table"
:visualization_settings {}}]]
(create-view! (mt/user->id :crowberto) "card" (:id card2))
Card [dataset {:name "rand-name"
:dataset true
:creator_id (mt/user->id :crowberto)
:display "table"
:visualization_settings {}}]]
(create-view! (mt/user->id :crowberto) "card" (:id dataset))
(create-view! (mt/user->id :crowberto) "dashboard" (:id dash1))
(create-view! (mt/user->id :crowberto) "card" (:id card1))
(create-view! (mt/user->id :crowberto) "card" 36478)
......@@ -133,6 +134,7 @@
:model_object {:id (:id card1)
:name (:name card1)
:collection_id nil
:dataset false
:description (:description card1)
:display (name (:display card1))}}
{:cnt 1
......@@ -145,13 +147,14 @@
:description (:description dash1)}}
{:cnt 1
:user_id (mt/user->id :crowberto)
:model "card"
:model_id (:id card2)
:model_object {:id (:id card2)
:name (:name card2)
:model "dataset"
:model_id (:id dataset)
:model_object {:id (:id dataset)
:name (:name dataset)
:dataset true
:collection_id nil
:description (:description card2)
:display (name (:display card2))}}]
:description (:description dataset)
:display (name (:display dataset))}}]
(for [recent-view (mt/user-http-request :crowberto :get 200 "activity/recent_views")]
(dissoc recent-view :max_ts))))))
......@@ -181,47 +184,48 @@
(deftest referenced-objects->existing-objects-test
(mt/with-temp Dashboard [{dashboard-id :id}]
(is (= {"dashboard" #{dashboard-id}, "card" nil}
(is (= {"dashboard" #{dashboard-id}}
(#'activity-api/referenced-objects->existing-objects {"dashboard" #{dashboard-id 0}
"card" #{0}})))))
(deftest add-model-exists-info-test
(mt/with-temp* [Dashboard [{dashboard-id :id}]
Card [{card-id :id}]]
Card [{card-id :id}]
Card [{dataset-id :id} {:dataset true}]]
(is (= [{:model "dashboard", :model_id dashboard-id, :model_exists true}
{:model "card", :model_id 0, :model_exists false}
{:model "dataset", :model_id dataset-id, :model_exists true}
{:model "dashboard"
:model_id 0
:model_exists false
:topic :dashboard-remove-cards
:details {:dashcards [{:card_id card-id, :exists true}
{:card_id 0, :exists false}]}}]
{:card_id 0, :exists false}
{:card_id dataset-id, :exists true}]}}]
(#'activity-api/add-model-exists-info [{:model "dashboard", :model_id dashboard-id}
{:model "card", :model_id 0}
{:model "card", :model_id dataset-id}
{:model "dashboard"
:model_id 0
:topic :dashboard-remove-cards
:details {:dashcards [{:card_id card-id}
{:card_id 0}]}}])))))
{:card_id 0}
{:card_id dataset-id}]}}])))))
(deftest activity-visibility-test
;; clear out all existing Activity entries
;;
;; TODO -- this is a bad pattern -- test shouldn't wipe out a bunch of other stuff like this. Just fetch all the
;; activities and look for the presence of this specific one.
(db/delete! Activity)
(mt/with-temp Activity [activity {:topic "user-joined"
:details {}
:timestamp #t "2019-02-15T11:55:00.000Z"}]
(letfn [(user-can-see-user-joined-activity? [user]
(seq (mt/user-http-request user :get 200 "activity")))]
:timestamp (java.time.ZonedDateTime/now)}]
(letfn [(activity-topics [user]
(into #{} (map :topic)
(mt/user-http-request user :get 200 "activity")))]
(testing "Only admins should get to see user-joined activities"
(testing "admin should see `:user-joined` activities"
(testing "Sanity check: admin should be able to read the activity"
(mt/with-test-user :crowberto
(is (models/can-read? activity))))
(is (user-can-see-user-joined-activity? :crowberto)))
(is (contains? (activity-topics :crowberto) "user-joined")))
(testing "non-admin should *not* see `:user-joined` activities"
(testing "Sanity check: non-admin should *not* be able to read the activity"
(mt/with-test-user :rasta
(is (not (models/can-read? activity)))))
(is (not (user-can-see-user-joined-activity? :rasta))))))))
(is (not (contains? (activity-topics :rasta) "user-joined"))))))))
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